diff --git a/.husky/.gitignore b/.husky/.gitignore deleted file mode 100644 index 31354ec13..000000000 --- a/.husky/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_ diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index d2ae35e84..000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -yarn lint-staged diff --git a/README.md b/README.md index 74da81313..957b41bf3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ The frontend uses the (patternfly v4 framework)[https://www.patternfly.org/v4/] | Command | Description | |-------------------------------------|-------------------------------------------------------------------| -| `prepare` | runs `husky install` | | `prebuild` | runs `yarn clean` | | `build` | build the project with webpack in prod mode | | `start` | runs the project with webpack in dev mode and live reload | @@ -70,12 +69,6 @@ The base url of the backend services can also be specified here, e.g.: API_BASEURL=http://localhost:8888 -### Pre-commit hook - -`husky` configures the git hooks. https://github.com/typicode/husky -`lint-staged` runs scripts on matched staged source files. https://github.com/okonet/lint-staged -There is no need to manually configure anything. Just by installing the dependencies, the git hooks are configured. - ### Testing the Web-console with Cypress Cypress can be configured via cypress.json (or command line arguments). It is always best to run the install commands from the web-console repository. diff --git a/changelogs/2.1.0/5781-events-tab.yml b/changelogs/2.1.0/5781-events-tab.yml new file mode 100644 index 000000000..53dd9ee46 --- /dev/null +++ b/changelogs/2.1.0/5781-events-tab.yml @@ -0,0 +1,6 @@ +description: Implement the Events tab on the Instance details page. +issue-nr: 5781 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5782-resource-tab.yml b/changelogs/2.1.0/5782-resource-tab.yml new file mode 100644 index 000000000..dcb67018e --- /dev/null +++ b/changelogs/2.1.0/5782-resource-tab.yml @@ -0,0 +1,6 @@ +description: Implement the Resource tab on the Instance details page. +issue-nr: 5782 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5842-diagnose-view-changes.yml b/changelogs/2.1.0/5842-diagnose-view-changes.yml new file mode 100644 index 000000000..a21705a8b --- /dev/null +++ b/changelogs/2.1.0/5842-diagnose-view-changes.yml @@ -0,0 +1,6 @@ +description: Add navigation button to Diagnose view from instance details page, add ability to adjust look back property for the diagnose +issue-nr: 5842 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5868-composer-v2.yml b/changelogs/2.1.0/5868-composer-v2.yml new file mode 100644 index 000000000..79dd7dea8 --- /dev/null +++ b/changelogs/2.1.0/5868-composer-v2.yml @@ -0,0 +1,11 @@ +description: "Complete redesign of the Instance Composer, main focus was to align its general functionalities with regular form, and improve the user experience. +This change includes: +A right sidebar, to have better access to the form fields of different parts of the instance +A left sidebar, from which we can drag and drop embedded entities and existing Inter-Service Relations from the inventory. +Inter-Service Relations can only be edited when opened individualy in the Instance Composer. +Zooming can now be done with a slider, and two new functionalities have been added. Zoom-to-fit and full-screen mode." +issue-nr: 5868 +change-type: minor +destination-branches: [master] +sections: + feature: "{{description}}" diff --git a/changelogs/2.1.0/5870-composer-feedback.yml b/changelogs/2.1.0/5870-composer-feedback.yml new file mode 100644 index 000000000..2b28fabc5 --- /dev/null +++ b/changelogs/2.1.0/5870-composer-feedback.yml @@ -0,0 +1,6 @@ +description: Add a feedback component to the composer to provide user with information about the missing required inter-service relations +issue-nr: 5870 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5916-simple-docs.yml b/changelogs/2.1.0/5916-simple-docs.yml new file mode 100644 index 000000000..64919673f --- /dev/null +++ b/changelogs/2.1.0/5916-simple-docs.yml @@ -0,0 +1,6 @@ +description: Simplify the documentation tab when there's only one item available. +issue-nr: 5916 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5921-input-description.yml b/changelogs/2.1.0/5921-input-description.yml new file mode 100644 index 000000000..1478c311e --- /dev/null +++ b/changelogs/2.1.0/5921-input-description.yml @@ -0,0 +1,6 @@ +description: Align all the input descriptions in the service instance forms +issue-nr: 5921 +change-type: minor +destination-branches: [master, iso7] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5931-markdown-icons.yml b/changelogs/2.1.0/5931-markdown-icons.yml new file mode 100644 index 000000000..d83a7cc72 --- /dev/null +++ b/changelogs/2.1.0/5931-markdown-icons.yml @@ -0,0 +1,6 @@ +description: Add support for emoji to the Documentation Tab +issue-nr: 5931 +change-type: minor +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5942-expert-button-banner.yml b/changelogs/2.1.0/5942-expert-button-banner.yml new file mode 100644 index 000000000..0fc914eaa --- /dev/null +++ b/changelogs/2.1.0/5942-expert-button-banner.yml @@ -0,0 +1,6 @@ +description: Add button to disable expert mode from the banner +issue-nr: 5942 +change-type: minor +destination-branches: [master, iso7] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5946-discovery-uri.yml b/changelogs/2.1.0/5946-discovery-uri.yml new file mode 100644 index 000000000..fee87a492 --- /dev/null +++ b/changelogs/2.1.0/5946-discovery-uri.yml @@ -0,0 +1,6 @@ +description: Adds discovery uri to the Discovered Resources Page +issue-nr: 5946 +change-type: patch +destination-branches: [master, iso7] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5949-th-warnings.yml b/changelogs/2.1.0/5949-th-warnings.yml new file mode 100644 index 000000000..8157a4c14 --- /dev/null +++ b/changelogs/2.1.0/5949-th-warnings.yml @@ -0,0 +1,6 @@ +description: Add screen reader text for empty columns headers to improve accessibility +issue-nr: 5949 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5951-emphasize-terminated-instances.yml b/changelogs/2.1.0/5951-emphasize-terminated-instances.yml new file mode 100644 index 000000000..6889f3608 --- /dev/null +++ b/changelogs/2.1.0/5951-emphasize-terminated-instances.yml @@ -0,0 +1,6 @@ +description: Emphasize terminated instances in the instance details view +issue-nr: 5951 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5965-global-modal.yml b/changelogs/2.1.0/5965-global-modal.yml new file mode 100644 index 000000000..f4c320ff5 --- /dev/null +++ b/changelogs/2.1.0/5965-global-modal.yml @@ -0,0 +1,6 @@ +description: Create a global modal component for use across the application, to improve performance and reduce code duplication. +issue-nr: 5965 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5969-embedded-entity-formstate.yml b/changelogs/2.1.0/5969-embedded-entity-formstate.yml new file mode 100644 index 000000000..7ab1c6378 --- /dev/null +++ b/changelogs/2.1.0/5969-embedded-entity-formstate.yml @@ -0,0 +1,6 @@ +description: Removing embedded entities in the form wasn't consistently removing the correct item. A unique identifier has been added to the form elements to ensure the correct item is removed. +issue-nr: 5969 +change-type: patch +destination-branches: [master] +sections: + bugfix: "{{description}}" diff --git a/changelogs/2.1.0/5972-dependabot.yml b/changelogs/2.1.0/5972-dependabot.yml new file mode 100644 index 000000000..cee5471d5 --- /dev/null +++ b/changelogs/2.1.0/5972-dependabot.yml @@ -0,0 +1,6 @@ +change-type: patch +description: 'Build(deps-dev): Bump @typescript-eslint/eslint-plugin from 8.1.0 to + 8.8.1' +destination-branches: +- master +sections: {} diff --git a/changelogs/2.1.0/5979-dependabot.yml b/changelogs/2.1.0/5979-dependabot.yml new file mode 100644 index 000000000..bc5ef8e98 --- /dev/null +++ b/changelogs/2.1.0/5979-dependabot.yml @@ -0,0 +1,5 @@ +change-type: patch +description: 'Build(deps-dev): Bump fetch-mock from 11.1.3 to 11.1.5' +destination-branches: +- master +sections: {} diff --git a/changelogs/2.1.0/5982-dependabot.yml b/changelogs/2.1.0/5982-dependabot.yml new file mode 100644 index 000000000..8b57c907c --- /dev/null +++ b/changelogs/2.1.0/5982-dependabot.yml @@ -0,0 +1,5 @@ +change-type: patch +description: 'Build(deps-dev): Bump @types/uuid from 9.0.8 to 10.0.0' +destination-branches: +- master +sections: {} diff --git a/changelogs/2.1.0/5984-e2e-labes.yml b/changelogs/2.1.0/5984-e2e-labes.yml new file mode 100644 index 000000000..0c07502b9 --- /dev/null +++ b/changelogs/2.1.0/5984-e2e-labes.yml @@ -0,0 +1,4 @@ +description: Update e2e 8th scenario to use the new Instance Details PAge +issue-nr: 5984 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/5986-header-colors.yml b/changelogs/2.1.0/5986-header-colors.yml new file mode 100644 index 000000000..670415a03 --- /dev/null +++ b/changelogs/2.1.0/5986-header-colors.yml @@ -0,0 +1,6 @@ +description: Unify colors and fonts in the composer entity headers +issue-nr: 5986 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5987-sidebar-visual-changes.yml b/changelogs/2.1.0/5987-sidebar-visual-changes.yml new file mode 100644 index 000000000..74e6de6e8 --- /dev/null +++ b/changelogs/2.1.0/5987-sidebar-visual-changes.yml @@ -0,0 +1,4 @@ +description: Adjusted the stencil/left sidebar elements to match design, fix issues with shadows on both sidebars +issue-nr: 5987 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/5988-view-mode-composer.yml b/changelogs/2.1.0/5988-view-mode-composer.yml new file mode 100644 index 000000000..7f2eb486c --- /dev/null +++ b/changelogs/2.1.0/5988-view-mode-composer.yml @@ -0,0 +1,6 @@ +description: Hide Left sidebar from the Instance Composer when in the view mode +issue-nr: 5988 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5989-actions-composer-flag-tests.yml b/changelogs/2.1.0/5989-actions-composer-flag-tests.yml new file mode 100644 index 000000000..640f6491e --- /dev/null +++ b/changelogs/2.1.0/5989-actions-composer-flag-tests.yml @@ -0,0 +1,4 @@ +description: Add v2 flag for coordinates metadata saved in Instance and tests for composer Actions +issue-nr: 5989 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/5990-improve-form-messages.yml b/changelogs/2.1.0/5990-improve-form-messages.yml new file mode 100644 index 000000000..b37d12833 --- /dev/null +++ b/changelogs/2.1.0/5990-improve-form-messages.yml @@ -0,0 +1,6 @@ +description: Improve information messages in the form in the Instance Composer +issue-nr: 5990 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5991-move-actions-buttons.yml b/changelogs/2.1.0/5991-move-actions-buttons.yml new file mode 100644 index 000000000..fb1c870b9 --- /dev/null +++ b/changelogs/2.1.0/5991-move-actions-buttons.yml @@ -0,0 +1,6 @@ +description: Move action buttons to the page header, to improve spacing +issue-nr: 5991 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5993-autocomplete-null-bug.yml b/changelogs/2.1.0/5993-autocomplete-null-bug.yml new file mode 100644 index 000000000..cada9d733 --- /dev/null +++ b/changelogs/2.1.0/5993-autocomplete-null-bug.yml @@ -0,0 +1,6 @@ +description: Resolve issue when inter-service relation attribute value is set to null AutoCompleteInput crashes. +issue-nr: 5993 +change-type: patch +destination-branches: [master, iso7] +sections: + bugfix: "{{description}}" diff --git a/changelogs/2.1.0/5997-improve-useeffects.yml b/changelogs/2.1.0/5997-improve-useeffects.yml new file mode 100644 index 000000000..43ffdc090 --- /dev/null +++ b/changelogs/2.1.0/5997-improve-useeffects.yml @@ -0,0 +1,6 @@ +description: Improve behavior of useEffects in the Instance Composer when related inventories are updated +issue-nr: 5997 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/5998-loose-relations.yml b/changelogs/2.1.0/5998-loose-relations.yml new file mode 100644 index 000000000..70dd278f5 --- /dev/null +++ b/changelogs/2.1.0/5998-loose-relations.yml @@ -0,0 +1,6 @@ +description: Add highlighting for not connected inter-service relations elements on the canvas in the Instance Composer +issue-nr: 5998 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6006-separate-actions-and-helpers.yml b/changelogs/2.1.0/6006-separate-actions-and-helpers.yml new file mode 100644 index 000000000..438e694c4 --- /dev/null +++ b/changelogs/2.1.0/6006-separate-actions-and-helpers.yml @@ -0,0 +1,4 @@ +description: separate actions and helpers file into smaller files and it's tests +issue-nr: 6006 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/6007-e2e-removal-scenario.yml b/changelogs/2.1.0/6007-e2e-removal-scenario.yml new file mode 100644 index 000000000..4ae2ce1f1 --- /dev/null +++ b/changelogs/2.1.0/6007-e2e-removal-scenario.yml @@ -0,0 +1,4 @@ +description: Add e2e scenario for removing an inter-service relation +issue-nr: 6047 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/6010-replace-json-mocks.yml b/changelogs/2.1.0/6010-replace-json-mocks.yml new file mode 100644 index 000000000..697cbcef3 --- /dev/null +++ b/changelogs/2.1.0/6010-replace-json-mocks.yml @@ -0,0 +1,4 @@ +description: Remove unused mocks and move JSON mocks to typescript +issue-nr: 6010 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/6011-composer-isEmbedded.yml b/changelogs/2.1.0/6011-composer-isEmbedded.yml new file mode 100644 index 000000000..733bd7415 --- /dev/null +++ b/changelogs/2.1.0/6011-composer-isEmbedded.yml @@ -0,0 +1,4 @@ +description: change property name from isEmbedded to isEmbeddedEntity in instance composer elements +issue-nr: 6011 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/6014-dependabot.yml b/changelogs/2.1.0/6014-dependabot.yml new file mode 100644 index 000000000..85d4d2e98 --- /dev/null +++ b/changelogs/2.1.0/6014-dependabot.yml @@ -0,0 +1,5 @@ +change-type: patch +description: "Build(deps): Bump http-proxy-middleware from 2.0.6 to 2.0.7" +destination-branches: + - master +sections: {} diff --git a/changelogs/2.1.0/6018-nested-status-properties.yml b/changelogs/2.1.0/6018-nested-status-properties.yml new file mode 100644 index 000000000..a4e20b6f6 --- /dev/null +++ b/changelogs/2.1.0/6018-nested-status-properties.yml @@ -0,0 +1,6 @@ +description: Support nested status properties in the Status Page +issue-nr: 6018 +change-type: patch +destination-branches: [master, iso7] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6022-dependabot.yml b/changelogs/2.1.0/6022-dependabot.yml new file mode 100644 index 000000000..0c3d325a2 --- /dev/null +++ b/changelogs/2.1.0/6022-dependabot.yml @@ -0,0 +1,5 @@ +change-type: patch +description: 'Build(deps): Bump @patternfly/react-table from 5.4.1 to 5.4.9' +destination-branches: +- master +sections: {} diff --git a/changelogs/2.1.0/6025-Patternfly-migration.yml b/changelogs/2.1.0/6025-Patternfly-migration.yml new file mode 100644 index 000000000..489c42cf0 --- /dev/null +++ b/changelogs/2.1.0/6025-Patternfly-migration.yml @@ -0,0 +1,6 @@ +description: Upgrade the Patternfly library to V6. +issue-nr: 6025 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6030-composer-relations-fix.yml b/changelogs/2.1.0/6030-composer-relations-fix.yml new file mode 100644 index 000000000..f80a0d491 --- /dev/null +++ b/changelogs/2.1.0/6030-composer-relations-fix.yml @@ -0,0 +1,6 @@ +description: Fix issue with missing inter-service relations on the canvas +issue-nr: 6030 +change-type: patch +destination-branches: [master] +sections: + bugfix: "{{description}}" diff --git a/changelogs/2.1.0/6031-composer-identifying-attr.yml b/changelogs/2.1.0/6031-composer-identifying-attr.yml new file mode 100644 index 000000000..e2734e3aa --- /dev/null +++ b/changelogs/2.1.0/6031-composer-identifying-attr.yml @@ -0,0 +1,6 @@ +description: Add identifying attributes to pool of displayed attributes in the body of the composer entities +issue-nr: 6031 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6034-read-only-embedded.yml b/changelogs/2.1.0/6034-read-only-embedded.yml new file mode 100644 index 000000000..fa31bf758 --- /dev/null +++ b/changelogs/2.1.0/6034-read-only-embedded.yml @@ -0,0 +1,6 @@ +description: Hide read-only embedded entities from the Canvas in the Instance Composer to make the view cleaner +issue-nr: 6034 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6035-buttons-change.yml b/changelogs/2.1.0/6035-buttons-change.yml new file mode 100644 index 000000000..788e1321e --- /dev/null +++ b/changelogs/2.1.0/6035-buttons-change.yml @@ -0,0 +1,6 @@ +description: Change the buttons in the Composer Sidebar +issue-nr: 6035 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6036-actions-composer-viewer.yml b/changelogs/2.1.0/6036-actions-composer-viewer.yml new file mode 100644 index 000000000..3e6c6f82f --- /dev/null +++ b/changelogs/2.1.0/6036-actions-composer-viewer.yml @@ -0,0 +1,4 @@ +description: Hide Form Actions buttons in the Composer Viewer +issue-nr: 6036 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/6037-multiple-default-embedded.yml b/changelogs/2.1.0/6037-multiple-default-embedded.yml new file mode 100644 index 000000000..8452e50a2 --- /dev/null +++ b/changelogs/2.1.0/6037-multiple-default-embedded.yml @@ -0,0 +1,6 @@ +description: Instance composer will render all required by default embedded entities in the canvas +issue-nr: 6037 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6043-dependabot.yml b/changelogs/2.1.0/6043-dependabot.yml new file mode 100644 index 000000000..7eb0ecde6 --- /dev/null +++ b/changelogs/2.1.0/6043-dependabot.yml @@ -0,0 +1,5 @@ +change-type: patch +description: 'Build(deps-dev): Bump @types/qs from 6.9.15 to 6.9.17' +destination-branches: +- master +sections: {} diff --git a/changelogs/2.1.0/6045-dependabot.yml b/changelogs/2.1.0/6045-dependabot.yml new file mode 100644 index 000000000..b1024256e --- /dev/null +++ b/changelogs/2.1.0/6045-dependabot.yml @@ -0,0 +1,5 @@ +change-type: patch +description: 'Build(deps-dev): Bump @testing-library/jest-dom from 6.4.8 to 6.6.3' +destination-branches: +- master +sections: {} diff --git a/changelogs/2.1.0/6047-improve-date-display-instance-details.yml b/changelogs/2.1.0/6047-improve-date-display-instance-details.yml new file mode 100644 index 000000000..272f14293 --- /dev/null +++ b/changelogs/2.1.0/6047-improve-date-display-instance-details.yml @@ -0,0 +1,6 @@ +description: Improve the display of timestamps in the instance details page. +issue-nr: 6047 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6050-remove-husky.yml b/changelogs/2.1.0/6050-remove-husky.yml new file mode 100644 index 000000000..42ac22421 --- /dev/null +++ b/changelogs/2.1.0/6050-remove-husky.yml @@ -0,0 +1,4 @@ +description: Remove Husky from the project. +issue-nr: 6050 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/6051-extract-dispatchers.yml b/changelogs/2.1.0/6051-extract-dispatchers.yml new file mode 100644 index 000000000..dfd890139 --- /dev/null +++ b/changelogs/2.1.0/6051-extract-dispatchers.yml @@ -0,0 +1,4 @@ +description: Extract event dispatchers to a separate files to improve code readability and to have a safe-guard in type checking in the composer. +issue-nr: 6047 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/6055-dependabot.yml b/changelogs/2.1.0/6055-dependabot.yml new file mode 100644 index 000000000..f2efca465 --- /dev/null +++ b/changelogs/2.1.0/6055-dependabot.yml @@ -0,0 +1,5 @@ +change-type: patch +description: 'Build(deps): Bump @eslint/plugin-kit from 0.2.2 to 0.2.3' +destination-branches: +- master +sections: {} diff --git a/changelogs/2.1.0/6057-dependabot.yml b/changelogs/2.1.0/6057-dependabot.yml new file mode 100644 index 000000000..bc4adaf7e --- /dev/null +++ b/changelogs/2.1.0/6057-dependabot.yml @@ -0,0 +1,5 @@ +change-type: patch +description: 'Build(deps): Bump cross-spawn from 7.0.3 to 7.0.6' +destination-branches: +- master +sections: {} diff --git a/changelogs/2.1.0/6058-instance-details-latest-information.yml b/changelogs/2.1.0/6058-instance-details-latest-information.yml new file mode 100644 index 000000000..0d5a84443 --- /dev/null +++ b/changelogs/2.1.0/6058-instance-details-latest-information.yml @@ -0,0 +1,6 @@ +description: Modify Service Details Page to use direct service instance data for latest version instead latest instance logs +issue-nr: 6058 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6062-slider-composer-fix.yml b/changelogs/2.1.0/6062-slider-composer-fix.yml new file mode 100644 index 000000000..157157d69 --- /dev/null +++ b/changelogs/2.1.0/6062-slider-composer-fix.yml @@ -0,0 +1,4 @@ +description: Fix composer slider issue of not updating its styled when updated +issue-nr: 6036 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/6063-dependabot.yml b/changelogs/2.1.0/6063-dependabot.yml new file mode 100644 index 000000000..f27471938 --- /dev/null +++ b/changelogs/2.1.0/6063-dependabot.yml @@ -0,0 +1,5 @@ +change-type: patch +description: 'Build(deps-dev): Bump @types/lodash from 4.17.7 to 4.17.13' +destination-branches: +- master +sections: {} diff --git a/changelogs/2.1.0/6064-dependabot.yml b/changelogs/2.1.0/6064-dependabot.yml new file mode 100644 index 000000000..b33bb1b09 --- /dev/null +++ b/changelogs/2.1.0/6064-dependabot.yml @@ -0,0 +1,5 @@ +change-type: patch +description: 'Build(deps-dev): Bump mocha from 10.7.3 to 10.8.2' +destination-branches: +- master +sections: {} diff --git a/changelogs/2.1.0/6070-dependabot.yml b/changelogs/2.1.0/6070-dependabot.yml new file mode 100644 index 000000000..8be31a8e2 --- /dev/null +++ b/changelogs/2.1.0/6070-dependabot.yml @@ -0,0 +1,5 @@ +change-type: patch +description: 'Build(deps): Bump @patternfly/react-tokens from 5.3.1 to 5.4.1' +destination-branches: +- master +sections: {} diff --git a/changelogs/2.1.0/6074-dependabot.yml b/changelogs/2.1.0/6074-dependabot.yml new file mode 100644 index 000000000..77afaa5c8 --- /dev/null +++ b/changelogs/2.1.0/6074-dependabot.yml @@ -0,0 +1,5 @@ +change-type: patch +description: 'Build(deps-dev): Bump @types/backbone from 1.4.19 to 1.4.22' +destination-branches: +- master +sections: {} diff --git a/changelogs/2.1.0/6087-white-spaces-stencil.yml b/changelogs/2.1.0/6087-white-spaces-stencil.yml new file mode 100644 index 000000000..d140f34d4 --- /dev/null +++ b/changelogs/2.1.0/6087-white-spaces-stencil.yml @@ -0,0 +1,4 @@ +description: Change stencil names to aria-labels to avoid issue with class names not handling white spaces +issue-nr: 6087 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/6103-buildmaster.yml b/changelogs/2.1.0/6103-buildmaster.yml new file mode 100644 index 000000000..294eb36bc --- /dev/null +++ b/changelogs/2.1.0/6103-buildmaster.yml @@ -0,0 +1,3 @@ +description: security alerts, fix master flakes +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/6104-Remove-deprecated-collapsibles.yml b/changelogs/2.1.0/6104-Remove-deprecated-collapsibles.yml new file mode 100644 index 000000000..1cfc99ebb --- /dev/null +++ b/changelogs/2.1.0/6104-Remove-deprecated-collapsibles.yml @@ -0,0 +1,6 @@ +description: Remove the collapsible functionality in the Service Inventory table. The new Instance Details page replaces the content of the collapsible sections. +issue-nr: 6104 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6105-move-progressbar.yml b/changelogs/2.1.0/6105-move-progressbar.yml new file mode 100644 index 000000000..866542e52 --- /dev/null +++ b/changelogs/2.1.0/6105-move-progressbar.yml @@ -0,0 +1,6 @@ +description: Move progress bar form resource tab to details section +issue-nr: 5782 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6120-status-page-fix.yml b/changelogs/2.1.0/6120-status-page-fix.yml new file mode 100644 index 000000000..41731d9d6 --- /dev/null +++ b/changelogs/2.1.0/6120-status-page-fix.yml @@ -0,0 +1,6 @@ +description: Fix spacing and font sizes in status page to improve UI and readability of the view +issue-nr: 6120 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6124-composer-fixes.yml b/changelogs/2.1.0/6124-composer-fixes.yml new file mode 100644 index 000000000..5a63994bc --- /dev/null +++ b/changelogs/2.1.0/6124-composer-fixes.yml @@ -0,0 +1,6 @@ +description: Fix slider display issue in firefox, fix rounding in the highlighter, fix fetching cache issue for composer initial load, fix overflow issue for text list field in composer +issue-nr: 6124 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6125-primary-button-inventory.yml b/changelogs/2.1.0/6125-primary-button-inventory.yml new file mode 100644 index 000000000..6a60b8e9d --- /dev/null +++ b/changelogs/2.1.0/6125-primary-button-inventory.yml @@ -0,0 +1,6 @@ +description: Move the option to go to the instance details into the row as primary action button. +issue-nr: 6125 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6133-documentation-tab-improvements.yml b/changelogs/2.1.0/6133-documentation-tab-improvements.yml new file mode 100644 index 000000000..23d2661a2 --- /dev/null +++ b/changelogs/2.1.0/6133-documentation-tab-improvements.yml @@ -0,0 +1,6 @@ +description: Improve the Documentation tab on the Instance details page when only one documentation section is available. +issue-nr: 6122 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6134-dropdown-fix.yml b/changelogs/2.1.0/6134-dropdown-fix.yml new file mode 100644 index 000000000..22c39427c --- /dev/null +++ b/changelogs/2.1.0/6134-dropdown-fix.yml @@ -0,0 +1,4 @@ +description: Fix un-responsive space in service inventory dropdown +issue-nr: 6134 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/6135-desc-diagnose.yml b/changelogs/2.1.0/6135-desc-diagnose.yml new file mode 100644 index 000000000..cc55eb589 --- /dev/null +++ b/changelogs/2.1.0/6135-desc-diagnose.yml @@ -0,0 +1,4 @@ +description: Remove confusing description from diagnose page +issue-nr: 6135 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/6138-embbedded-bug-composer.yml b/changelogs/2.1.0/6138-embbedded-bug-composer.yml new file mode 100644 index 000000000..f3550c877 --- /dev/null +++ b/changelogs/2.1.0/6138-embbedded-bug-composer.yml @@ -0,0 +1,6 @@ +description: Fix issue when toggling embedded entities of the same type in the instance composer +issue-nr: 6138 +change-type: patch +destination-branches: [master] +sections: + bugfix: "{{description}}" diff --git a/changelogs/2.1.0/6142-version-tags.yml b/changelogs/2.1.0/6142-version-tags.yml new file mode 100644 index 000000000..b6fde6ae7 --- /dev/null +++ b/changelogs/2.1.0/6142-version-tags.yml @@ -0,0 +1,6 @@ +description: Improve the tags for the versions on the instance details page. +issue-nr: 6142 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/6149-update-info-color.yml b/changelogs/2.1.0/6149-update-info-color.yml new file mode 100644 index 000000000..9f2490b32 --- /dev/null +++ b/changelogs/2.1.0/6149-update-info-color.yml @@ -0,0 +1,4 @@ +description: Adjust the info color to be blue instead of the new PF purple color. +issue-nr: 6149 +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/6150-infinite-logs.yml b/changelogs/2.1.0/6150-infinite-logs.yml new file mode 100644 index 000000000..a3789e194 --- /dev/null +++ b/changelogs/2.1.0/6150-infinite-logs.yml @@ -0,0 +1,6 @@ +description: Add Infinite query to History section +issue-nr: 6150 +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/buildmaster-act-removal.yml b/changelogs/2.1.0/buildmaster-act-removal.yml new file mode 100644 index 000000000..1d634cce5 --- /dev/null +++ b/changelogs/2.1.0/buildmaster-act-removal.yml @@ -0,0 +1,3 @@ +description: Remove the act() function from the tests. act() around userEvents is no longer required for the latest version of @testing-library. +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/buildmaster-destination-fix.yml b/changelogs/2.1.0/buildmaster-destination-fix.yml new file mode 100644 index 000000000..e5fac22c9 --- /dev/null +++ b/changelogs/2.1.0/buildmaster-destination-fix.yml @@ -0,0 +1,3 @@ +description: Remove iso7 as a destination branch from task 5949 to avoid issues in the pipeline +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/buildmaster-fix-changelog-destination.yml b/changelogs/2.1.0/buildmaster-fix-changelog-destination.yml new file mode 100644 index 000000000..4b0c646d3 --- /dev/null +++ b/changelogs/2.1.0/buildmaster-fix-changelog-destination.yml @@ -0,0 +1,3 @@ +description: Fix changelog destination +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/buildmaster-fix-composer-flake.yml b/changelogs/2.1.0/buildmaster-fix-composer-flake.yml new file mode 100644 index 000000000..82d252e2c --- /dev/null +++ b/changelogs/2.1.0/buildmaster-fix-composer-flake.yml @@ -0,0 +1,3 @@ +description: Fix composer flake +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/buildmaster-oss-e2e-6-scenario.yml b/changelogs/2.1.0/buildmaster-oss-e2e-6-scenario.yml new file mode 100644 index 000000000..908110040 --- /dev/null +++ b/changelogs/2.1.0/buildmaster-oss-e2e-6-scenario.yml @@ -0,0 +1,3 @@ +description: Fix failing 6th scenario in the E2E test suite for OSS +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/buildmaster-oss-e2e-fix.yml b/changelogs/2.1.0/buildmaster-oss-e2e-fix.yml new file mode 100644 index 000000000..908110040 --- /dev/null +++ b/changelogs/2.1.0/buildmaster-oss-e2e-fix.yml @@ -0,0 +1,3 @@ +description: Fix failing 6th scenario in the E2E test suite for OSS +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/buildmaster-unlock-scenario-3.4.yml b/changelogs/2.1.0/buildmaster-unlock-scenario-3.4.yml new file mode 100644 index 000000000..74fea8b7f --- /dev/null +++ b/changelogs/2.1.0/buildmaster-unlock-scenario-3.4.yml @@ -0,0 +1,3 @@ +description: unlock scenario 3.4 after core fixed the issue on their side +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/buildmaster-update-e2e-env-settings.yml b/changelogs/2.1.0/buildmaster-update-e2e-env-settings.yml new file mode 100644 index 000000000..cb96963f3 --- /dev/null +++ b/changelogs/2.1.0/buildmaster-update-e2e-env-settings.yml @@ -0,0 +1,3 @@ +description: Update the Environment scenario. Some environment settings have been removed in the Orchestrator, and shouldn't be asserted anymore in the E2E test. +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/buildmaster-update-e2e-oss.yml b/changelogs/2.1.0/buildmaster-update-e2e-oss.yml new file mode 100644 index 000000000..ffc2d5a2b --- /dev/null +++ b/changelogs/2.1.0/buildmaster-update-e2e-oss.yml @@ -0,0 +1,3 @@ +description: The desired state now displays the attribute for receive_events. Updated the E2E to assert this in the oss suit. +change-type: patch +destination-branches: [master, iso7] diff --git a/changelogs/2.1.0/buildmaster-update-e2e-receiveEvents.yml b/changelogs/2.1.0/buildmaster-update-e2e-receiveEvents.yml new file mode 100644 index 000000000..321dbdfdc --- /dev/null +++ b/changelogs/2.1.0/buildmaster-update-e2e-receiveEvents.yml @@ -0,0 +1,3 @@ +description: The desired state now displays the attribute for receive_events. Updated the E2E to assert this. +change-type: patch +destination-branches: [master, iso7] diff --git a/changelogs/2.1.0/buildmaster-update-iso8-e2e.yml b/changelogs/2.1.0/buildmaster-update-iso8-e2e.yml new file mode 100644 index 000000000..882a325a7 --- /dev/null +++ b/changelogs/2.1.0/buildmaster-update-iso8-e2e.yml @@ -0,0 +1,3 @@ +description: Update the ISO-8 e2e test to match the updated local-setup adjustments. +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/buildmaster-update-oss-4th.yml b/changelogs/2.1.0/buildmaster-update-oss-4th.yml new file mode 100644 index 000000000..8cc1d44dc --- /dev/null +++ b/changelogs/2.1.0/buildmaster-update-oss-4th.yml @@ -0,0 +1,3 @@ +description: Updated the E2E timeout to fix the flake in4th scenario of oss suit. +change-type: patch +destination-branches: [master, iso7] diff --git a/changelogs/2.1.0/composer-styling-pf6.yml b/changelogs/2.1.0/composer-styling-pf6.yml new file mode 100644 index 000000000..89c6d9de4 --- /dev/null +++ b/changelogs/2.1.0/composer-styling-pf6.yml @@ -0,0 +1,3 @@ +description: match composer element styling with patternfly v6, resolve e2e issues after upgrade +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/oss-test-flake.yml b/changelogs/2.1.0/oss-test-flake.yml new file mode 100644 index 000000000..9b7f49139 --- /dev/null +++ b/changelogs/2.1.0/oss-test-flake.yml @@ -0,0 +1,3 @@ +description: Updated the E2E timeout to fix the flake in 4th scenario of oss suit. +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/pf-6-align-colors-labels.yml b/changelogs/2.1.0/pf-6-align-colors-labels.yml new file mode 100644 index 000000000..69e7f5599 --- /dev/null +++ b/changelogs/2.1.0/pf-6-align-colors-labels.yml @@ -0,0 +1,5 @@ +description: Align colors of labels and fix colors of progress bars +change-type: patch +destination-branches: [master] +sections: + minor-improvement: "{{description}}" diff --git a/changelogs/2.1.0/pf-6-cleanup.yml b/changelogs/2.1.0/pf-6-cleanup.yml new file mode 100644 index 000000000..39804d9db --- /dev/null +++ b/changelogs/2.1.0/pf-6-cleanup.yml @@ -0,0 +1,3 @@ +description: Last cleanup phase for PF6 migration. +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/pf-6-migration-feedback.yml b/changelogs/2.1.0/pf-6-migration-feedback.yml new file mode 100644 index 000000000..637c8a3d2 --- /dev/null +++ b/changelogs/2.1.0/pf-6-migration-feedback.yml @@ -0,0 +1,3 @@ +description: Patch based on feedback from PF6 migration. +change-type: patch +destination-branches: [master] diff --git a/changelogs/2.1.0/pf-6-migration-fonts.yml b/changelogs/2.1.0/pf-6-migration-fonts.yml new file mode 100644 index 000000000..9f2327dc9 --- /dev/null +++ b/changelogs/2.1.0/pf-6-migration-fonts.yml @@ -0,0 +1,3 @@ +description: Fix webpack configuration to use the correct path for fonts. Align vertical alignment of text to be centered in rows. +change-type: patch +destination-branches: [master] diff --git a/cypress.config.cjs b/cypress.config.cjs index adffa3cf1..9152b8753 100644 --- a/cypress.config.cjs +++ b/cypress.config.cjs @@ -3,12 +3,11 @@ const { defineConfig } = require("cypress"); module.exports = defineConfig({ env: { edition: "iso", - project: "lsm-frontend", }, video: false, reporter: "junit", viewportWidth: 1500, - viewportHeight: 700, + viewportHeight: 900, reporterOptions: { mochaFile: "cypress/reports/junit/test-report-[hash].xml", }, diff --git a/cypress/e2e/scenario-1-environment.cy.js b/cypress/e2e/scenario-1-environment.cy.js index 9047bd1b6..6ddbfb816 100644 --- a/cypress/e2e/scenario-1-environment.cy.js +++ b/cypress/e2e/scenario-1-environment.cy.js @@ -12,7 +12,7 @@ beforeEach(() => { cy.request("/api/v1/project").as("projects"); cy.get("@projects").then((response) => { response.body.projects.map((project) => { - if (project.name !== Cypress.env("project")) { + if (!/frontend/.test(project.name)) { cy.request("DELETE", `api/v1/project/${project.id}`); } }); @@ -45,7 +45,7 @@ const fillCreateEnvForm = ({ cy.get('[aria-label="Description-input"]').type("Test description"); cy.get('[aria-label="Repository-input"]').type("repository"); cy.get('[aria-label="Branch-input"]').type("branch"); - cy.get("#simple-text-file-filename").selectFile( + cy.get("#file-upload-filename").selectFile( { contents: "@icon", fileName: "icon.png", @@ -62,9 +62,8 @@ const fillCreateEnvForm = ({ /** * Function is responsible for going through delete Environment process with assertions selection that covers test cases * @param {*} name - string - * @param {*} projectName - string */ -const deleteEnv = (name, projectName) => { +const deleteEnv = (name) => { cy.get("button").contains("Delete environment").click(); cy.get('[aria-label="delete"]').should("be.disabled"); @@ -72,7 +71,7 @@ const deleteEnv = (name, projectName) => { cy.get('[aria-label="delete"]').click(); cy.url().should("eq", Cypress.config().baseUrl + "/console"); - cy.get(".pf-v5-c-card").contains(projectName).should("not.exist"); + cy.get(`[aria-label="Select-environment-${name}"]`).should("not.exist"); }; /** @@ -82,7 +81,7 @@ const deleteEnv = (name, projectName) => { const openSettings = (envName) => { cy.wait(2000); - cy.get(".pf-v5-c-nav__item").contains("Settings").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]').contains("Settings").click(); cy.url().should("contain", "/console/settings?env="); cy.get('[aria-label="Name-value"]').should("contain", envName); }; @@ -116,26 +115,22 @@ describe("Environment", () => { shouldPassEnvName: false, }); - cy.get("button").contains("Submit").should("be.disabled"); + cy.get('[aria-label="submit"]').should("be.disabled"); cy.get('[aria-label="Name-input"]').type(testName(2)); cy.get("button").contains("Submit").click(); cy.wait(1000); // test to check redirection to right page. OSS it should be the Desired state page instead of the service catalog. if (Cypress.env("edition") === "iso") { - cy.get(".pf-v5-c-title") - .contains("Service Catalog") - .should("to.be.visible"); + cy.get("h1").contains("Service Catalog").should("to.be.visible"); } else { - cy.get(".pf-v5-c-title") - .contains("Desired State") - .should("to.be.visible"); + cy.get("h1").contains("Desired State").should("to.be.visible"); } - //go back to home and check if env is visible - cy.get(".pf-v5-c-breadcrumb__item").eq(0).click(); + // go back to home and check if env is visible + cy.get('[aria-label="BreadcrumbItem"]').contains("Home").click(); - cy.get('[aria-label="Environment card"]').contains(testName(2)).click(); + cy.get(`[aria-label="Select-environment-${testName(2)}"]`).click(); openSettings(testName(2)); deleteEnv(testName(2), testProjectName(2)); @@ -224,44 +219,46 @@ describe("Environment", () => { // specific to ISO if (Cypress.env("edition") === "iso") { it("1.5 Clear environment", function () { - //Fill The form and submit + // Fill The form and submit cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); - cy.get('[aria-label="ServiceCatalog-Success"]', { - timeout: 30000, + cy.get('[aria-label="ServiceCatalog-Empty"]', { + timeout: 10000, }).should("to.be.visible"); - //Go to settings + // Go to settings openSettings("test"); - //Cancel Clear Env and expect nothing to change + // Cancel Clear Env and expect nothing to change cy.get("button").contains("Clear environment").click(); cy.get("button").contains("Cancel").click(); cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__link").contains("Service Catalog").click(); - cy.get('[aria-label="ServiceCatalog-Success"]', { - timeout: 20000, + cy.get('[aria-label="ServiceCatalog-Empty"]', { + timeout: 10000, }).should("to.be.visible"); - //Go to settings and get Id of an environment + // Go to settings and get Id of an environment openSettings("test"); - //Clear Env + // Clear Env cy.get("button").contains("Clear environment").click(); cy.get('[aria-label="clear environment check"]').type("test"); cy.get("button") .contains("I understand the consequences, clear this environment") .click(); cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains("frontend").click(); - cy.get(".pf-v5-c-nav__link").contains("Service Catalog").click(); + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") + .click(); cy.get('[aria-label="ServiceCatalog-Empty"]').should("to.be.visible"); cy.get("button").contains("Update Service Catalog").click(); @@ -274,7 +271,7 @@ describe("Environment", () => { }); } - it("1.6 Edit environment configuration", function () { + xit("1.6 Edit environment configuration", function () { cy.visit("/console/environment/create"); fillCreateEnvForm({ envName: testName(6), @@ -294,22 +291,8 @@ describe("Environment", () => { openSettings(testName(6), testProjectName(6)); cy.get("button").contains("Configuration").click(); - //Change agent_trigger_method_on_auto_deploy - cy.get( - '[aria-label="EnumInput-agent_trigger_method_on_auto_deployFilterInput"]', - ).click(); - cy.get('[role="option"]').contains("push_full_deploy").click(); - cy.get('[data-testid="Warning"]').should("exist"); - cy.get('[aria-label="Row-agent_trigger_method_on_auto_deploy"]') - .find('[aria-label="SaveAction"]') - .click(); - cy.get('[data-testid="Warning"]').should("not.exist"); - cy.get( - '[aria-label="EnumInput-agent_trigger_method_on_auto_deployFilterInput"]', - ).should("have.value", "push_full_deploy"); - //Change auto_deploy - cy.get('[aria-label="Row-auto_deploy"]').find(".pf-v5-c-switch").click(); + cy.get('[aria-label="Row-auto_deploy"]').find(".pf-v6-c-switch").click(); cy.get('[data-testid="Warning"]').should("exist"); cy.get('[aria-label="Row-auto_deploy"]') .find('[aria-label="SaveAction"]') @@ -325,12 +308,12 @@ describe("Environment", () => { .click(); cy.get('[data-testid="Warning"]').should("not.exist"); cy.get('[aria-label="Row-auto_full_compile"]') - .find(".pf-v5-c-form-control input") + .find(".pf-v6-c-form-control input") .should("have.value", "1 2 3 4 5"); //Change autostart_agent_deploy_interval cy.get('[aria-label="Row-autostart_agent_deploy_interval"]') - .find(".pf-v5-c-form-control") + .find(".pf-v6-c-form-control") .type("{selectAll}610"); cy.get('[data-testid="Warning"]').should("exist"); cy.get('[aria-label="Row-autostart_agent_deploy_interval"]') @@ -338,41 +321,12 @@ describe("Environment", () => { .click(); cy.get('[data-testid="Warning"]').should("not.exist"); cy.get('[aria-label="Row-autostart_agent_deploy_interval"]') - .find(".pf-v5-c-form-control input") + .find(".pf-v6-c-form-control input") .should("have.value", "610"); - //Change autostart_agent_deploy_splay_time - cy.get('[aria-label="Row-autostart_agent_deploy_splay_time"]') - .find(".pf-v5-c-form-control") - .type("{selectAll}20"); - cy.get('[data-testid="Warning"]').should("exist"); - cy.get('[aria-label="Row-autostart_agent_deploy_splay_time"]') - .find('[aria-label="SaveAction"]') - .click(); - cy.get('[data-testid="Warning"]').should("not.exist"); - cy.get('[aria-label="Row-autostart_agent_deploy_splay_time"]') - .find(".pf-v5-c-form-control input") - .should("have.value", "20"); - - //Change autostart_agent_map - cy.get('[aria-label="Row-autostart_agent_map"]') - .find('[aria-label="editEntryValue"]') - .filter((key, $el) => { - return $el.value === "local:"; - }) - .type("{selectAll}{backspace}new value"); - cy.get('[data-testid="Warning"]').should("exist"); - cy.get('[aria-label="Row-autostart_agent_map"]') - .find('[aria-label="SaveAction"]') - .click(); - cy.get('[data-testid="Warning"]').should("not.exist"); - cy.get('[aria-label="Row-autostart_agent_map"]') - .find('[aria-label="editEntryValue"]') - .should("have.value", "new value"); - //Change autostart_agent_repair_interval cy.get('[aria-label="Row-autostart_agent_repair_interval"]') - .find(".pf-v5-c-form-control") + .find(".pf-v6-c-form-control") .type("{selectAll}86410"); cy.get('[data-testid="Warning"]').should("exist"); cy.get('[aria-label="Row-autostart_agent_repair_interval"]') @@ -380,25 +334,12 @@ describe("Environment", () => { .click(); cy.get('[data-testid="Warning"]').should("not.exist"); cy.get('[aria-label="Row-autostart_agent_repair_interval"]') - .find(".pf-v5-c-form-control input") + .find(".pf-v6-c-form-control input") .should("have.value", "86410"); - //Change autostart_agent_repair_splay_time - cy.get('[aria-label="Row-autostart_agent_repair_splay_time"]') - .find(".pf-v5-c-form-control") - .type("{selectAll}610"); - cy.get('[data-testid="Warning"]').should("exist"); - cy.get('[aria-label="Row-autostart_agent_repair_splay_time"]') - .find('[aria-label="SaveAction"]') - .click(); - cy.get('[data-testid="Warning"]').should("not.exist"); - cy.get('[aria-label="Row-autostart_agent_repair_splay_time"]') - .find(".pf-v5-c-form-control input") - .should("have.value", "610"); - //Change autostart_on_start cy.get('[aria-label="Row-autostart_on_start"]') - .find(".pf-v5-c-switch") + .find(".pf-v6-c-switch") .click(); cy.get('[data-testid="Warning"]').should("exist"); cy.get('[aria-label="Row-autostart_on_start"]') @@ -408,7 +349,7 @@ describe("Environment", () => { //Change available_versions_to_keep cy.get('[aria-label="Row-available_versions_to_keep"]') - .find(".pf-v5-c-form-control") + .find(".pf-v6-c-form-control") .type("{selectAll}110"); cy.get('[data-testid="Warning"]').should("exist"); cy.get('[aria-label="Row-available_versions_to_keep"]') @@ -416,28 +357,14 @@ describe("Environment", () => { .click(); cy.get('[data-testid="Warning"]').should("not.exist"); cy.get('[aria-label="Row-available_versions_to_keep"]') - .find(".pf-v5-c-form-control input") + .find(".pf-v6-c-form-control input") .should("have.value", "110"); - //Change environment_agent_trigger_method - cy.get( - '[aria-label="EnumInput-environment_agent_trigger_methodFilterInput"]', - ).click(); - cy.get('[role="option"]').contains("push_full_deploy").click(); - cy.get('[data-testid="Warning"]').should("exist"); - cy.get('[aria-label="Row-environment_agent_trigger_method"]') - .find('[aria-label="SaveAction"]') - .click(); - cy.get('[data-testid="Warning"]').should("not.exist"); - cy.get( - '[aria-label="EnumInput-environment_agent_trigger_methodFilterInput"]', - ).should("have.value", "push_full_deploy"); - // specific to ISO if (Cypress.env("edition") === "iso") { // Change lsm_partial_compile cy.get('[aria-label="Row-lsm_partial_compile"]') - .find(".pf-v5-c-switch") + .find(".pf-v6-c-switch") .click(); cy.get('[data-testid="Warning"]').should("exist"); @@ -449,7 +376,7 @@ describe("Environment", () => { //change notification_retention cy.get('[aria-label="Row-notification_retention"]') - .find(".pf-v5-c-form-control") + .find(".pf-v6-c-form-control") .type("{selectAll}375"); cy.get('[data-testid="Warning"]').should("exist"); cy.get('[aria-label="Row-notification_retention"]') @@ -457,12 +384,12 @@ describe("Environment", () => { .click(); cy.get('[data-testid="Warning"]').should("not.exist"); cy.get('[aria-label="Row-notification_retention"]') - .find(".pf-v5-c-form-control input") + .find(".pf-v6-c-form-control input") .should("have.value", "375"); //Change protected_environment cy.get('[aria-label="Row-protected_environment"]') - .find(".pf-v5-c-switch") + .find(".pf-v6-c-switch") .click(); cy.get('[data-testid="Warning"]').should("exist"); cy.get('[aria-label="Row-protected_environment"]') @@ -470,19 +397,9 @@ describe("Environment", () => { .click(); cy.get('[data-testid="Warning"]').should("not.exist"); - //Change push_on_auto_deploy - cy.get('[aria-label="Row-push_on_auto_deploy"]') - .find(".pf-v5-c-switch") - .click(); - cy.get('[data-testid="Warning"]').should("exist"); - cy.get('[aria-label="Row-push_on_auto_deploy"]') - .find('[aria-label="SaveAction"]') - .click(); - cy.get('[data-testid="Warning"]').should("not.exist"); - //Change resource_action_logs_retention cy.get('[aria-label="Row-resource_action_logs_retention"]') - .find(".pf-v5-c-form-control") + .find(".pf-v6-c-form-control") .type("{selectAll}8"); cy.get('[data-testid="Warning"]').should("exist"); cy.get('[aria-label="Row-resource_action_logs_retention"]') @@ -490,11 +407,11 @@ describe("Environment", () => { .click(); cy.get('[data-testid="Warning"]').should("not.exist"); cy.get('[aria-label="Row-resource_action_logs_retention"]') - .find(".pf-v5-c-form-control input") + .find(".pf-v6-c-form-control input") .should("have.value", "8"); //change server_compile - cy.get('[aria-label="Row-server_compile"]').find(".pf-v5-c-switch").click(); + cy.get('[aria-label="Row-server_compile"]').find(".pf-v6-c-switch").click(); cy.get('[data-testid="Warning"]').should("exist"); cy.get('[aria-label="Row-server_compile"]') .find('[aria-label="SaveAction"]') @@ -503,7 +420,7 @@ describe("Environment", () => { //re-enable to delete env cy.get('[aria-label="Row-protected_environment"]') - .find(".pf-v5-c-switch") + .find(".pf-v6-c-switch") .click(); cy.get('[data-testid="Warning"]').should("exist"); cy.get('[aria-label="Row-protected_environment"]') @@ -511,7 +428,7 @@ describe("Environment", () => { .click(); cy.get('[data-testid="Warning"]').should("not.exist"); - cy.get(".pf-v5-c-tabs__list") + cy.get(".pf-v6-c-tabs__list") .find("button") .contains("Environment") .click(); diff --git a/cypress/e2e/scenario-2.1-basic-service.cy.js b/cypress/e2e/scenario-2.1-basic-service.cy.js index ced2a3efb..a58acfb0e 100644 --- a/cypress/e2e/scenario-2.1-basic-service.cy.js +++ b/cypress/e2e/scenario-2.1-basic-service.cy.js @@ -1,12 +1,12 @@ /** * Shorthand method to clear the environment being passed. - * By default, if no arguments are passed it will target the 'lsm-frontend' environment. + * By default, if no arguments are passed it will target the 'test' environment. * * @param {string} nameEnvironment */ -const clearEnvironment = (nameEnvironment = "lsm-frontend") => { +const clearEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -40,14 +40,14 @@ const checkStatusCompile = (id) => { }; /** - * Will by default execute the force update on the 'lsm-frontend' environment if no arguments are being passed. + * Will by default execute the force update on the 'test' environment if no arguments are being passed. * This method can be executed standalone, but is part of the cleanup cycle that is needed before running a scenario. * * @param {string} nameEnvironment */ -const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { +const forceUpdateEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -78,10 +78,10 @@ if (Cypress.env("edition") === "iso") { "/lsm/v1/service_inventory/basic-service?include_deployment_progress=True&limit=20&&sort=created_at.desc", ).as("GetServiceInventory"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); cy.get("#basic-service").contains("Show inventory").click(); // make sure the call to get inventory has been executed @@ -112,10 +112,10 @@ if (Cypress.env("edition") === "iso") { it("2.1.2 Add Instance Submit form, INVALID form, EDIT form, VALID form", () => { // Go from Home page to Service Inventory of Basic-service cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); cy.get("#basic-service").contains("Show inventory").click(); cy.get('[aria-label="ServiceInventory-Empty"]').should("to.be.visible"); @@ -151,16 +151,19 @@ if (Cypress.env("edition") === "iso") { cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); // check whether there are two options available in the dropdown to copy the id/identifier. - cy.get('[aria-label="IdentityCell-basic-service"]').click(); + cy.get('[aria-label="IdentityCell-basic-service"]').within(() => { + cy.get('[aria-label="Copy to clipboard"]').click(); + }); + cy.get('[role="menuitem"]').should("have.length", 2); }); it("2.1.3 Edit previously created instance, Instance Details history, documentation tab", () => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); // Expect to find one badge on the basic-service row. cy.get("#basic-service") .get('[aria-label="Number of instances by label"]') @@ -169,12 +172,9 @@ if (Cypress.env("edition") === "iso") { cy.get("#basic-service").contains("Show inventory").click(); // Check Instance Details page - cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }).click(); + // cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }).click(); // The first button should be the one redirecting to the details page. - cy.get(".pf-v5-c-menu__item") - .first() - .contains("Instance Details") - .click(); + cy.get('[aria-label="instance-details-link"]').first().click(); // Check if there are three versions in the history table cy.get('[aria-label="History-Row"]', { timeout: 60000 }).should( @@ -226,7 +226,9 @@ if (Cypress.env("edition") === "iso") { cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }).click(); // The fourth button in the dropdown should be the edit button. - cy.get(".pf-v5-c-menu__item").eq(4).contains("Edit").click(); + cy.get('[role="menuitem"]') + .contains(/^Edit$/) + .click(); // check if amount of fields is lesser than create amount. cy.get("form").find("input").should("have.length.of.at.most", 11); @@ -240,36 +242,35 @@ if (Cypress.env("edition") === "iso") { cy.get("#address_r1").type("1.2.3.8/32"); cy.get("button").contains("Confirm").click(); - // expect to land on Service Inventory page and to find attributes tab button - cy.get("#expand-toggle0").click(); - cy.get(".pf-v5-c-tabs__list") - .contains("Attributes", { timeout: 20000 }) + // check attributes on instance details page + cy.get('[aria-label="instance-details-link"]', { timeout: 20000 }) + .first() .click(); + cy.get(".pf-v6-c-tabs__list").contains("Attributes").click(); + + // Expect to find new value as candidate and old value in active + cy.get('[aria-label="address_r1_value"]').should("contain", "1.2.3.5/32"); - // Expect to find new value as candidate and old value in active and no rollback value - cy.get('[aria-label="Row-address_r1"') - .find('[data-label="candidate"]', { timeout: 20000 }) - .should("contain", "1.2.3.8/32"); - cy.get('[aria-label="Row-address_r1"') - .find('[data-label="active"]') - .should("contain", "1.2.3.5/32"); - cy.get('[aria-label="Row-address_r1"') - .find('[data-label="rollback"]') - .should("contain", ""); + // change to candidate attribute set + cy.get('[aria-label="Select-AttributeSet"]').select( + "candidate_attributes", + ); + + cy.get('[aria-label="address_r1_value"]').should("contain", "1.2.3.8/32"); }); it("2.1.4 Duplicate instance with Editor", () => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); cy.get("#basic-service").contains("Show inventory").click(); cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }) .eq(0) .click(); - cy.get(".pf-v5-c-menu__item").contains("Duplicate").click(); + cy.get('[role="menuitem"]').contains("Duplicate").click(); // toggle to JSON editor cy.get("#editorButton").click(); @@ -290,7 +291,7 @@ if (Cypress.env("edition") === "iso") { ); // expect submit button to be disabled - cy.get("button").contains("Confirm").should("be.disabled"); + cy.get('[aria-label="submit"]').should("be.disabled"); // expect Form button to be disabled cy.get("#formButton").should("be.disabled"); @@ -305,7 +306,7 @@ if (Cypress.env("edition") === "iso") { ); // expect submit button to be enabled - cy.get("button").contains("Confirm").should("be.enabled"); + cy.get('[aria-label="submit"]').should("be.enabled"); // expect Form button to be enabled cy.get("#formButton").should("be.enabled"); @@ -349,10 +350,10 @@ if (Cypress.env("edition") === "iso") { "/lsm/v1/service_inventory/basic-service?include_deployment_progress=True&limit=20&&sort=created_at.desc", ).as("GetServiceInventory"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); cy.get("#basic-service").contains("Show inventory").click(); // make sure the call to get inventory has been executed @@ -364,7 +365,7 @@ if (Cypress.env("edition") === "iso") { // expect Form and submit buttons to be disabled cy.get("#formButton").should("be.disabled"); - cy.get("button").contains("Confirm").should("be.disabled"); + cy.get('[aria-label="submit"]').should("be.disabled"); // Cancel form should still be possible. cy.get("button").contains("Cancel").click(); @@ -378,10 +379,10 @@ if (Cypress.env("edition") === "iso") { it("2.1.6 Instance Details page", () => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); // Expect to find one badge on the basic-service row. cy.get("#basic-service") .get('[aria-label="Number of instances by label"]') @@ -390,14 +391,9 @@ if (Cypress.env("edition") === "iso") { cy.get("#basic-service").contains("Show inventory").click(); // Check Instance Details page - cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }) + cy.get('[aria-label="instance-details-link"]', { timeout: 20000 }) .last() .click(); - // The first button should be the one redirecting to the details page. - cy.get(".pf-v5-c-menu__item") - .first() - .contains("Instance Details") - .click(); // click on the attributes tab cy.get('[aria-label="attributes-content"]').click(); @@ -416,7 +412,7 @@ if (Cypress.env("edition") === "iso") { .should("contain", "address_r1"); // this row should contain the active_attribute value 1.2.3.5/32 - cy.get('[data-testid="address_r1"]').should("contain", "1.2.3.5/32"); + cy.get('[aria-label="address_r1_value"]').should("contain", "1.2.3.5/32"); // assert you can reset the sorting cy.get('[aria-label="table-options"]').click(); @@ -431,7 +427,7 @@ if (Cypress.env("edition") === "iso") { ); // assert that the address_r1 attribute value is now the candidate value 1.2.3.8/32 - cy.get('[data-testid="address_r1"]').should("contain", "1.2.3.8/32"); + cy.get('[aria-label="address_r1_value"]').should("contain", "1.2.3.8/32"); // click on the JSON-editor tab cy.get("#JSON").click(); @@ -457,7 +453,7 @@ if (Cypress.env("edition") === "iso") { // Update the state to setting_start cy.get('[aria-label="Actions-Toggle"]').click(); - cy.get(".pf-v5-c-menu__item").last().click(); + cy.get('[role="menuitem"]').last().click(); // Confirm in the modal cy.get("button").contains("Yes").click(); @@ -468,6 +464,60 @@ if (Cypress.env("edition") === "iso") { expect($rows[1]).to.contain("setting_inprogress"); expect($rows[2]).to.contain("setting_start"); }); + + // click on the events tab + cy.get('[aria-label="events-content"]').click(); + + // check that there are three rows + cy.get('[aria-label="Event-table-row"]').should("have.length", 3); + + // open the first row of the events to confirm the data is correct. We can't assert all exact strings because the id's and dates are variable. + cy.get("#expand-toggle0").click(); + cy.get('[aria-label="Event-details-0"]').should( + "contain", + '"service_instance_version": 8,', + ); + + // close the row again and click on the export link in the second row. Expect to land on the compile report page. + cy.get('[aria-label="Event-compile-1"]').should("contain", "Export"); + cy.get('[aria-label="Event-compile-1"] > a').click(); + + cy.get("h1").contains("Compile Details").should("to.exist"); + + // go back to the service inventory + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") + .click(); + cy.get("#basic-service").contains("Show inventory").click(); + + // go back to the details page + cy.get('[aria-label="instance-details-link"]', { timeout: 20000 }) + .last() + .click(); + + // change version and go to events page. The second version should contain a validation report. + cy.get('[aria-label="History-Row"]').eq(7).click(); + cy.get('[data-testid="selected-version"]').should( + "have.text", + "Version: 2", + ); + + cy.get('[aria-label="events-content"]').click(); + + cy.get('[aria-label="Event-compile-2"]').should("contain", "Export"); + cy.get('[aria-label="Event-compile-1"]').should("contain", "Validation"); + + // check that there are four rows for this version + cy.get('[aria-label="Event-table-row"]').should("have.length", 4); + + // check the source/target states are correct + cy.get('[aria-label="Event-source-0"]').should("contain", "start"); + cy.get('[aria-label="Event-target-0"]').should("contain", "creating"); + + // click on "see all events" and confirm you are redirected on the events page. + cy.get("a").contains("See all events").click(); + + cy.get("h1").contains("Service Instance Events").should("to.exist"); }); it("2.1.7 Delete previously created instance", () => { @@ -482,32 +532,29 @@ if (Cypress.env("edition") === "iso") { "/lsm/v1/service_inventory/basic-service?include_deployment_progress=True&limit=20&&sort=created_at.desc", ).as("GetServiceInventory"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") - .click(); + cy.get(`[aria-label="Select-environment-test"]`).click(); // START WORKAROUND // TODO: Remove workaround for race condition. // Must be done after https://github.com/inmanta/inmanta-lsm/issues/1249 // Linked to: https://github.com/orgs/inmanta/projects/1?pane=issue&itemId=25836961 - cy.get(".pf-v5-c-nav__link").contains("Compile Reports").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Compile Reports") + .click(); cy.get("button", { timeout: 60000 }).contains("Recompile").click(); // END WORKAROUND. - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") + .click(); cy.get("#basic-service").contains("Show inventory").click(); // Check Instance Details page - cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }) + cy.get('[aria-label="instance-details-link"]', { timeout: 20000 }) .first() .click(); - // The first button should be the one redirecting to the details page. - cy.get(".pf-v5-c-menu__item") - .first() - .contains("Instance Details") - .click(); // Check the state of the instance is up in the history section. cy.get('[aria-label="History-Row"]', { timeout: 60000 }).should( @@ -524,25 +571,17 @@ if (Cypress.env("edition") === "iso") { cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }) .last() .click(); - cy.get(".pf-v5-c-menu__item").contains("More actions").click(); - cy.get(".pf-v5-c-menu__item").contains("Delete").click(); - cy.get(".pf-v5-c-modal-box__title-text").should( - "contain", - "Delete instance", - ); - cy.get(".pf-v5-c-form__actions").contains("No").click(); + cy.get('[role="menuitem"]').contains("Delete").click(); + cy.get(".pf-v6-c-modal-box__header").should("contain", "Delete instance"); + cy.get(".pf-v6-c-form__actions").contains("No").click(); // delete the instance. cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }) .last() .click(); - cy.get(".pf-v5-c-menu__item").contains("More actions").click(); - cy.get(".pf-v5-c-menu__item").contains("Delete").click(); - cy.get(".pf-v5-c-modal-box__title-text").should( - "contain", - "Delete instance", - ); - cy.get(".pf-v5-c-form__actions").contains("Yes").click(); + cy.get('[role="menuitem"]').contains("Delete").click(); + cy.get(".pf-v6-c-modal-box__header").should("contain", "Delete instance"); + cy.get(".pf-v6-c-form__actions").contains("Yes").click(); // check response if instance has been deleted successfully. cy.wait("@DeleteInstance").its("response.statusCode").should("eq", 200); diff --git a/cypress/e2e/scenario-2.2-child-parent-service.cy.js b/cypress/e2e/scenario-2.2-child-parent-service.cy.js index 15ef15df5..9af285628 100644 --- a/cypress/e2e/scenario-2.2-child-parent-service.cy.js +++ b/cypress/e2e/scenario-2.2-child-parent-service.cy.js @@ -4,9 +4,9 @@ * * @param {string} nameEnvironment */ -const clearEnvironment = (nameEnvironment = "lsm-frontend") => { +const clearEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -45,9 +45,9 @@ const checkStatusCompile = (id) => { * * @param {string} nameEnvironment */ -const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { +const forceUpdateEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -70,10 +70,10 @@ if (Cypress.env("edition") === "iso") { }); it("2.2.1 Add Instance on parent-service", () => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); cy.get("#parent-service").contains("Show inventory").click(); cy.get('[aria-label="ServiceInventory-Empty"]').should("to.be.visible"); // Add an instance and fill form @@ -87,31 +87,6 @@ if (Cypress.env("edition") === "iso") { // Check if only one row has been added to the table. cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); - // open row from element - cy.get("#expand-toggle0").click(); - - // Go to ressource tab expect it be empty - cy.get(".pf-v5-c-tabs__item-text").contains("Resources").click(); - cy.get('[aria-label="ResourceTable-Empty"]').should("to.be.visible"); - - cy.intercept("**/resources**").as("GetVersion"); - - // expect one item with deployed state - cy.get('[aria-label="ResourceTable-Success"]', { timeout: 60000 }).should( - ($table) => { - expect($table).to.have.length(1); - - const $td = $table.find("td"); - - // there can only be 2 table-data cells available - expect($td).to.have.length(2); - expect($td.eq(0), "first item").to.have.text( - "frontend_model::TestResource[internal,name=default-0001]", - ); - expect($td.eq(1), "second item").to.have.text("deployed"); - }, - ); - // click on service catalog in breadcrumb cy.get('[aria-label="BreadcrumbItem"]') .contains("Service Catalog") @@ -139,34 +114,18 @@ if (Cypress.env("edition") === "iso") { it("2.2.2 Remove Parent Service and Child Service", () => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); cy.get("#parent-service").contains("Show inventory").click(); - // open row from element - cy.get("#expand-toggle0", { timeout: 20000 }).click(); - // try delete item (Should not be possible) cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }).click(); - cy.get(".pf-v5-c-menu__item").contains("More actions").click(); - cy.get(".pf-v5-c-menu__item").contains("Delete").click(); - - cy.get(".pf-v5-c-modal-box__title-text").should( - "contain", - "Delete instance", - ); - cy.get(".pf-v5-c-form__actions").contains("Yes").click(); + cy.get('[role="menuitem"]').contains("Delete").click(); - // check status change before compile - cy.get('[aria-label="InstanceRow-Intro"]:first', { timeout: 20000 }) - .find('[data-label="State"]') - .should("contain", "delete_validating_up"); - - cy.get('[aria-label="InstanceRow-Intro"]:first') - .find('[data-label="State"]', { timeout: 60000 }) - .should("not.contain", "delete_validating_up"); + cy.get(".pf-v6-c-modal-box__header").should("contain", "Delete instance"); + cy.get(".pf-v6-c-form__actions").contains("Yes").click(); // click on service catalog in breadcrumb cy.get('[aria-label="BreadcrumbItem"]') @@ -175,19 +134,12 @@ if (Cypress.env("edition") === "iso") { cy.get("#child-service").contains("Show inventory").click(); - // open row from element - cy.get("#expand-toggle0").click(); - // try delete item (Should be possible) cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }).click(); - cy.get(".pf-v5-c-menu__item").contains("More actions").click(); - cy.get(".pf-v5-c-menu__item").contains("Delete").click(); + cy.get('[role="menuitem"]').contains("Delete").click(); - cy.get(".pf-v5-c-modal-box__title-text").should( - "contain", - "Delete instance", - ); - cy.get(".pf-v5-c-form__actions").contains("Yes").click(); + cy.get(".pf-v6-c-modal-box__header").should("contain", "Delete instance"); + cy.get(".pf-v6-c-form__actions").contains("Yes").click(); cy.get('[aria-label="ServiceInventory-Empty"]', { timeout: 220000, diff --git a/cypress/e2e/scenario-2.3-embedded-entity.cy.js b/cypress/e2e/scenario-2.3-embedded-entity.cy.js index 421a5883b..a9851a3ab 100644 --- a/cypress/e2e/scenario-2.3-embedded-entity.cy.js +++ b/cypress/e2e/scenario-2.3-embedded-entity.cy.js @@ -4,9 +4,9 @@ * * @param {string} nameEnvironment */ -const clearEnvironment = (nameEnvironment = "lsm-frontend") => { +const clearEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -45,9 +45,9 @@ const checkStatusCompile = (id) => { * * @param {string} nameEnvironment */ -const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { +const forceUpdateEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -72,10 +72,10 @@ if (Cypress.env("edition") === "iso") { // Go from Home page to Service Inventory of embedded-service cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); cy.get("#embedded-entity-service").contains("Show inventory").click(); // make sure the call to get inventory has been executed @@ -114,10 +114,10 @@ if (Cypress.env("edition") === "iso") { it("2.3.2 - show diagonse view", () => { // Go from Home page to Service Inventory of Embedded-service cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); // Expect to find one badge on the embedded-service row. cy.get("#embedded-entity-service") .get('[aria-label="Number of instances by label"]') @@ -127,11 +127,8 @@ if (Cypress.env("edition") === "iso") { cy.get('[aria-label="ServiceInventory-Success"]').should("to.be.visible"); // Check Instance Details page - cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }).click(); - // The first button should be the one redirecting to the details page. - cy.get(".pf-v5-c-menu__item") + cy.get('[aria-label="instance-details-link"]', { timeout: 20000 }) .first() - .contains("Instance Details") .click(); // Check if there are three versions in the history table @@ -155,13 +152,8 @@ if (Cypress.env("edition") === "iso") { .contains("Service Inventory: embedded-entity-service") .click(); - cy.get("#expand-toggle0").click(); - - // expect row to be expanded - cy.get(".pf-v5-c-table__expandable-row-content").should("to.be.visible"); - cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }).click(); - cy.get(".pf-v5-c-menu__item").contains("Diagnose").click(); + cy.get('[role="menuitem"]').contains("Diagnose").click(); // Diagonse sub-page should open and be empty cy.get("h1").contains("Diagnose Service Instance").should("be.visible"); @@ -169,78 +161,53 @@ if (Cypress.env("edition") === "iso") { cy.visit("/console/"); }); - it("2.3.3 - Show history view", () => { + it("2.3.3 - Deploy progress bar should navigate to Resources of instance details", () => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); // Expect to find one badge on the embedded-service row. cy.get("#embedded-entity-service") .get('[aria-label="Number of instances by label"]') .children() .should("have.length", 1); cy.get("#embedded-entity-service").contains("Show inventory").click(); - cy.get("#expand-toggle0").click(); - // expect row to be expanded - cy.get(".pf-v5-c-table__expandable-row-content").should("to.be.visible"); + cy.get('[aria-label="deploy-progress"]', { timeout: 20000 }) + .first() + .click(); - // Expect to find status tab - cy.get(".pf-v5-c-tabs__list li:first").should( - "have.class", - "pf-m-current", + cy.get('[aria-label="resources-content"]').should( + "have.attr", + "aria-selected", + "true", ); - //await for instance state to change to up - cy.get('[data-label="State"]') - .find(".pf-v5-c-label.pf-m-green", { timeout: 60000 }) - .should("contain", "up"); - cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }).click(); - cy.get(".pf-v5-c-menu__item").contains("More actions").click(); - cy.get(".pf-v5-c-menu__item").contains("History").click(); - - // History sub-page should open and be empty then go to Home page - cy.get("h1").contains("Service Instance History").should("be.visible"); - // due to lack of Id in rows I had to assert that each toggle button is separeate history log - cy.get(".pf-v5-c-table") - .find(".pf-v5-c-table__toggle") - .should("have.length", 3); cy.visit("/console/"); }); it("2.3.4 Delete previously created instance", () => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); cy.get("#embedded-entity-service").contains("Show inventory").click(); - // expand first row - cy.get("#expand-toggle0", { timeout: 20000 }).click(); - // delete but cancel deletion in modal cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }).click(); - cy.get(".pf-v5-c-menu__item").contains("More actions").click(); - cy.get(".pf-v5-c-menu__item").contains("Delete").click(); + cy.get('[role="menuitem"]').contains("Delete").click(); - cy.get(".pf-v5-c-modal-box__title-text").should( - "contain", - "Delete instance", - ); - cy.get(".pf-v5-c-form__actions").contains("No").click(); + cy.get(".pf-v6-c-modal-box__header").should("contain", "Delete instance"); + cy.get(".pf-v6-c-form__actions").contains("No").click(); cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }).click(); - cy.get(".pf-v5-c-menu__item").contains("More actions").click(); - cy.get(".pf-v5-c-menu__item").contains("Delete").click(); + cy.get('[role="menuitem"]').contains("Delete").click(); - cy.get(".pf-v5-c-modal-box__title-text").should( - "contain", - "Delete instance", - ); - cy.get(".pf-v5-c-form__actions").contains("Yes").click(); + cy.get(".pf-v6-c-modal-box__header").should("contain", "Delete instance"); + cy.get(".pf-v6-c-form__actions").contains("Yes").click(); // check response if instance has been deleted succesfully. cy.get('[aria-label="ServiceInventory-Empty"]', { diff --git a/cypress/e2e/scenario-2.4-expert-mode.cy.js b/cypress/e2e/scenario-2.4-expert-mode.cy.js index 3f3f268c6..313c1751a 100644 --- a/cypress/e2e/scenario-2.4-expert-mode.cy.js +++ b/cypress/e2e/scenario-2.4-expert-mode.cy.js @@ -4,9 +4,9 @@ * * @param {string} nameEnvironment */ -const clearEnvironment = (nameEnvironment = "lsm-frontend") => { +const clearEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -45,9 +45,9 @@ const checkStatusCompile = (id) => { * * @param {string} nameEnvironment */ -const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { +const forceUpdateEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -78,10 +78,10 @@ if (Cypress.env("edition") === "iso") { "/lsm/v1/service_inventory/basic-service?include_deployment_progress=True&limit=20&&sort=created_at.desc", ).as("GetServiceInventory"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); cy.get("#basic-service").contains("Show inventory").click(); // Make sure the call to get inventory has been executed @@ -109,12 +109,14 @@ if (Cypress.env("edition") === "iso") { cy.get('[aria-label="ServiceInventory-Success"]').should("to.be.visible"); // Go to the settings, then to the configuration tab - cy.get(".pf-v5-c-nav__item").contains("Settings").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Settings") + .click(); cy.get("button").contains("Configuration").click(); // Change enable_lsm_expert_mode cy.get('[aria-label="Row-enable_lsm_expert_mode"]') - .find(".pf-v5-c-switch") + .find(".pf-v6-c-switch") .click(); cy.get('[data-testid="Warning"]').should("exist"); cy.get('[aria-label="Row-enable_lsm_expert_mode"]') @@ -127,48 +129,48 @@ if (Cypress.env("edition") === "iso") { // Go back to service inventory cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); cy.get("#basic-service").contains("Show inventory").click(); // Go to the instance details - cy.get('[aria-label="row actions toggle"]').click(); - cy.get(".pf-v5-c-menu__item") + cy.get('[aria-label="instance-details-link"]', { timeout: 20000 }) .first() - .contains("Instance Details") .click(); // expect to find in the history the up state as last - cy.get('[aria-label="History-Row"]').should(($rows) => { - expect($rows[0]).to.contain("up"); - expect($rows[0]).to.contain(3); - expect($rows).to.have.length(3); - }); + cy.get('[aria-label="History-Row"]', { timeout: 30000 }).should( + ($rows) => { + expect($rows[0]).to.contain("up"); + expect($rows[0]).to.contain(3); + expect($rows).to.have.length(3); + }, + ); // force state to creating cy.get('[aria-label="Expert-Actions-Toggle"]').click(); - cy.get(".pf-v5-c-menu__item").contains("creating").click(); + cy.get('[role="menuitem"]').contains("creating").click(); // add an operation to the force state action cy.get("#operation-select").select("clear candidate"); cy.get("button").contains("Yes").click(); - // expect to find in the history the creating state as last - cy.get('[aria-label="History-Row"]').should(($rows) => { - expect($rows[0]).to.contain("creating"); - expect($rows[0]).to.contain(4); - expect($rows).to.have.length(4); - }); + // expect to find in the history the creating state after the up state + cy.get('[data-testid="version-3-state"]').should("have.text", "up"); + cy.get('[data-testid="version-4-state"]', { timeout: 60000 }).should( + "have.text", + "creating", + ); }); it("2.4.2 Edit instance attributes", () => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); // Expect to find one badge on the basic-service row. cy.get("#basic-service") @@ -178,10 +180,8 @@ if (Cypress.env("edition") === "iso") { cy.get("#basic-service").contains("Show inventory").click(); // Go to the instance details - cy.get('[aria-label="row actions toggle"]').click(); - cy.get(".pf-v5-c-menu__item") - .first() - .contains("Instance Details") + cy.get('[aria-label="instance-details-link"]', { timeout: 20000 }) + .last() .click(); // Go to the attributes tab and select the JSON view @@ -192,7 +192,7 @@ if (Cypress.env("edition") === "iso") { cy.get(".mtk20").contains("name").type("{home}{rightArrow}{del}"); // expect the Force Update to be disabled - cy.get("button").contains("Force Update").should("be.disabled"); + cy.get('[aria-label="Expert-Submit-Button"]').should("be.disabled"); // Adjust the name property of the instance and make editor valid again cy.get(".mtk20").contains("ame").type("{home}{rightArrow}n"); @@ -205,7 +205,7 @@ if (Cypress.env("edition") === "iso") { ); // confirm edit - cy.get("button").contains("Force Update").click(); + cy.get('[aria-label="Expert-Submit-Button"]').click(); cy.get("button").contains("Yes").click(); // Go back to inventory using the breadcrumbs @@ -220,10 +220,10 @@ if (Cypress.env("edition") === "iso") { it("2.4.3 Destroy previously created instance", () => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); // Expect to find one badge on the basic-service row. cy.get("#basic-service") @@ -233,15 +233,13 @@ if (Cypress.env("edition") === "iso") { cy.get("#basic-service").contains("Show inventory").click(); // Go to the instance details - cy.get('[aria-label="row actions toggle"]').click(); - cy.get(".pf-v5-c-menu__item") + cy.get('[aria-label="instance-details-link"]', { timeout: 20000 }) .first() - .contains("Instance Details") .click(); // Open Expert menu cy.get('[aria-label="Expert-Actions-Toggle"]').click(); - cy.get(".pf-v5-c-menu__item").contains("Destroy").click(); + cy.get('[role="menuitem"]').contains("Destroy").click(); // confirm action cy.get("button").contains("Yes").click(); @@ -249,16 +247,8 @@ if (Cypress.env("edition") === "iso") { // expect to be redirected on the inventory page, and table to be empty cy.get('[aria-label="ServiceInventory-Empty"]').should("to.be.visible"); - // At the end go back to settings and turn expert mode off - cy.get(".pf-v5-c-nav__item").contains("Settings").click(); - cy.get("button").contains("Configuration").click(); - cy.get('[aria-label="Row-enable_lsm_expert_mode"]') - .find(".pf-v5-c-switch") - .click(); - cy.get('[data-testid="Warning"]').should("exist"); - cy.get('[aria-label="Row-enable_lsm_expert_mode"]') - .find('[aria-label="SaveAction"]') - .click(); + // At the end turn expert mode off through the banner + cy.get("button").contains("Disable").click(); cy.get('[data-testid="Warning"]').should("not.exist"); cy.get("[id='expert-mode-banner']").should("not.exist"); }); diff --git a/cypress/e2e/scenario-2.4-old-expert-mode.cy.js b/cypress/e2e/scenario-2.4-old-expert-mode.cy.js deleted file mode 100644 index d81c14cf8..000000000 --- a/cypress/e2e/scenario-2.4-old-expert-mode.cy.js +++ /dev/null @@ -1,370 +0,0 @@ -/** - * @note These tests should be deleted once the collapsible sections are removed from the inventory. - */ - -/** - * Shorthand method to clear the environment being passed. - * By default, if no arguments are passed it will target the 'lsm-frontend' environment. - * - * @param {string} nameEnvironment - */ -const clearEnvironment = (nameEnvironment = "lsm-frontend") => { - cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); - cy.url().then((url) => { - const location = new URL(url); - const id = location.searchParams.get("env"); - - cy.request("DELETE", `/api/v1/decommission/${id}`); - }); -}; - -/** - * based on the environment id, it will recursively check if a compile is pending. - * It will continue the recursion as long as the statusCode is equal to 200 - * - * @param {string} id - */ -const checkStatusCompile = (id) => { - let statusCodeCompile = 200; - - if (statusCodeCompile === 200) { - cy.intercept(`/api/v1/notify/${id}`).as("IsCompiling"); - // the timeout is necessary to avoid errors. - // Cypress doesn't support while loops and this was the only workaround to wait till the statuscode is not 200 anymore. - // the default timeout in cypress is 5000, but since we have recursion it goes into timeout for the nested awaits because of the recursion. - cy.wait("@IsCompiling", { timeout: 10000 }).then((req) => { - statusCodeCompile = req.response.statusCode; - - if (statusCodeCompile === 200) { - checkStatusCompile(id); - } - }); - } -}; - -/** - * Will by default execute the force update on the 'lsm-frontend' environment if no argumenst are being passed. - * This method can be executed standalone, but is part of the cleanup cycle that is needed before running a scenario. - * - * @param {string} nameEnvironment - */ -const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { - cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); - cy.url().then((url) => { - const location = new URL(url); - const id = location.searchParams.get("env"); - - cy.request({ - method: "POST", - url: `/lsm/v1/exporter/export_service_definition`, - headers: { "X-Inmanta-Tid": id }, - body: { force_update: true }, - }); - checkStatusCompile(id); - }); -}; - -if (Cypress.env("edition") === "iso") { - describe("Scenario 2.4 Service Catalog - basic-service", () => { - before(() => { - clearEnvironment(); - forceUpdateEnvironment(); - }); - - it("2.4.1 Force new state in the instance", () => { - // Go from Home page to Service Inventory of Basic-service - cy.visit("/console/"); - - cy.intercept( - "GET", - "/lsm/v1/service_inventory/basic-service?include_deployment_progress=True&limit=20&&sort=created_at.desc", - ).as("GetServiceInventory"); - - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") - .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); - cy.get("#basic-service").contains("Show inventory").click(); - - // Make sure the call to get inventory has been executed - cy.wait("@GetServiceInventory"); - cy.get('[aria-label="ServiceInventory-Empty"]').should("to.be.visible"); - - // Add an instance and fill form - cy.get("#add-instance-button").click(); - cy.get("#ip_r1").type("1.2.3.4"); - cy.get("#interface_r1_name").type("eth0"); - cy.get("#address_r1").type("1.2.3.5"); - cy.get("#vlan_id_r1").type("1"); - cy.get("#ip_r2").type("1.2.2.4"); - cy.get("#interface_r2_name").type("interface-vlan"); - cy.get("#address_r2").type("1.2.2.3"); - cy.get("#vlan_id_r2").type("2"); - cy.get("#service_id").type("0001"); - cy.get("#name").type("basic-service"); - cy.get("button").contains("Confirm").click(); - - // Make sure the call to get inventory has been executed - cy.wait("@GetServiceInventory"); - - // Check if the view is still empty, also means we have been redirected as expected. - cy.get('[aria-label="ServiceInventory-Success"]').should("to.be.visible"); - - // Go to the settings, then to the configuration tab - cy.get(".pf-v5-c-nav__item").contains("Settings").click(); - cy.get("button").contains("Configuration").click(); - - // Change enable_lsm_expert_mode - cy.get('[aria-label="Row-enable_lsm_expert_mode"]') - .find(".pf-v5-c-switch") - .click(); - cy.get('[data-testid="Warning"]').should("exist"); - cy.get('[aria-label="Row-enable_lsm_expert_mode"]') - .find('[aria-label="SaveAction"]') - .click(); - cy.get('[data-testid="Warning"]').should("not.exist"); - cy.get("[id='expert-mode-banner']") - .should("exist") - .and("contain", "LSM expert mode is enabled, proceed with caution."); - - // Go back to service inventory - cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") - .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); - cy.get("#basic-service").contains("Show inventory").click(); - cy.get("#expand-toggle0").click(); - - // Wait until state is up - cy.get('[aria-label="InstanceRow-Intro"]:first') - .find('[data-label="State"]', { timeout: 60000 }) - .should("contain", "up"); - - cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }).click(); - cy.get(".pf-v5-c-menu__item") - .eq(4) - .should("not.have.class", "pf-disabled"); - - cy.get(".pf-v5-c-menu__item").contains("Force State").click(); - cy.get(".pf-v5-c-menu__item").contains("setting_start").click(); - - // Modal title for confirmation of Destroying instance should be visible - cy.get(".pf-v5-c-modal-box__title-text") - .contains("Confirm force state transfer") - .should("be.visible"); - - // Cancel modal and expect nothing to change - cy.get("button").contains("No").click(); - - cy.get('[aria-label="InstanceRow-Intro"]:first') - .find('[data-label="State"]') - .should("contain", "up"); - - // Push new state, confirm modal and expect new value in the State data cell - cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }).click(); - cy.get(".pf-v5-c-menu__item").contains("Force State").click(); - cy.get(".pf-v5-c-menu__item").contains("setting_start").click(); - - cy.get("button").contains("Yes").click(); - - cy.get('[aria-label="InstanceRow-Intro"]:first') - .find('[data-label="State"]', { timeout: 40000 }) - .should("contain", "setting_start"); - }); - - it("2.4.2 Edit instance attributes", () => { - cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") - .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); - - // Expect to find one badge on the basic-service row. - cy.get("#basic-service") - .get('[aria-label="Number of instances by label"]') - .children() - .should("have.length", 1); - cy.get("#basic-service").contains("Show inventory").click(); - cy.get("#expand-toggle0").click(); - - // Expect row to be expanded - cy.get(".pf-v5-c-table__expandable-row-content").should("to.be.visible"); - - // Expect to find status tab - cy.get(".pf-v5-c-tabs__list li:first").should( - "have.class", - "pf-m-current", - ); - - // Expect edit button to be disabled after previous state change - cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }).click(); - - // The fourth button in the dropdown should be the edit button. - cy.get(".pf-v5-c-menu__item").eq(4).should("be.disabled"); - - // Expect to land on Service Inventory page and to find attributes tab button - cy.get(".pf-v5-c-tabs__list") - .contains("Attributes", { timeout: 20000 }) - .click(); - - // Find and check initial values for targetted attribute address_r1 - cy.get('[aria-label="Row-address_r1"') - .find('[data-label="active"]', { timeout: 20000 }) - .should("contain", "1.2.3.5/32"); - cy.get('[aria-label="Row-address_r1"') - .find('[data-label="active"]') - .find("button") - .click(); - - // Type invalid value and submit - cy.get('[aria-label="new-attribute-input"]').type( - "{selectall}{backspace}invalid", - ); - cy.get('[data-testid="inline-submit"]').click(); - - // Expect dialog to pop-up, and after canceling it won't affect input state - cy.get(".pf-v5-c-modal-box__title-text") - .contains("Update Attribute") - .should("be.visible"); - cy.get('[data-testid="dialog-cancel"]').click(); - cy.get('[aria-label="new-attribute-input"]', { timeout: 20000 }).should( - "have.value", - "invalid", - ); - - // Send invalid value and expect toast alert with error message - cy.get('[data-testid="inline-submit"]').click(); - cy.get('[data-testid="dialog-submit"]').click(); - cy.get('[data-testid="ToastAlert"]') - .contains("Setting new attribute failed") - .should("be.visible"); - cy.get( - '[aria-label="Close Danger alert: alert: Setting new attribute failed"]', - ).click(); - - // Pass valid value then submit and expect new value to be pushed to the cell - cy.get('[aria-label="new-attribute-input"]').type( - "{selectall}{backspace}1.2.3.8/32", - ); - cy.get('[data-testid="inline-submit"]').click(); - cy.get('[data-testid="dialog-submit"]').click(); - cy.get('[aria-label="Row-address_r1"') - .find('[data-label="active"]', { timeout: 20000 }) - .should("contain", "1.2.3.8/32"); - - // interface_r1_name - cy.get('[aria-label="Row-interface_r1_name"') - .find('[data-label="active"]') - .should("contain", "eth0"); - cy.get('[aria-label="Row-interface_r1_name"') - .find('[data-label="active"]') - .find("button") - .click(); - - // Type invalid value and submit - cy.get('[aria-label="new-attribute-input"]').type( - "{selectall}{backspace}eth1", - ); - cy.get('[data-testid="inline-submit"]').click(); - - // Expect dialog to pop-up, and after submiting xpect new value to be pushed to the cell - cy.get('[data-testid="dialog-submit"]').click(); - cy.get('[aria-label="Row-interface_r1_name"') - .find('[data-label="active"]', { timeout: 20000 }) - .should("contain", "eth1"); - - // should_deploy_fail - cy.get('[aria-label="Row-should_deploy_fail"') - .find('[data-label="active"]') - .should("contain", "false"); - - cy.get('[aria-label="Row-should_deploy_fail"') - .find('[data-label="active"]') - .find("button") - .click(); - cy.get(".pf-v5-c-switch__toggle").click(); - cy.get('[data-testid="inline-submit"]').click(); - cy.get('[data-testid="dialog-submit"]').click(); - - cy.get('[aria-label="Row-should_deploy_fail"') - .find('[data-label="active"]') - .should("contain", "true"); - - // vlan_id_r1 - cy.get('[aria-label="Row-vlan_id_r1"') - .find('[data-label="active"]') - .should("contain", "1"); - cy.get('[aria-label="Row-vlan_id_r1"') - .find('[data-label="active"]') - .find("button") - .click(); - - // Type invalid value and submit - cy.get('[aria-label="new-attribute-input"]').type( - "{selectall}{backspace}5", - ); - cy.get('[data-testid="inline-submit"]').click(); - - // Expect dialog to pop-up, and after submiting xpect new value to be pushed to the cell - cy.get('[data-testid="dialog-submit"]').click(); - cy.get('[aria-label="Row-vlan_id_r1"') - .find('[data-label="active"]') - .should("contain", "5"); - }); - - it("2.4.3 Destroy previously created instance", () => { - cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") - .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); - - // Expect to find one badge on the basic-service row. - cy.get("#basic-service") - .get('[aria-label="Number of instances by label"]') - .children() - .should("have.length", 1); - cy.get("#basic-service").contains("Show inventory").click(); - cy.get("#expand-toggle0").click(); - - // Expect row to be expanded - cy.get(".pf-v5-c-table__expandable-row-content").should("to.be.visible"); - - // Expect to find status tab - cy.get(".pf-v5-c-tabs__list li:first").should( - "have.class", - "pf-m-current", - ); - - // Click on destroy button - cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }).click(); - cy.get(".pf-v5-c-menu__item").contains("More actions").click(); - cy.get(".pf-v5-c-menu__item").contains("Destroy").click(); - - // Modal title for confirmation of Destroying instance should be visible - cy.get(".pf-v5-c-modal-box__title-text") - .contains("Destroy instance") - .should("be.visible"); - - // Confirm modal and expect to new view almost appear informing that there is no instances of that service found - cy.get("button").contains("Yes").click(); - cy.get('[aria-label="ServiceInventory-Empty"').should("to.be.visible"); - - // At the end go back to settings and turn expert mode off - cy.get(".pf-v5-c-nav__item").contains("Settings").click(); - cy.get("button").contains("Configuration").click(); - cy.get('[aria-label="Row-enable_lsm_expert_mode"]') - .find(".pf-v5-c-switch") - .click(); - cy.get('[data-testid="Warning"]').should("exist"); - cy.get('[aria-label="Row-enable_lsm_expert_mode"]') - .find('[aria-label="SaveAction"]') - .click(); - cy.get('[data-testid="Warning"]').should("not.exist"); - cy.get("[id='expert-mode-banner']").should("not.exist"); - }); - }); -} diff --git a/cypress/e2e/scenario-3-service-details.cy.js b/cypress/e2e/scenario-3-service-details.cy.js index b61d58fc7..e3bc1a1ce 100644 --- a/cypress/e2e/scenario-3-service-details.cy.js +++ b/cypress/e2e/scenario-3-service-details.cy.js @@ -4,9 +4,9 @@ * * @param {string} nameEnvironment */ -const clearEnvironment = (nameEnvironment = "lsm-frontend") => { +const clearEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -45,9 +45,9 @@ const checkStatusCompile = (id) => { * * @param {string} nameEnvironment */ -const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { +const forceUpdateEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -71,10 +71,10 @@ if (Cypress.env("edition") === "iso") { it("3.1 Check empty state", () => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); // Click on kebab menu and select Show Details on basic-service cy.get("#basic-service", { timeout: 60000 }) @@ -142,21 +142,20 @@ if (Cypress.env("edition") === "iso") { // Go to Lifecycle states cy.get("button").contains("Lifecycle States").click(); - // Expect to find table with 16 different state rows. + // Expect to find table with 17 different state rows. cy.get('[aria-label="Lifecycle"').should(($table) => { const $tableBody = $table.find("tbody"); const $dataRows = $tableBody.find("tr"); - expect($dataRows).to.have.length(16); + expect($dataRows).to.have.length(17); }); // Go to Config tab cy.get("button").contains("Config").click(); - // Expect it to be an empty table - cy.get(".pf-v5-c-empty-state") - .should("contain", "There is nothing here") - .and("contain", "No settings found"); + // Expect it to have setting in the config + cy.get('[aria-label="ServiceConfig"]').should("be.visible"); + cy.get('[aria-label="SettingsList"]').should("have.length", 1); // Go to Callback tab cy.get("button").contains("Callbacks").click(); @@ -172,10 +171,10 @@ if (Cypress.env("edition") === "iso") { it("3.2 Create success instance and check details", () => { // Select 'test' environment cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); // click on Show Inventory on basic-service cy.get("#basic-service", { timeout: 60000 }) @@ -241,10 +240,10 @@ if (Cypress.env("edition") === "iso") { it("3.3 Create a failed Instance by Duplicating and check details", () => { // Select 'test' environment cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); // click on Show Inventory on basic-service cy.get("#basic-service", { timeout: 60000 }) @@ -255,13 +254,13 @@ if (Cypress.env("edition") === "iso") { cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }) .eq(0) .click(); - cy.get(".pf-v5-c-menu__item").contains("Duplicate").click(); + cy.get('[role="menuitem"]').contains("Duplicate").click(); // Create a failed instance on basic-service cy.get("#service_id").type("0008"); cy.get("#name").clear(); cy.get("#name").type("failed"); - cy.get(".pf-v5-c-switch").first().click(); + cy.get(".pf-v6-c-switch").first().click(); cy.get("button").contains("Confirm").click(); // Expect the number in the chart to be 2 @@ -284,28 +283,12 @@ if (Cypress.env("edition") === "iso") { cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 2); // Check if the newly added instance has failed. - cy.get('[aria-label="InstanceRow-Intro"]') - .first() - .should(($row) => { - const $cols = $row.find("td"); - - expect($cols.eq(1), "name").to.have.text("failed"); - }); - // long timeout justified by the fact that a few compiles are already queued at this point and status change will only be changed after. - cy.get(".pf-v5-c-label.pf-m-red", { timeout: 120000 }).should( - "contain", - "failed", - ); + cy.get(".pf-v6-c-label", { timeout: 120000 }).should("contain", "failed"); // Check Instance Details page - cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }) - .first() - .click(); - // The first button should be the one redirecting to the details page. - cy.get(".pf-v5-c-menu__item") + cy.get('[aria-label="instance-details-link"]', { timeout: 50000 }) .first() - .contains("Instance Details") .click(); // Check the state of the instance is failed in the history section. @@ -345,10 +328,10 @@ if (Cypress.env("edition") === "iso") { it("3.4 Callbacks", () => { // Select card 'test' environment on home page cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); // Click on kebab menu and select Show Details on basic-service cy.get("#basic-service", { timeout: 60000 }) @@ -398,41 +381,41 @@ if (Cypress.env("edition") === "iso") { cy.get("button").contains("http://localhost:1234").click(); // Expect to see all values except ALLOCATION_UPDATE to have text-decoration: line-through - cy.get(".pf-v5-c-description-list__description ul").should(($ul) => { + cy.get(".pf-v6-c-description-list__description ul").should(($ul) => { const $list = $ul.find("li"); expect($list).to.have.length(10); }); - cy.get(".pf-v5-c-description-list__description li") + cy.get(".pf-v6-c-description-list__description li") .first() .should("have.css", "text-decoration") .and("contain", "none solid"); - cy.get(".pf-v5-c-description-list__description li") + cy.get(".pf-v6-c-description-list__description li") .eq(1) .should("have.css", "text-decoration") .and("contain", "line-through solid"); - cy.get(".pf-v5-c-description-list__description li") + cy.get(".pf-v6-c-description-list__description li") .eq(2) .should("have.css", "text-decoration") .and("contain", "line-through solid"); - cy.get(".pf-v5-c-description-list__description li") + cy.get(".pf-v6-c-description-list__description li") .eq(3) .should("have.css", "text-decoration") .and("contain", "line-through solid"); - cy.get(".pf-v5-c-description-list__description li") + cy.get(".pf-v6-c-description-list__description li") .eq(4) .should("have.css", "text-decoration") .and("contain", "line-through solid"); - cy.get(".pf-v5-c-description-list__description li") + cy.get(".pf-v6-c-description-list__description li") .eq(5) .should("have.css", "text-decoration") .and("contain", "line-through solid"); - cy.get(".pf-v5-c-description-list__description li") + cy.get(".pf-v6-c-description-list__description li") .eq(6) .should("have.css", "text-decoration") .and("contain", "line-through solid"); - cy.get(".pf-v5-c-description-list__description li") + cy.get(".pf-v6-c-description-list__description li") .eq(7) .should("have.css", "text-decoration") .and("contain", "line-through solid"); @@ -464,10 +447,10 @@ if (Cypress.env("edition") === "iso") { it("3.5 Delete Service", () => { // Select card 'test' environment on home page cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); // Click on Delete button cy.get("#basic-service", { timeout: 60000 }) @@ -499,8 +482,7 @@ if (Cypress.env("edition") === "iso") { cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }) .eq(0) .click(); - cy.get(".pf-v5-c-menu__item").contains("More actions").click(); - cy.get(".pf-v5-c-menu__item").contains("Delete").click(); + cy.get('[role="menuitem"]').contains("Delete").click(); // Confirm deletion cy.get("#submit").click(); @@ -509,8 +491,7 @@ if (Cypress.env("edition") === "iso") { cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }) .eq(1) .click(); - cy.get(".pf-v5-c-menu__item").contains("More actions").click(); - cy.get(".pf-v5-c-menu__item").contains("Delete").click(); + cy.get('[role="menuitem"]').contains("Delete").click(); // Confirm deletion cy.get("#submit", { timeout: 20000 }).click(); diff --git a/cypress/e2e/scenario-4-desired-state.cy.js b/cypress/e2e/scenario-4-desired-state.cy.js index 1d6947d5f..61a3987df 100644 --- a/cypress/e2e/scenario-4-desired-state.cy.js +++ b/cypress/e2e/scenario-4-desired-state.cy.js @@ -4,9 +4,9 @@ * * @param {string} nameEnvironment */ -const clearEnvironment = (nameEnvironment = "lsm-frontend") => { +const clearEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -45,9 +45,9 @@ const checkStatusCompile = (id) => { * * @param {string} nameEnvironment */ -const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { +const forceUpdateEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -63,7 +63,6 @@ const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { }; const isIso = Cypress.env("edition") === "iso"; -const PROJECT = Cypress.env("project") || "lsm-frontend"; describe("Scenario 4 Desired State", () => { if (isIso) { @@ -76,10 +75,12 @@ describe("Scenario 4 Desired State", () => { it("4.1 Initial setup", () => { // Go from Home page to Service Inventory of Basic-service cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(PROJECT).click(); + cy.get(`[aria-label="Select-environment-test"]`).click(); if (isIso) { - cy.get(".pf-v5-c-nav__link").contains("Service Catalog").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") + .click(); cy.get("#basic-service").contains("Show inventory").click(); cy.get('[aria-label="ServiceInventory-Empty"]').should("to.be.visible"); @@ -108,7 +109,9 @@ describe("Scenario 4 Desired State", () => { } //got to desired stated page - cy.get(".pf-v5-c-nav__link").contains("Desired State").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Desired State") + .click(); if (!isIso) { // Hit the compile button; OSS don't get compiled on initial state. @@ -164,7 +167,7 @@ describe("Scenario 4 Desired State", () => { // go to details of first resource cy.get("tbody").eq(0).contains("Show Details").click(); - cy.get(".pf-v5-c-content").should( + cy.get(".pf-v6-c-content--small").should( "have.text", "frontend_model::TestResource[internal,name=default-0001]", ); @@ -193,83 +196,92 @@ describe("Scenario 4 Desired State", () => { cy.get("tbody").eq(0).contains("Show Details").click(); // Go through each row and check value - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(1) - .find(".pf-v5-c-description-list__term") + .find(".pf-v6-c-description-list__term") .should("have.text", "next_desired_state_version"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(1) - .find(".pf-v5-c-description-list__description") + .find(".pf-v6-c-description-list__description") .should("have.text", "4"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(2) - .find(".pf-v5-c-description-list__term") + .find(".pf-v6-c-description-list__term") .should("have.text", "next_version"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(2) - .find(".pf-v5-c-description-list__description") + .find(".pf-v6-c-description-list__description") .should("have.text", "4"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(3) - .find(".pf-v5-c-description-list__term") + .find(".pf-v6-c-description-list__term") .should("have.text", "purge_on_delete"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(3) - .find(".pf-v5-c-description-list__description") + .find(".pf-v6-c-description-list__description") .should("have.text", "false"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(4) - .find(".pf-v5-c-description-list__term") + .find(".pf-v6-c-description-list__term") .should("have.text", "purged"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(4) - .find(".pf-v5-c-description-list__description") + .find(".pf-v6-c-description-list__description") .should("have.text", "false"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(5) - .find(".pf-v5-c-description-list__term") - .should("have.text", "requires"); - cy.get(".pf-v5-c-description-list__group") + .find(".pf-v6-c-description-list__term") + .should("have.text", "receive_events"); + cy.get(".pf-v6-c-description-list__group") .eq(5) - .find(".pf-v5-c-description-list__description") + .find(".pf-v6-c-description-list__description") + .should("include.text", "true"); + + cy.get(".pf-v6-c-description-list__group") + .eq(6) + .find(".pf-v6-c-description-list__term") + .should("have.text", "requires"); + cy.get(".pf-v6-c-description-list__group") + .eq(6) + .find(".pf-v6-c-description-list__description") .should( "include.text", "frontend_model::TestResource[internal,name=default-0001]", ); - cy.get(".pf-v5-c-description-list__group") - .eq(6) - .find(".pf-v5-c-description-list__term") + cy.get(".pf-v6-c-description-list__group") + .eq(7) + .find(".pf-v6-c-description-list__term") .should("have.text", "resources"); - cy.get(".pf-v5-c-description-list__group") - .eq(6) - .find(".pf-v5-c-description-list__description") + cy.get(".pf-v6-c-description-list__group") + .eq(7) + .find(".pf-v6-c-description-list__description") .should( "include.text", '"frontend_model::TestResource[internal,name=default-0001]"', ); - cy.get(".pf-v5-c-description-list__group") - .eq(7) - .find(".pf-v5-c-description-list__term") + cy.get(".pf-v6-c-description-list__group") + .eq(8) + .find(".pf-v6-c-description-list__term") .should("have.text", "send_event"); - cy.get(".pf-v5-c-description-list__group") - .eq(7) - .find(".pf-v5-c-description-list__description") + cy.get(".pf-v6-c-description-list__group") + .eq(8) + .find(".pf-v6-c-description-list__description") .should("have.text", "false"); - cy.get(".pf-v5-c-description-list__group") - .eq(8) - .find(".pf-v5-c-description-list__term") + cy.get(".pf-v6-c-description-list__group") + .eq(9) + .find(".pf-v6-c-description-list__term") .should("have.text", "service_entity"); - cy.get(".pf-v5-c-description-list__group") - .eq(8) - .find(".pf-v5-c-description-list__description") + cy.get(".pf-v6-c-description-list__group") + .eq(9) + .find(".pf-v6-c-description-list__description") .should("have.text", "basic-service"); } @@ -287,7 +299,7 @@ describe("Scenario 4 Desired State", () => { // Delete the retired version. cy.get("tbody").eq(1).find('[aria-label="actions-toggle"]').click(); - cy.get(".pf-v5-c-menu__item").contains("Delete").click(); + cy.get('[role="menuitem"]').contains("Delete").click(); cy.get("#cancel").click(); cy.get("@TABLE_LENGTH").then((length) => { @@ -298,14 +310,14 @@ describe("Scenario 4 Desired State", () => { }); cy.get("tbody").eq(1).find('[aria-label="actions-toggle"]').click(); - cy.get(".pf-v5-c-menu__item").contains("Delete").click(); + cy.get('[role="menuitem"]').contains("Delete").click(); cy.get("#submit").click(); // The active version should remain in the table. cy.get("@TABLE_LENGTH").then((length) => { cy.get("tbody", { timeout: 30000 }).should( "have.length", - isIso ? length - 1 : length + 1, + isIso ? length - 1 : length, ); }); @@ -329,15 +341,19 @@ describe("Scenario 4 Desired State", () => { }); // Update the settings to disable the auto-deploy setting. - cy.get(".pf-v5-c-nav__item").contains("Settings").click(); - cy.get(".pf-v5-c-tabs__link").contains("Configuration").click(); - cy.get('[aria-label="Row-auto_deploy"]').find(".pf-v5-c-switch").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Settings") + .click(); + cy.get(".pf-v6-c-tabs__link").contains("Configuration").click(); + cy.get('[aria-label="Row-auto_deploy"]').find(".pf-v6-c-switch").click(); cy.get('[aria-label="Row-auto_deploy"]') .find('[aria-label="SaveAction"]') .click(); // Go back to the desired state page. - cy.get(".pf-v5-c-nav__link").contains("Desired State").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Desired State") + .click(); // Recompile, to get a candidate version. cy.get("button", { timeout: 30000 }).contains("Recompile").click(); @@ -347,11 +363,11 @@ describe("Scenario 4 Desired State", () => { cy.get("tbody") .eq(0) - .find('[data-label="Status"]') + .find('[data-label="Status"]', { timeout: 30000 }) .should("have.text", "candidate"); cy.get("tbody").eq(0).find('[aria-label="actions-toggle"]').click(); - cy.get(".pf-v5-c-menu__item").contains("Promote").click(); + cy.get('[role="menuitem"]').contains("Promote").click(); cy.get("tbody") .eq(0) .find('[data-label="Status"]') @@ -364,38 +380,42 @@ describe("Scenario 4 Desired State", () => { }); // Turn back the auto-deploy setting to true. - cy.get(".pf-v5-c-nav__item").contains("Settings").click(); - cy.get(".pf-v5-c-tabs__link").contains("Configuration").click(); - cy.get('[aria-label="Row-auto_deploy"]').find(".pf-v5-c-switch").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Settings") + .click(); + cy.get(".pf-v6-c-tabs__link").contains("Configuration").click(); + cy.get('[aria-label="Row-auto_deploy"]').find(".pf-v6-c-switch").click(); cy.get('[aria-label="Row-auto_deploy"]') .find('[aria-label="SaveAction"]') .click(); // Get back to the Desired State page. - cy.get(".pf-v5-c-nav__link").contains("Desired State").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Desired State") + .click(); cy.get("tbody").eq(0).find('[aria-label="actions-toggle"]').click(); - cy.get(".pf-v5-c-menu__item").contains("Select for compare").click(); + cy.get('[role="menuitem"]').contains("Select for compare").click(); cy.get("tbody").eq(1).find('[aria-label="actions-toggle"]').click(); - cy.get(".pf-v5-c-menu__item").contains("Compare with selected").click(); - cy.get(".pf-v5-c-title").eq(0).should("have.text", "Compare"); + cy.get('[role="menuitem"]').contains("Compare with selected").click(); + cy.get("h1").eq(0).should("have.text", "Compare"); //go back - cy.get(".pf-v5-c-nav__link").contains("Desired State").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Desired State") + .click(); cy.get("tbody") .eq(isIso ? -2 : -1) .find('[aria-label="actions-toggle"]') .click(); - cy.get(".pf-v5-c-menu__item") - .contains("Compare with current state") - .click(); + cy.get('[role="menuitem"]').contains("Compare with current state").click(); - cy.get(".pf-v5-c-title").should("have.text", "Compliance Check"); + cy.get("h1").should("have.text", "Compliance Check"); cy.get('[aria-label="ReportListSelect"]') .contains("No Dry runs exist") .should("be.visible"); - cy.get(".pf-v5-c-button").contains("Perform dry run").click(); + cy.get(".pf-v6-c-button").contains("Perform dry run").click(); cy.get('[aria-label="StatusFilter"]').click(); cy.get('[role="option"]').contains("unmodified").click(); @@ -424,7 +444,7 @@ describe("Scenario 4 Desired State", () => { }); // expect diff module to say No changes have been found - cy.get(".pf-v5-c-card__expandable-content", { timeout: 20000 }).should( + cy.get(".pf-v6-c-card__expandable-content", { timeout: 20000 }).should( ($expandableRow) => { expect($expandableRow).to.have.length(isIso ? 2 : 5); @@ -444,21 +464,21 @@ describe("Scenario 4 Desired State", () => { ); // go back to desired state page - cy.get(".pf-v5-c-nav__link").contains("Desired State").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Desired State") + .click(); // click on version latest kebab menu cy.get("tbody").eq(0).find('[aria-label="actions-toggle"]').click(); // select Compare with current state - cy.get(".pf-v5-c-menu__item") - .contains("Compare with current state") - .click(); + cy.get('[role="menuitem"]').contains("Compare with current state").click(); // expect to land on compliance check page - cy.get(".pf-v5-c-title").should("have.text", "Compliance Check"); + cy.get("h1").should("have.text", "Compliance Check"); // Expect diff-module to be empty - cy.get(".pf-v5-c-page__main-section") + cy.get(".pf-v6-c-page__main-section") .eq(1) .children() .should("have.length", 1); @@ -468,7 +488,7 @@ describe("Scenario 4 Desired State", () => { .should("be.visible"); // perform dry-run - cy.get(".pf-v5-c-button").contains("Perform dry run").click(); + cy.get(".pf-v6-c-button").contains("Perform dry run").click(); cy.get('[aria-label="StatusFilter"]').click(); cy.get('[role="option"]').contains("unmodified").click(); @@ -493,7 +513,7 @@ describe("Scenario 4 Desired State", () => { }); // await the end of the dry-run and expect to find two rows with expandable content. - cy.get(".pf-v5-c-card__expandable-content", { timeout: 20000 }).should( + cy.get(".pf-v6-c-card__expandable-content", { timeout: 20000 }).should( ($expandableRow) => { expect($expandableRow).to.have.length(isIso ? 2 : 5); expect($expandableRow.eq(0), "first-row").to.have.text( @@ -517,7 +537,7 @@ describe("Scenario 4 Desired State", () => { // expect diff-module to only show the modified file.Only for ISO, the table would be empty on OSS. if (isIso) { - cy.get(".pf-v5-c-card__expandable-content", { timeout: 20000 }).should( + cy.get(".pf-v6-c-card__expandable-content", { timeout: 20000 }).should( ($expandableRow) => { expect($expandableRow).to.have.length(1); @@ -531,15 +551,15 @@ describe("Scenario 4 Desired State", () => { } // go back to desired state - cy.get(".pf-v5-c-nav__link").contains("Desired State").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Desired State") + .click(); // click again on kebab menu of version 5 cy.get("tbody").eq(0).find('[aria-label="actions-toggle"]').click(); // select again compare with current state - cy.get(".pf-v5-c-menu__item") - .contains("Compare with current state") - .click(); + cy.get('[role="menuitem"]').contains("Compare with current state").click(); cy.get('[aria-label="StatusFilter"]').click(); cy.get('[role="option"]').contains("unmodified").click(); @@ -556,7 +576,7 @@ describe("Scenario 4 Desired State", () => { } }); // expect the view to still contain the diff of the last dry-run comparison - cy.get(".pf-v5-c-card__expandable-content", { timeout: 20000 }).should( + cy.get(".pf-v6-c-card__expandable-content", { timeout: 20000 }).should( ($expandableRow) => { expect($expandableRow).to.have.length(isIso ? 2 : 5); expect($expandableRow.eq(0), "first-row").to.have.text( @@ -572,7 +592,7 @@ describe("Scenario 4 Desired State", () => { ); // click on Perform dry run - cy.get(".pf-v5-c-button").contains("Perform dry run").click(); + cy.get(".pf-v6-c-button").contains("Perform dry run").click(); // click on the dropdown containing the different dry-runs cy.get('[aria-label="ReportListSelect"]').click(); diff --git a/cypress/e2e/scenario-5-compile-reports.cy.js b/cypress/e2e/scenario-5-compile-reports.cy.js index 7ce5486dc..81b045875 100644 --- a/cypress/e2e/scenario-5-compile-reports.cy.js +++ b/cypress/e2e/scenario-5-compile-reports.cy.js @@ -4,9 +4,9 @@ * * @param {string} nameEnvironment */ -const clearEnvironment = (nameEnvironment = "lsm-frontend") => { +const clearEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -45,9 +45,9 @@ const checkStatusCompile = (id) => { * * @param {string} nameEnvironment */ -const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { +const forceUpdateEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -63,7 +63,6 @@ const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { }; const isIso = Cypress.env("edition") === "iso"; -const PROJECT = Cypress.env("project") || "lsm-frontend"; describe("5 Compile reports", () => { if (isIso) { @@ -76,10 +75,12 @@ describe("5 Compile reports", () => { it("5.1 initial state", () => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(PROJECT).click(); + cy.get(`[aria-label="Select-environment-test"]`).click(); // go to compile reports page - cy.get(".pf-v5-c-nav__link").contains("Compile Reports").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Compile Reports") + .click(); // expect it to have 2 item shown in the table, or 3 if it is the OSS edition cy.get("tbody", { timeout: 30000 }).should(($tableBody) => { @@ -123,27 +124,27 @@ describe("5 Compile reports", () => { cy.get("button").contains("Show Details").eq(0).click(); // Expect to be redirected to compile details page - cy.get(".pf-v5-c-title").contains("Compile Details").should("to.exist"); + cy.get("h1").contains("Compile Details").should("to.exist"); // Expect message to be : Compile triggered from the console - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(3) .should("contain", "Compile triggered from the console"); // Expect to have no environment variables - cy.get(".pf-v5-c-code-block__content").should("have.text", "{}"); + cy.get(".pf-v6-c-code-block__content").should("have.text", "{}"); - // Expect to have 2 stages in collapsible + // Expect to have 3 stages in collapsible cy.get("tbody").should(($rowElements) => { - expect($rowElements).to.have.length(2); + expect($rowElements).to.have.length(3); }); // Click on init stage arrow cy.get("#expand-toggle0").click(); // expect to see Command Empty, Return code 0 an output stream and no error stream. - cy.get(".pf-v5-c-table__expandable-row.pf-m-expanded") - .find(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-table__expandable-row.pf-m-expanded") + .find(".pf-v6-c-description-list__group") .should(($rowGroups) => { expect($rowGroups).to.have.length(4); @@ -161,8 +162,10 @@ describe("5 Compile reports", () => { cy.visit("/console/"); // click on test environment card - cy.get('[aria-label="Environment card"]').contains(PROJECT).click(); - cy.get(".pf-v5-c-nav__link").contains("Service Catalog").click(); + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") + .click(); // Click on Show Inventory on basic service cy.get("#basic-service").contains("Show inventory").click(); @@ -185,7 +188,9 @@ describe("5 Compile reports", () => { cy.get(".pf-v5-c-chart").should("be.visible"); // Go to compiled Reports page - cy.get(".pf-v5-c-nav__link").contains("Compile Reports").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Compile Reports") + .click(); // Expect all compiles to be succesful cy.get("tbody", { timeout: 60000 }).should(($tableBody) => { @@ -214,10 +219,10 @@ describe("5 Compile reports", () => { cy.get("button").contains("Show Details").eq(0).click(); // Expect to be redirected to compile details page - cy.get(".pf-v5-c-title").contains("Compile Details").should("to.exist"); + cy.get("h1").contains("Compile Details").should("to.exist"); // Expect trigger to be lsm_export - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(4) .should("contain", "lsm_export"); @@ -233,10 +238,10 @@ describe("5 Compile reports", () => { cy.visit("/console/"); // click on test environment card - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__link").contains("Service Catalog").click(); // Click on Show Inventory on basic-service cy.get("#basic-service").contains("Show inventory").click(); @@ -245,7 +250,7 @@ describe("5 Compile reports", () => { cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }) .eq(0) .click(); - cy.get(".pf-v5-c-menu__item").contains("Duplicate").click(); + cy.get('[role="menuitem"]').contains("Duplicate").click(); // Add Instance cy.get("#service_id").clear(); @@ -255,7 +260,7 @@ describe("5 Compile reports", () => { // Expect to see a rejected service instance in the table cy.get("tbody", { timeout: 30000 }).should(($tableBody) => { - const $rows = $tableBody.find(".pf-v5-c-table__expandable-row"); + const $rows = $tableBody.find('tr[aria-label="InstanceRow-Intro"]'); expect($rows).to.have.length(2); @@ -263,7 +268,9 @@ describe("5 Compile reports", () => { }); // Go to the compile report page - cy.get(".pf-v5-c-nav__link").contains("Compile Reports").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Compile Reports") + .click(); // expect the last compile to be failed. cy.get("tbody", { timeout: 30000 }).should(($tableBody) => { @@ -282,13 +289,13 @@ describe("5 Compile reports", () => { cy.get("button").contains("Show Details").eq(0).click(); // Expect to be redirected to compile details page - cy.get(".pf-v5-c-title").contains("Compile Details").should("to.exist"); + cy.get("h1").contains("Compile Details").should("to.exist"); // Expect trigger to be lsm - cy.get(".pf-v5-c-description-list__group").eq(4).should("contain", "lsm"); + cy.get(".pf-v6-c-description-list__group").eq(4).should("contain", "lsm"); // Expect Error Type : inmanta.ast.AttributeException - cy.get(".pf-v5-c-description-list__description") + cy.get(".pf-v6-c-description-list__description") .eq(6) .should("contain", "inmanta.ast.AttributeException"); }); @@ -298,10 +305,10 @@ describe("5 Compile reports", () => { cy.visit("/console/"); // click on test environment card - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") .click(); - cy.get(".pf-v5-c-nav__link").contains("Service Catalog").click(); // Click on Show Inventory on basic-service cy.get("#basic-service").contains("Show inventory").click(); @@ -310,20 +317,16 @@ describe("5 Compile reports", () => { cy.get('[aria-label="row actions toggle"]', { timeout: 60000 }) .eq(0) .click(); - cy.get(".pf-v5-c-menu__item").contains("More actions").click(); - cy.get(".pf-v5-c-menu__item").contains("Delete").click(); - // confirm modal - cy.get(".pf-v5-c-form__actions").contains("Yes").click(); + cy.get('[role="menuitem"]').contains("Delete").click(); - // expect resource to be deleted - cy.get(".pf-v5-c-table__toggle", { timeout: 25000 }).should( - "have.length", - 1, - ); + // confirm modal + cy.get(".pf-v6-c-form__actions").contains("Yes").click(); // go back to Compile Reports - cy.get(".pf-v5-c-nav__link").contains("Compile Reports").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Compile Reports") + .click(); // Expect no new compiles to be visible. The last compile report is a failed one. cy.get("tbody", { timeout: 30000 }).should(($tableBody) => { @@ -360,12 +363,12 @@ describe("5 Compile reports", () => { cy.visit("/console/"); // click on test environment card - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") - .click(); + cy.get(`[aria-label="Select-environment-test"]`).click(); // Go to the compile report page - cy.get(".pf-v5-c-nav__link").contains("Compile Reports").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Compile Reports") + .click(); // Click on filter dropdown cy.get('[aria-label="StatusFilterInput"]').click(); diff --git a/cypress/e2e/scenario-6-resources.cy.js b/cypress/e2e/scenario-6-resources.cy.js index e293525c2..ce2f2ca6a 100644 --- a/cypress/e2e/scenario-6-resources.cy.js +++ b/cypress/e2e/scenario-6-resources.cy.js @@ -4,9 +4,9 @@ * * @param {string} nameEnvironment */ -const clearEnvironment = (nameEnvironment = "lsm-frontend") => { +const clearEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -45,9 +45,9 @@ const checkStatusCompile = (id) => { * * @param {string} nameEnvironment */ -const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { +const forceUpdateEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -63,7 +63,6 @@ const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { }; const isIso = Cypress.env("edition") === "iso"; -const PROJECT = Cypress.env("project") || "lsm-frontend"; describe("Scenario 6 : Resources", () => { if (isIso) { @@ -77,10 +76,12 @@ describe("Scenario 6 : Resources", () => { // Select Test environment cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(PROJECT).click(); + cy.get(`[aria-label="Select-environment-test"]`).click(); // Go to Resources page by clicking on navbar - cy.get(".pf-v5-c-nav__link").contains("Resources").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Resources") + .click(); // Expect 0/0 resources to be visible cy.get('[aria-label="Deployment state summary"]').should( @@ -97,10 +98,12 @@ describe("Scenario 6 : Resources", () => { // Select Test environment cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(PROJECT).click(); + cy.get(`[aria-label="Select-environment-test"]`).click(); // Go to Service Catalog - cy.get(".pf-v5-c-nav__link").contains("Service Catalog").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") + .click(); // Select Show Inventory on basic-service cy.get("#basic-service").contains("Show inventory").click(); @@ -122,7 +125,9 @@ describe("Scenario 6 : Resources", () => { cy.get(".pf-v5-c-chart").should("be.visible"); // Go back to Resources page - cy.get(".pf-v5-c-nav__link").contains("Resources").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Resources") + .click(); // Expect two rows to be added to the table // lsm::LifecycleTransfer @@ -146,24 +151,28 @@ describe("Scenario 6 : Resources", () => { .click(); // Expect to find this information in table : - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(0) .should("contain", "name") .and("contain", "default-0001"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(1) .should("contain", "purge_on_delete") .and("contain", "false"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(2) .should("contain", "purged") .and("contain", "false"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(3) - .should("contain", "send_event") + .should("contain", "receive_events") .and("contain", "true"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(4) + .should("contain", "send_event") + .and("contain", "true"); + cy.get(".pf-v6-c-description-list__group") + .eq(5) .should("contain", "should_deploy_fail") .and("contain", "false"); @@ -192,29 +201,33 @@ describe("Scenario 6 : Resources", () => { cy.get('[aria-label="Details"]').click(); // Expect content to be the same as on main Desired State tab - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(2) .should("contain", "name") .and("contain", "default-0001"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(3) .should("contain", "purge_on_delete") .and("contain", "false"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(4) .should("contain", "purged") .and("contain", "false"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(5) - .should("contain", "send_event") + .should("contain", "receive_events") .and("contain", "true"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(6) + .should("contain", "send_event") + .and("contain", "true"); + cy.get(".pf-v6-c-description-list__group") + .eq(7) .should("contain", "should_deploy_fail") .and("contain", "false"); // Expect requires tab to have no requirements - cy.get(".pf-v5-c-tabs__list") + cy.get(".pf-v6-c-tabs__list") .eq(1) .find("button") .contains("Requires") @@ -226,17 +239,17 @@ describe("Scenario 6 : Resources", () => { // Go to logs tab cy.get("button").contains("Logs").click(); - // Expect it to have : 9 log messages + // Expect it to have : 7 log messages cy.get('[aria-label="ResourceLogRow"]', { timeout: 40000 }).should( "to.have.length.of.at.least", - 8, + 7, ); // make sure the default is 100 instead of 20 like on other pages with pagination. cy.get( - '[aria-label="PaginationWidget-top"] .pf-v5-c-menu-toggle', + '[aria-label="PaginationWidget-top"] .pf-v6-c-menu-toggle', ).click(); - cy.contains(".pf-v5-c-menu__list-item", "100") + cy.contains(".pf-v6-c-menu__list-item", "100") .find("svg") .should("exist"); @@ -249,7 +262,7 @@ describe("Scenario 6 : Resources", () => { cy.get('[aria-label="Details"]').eq(0).click(); // Expect to find "Setting deployed due to known good status" displayed in expansion. - cy.get(".pf-v5-c-description-list__text").should( + cy.get(".pf-v6-c-description-list__text").should( "contain", "Setting deployed due to known good status", ); @@ -258,10 +271,12 @@ describe("Scenario 6 : Resources", () => { it("6.3 Log message filtering", () => { // Select Test environment cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(PROJECT).click(); + cy.get(`[aria-label="Select-environment-test"]`).click(); // Go to Resources page - cy.get(".pf-v5-c-nav__link").contains("Resources").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Resources") + .click(); // click on frontend_model::TestResource Show Details cy.get('[aria-label="Resource Table Row"]') @@ -283,23 +298,25 @@ describe("Scenario 6 : Resources", () => { 6, ); - // Click on clear filters - cy.get(".pf-v5-c-chip").find("button").click(); + // Remove INFO filter + cy.get('[aria-label="Close INFO"]').click(); // Expect amount of rows to be bigger than before filtering. cy.get('[aria-label="ResourceLogRow"]').should( "to.have.length.of.at.least", - 8, + 7, ); }); it("6.4 Resources with multiple dependencies", () => { // Select Test environment cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(PROJECT).click(); + cy.get(`[aria-label="Select-environment-test"]`).click(); // Go to Service Catalog page - cy.get(".pf-v5-c-nav__link").contains("Service Catalog").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") + .click(); // Click on Show Inventory on dependency-service cy.get("#dependency-service").contains("Show inventory").click(); @@ -323,7 +340,9 @@ describe("Scenario 6 : Resources", () => { cy.get(".pf-v5-c-chart").should("be.visible"); // Go to Resource page - cy.get(".pf-v5-c-nav__link").contains("Resources").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Resources") + .click(); // Expect to find 7 rows now in the resource table. cy.get('[aria-label="Resource Table Row"]', { timeout: 60000 }).should( @@ -349,7 +368,7 @@ describe("Scenario 6 : Resources", () => { // Click open collapsible row for resource waiting-entity cy.get( - '[aria-label="Toggle-frontend_model::TestResource[internal,name=waiting-entity]"]', + '[aria-label="Toggle-frontend_model::TestResource[internal,name=waiting-entity]"] > button', { timeout: 20000 }, ).click(); // Expect to find three rows with @@ -425,12 +444,14 @@ describe("Scenario 6 : Resources", () => { .click(); // check title from this page, should have the name of the resource - cy.get(".pf-v5-c-content") + cy.get(".pf-v6-c-content") .contains("frontend_model::TestResource[internal,name=a]") .should("to.be.visible"); // go back to Resource page - cy.get(".pf-v5-c-nav__link").contains("Resources").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Resources") + .click(); // click show details on resource with value waiting-entity cy.get('[aria-label="Resource Table Row"]') @@ -447,7 +468,7 @@ describe("Scenario 6 : Resources", () => { .click(); // Expect to be on the same page with same title as before. - cy.get(".pf-v5-c-content") + cy.get(".pf-v6-c-content") .contains("frontend_model::TestResource[internal,name=a]") .should("to.be.visible"); }); @@ -456,10 +477,12 @@ describe("Scenario 6 : Resources", () => { // Select Test environment cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(PROJECT).click(); + cy.get(`[aria-label="Select-environment-test"]`).click(); // Go to Service Catalog - cy.get(".pf-v5-c-nav__link").contains("Service Catalog").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") + .click(); // Select Show Inventory on dependency-service and add one with 41 dependencies cy.get("#dependency-service").contains("Show inventory").click(); @@ -566,28 +589,30 @@ describe("Scenario 6 : Resources", () => { }).should("not.to.exist"); //Go to resources page - cy.get(".pf-v5-c-nav__link").contains("Resources").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Resources") + .click(); cy.get('[aria-label="LegendItem-deployed"]', { timeout: 60000 }).should( "have.text", "49", ); cy.get( - "#PaginationWidget-top-top-toggle > .pf-v5-c-menu-toggle__text > b:first-of-type", + "#PaginationWidget-top-top-toggle > .pf-v6-c-menu-toggle__text > b:first-of-type", ).should("have.text", "1 - 20"); //Go to next page cy.get('[aria-label="Go to next page"]').first().click(); cy.get('[aria-label="ResourcesView-Success"]').should("be.visible"); cy.get( - "#PaginationWidget-top-top-toggle > .pf-v5-c-menu-toggle__text > b:first-of-type", + "#PaginationWidget-top-top-toggle > .pf-v6-c-menu-toggle__text > b:first-of-type", ).should("have.text", "21 - 40"); //Go to last page cy.get('[aria-label="Go to next page"]').first().click(); cy.get('[aria-label="ResourcesView-Success"]').should("be.visible"); cy.get( - "#PaginationWidget-top-top-toggle > .pf-v5-c-menu-toggle__text > b:first-of-type", + "#PaginationWidget-top-top-toggle > .pf-v6-c-menu-toggle__text > b:first-of-type", ).should("have.text", "41 - 49"); //Go to previous page @@ -595,29 +620,24 @@ describe("Scenario 6 : Resources", () => { cy.get('[aria-label="ResourcesView-Success"]').should("be.visible"); cy.get( - "#PaginationWidget-top-top-toggle > .pf-v5-c-menu-toggle__text > b:first-of-type", - ).should("have.text", "21 - 40"); - - //Change page and come back to check if we are at the same page as we was - cy.get(".pf-v5-c-nav__link").contains("Dashboard").click(); - cy.go("back"); - cy.get( - "#PaginationWidget-top-top-toggle > .pf-v5-c-menu-toggle__text > b:first-of-type", + "#PaginationWidget-top-top-toggle > .pf-v6-c-menu-toggle__text > b:first-of-type", ).should("have.text", "21 - 40"); // Change sorting and expect to be redirected to the first page of the table cy.get("button").contains("Type").click(); cy.get( - "#PaginationWidget-top-top-toggle > .pf-v5-c-menu-toggle__text > b:first-of-type", + "#PaginationWidget-top-top-toggle > .pf-v6-c-menu-toggle__text > b:first-of-type", ).should("have.text", "1 - 20"); }); } else { it("6.6 Resources for OSS", () => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(PROJECT).click(); + cy.get(`[aria-label="Select-environment-test"]`).click(); - cy.get(".pf-v5-c-nav__link").contains("Resources").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Resources") + .click(); cy.get('[aria-label="Resource Table Row"]', { timeout: 30000 }).should( "have.length", @@ -646,24 +666,28 @@ describe("Scenario 6 : Resources", () => { .click(); // Expect to find the right information on the details page. - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(0) .should("contain", "name") .and("contain", "a"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(1) .should("contain", "purge_on_delete") .and("contain", "false"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(2) .should("contain", "purged") .and("contain", "false"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(3) - .should("contain", "send_event") + .should("contain", "receive_events") .and("contain", "true"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(4) + .should("contain", "send_event") + .and("contain", "true"); + cy.get(".pf-v6-c-description-list__group") + .eq(5) .should("contain", "should_deploy_fail") .and("contain", "false"); @@ -691,29 +715,33 @@ describe("Scenario 6 : Resources", () => { // click row open cy.get('[aria-label="Details"]').click(); // Expect content to be the same as on main Desired State tab - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(2) .should("contain", "name") .and("contain", "a"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(3) .should("contain", "purge_on_delete") .and("contain", "false"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(4) .should("contain", "purged") .and("contain", "false"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(5) - .should("contain", "send_event") + .should("contain", "receive_events") .and("contain", "true"); - cy.get(".pf-v5-c-description-list__group") + cy.get(".pf-v6-c-description-list__group") .eq(6) + .should("contain", "send_event") + .and("contain", "true"); + cy.get(".pf-v6-c-description-list__group") + .eq(7) .should("contain", "should_deploy_fail") .and("contain", "false"); // Expect requires tab to have no requirements - cy.get(".pf-v5-c-tabs__list") + cy.get(".pf-v6-c-tabs__list") .eq(1) .find("button") .contains("Requires") @@ -724,17 +752,17 @@ describe("Scenario 6 : Resources", () => { // Go to logs tab cy.get("button").contains("Logs").click(); - // Expect it to have : 12 log messages + // Expect it to have : 8 log messages cy.get('[aria-label="ResourceLogRow"]', { timeout: 40000 }).should( "to.have.length.of.at.least", - 12, + 5, ); // make sure the default is 100 instead of 20 like on other pages with pagination. cy.get( - '[aria-label="PaginationWidget-top"] .pf-v5-c-menu-toggle', + '[aria-label="PaginationWidget-top"] .pf-v6-c-menu-toggle', ).click(); - cy.contains(".pf-v5-c-menu__list-item", "100") + cy.contains(".pf-v6-c-menu__list-item", "100") .find("svg") .should("exist"); @@ -747,7 +775,7 @@ describe("Scenario 6 : Resources", () => { cy.get('[aria-label="Details"]').eq(0).click(); // Expect to find "Setting deployed due to known good status" displayed in expansion. - cy.get(".pf-v5-c-description-list__text").should( + cy.get(".pf-v6-c-description-list__text").should( "contain", "Setting deployed due to known good status", ); diff --git a/cypress/e2e/scenario-7.1-keycloak.cy.js b/cypress/e2e/scenario-7.1-keycloak.cy.js index f0e99c15d..c4be2a8bb 100644 --- a/cypress/e2e/scenario-7.1-keycloak.cy.js +++ b/cypress/e2e/scenario-7.1-keycloak.cy.js @@ -16,7 +16,7 @@ if (Cypress.env("keycloak")) { cy.get("[id=toggle-button]", { timeout: 20000 }).should("contain", "admin"); cy.get("[id=toggle-button]").click(); - cy.get(".pf-v5-c-menu__item").contains("Logout").click(); + cy.get('[role="menuitem"]').contains("Logout").click(); cy.origin("http://127.0.0.1:8080", () => { cy.get("[id=username]").should("be.visible"); diff --git a/cypress/e2e/scenario-7.2-local-auth.cy.js b/cypress/e2e/scenario-7.2-local-auth.cy.js index f615a846d..6b0240100 100644 --- a/cypress/e2e/scenario-7.2-local-auth.cy.js +++ b/cypress/e2e/scenario-7.2-local-auth.cy.js @@ -10,7 +10,7 @@ if (Cypress.env("local-auth")) { cy.get("[id=toggle-button]", { timeout: 20000 }).should("contain", "admin"); cy.get("[id=toggle-button]").click(); - cy.get(".pf-v5-c-menu__item").contains("Logout").click(); + cy.get('[role="menuitem"]').contains("Logout").click(); cy.get('[id="pf-login-username-id"]').should("be.visible"); cy.get('[id="pf-login-password-id"]').should("be.visible"); diff --git a/cypress/e2e/scenario-7.3-user-management.cy.js b/cypress/e2e/scenario-7.3-user-management.cy.js index cf6457b10..31b497ded 100644 --- a/cypress/e2e/scenario-7.3-user-management.cy.js +++ b/cypress/e2e/scenario-7.3-user-management.cy.js @@ -10,19 +10,19 @@ if (Cypress.env("local-auth")) { cy.get("[id=toggle-button]", { timeout: 20000 }).should("contain", "admin"); cy.get("[id=toggle-button]").click(); - cy.get(".pf-v5-c-menu__item").contains("User Management").click(); + cy.get('[role="menuitem"]').contains("User Management").click(); cy.get("h1").contains("User Management").should("be.visible"); cy.get('[data-testid="user-row"]').should("have.length", 1); - cy.get("button").contains("Add User").click(); + cy.get('[aria-label="add_user-button"]').click(); cy.get("h1").contains("Add User").should("be.visible"); cy.get('[aria-label="input-username"]').type("new_user"); cy.get('[aria-label="input-password"]').type("short"); - cy.get("button").contains("Add").click(); + cy.get('[aria-label="confirm-button"]').click(); cy.get("span") .contains( @@ -32,7 +32,7 @@ if (Cypress.env("local-auth")) { cy.get('[aria-label="input-password"]').clear().type("password"); - cy.get("button").contains("Add").click(); + cy.get('[aria-label="confirm-button"]').click(); cy.get('[data-testid="user-row"]').should("have.length", 2); }); @@ -48,7 +48,7 @@ if (Cypress.env("local-auth")) { cy.get("[id=toggle-button]", { timeout: 20000 }).should("contain", "admin"); cy.get("[id=toggle-button]").click(); - cy.get(".pf-v5-c-menu__item").contains("User Management").click(); + cy.get('[role="menuitem"]').contains("User Management").click(); cy.get("h1").contains("User Management").should("be.visible"); diff --git a/cypress/e2e/scenario-8-instance-composer.cy.js b/cypress/e2e/scenario-8-instance-composer.cy.js index 1d09b6f25..0c5fbcb0f 100644 --- a/cypress/e2e/scenario-8-instance-composer.cy.js +++ b/cypress/e2e/scenario-8-instance-composer.cy.js @@ -1,827 +1,1046 @@ -//TODO: tests are commented out as there is ongoing redesign of the instance composer which will affect how the user interact with it and how e2e test should look like - -// /** -// * Shorthand method to clear the environment being passed. -// * By default, if no arguments are passed it will target the 'lsm-frontend' environment. -// * -// * @param {string} nameEnvironment -// */ -// const clearEnvironment = (nameEnvironment = "lsm-frontend") => { -// cy.visit("/console/"); -// cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); -// cy.url().then((url) => { -// const location = new URL(url); -// const id = location.searchParams.get("env"); -// cy.request("DELETE", `/api/v1/decommission/${id}`); -// }); -// }; - -// /** -// * based on the environment id, it will recursively check if a compile is pending. -// * It will continue the recursion as long as the statusCode is equal to 200 -// * -// * @param {string} id -// */ -// const checkStatusCompile = (id) => { -// let statusCodeCompile = 200; - -// if (statusCodeCompile === 200) { -// cy.intercept(`/api/v1/notify/${id}`).as("IsCompiling"); -// // the timeout is necessary to avoid errors. -// // Cypress doesn't support while loops and this was the only workaround to wait till the statuscode is not 200 anymore. -// // the default timeout in cypress is 5000, but since we have recursion it goes into timeout for the nested awaits because of the recursion. -// cy.wait("@IsCompiling", { timeout: 15000 }).then((req) => { -// statusCodeCompile = req.response.statusCode; - -// if (statusCodeCompile === 200) { -// checkStatusCompile(id); -// } -// }); -// } -// }; - -// /** -// * Will by default execute the force update on the 'lsm-frontend' environment if no argumenst are being passed. -// * This method can be executed standalone, but is part of the cleanup cycle that is needed before running a scenario. -// * -// * @param {string} nameEnvironment -// */ -// const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { -// cy.visit("/console/"); -// cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); -// cy.url().then((url) => { -// const location = new URL(url); -// const id = location.searchParams.get("env"); -// cy.request({ -// method: "POST", -// url: `/lsm/v1/exporter/export_service_definition`, -// headers: { "X-Inmanta-Tid": id }, -// body: { force_update: true }, -// }); -// checkStatusCompile(id); -// }); -// }; - -// if (Cypress.env("edition") === "iso") { -// describe("Scenario 8 - Instance Composer", () => { -// before(() => { -// clearEnvironment(); -// forceUpdateEnvironment(); -// }); - -// it("8.1 create instance with embedded entities", () => { -// // Select 'test' environment -// cy.visit("/console/"); -// cy.get('[aria-label="Environment card"]') -// .contains("lsm-frontend") -// .click(); -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); - -// // click on Show Inventory on embedded-entity-service-extra, expect one instance already -// cy.get("#embedded-entity-service-extra", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// cy.get('[aria-label="ServiceInventory-Empty"]').should("to.be.visible"); - -// // click on add instance with composer -// cy.get('[aria-label="AddInstanceToggle"]').click(); -// cy.get("#add-instance-composer-button").click(); - -// // Create instance on embedded-entity-service-extra -// cy.get(".canvas").should("be.visible"); - -// // Open and fill instance form of core attributes -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text") -// .contains("embedded-entity-service-extra") -// .click(); - -// cy.get("#service_id").type("0002"); -// cy.get("#name").type("embedded-service"); -// cy.get("button").contains("Confirm").click(); - -// //try to deploy instance with only core attributes and expect 2 errors -// cy.get("button").contains("Deploy").click(); -// cy.get('[data-testid="ToastAlert"]') -// .contains( -// "Invalid request: 2 validation errors for embedded-entity-service-extra", -// ) -// .should("be.visible"); -// cy.get(".pf-v5-c-alert__action > .pf-v5-c-button").click(); - -// // add ro_meta core entity -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text") -// .contains("ro_meta (embedded-entity-service-extra)") -// .click(); - -// cy.get("#name").type("ro_meta"); -// cy.get("#meta_data").type("meta_data1"); -// cy.get("#other_data").type("1"); -// cy.get("button").contains("Confirm").click(); - -// cy.get('[joint-selector="headerLabel"]').contains("ro_meta").click(); -// cy.get('[data-action="link"]') -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 700, -// clientY: 300, -// }) -// .trigger("mouseup"); - -// //try to deploy instance with only one required embedded attribute connected and expect 1 error -// cy.get("button").contains("Deploy").click(); -// cy.get('[data-testid="ToastAlert"]') -// .contains( -// "Invalid request: 1 validation error for embedded-entity-service-extra", -// ) -// .should("be.visible"); -// cy.get(".pf-v5-c-alert__action > .pf-v5-c-button").click(); - -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text") -// .contains("rw_meta (embedded-entity-service-extra)") -// .click(); -// cy.get("#name").type("rw_meta"); -// cy.get("#meta_data").type("meta_data2"); -// cy.get("#other_data").type("2"); -// cy.get("button").contains("Confirm").click(); - -// cy.get('[joint-selector="headerLabel"]').contains("rw_meta").click(); -// cy.get('[data-action="link"]') -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 300, -// clientY: 300, -// }) -// .trigger("mouseup"); - -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); - -// cy.get(".pf-v5-c-menu__item-text") -// .contains("rw_files (embedded-entity-service-extra)") -// .click(); -// cy.get("#name").type("rw_files1"); -// cy.get("#data").type("data1"); -// cy.get("button").contains("Confirm").click(); - -// cy.get('[joint-selector="headerLabel"]') -// .contains("rw_files") -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 400, -// clientY: 500, -// }) -// .trigger("mouseup"); - -// //move root entity to the non-default position to assert persisting of the position works as intended by next scenario -// cy.get('[joint-selector="headerLabel"]') -// .contains("embedded-entity") -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 1100, -// clientY: 450, -// }) -// .trigger("mouseup"); - -// cy.get('[joint-selector="headerLabel"]').contains("rw_files").click(); -// cy.get('[data-action="link"]') -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 1200, -// clientY: 300, -// }) -// .trigger("mouseup"); - -// cy.get("button").contains("Deploy").click(); - -// // Check if only one row has been added to the table. -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); -// // await until all instances are being deployed and up -// cy.get('[data-label="State"]', { timeout: 90000 }).should( -// "have.text", -// "up", -// ); -// }); - -// it("8.2 edit instance with embedded entities", () => { -// // Select 'test' environment -// cy.visit("/console/"); -// cy.get('[aria-label="Environment card"]') -// .contains("lsm-frontend") -// .click(); -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); - -// // click on Show Inventory on embedded-entity-service-extra, expect one instance already -// cy.get("#embedded-entity-service-extra", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// cy.get('[aria-label="ServiceInventory-Success"]').should("to.be.visible"); - -// // click on kebab menu on embedded-entity-service-extra -// cy.get('[aria-label="row actions toggle"]').eq(0).click(); -// cy.get("button").contains("Edit in Composer").click(); - -// // Expect to be redirected to Instance Composer view with embedded-entity-service-extra shape visible -// cy.get(".canvas").should("be.visible"); -// cy.get('[data-type="app.ServiceEntityBlock"]').should("be.visible"); -// cy.get('[joint-selector="headerLabel"]') -// .contains("embedded-") -// .should("exist"); -// cy.get('[joint-selector="headerLabel"]') -// .contains("ro_meta") -// .should("exist"); -// cy.get('[joint-selector="headerLabel"]') -// .contains("rw_meta") -// .should("exist"); -// cy.get('[joint-selector="headerLabel"]') -// .contains("rw_files") -// .should("exist"); - -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text") -// .contains("rw_files (embedded-entity-service-extra)") -// .click(); -// cy.get("#name").type("rw_files2"); -// cy.get("#data").type("data2"); -// cy.get("button").contains("Confirm").click(); - -// cy.get('[joint-selector="itemLabel_name_value"]') -// .contains("rw_files2") //easiest way to differentiate same type of entities is by the unique attributes values -// .click(); -// cy.get(".zoom-out").click(); -// cy.get(".zoom-out").click(); -// cy.get(".zoom-out").click(); -// cy.get(".zoom-out").click(); -// cy.get(".zoom-out").click(); - -// //try to add rw embedded entity which shouldn't be possible -// cy.get('[data-type="Link"]').should("have.length", "3"); -// cy.get('[data-action="link"]') -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 800, -// clientY: 300, -// }) -// .trigger("mouseup"); - -// cy.get('[data-type="Link"]').should("have.length", "3"); -// cy.get('[data-action="delete"]').click(); - -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text") -// .contains("ro_files (embedded-entity-service-extra)") -// .click(); -// cy.get("#name").type("ro_files2"); -// cy.get("#data").type("data2"); -// cy.get("button").contains("Confirm").click(); - -// cy.get(".zoom-out").click(); - -// cy.get('[joint-selector="headerLabel"]').contains("ro_files").click(); -// cy.get('[data-action="link"]') -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 1200, -// clientY: 400, -// }) -// .trigger("mouseup"); - -// cy.get("button").contains("Deploy").click(); - -// // Check if only one row has been added to the table. -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); -// // await until all instances are being deployed and up -// cy.get('[data-label="State"]', { timeout: 90000 }).should( -// "have.text", -// "up", -// ); -// cy.get("#expand-toggle0").click(); -// cy.get("button").contains("Attributes").click(); -// cy.get('[aria-label="Toggle-ro_files"]').click(); -// cy.get('[aria-label="Toggle-ro_files$0"]').click(); -// cy.get( -// '[aria-label="Row-ro_files$0$data"] > [data-label="candidate"]', -// ).should("have.text", "data2"); -// cy.get( -// '[aria-label="Row-ro_files$0$name"] > [data-label="candidate"]', -// ).should("have.text", "ro_files2"); -// //go back to composer to further edit component -// cy.get('[aria-label="row actions toggle"]').eq(0).click(); -// cy.get("button").contains("Edit in Composer").click(); - -// //try to delete optional rw embedded entity -// cy.get(".zoom-out").click(); -// cy.get('[joint-selector="headerLabel"]').contains("rw_files").click(); -// cy.get('[data-action="delete"]').click(); - -// cy.get("button").contains("Deploy").click(); -// cy.get('[data-testid="ToastAlert"]') -// .contains( -// "Attribute rw_files cannot be updated because it has the attribute modifier rw", -// ) -// .should("be.visible"); -// cy.reload(); - -// //try to delete required embedded entity -// cy.get('[joint-selector="headerLabel"]').contains("ro_meta").click(); -// cy.get('[data-action="delete"]').click(); -// cy.get("button").contains("Deploy").click(); -// cy.get('[data-testid="ToastAlert"]') -// .contains("invalid: 1 validation error for dict") -// .should("be.visible"); -// cy.reload(); - -// cy.get('[joint-selector="headerLabel"]').contains("ro_files").click(); -// cy.get('[data-action="delete"]').click(); - -// cy.get('[joint-selector="headerLabel"]').contains("ro_meta").click(); -// cy.get('[data-action="edit"]').click(); -// cy.get("#meta_data").type("{backspace}-new"); -// cy.get("#name").should("not.exist"); -// cy.get("button").contains("Confirm").click(); - -// cy.get("button").contains("Deploy").click(); - -// // Check if only one row has been added to the table. -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); -// // await until all instances are being deployed and up -// cy.get('[data-label="State"]', { timeout: 90000 }).should( -// "have.text", -// "up", -// ); - -// cy.get("button").contains("Attributes").click(); -// cy.get('[aria-label="Row-ro_files"] > [data-label="candidate"]').should( -// "have.text", -// "{}", -// ); -// cy.get( -// '[aria-label="Row-ro_meta$meta_data"] > [data-label="candidate"]', -// ).should("have.text", "meta_data-new"); -// }); - -// it("8.3 create instances with inter-service relation", () => { -// // Select 'test' environment -// cy.visit("/console/"); -// cy.get('[aria-label="Environment card"]') -// .contains("lsm-frontend") -// .click(); -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); - -// // click on Show Inventory on parent-service, expect one instance already -// cy.get("#parent-service", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// cy.get('[aria-label="ServiceInventory-Empty"]').should("to.be.visible"); - -// // click on add instance in composer -// cy.get('[aria-label="AddInstanceToggle"]').click(); -// cy.get("#add-instance-composer-button").click(); -// cy.get(".canvas").should("be.visible"); - -// // Open and fill instance form of parent-service -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text").contains("parent-service").click(); - -// cy.get("#service_id").type("0003"); -// cy.get("#name").type("parent-service"); -// cy.get("button").contains("Confirm").click(); - -// // Open and fill instance form of child-service and connect it with parent-service -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text").contains("child-service").click(); - -// cy.get("#service_id").type("0004"); -// cy.get("#name").type("child-service"); -// cy.get("button").contains("Confirm").click(); - -// //check if errors is returned when we deployed service without inter-service relation set -// cy.get("button").contains("Deploy").click(); -// cy.get('[data-testid="ToastAlert"]') -// .contains(`Invalid request: 1 validation error for child-service`) -// .should("be.visible"); -// cy.get(".pf-v5-c-alert__action > .pf-v5-c-button").click(); - -// //connect services -// cy.get('[joint-selector="headerLabel"]') -// .contains("child-service") -// .click(); -// cy.get('[data-action="link"]') -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 700, -// clientY: 300, -// }) -// .trigger("mouseup"); - -// cy.get("button").contains("Deploy").click(); - -// // Check if only one row has been added to the table. -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); -// // await until all instances are being deployed and up -// cy.get('[data-label="State"]', { timeout: 90000 }).should( -// "have.text", -// "up", -// ); - -// //Check child-service -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); -// cy.get("#child-service", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); -// cy.get('[data-label="State"]', { timeout: 90000 }).should( -// "have.text", -// "up", -// ); - -// //go back to parent-service inventory view -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); -// // click on Show Inventory on parent-service, expect one instance already -// cy.get("#parent-service", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// // click on add instance in composer -// cy.get('[aria-label="AddInstanceToggle"]').click(); -// cy.get("#add-instance-composer-button").click(); -// cy.get(".canvas").should("be.visible"); - -// // Open and fill instance form of parent-service -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text").contains("parent-service").click(); - -// cy.get("#service_id").type("0006"); -// cy.get("#name").type("parent-service2"); -// cy.get("button").contains("Confirm").click(); - -// // Open and fill instance forms for container-service then connect it with parent-service -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text").contains("container-service").click(); - -// cy.get("#service_id").type("0007"); -// cy.get("#name").type("container-service"); -// cy.get("button").contains("Confirm").click(); - -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text") -// .contains("child_container (container-service)") -// .click(); -// cy.get("#name").type("child_container"); -// cy.get("button").contains("Confirm").click(); - -// cy.get('[joint-selector="headerLabel"]') -// .contains("child_container") -// .click(); -// cy.get('[data-action="link"]') -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 700, -// clientY: 300, -// }) -// .trigger("mouseup"); -// cy.get('[joint-selector="headerLabel"]') -// .contains("child_container") -// .click(); -// cy.get('[data-action="link"]') -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 400, -// clientY: 300, -// }) -// .trigger("mouseup"); -// cy.get("button").contains("Deploy").click(); - -// // Check if only one row has been added to the table. -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 2); -// // await until all instances are being deployed and up -// cy.get('[data-label="State"]', { timeout: 90000 }) -// .eq(0, { timeout: 90000 }) -// .should("have.text", "up"); - -// //Check container-service -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); -// cy.get("#container-service", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); -// cy.get('[data-label="State"]', { timeout: 90000 }).should( -// "have.text", -// "up", -// ); - -// //go back to parent-service inventory view -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); -// // click on Show Inventory on parent-service, expect one instance already -// cy.get("#parent-service", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// // click on add instance in composer -// cy.get('[aria-label="AddInstanceToggle"]').click(); -// cy.get("#add-instance-composer-button").click(); -// cy.get(".canvas").should("be.visible"); - -// // Open and fill instance form of parent-service -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text").contains("parent-service").click(); - -// cy.get("#service_id").type("0008"); -// cy.get("#name").type("parent-service3"); -// cy.get("button").contains("Confirm").click(); -// // Open and fill instance form of child-with-many-parents-service then connect it with parent-service -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text") -// .contains("child-with-many-parents-service") -// .click(); -// cy.get("#service_id").type("0009"); -// cy.get("#name").type("child-with-many-parents"); -// cy.get("button").contains("Confirm").click(); - -// cy.get('[joint-selector="headerLabel"]').contains("child-with").click(); -// cy.get('[data-action="link"]') -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 700, -// clientY: 300, -// }) -// .trigger("mouseup"); - -// cy.get("button").contains("Deploy").click(); -// // Check if only one row has been added to the table. -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 3); -// // await until all instances are being deployed and up -// cy.get('[data-label="State"]', { timeout: 90000 }) -// .eq(0, { timeout: 90000 }) -// .should("have.text", "up"); -// //Check child-with-many-parents-service -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); -// cy.get("#child-with-many-parents-service", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); -// cy.get('[data-label="State"]', { timeout: 90000 }).should( -// "have.text", -// "up", -// ); -// }); - -// it("8.4 edit instances inter-service relation", () => { -// // Select 'test' environment -// cy.visit("/console/"); -// cy.get('[aria-label="Environment card"]') -// .contains("lsm-frontend") -// .click(); -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); - -// // click on Show Inventory on parent-service, expect three instances already -// cy.get("#parent-service", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// cy.get('[aria-label="ServiceInventory-Success"]').should("to.be.visible"); - -// // click on kebab menu on first created parent-service -// cy.get(".pf-v5-c-drawer__content").scrollTo("bottom"); -// cy.get('[aria-label="row actions toggle"]').eq(2).click(); -// cy.get("button").contains("Edit in Composer").click(); - -// //Open and fill instance form of parent-service -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text").contains("parent-service").click(); - -// cy.get("#service_id").type("00010"); -// cy.get("#name").type("new-parent-service"); -// cy.get("button").contains("Confirm").click(); -// cy.get('[data-type="Link"]').trigger("mouseover", { force: true }); -// cy.get(".joint-link_remove-circle").click(); -// cy.get(".joint-link_remove-circle").click(); -// cy.get(".joint-paper-scroller.joint-theme-default") -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 900, -// clientY: 200, -// }) -// .trigger("mouseup"); -// //connect services -// cy.get('[joint-selector="headerLabel"]') -// .contains("child-service") -// .click(); -// cy.get('[data-action="link"]') -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 900, -// clientY: 300, -// }) -// .trigger("mouseup"); -// cy.get("button").contains("Deploy").click(); - -// // Check if only one row has been added to the table. -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 4); -// cy.get('[data-label="State"]', { timeout: 90000 }) -// .eq(0, { timeout: 90000 }) -// .should("have.text", "up"); - -// // check if attribute was edited correctly -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); -// cy.get("#child-service", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); -// cy.get('[data-label="State"]', { timeout: 90000 }).should( -// "have.text", -// "up", -// ); -// cy.get("#expand-toggle0").click(); -// cy.get("button").contains("Attributes").click(); -// cy.get( -// '[aria-label="Row-parent_entity"] > [data-label="active"] > .pf-v5-l-flex > div > .pf-v5-c-button', -// ).should("have.text", "new-parent-service"); - -// // go back to parent-service inventory view -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); -// cy.get("#parent-service", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// cy.get(".pf-v5-c-drawer__content").scrollTo("bottom"); -// cy.get('[aria-label="row actions toggle"]').eq(2).click(); -// cy.get("button").contains("Edit in Composer").click(); - -// // move container out of the way and remove connection -// cy.get('[joint-selector="headerLabel"]') -// .contains("container-service") -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 1200, -// clientY: 400, -// }) -// .trigger("mouseup"); -// cy.get('[data-type="Link"]').eq(0).trigger("mouseover", { force: true }); -// cy.get(".joint-link_remove-circle").click(); -// cy.get(".joint-link_remove-circle").click(); - -// //Open and fill instance form of parent-service -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text").contains("parent-service").click(); -// cy.get("#service_id").type("00011"); -// cy.get("#name").type("new-parent-service2"); -// cy.get("button").contains("Confirm").click(); - -// //connect child_container to new parent-service -// cy.get('[joint-selector="headerLabel"]') -// .contains("child_container") -// .click(); - -// cy.get('[data-action="link"]') -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 1100, -// clientY: 300, -// }) -// .trigger("mouseup"); -// cy.get("button").contains("Deploy").click(); - -// // Check if only one row has been added to the table. -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 5); -// // await until all instances are being deployed and up -// cy.get('[data-label="State"]', { timeout: 90000 }) -// .eq(0, { timeout: 90000 }) -// .should("have.text", "up"); -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); - -// // check if attribute was edited correctly -// cy.get("#container-service", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); -// cy.get('[data-label="State"]', { timeout: 90000 }).should( -// "have.text", -// "up", -// ); -// cy.get("#expand-toggle0").click(); -// cy.get("button").contains("Attributes").click(); -// cy.get('[aria-label="Toggle-child_container"]').click(); -// cy.get( -// '[aria-label="Row-child_container$parent_entity"] > [data-label="active"] > .pf-v5-l-flex > div > .pf-v5-c-button', -// ).should("have.text", "new-parent-service2"); -// //go back to parent-service inventory view -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); -// // click on Show Inventory on parent-service -// cy.get("#parent-service", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// cy.get(".pf-v5-c-drawer__content").scrollTo("bottom"); -// cy.get('[aria-label="row actions toggle"]').eq(2).click(); -// cy.get("button").contains("Edit in Composer").click(); - -// //Open and fill instance form of parent-service -// cy.get('[aria-label="new-entity-button"]').click(); -// cy.get('[aria-label="service-picker"]').click(); -// cy.get(".pf-v5-c-menu__item-text").contains("parent-service").click(); -// cy.get("#service_id").type("00012"); -// cy.get("#name").type("new-parent-service3"); -// cy.get("button").contains("Confirm").click(); - -// // connect child-with-many-parents to new parent-service -// cy.get('[joint-selector="headerLabel"]').contains("child-with").click(); -// cy.get('[data-action="link"]') -// .trigger("mouseover") -// .trigger("mousedown") -// .trigger("mousemove", { -// clientX: 900, -// clientY: 300, -// }) -// .trigger("mouseup"); -// cy.get("button").contains("Deploy").click(); - -// // Check if only one row has been added to the table. -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 6); -// // await until all instances are being deployed and up -// cy.get('[data-label="State"]', { timeout: 90000 }) -// .eq(0, { timeout: 90000 }) -// .should("have.text", "up"); -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); - -// // check if attribute was edited correctly -// cy.get("#child-with-many-parents-service", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); -// cy.get('[data-label="State"]', { timeout: 90000 }).should( -// "have.text", -// "up", -// ); -// cy.get("#expand-toggle0").click(); -// cy.get("button").contains("Attributes").click(); -// cy.get( -// '[aria-label="Row-parent_entity"] > [data-label="active"] > .pf-v5-l-flex > div > .pf-v5-c-button', -// ) -// .eq(0) -// .should("have.text", "parent-service3"); -// cy.get( -// '[aria-label="Row-parent_entity"] > [data-label="active"] > .pf-v5-l-flex > div > .pf-v5-c-button', -// ) -// .eq(1) -// .should("have.text", "new-parent-service3"); -// }); - -// it("8.5 delete instance", () => { -// // Select 'test' environment -// cy.visit("/console/"); -// cy.get('[aria-label="Environment card"]') -// .contains("lsm-frontend") -// .click(); -// cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); - -// // click on Show Inventory on embedded-entity-service-extra, expect one instance already -// cy.get("#parent-service", { timeout: 60000 }) -// .contains("Show inventory") -// .click(); -// cy.get('[aria-label="ServiceInventory-Success"]').should("to.be.visible"); -// cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 6); - -// // click on kebab menu on embedded-entity-service-extra -// cy.get('[aria-label="row actions toggle"]').eq(5).click(); -// cy.get("button").contains("Edit in Composer").click(); -// cy.get('[joint-selector="headerLabel"]') -// .contains("parent-service") -// .click(); -// cy.get('[data-action="delete"]').click(); -// cy.get("button").contains("Deploy").click(); - -// // Check if only one row has been deleted to the table. -// cy.get('[aria-label="InstanceRow-Intro"]', { timeout: 90000 }).should( -// "have.length", -// 5, -// ); -// }); -// }); -// } +/** + * Shorthand method to clear the environment being passed. + * By default, if no arguments are passed it will target the 'lsm-frontend' environment. + * + * @param {string} nameEnvironment + */ +const clearEnvironment = (nameEnvironment = "test") => { + cy.visit("/console/"); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); + cy.url().then((url) => { + const location = new URL(url); + const id = location.searchParams.get("env"); + + cy.request("DELETE", `/api/v1/decommission/${id}`); + }); +}; + +/** + * based on the environment id, it will recursively check if a compile is pending. + * It will continue the recursion as long as the statusCode is equal to 200 + * + * @param {string} id + */ +const checkStatusCompile = (id) => { + let statusCodeCompile = 200; + + if (statusCodeCompile === 200) { + cy.intercept(`/api/v1/notify/${id}`).as("IsCompiling"); + // the timeout is necessary to avoid errors. + // Cypress doesn't support while loops and this was the only workaround to wait till the statuscode is not 200 anymore. + // the default timeout in cypress is 5000, but since we have recursion it goes into timeout for the nested awaits because of the recursion. + cy.wait("@IsCompiling", { timeout: 15000 }).then((req) => { + statusCodeCompile = req.response.statusCode; + + if (statusCodeCompile === 200) { + checkStatusCompile(id); + } + }); + } +}; + +/** + * Will by default execute the force update on the 'lsm-frontend' environment if no argumenst are being passed. + * This method can be executed standalone, but is part of the cleanup cycle that is needed before running a scenario. + * + * @param {string} nameEnvironment + */ +const forceUpdateEnvironment = (nameEnvironment = "test") => { + cy.visit("/console/"); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); + cy.url().then((url) => { + const location = new URL(url); + const id = location.searchParams.get("env"); + + cy.request({ + method: "POST", + url: `/lsm/v1/exporter/export_service_definition`, + headers: { "X-Inmanta-Tid": id }, + body: { force_update: true }, + }); + checkStatusCompile(id); + }); +}; + +if (Cypress.env("edition") === "iso") { + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + + describe("Scenario 8 - Instance Composer", async () => { + before(() => { + clearEnvironment(); + forceUpdateEnvironment(); + }); + + // Note: The fullscreen mode is tested in Jest. In Cypress this functionality has to be stubbed, and would be redundant with the Unit tests. + it("8.1 composer opens up has its default panning working", () => { + // Select 'test' environment + cy.visit("/console/"); + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") + .click(); + + // click on Show Inventory on #container-service, expect no instances + cy.get("#container-service", { timeout: 60000 }) + .contains("Show inventory") + .click(); + cy.get('[aria-label="ServiceInventory-Empty"]').should("to.be.visible"); + // click on add instance with composer + cy.get('[aria-label="AddInstanceToggle"]').click(); + cy.get("#add-instance-composer-button").click(); + + // Expect Canvas to be visible and default instances to be present + cy.get(".canvas").should("be.visible"); + cy.get('[data-type="app.ServiceEntityBlock"').should("have.length", 2); + + //expect Zoom Handler and all its component visible and in default state + cy.get('[data-testid="zoomHandler"').should("be.visible"); + cy.get('[data-testid="fullscreen"').should("be.visible"); + cy.get('[data-testid="fit-to-screen"').should("be.visible"); + + cy.get('[data-testid="slider-input"').should("be.visible"); + cy.get('[data-testid="slider-input"').should("have.value", "120"); + + cy.get('[data-testid="slider-output"').should("be.visible"); + cy.get('[data-testid="slider-output"').should("have.text", "120"); + + cy.get(".units").should("be.visible"); + cy.get(".units").contains("%"); //should('have.text', '%'); won't work because of the special character + + //assertion that fit-to-screen button works can be only done by checking output and the input value, as I couldn't extract the transform property from the `.joint-layers` element + cy.get('[data-testid="fit-to-screen"').click(); + + cy.get('[data-testid="slider-input"').should("have.value", "220"); + cy.get('[data-testid="slider-output"').should("have.text", "220"); + + //assert that zoom button works + cy.get('[data-testid="slider-input"') + .invoke("val", 300) + .trigger("change"); + cy.get('[data-testid="slider-input"').should("have.value", "300"); + cy.get('[data-testid="slider-output"').should("have.text", "300"); + + cy.get('[data-testid="slider-input"').invoke("val", 80).trigger("change"); + cy.get('[data-testid="slider-input"').should("have.value", "80"); + cy.get('[data-testid="slider-output"').should("have.text", "80"); + + //expect Left Sidebar and it's all component visible and in default state + cy.get(".left_sidebar").should("be.visible"); + cy.get("#tabs-toolbar").should("be.visible"); + + cy.get("#instance-stencil").should("be.visible"); + cy.get("#inventory-stencil").should("not.be.visible"); + + //expect Left sidebar to have ability to switch between tabs + cy.get("#inventory-tab").click(); + + cy.get("#instance-stencil").should("not.be.visible"); + cy.get("#inventory-stencil").should("be.visible"); + + cy.get("#new-tab").click(); + + cy.get("#instance-stencil").should("be.visible"); + cy.get("#inventory-stencil").should("not.be.visible"); + }); + + it("8.2 composer create view can perform it's required functions and deploy created instance", () => { + // Select 'test' environment + cy.visit("/console/"); + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") + .click(); + + //Add parent instance + // click on Show Inventory of parent-service, expect no instances + cy.get("#parent-service", { timeout: 60000 }) + .contains("Show inventory") + .click(); + + cy.get('[aria-label="ServiceInventory-Empty"]').should("to.be.visible"); + // click on add instance with composer + cy.get('[aria-label="AddInstanceToggle"]').click(); + cy.get("#add-instance-composer-button").click(); + + // Expect Canvas to be visible + cy.get(".canvas").should("be.visible"); + + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("parent-service") + .click(); + + //assert that core instance can't be removed, and cancel button is by default disabled + cy.get('[data-testid="Composer-Container"]').within(() => { + cy.get("button") + .contains("span", "Cancel") + .parent() + .should("be.disabled"); + cy.get("button") + .contains("span", "Remove") + .parent() + .should("be.disabled"); + }); + + //fill parent attributes + cy.get('[aria-label="TextInput-name"]').type("test_name"); + cy.get('[aria-label="TextInput-service_id"]').type("test_id"); + + //clear all inputs and assert that cancel button is enabled + cy.get('[data-testid="Composer-Container"]').within(() => { + cy.get("button") + .contains("span", "Cancel") + .parent() + .should("be.enabled"); + cy.get("button").contains("Cancel").click(); + }); + + //assert that inputs are cleared and cancel button is set back to disabled + cy.get('[aria-label="TextInput-name"]').should("have.value", ""); + cy.get('[aria-label="TextInput-service_id"]').should("have.value", ""); + cy.get('[data-testid="Composer-Container"]').within(() => { + cy.get("button") + .contains("span", "Cancel") + .parent() + .should("be.disabled"); + }); + + //fill parent attributes + cy.get('[aria-label="TextInput-name"]').type("test_name"); + cy.get('[aria-label="TextInput-service_id"]').type("test_id"); + cy.get('[joint-selector="itemLabel_name"]') + .contains("name") + .should("be.visible"); + + cy.get('[joint-selector="itemLabel_name"]') + .contains("name") + .should("be.visible"); + cy.get('[joint-selector="itemLabel_name_value"]') + .contains("test_name") + .should("be.visible"); + + cy.get("button").contains("Deploy").click(); + + cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); + + //add another parent instance + cy.get('[aria-label="AddInstanceToggle"]').click(); + cy.get("#add-instance-composer-button").click(); + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("parent-service") + .click(); + + //fill parent attributes + cy.get('[aria-label="TextInput-name"]').type("test_name2"); + cy.get('[aria-label="TextInput-service_id"]').type("test_id2"); + + cy.get("button").contains("Deploy").click(); + + cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 2); + // await until two parent_service are deployed and up + cy.get('[data-label="State"]', { timeout: 90000 }) + .eq(1, { timeout: 90000 }) + .should("have.text", "up", { timeout: 90000 }); + cy.get('[data-label="State"]', { timeout: 90000 }) + .eq(0, { timeout: 90000 }) + .should("have.text", "up", { timeout: 90000 }); + + //Add child_service instance + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") + .click(); + // click on Show Inventory of many-defaults service, expect no instances + cy.get("#many-defaults", { timeout: 60000 }) + .contains("Show inventory") + .click(); + cy.get('[aria-label="ServiceInventory-Empty"]').should("to.be.visible"); + // click on add instance with composer + cy.get('[aria-label="AddInstanceToggle"]').click(); + cy.get("#add-instance-composer-button").click(); + + // Expect Canvas to be visible + cy.get(".canvas").should("be.visible"); + + //assert if default entities are present, on init on the canvas we should have already basic required structure for the service instance + cy.get('[data-type="app.ServiceEntityBlock"').should("have.length", 2); + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("embedded") + .should("be.visible"); + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("many-defaults") + .should("be.visible"); + cy.get('[data-type="Link"').should("be.visible"); + + //assert default embedded entities are present and first one is disabled as it reached its max limit + cy.get("#instance-stencil").within(() => { + cy.get('[aria-labelledby="bodyTwo_embedded"]').should("be.visible"); + cy.get('[aria-labelledby="bodyTwo_embedded"]').should( + "have.class", + "stencil_body-disabled", + ); + + cy.get('[aria-labelledby="bodyTwo_extra_embedded"]').should( + "be.visible", + ); + cy.get('[aria-labelledby="bodyTwo_extra_embedded"]').should( + "have.not.class", + "stencil_body-disabled", + ); + }); + + cy.get('[aria-label="service-description"').should( + "have.text", + "Service entity with many default attributes.", + ); + + //assert that core instance have all attributes, can't be removed + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("many-defaults") + .click(); + cy.get("button") + .contains("span", "Remove") + .parent() + .should("be.disabled"); + cy.get("input").should("have.length", 21); + + //fill some of core attributes + + //strings + cy.get('[aria-label="TextInput-name"]').type("many-defaults"); + cy.get('[aria-label="TextInput-default_string"]').type( + "{backspace}default_string", + ); + cy.get('[aria-label="TextInput-default_empty_string"]').type( + "default_string_1", + ); + cy.get('[aria-label="TextInput-default_nullable_string"]').type( + "default_string_2", + ); + + //ints + cy.get('[aria-label="TextInput-default_int"]').type("{backspace}20"); + cy.get('[aria-label="TextInput-default_empty_int"]').type( + "{backspace}30", + ); + cy.get('[aria-label="TextInput-default_nullable_int"]').type( + "{backspace}40", + ); + + //floats + cy.get('[aria-label="TextInput-default_float"]').type("{backspace}2.0"); + cy.get('[aria-label="TextInput-default_empty_float"]').type( + "{backspace}3.0", + ); + cy.get('[aria-label="TextInput-default_nullable_float"]').type( + "{backspace}4.0", + ); + + //booleans + cy.get('[aria-label="BooleanToggleInput-default_bool"]').within(() => { + cy.get(".pf-v6-c-switch").click(); + }); + cy.get('[aria-label="BooleanToggleInput-default_empty_bool"]').within( + () => { + cy.get(".pf-v6-c-switch").click(); + }, + ); + cy.get("#default_nullable_bool-false").click(); + + //Dict values + cy.get('[aria-label="TextInput-default_dict"]').type( + '{selectAll}{backspace}{{}"test":"value"{}}', + ); + cy.get('[aria-label="TextInput-default_empty_dict"]').type( + '{selectAll}{backspace}{{}"test1":"value1"{}}', + ); + cy.get('[aria-label="TextInput-default_nullable_dict"]').type( + '{{}"test2":"value2"{}}', + ); + + //assert that embedded instance have all attributes, this particular embedded entity can't be removed but can be edited + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("embedded") + .click(); + cy.get("button") + .contains("span", "Remove") + .parent() + .should("be.disabled"); + cy.get("input").should("have.length", 21); + + //fill some of embedded attributes, they are exactly the same as core attributes so we need to check only one fully, as the logic is the same + + //strings + cy.get('[aria-label="TextInput-name"]').type("embedded"); + cy.get('[aria-label="TextInput-default_string"]').type( + "{backspace}default_string", + ); + cy.get('[aria-label="TextInput-default_empty_string"]').type( + "default_string_1", + ); + cy.get('[aria-label="TextInput-default_nullable_string"]').type( + "default_string_2", + ); + + //ints + cy.get('[aria-label="TextInput-default_int"]').type("{backspace}21"); + cy.get('[aria-label="TextInput-default_empty_int"]').type("{backspace}1"); + cy.get('[aria-label="TextInput-default_nullable_int"]').type( + "{backspace}41", + ); + + //floats + cy.get('[aria-label="TextInput-default_float"]').type("{backspace}2.1"); + cy.get('[aria-label="TextInput-default_empty_float"]').type( + "{backspace}3.1", + ); + cy.get('[aria-label="TextInput-default_nullable_float"]').type( + "{backspace}4.1", + ); + + //Drag extra_embedded onto canvas and assert that is highlighted as loose element + cy.get('[aria-labelledby="bodyTwo_extra_embedded"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 800, + clientY: 500, + }) + .trigger("mouseup"); + + cy.get(".joint-loose_element-highlight").should("be.visible"); + + //assert that extra_embedded instance have all attributes, can be removed + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("extra_embedded") + .click(); + cy.get("button").contains("span", "Remove").parent().should("be.enabled"); + cy.get("input").should("have.length", 21); + + //remove extra_embedded instance to simulate that user added that by a mistake yet want to remove it + cy.get("button").contains("Remove").click(); + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("extra_embedded") + .should("not.exist"); + + //Drag once again extra_embedded onto canvas and assert that is highlighted as loose element + cy.get('[aria-labelledby="bodyTwo_extra_embedded"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 800, + clientY: 500, + }) + .trigger("mouseup"); + + cy.get(".joint-loose_element-highlight").should("be.visible"); + + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("extra_embedded") + .click(); + //fill some of embedded attributes, they are exactly the same as core attributes so we need to check only one fully, as the logic is the same + + //strings + cy.get('[aria-label="TextInput-name"]').type("extra_embedded"); + cy.get('[aria-label="TextInput-default_string"]').type( + "{backspace}default_string", + ); + cy.get('[aria-label="TextInput-default_empty_string"]').type( + "default_string_1", + ); + cy.get('[aria-label="TextInput-default_nullable_string"]').type( + "default_string_2", + ); + + //ints + cy.get('[aria-label="TextInput-default_int"]').type("{backspace}21"); + cy.get('[aria-label="TextInput-default_empty_int"]').type( + "{backspace}31", + ); + cy.get('[aria-label="TextInput-default_nullable_int"]').type( + "{backspace}41", + ); + + //floats + cy.get('[aria-label="TextInput-default_float"]').type("{backspace}2.1"); + cy.get('[aria-label="TextInput-default_empty_float"]').type( + "{backspace}3.1", + ); + cy.get('[aria-label="TextInput-default_nullable_float"]').type( + "{backspace}4.1", + ); + + //connect core instance with extra_embedded instance + cy.get('[data-name="fit-to-screen"]').click(); + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("many-defaults") + .click(); + cy.get('[data-action="link"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 800, + clientY: 700, + }) + .trigger("mouseup"); + + cy.get('[data-type="Link"]').should("have.length", 2); + cy.get(".joint-loose_element-highlight").should("not.exist"); + + //add parent instance to the canvas and connect it to the core instance + cy.get("#inventory-tab").click(); + + cy.get('[aria-labelledby="bodyTwo_test_name"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 800, + clientY: 500, + }) + .trigger("mouseup"); + + //highlighted loose element should be visible + cy.get(".joint-loose_element-highlight").should("be.visible"); + + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("many-defaults") + .click(); + cy.get('[data-action="link"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 800, + clientY: 500, + }) + .trigger("mouseup"); + + //highlighted loose element should be removed + cy.get(".joint-loose_element-highlight").should("not.exist"); + + cy.get('[data-type="Link"]').should("have.length", 3); + + //add another parent instance to the canvas and connect it to the embedded instance + cy.get('[data-name="fit-to-screen"]').click(); + + cy.get('[aria-labelledby="bodyTwo_test_name2"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 600, + clientY: 400, + }) + .trigger("mouseup"); + cy.get('[data-name="fit-to-screen"]').click(); + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("embedded") + .click(); + cy.get('[data-action="link"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 600, + clientY: 400, + }) + .trigger("mouseup"); + cy.get('[data-type="Link"]').should("have.length", 4); + + cy.get("button").contains("Deploy").click(); + + //assert that many-defaults is deployed and up + + cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); + // await until parent_service is deployed and up + cy.get('[data-label="State"]', { timeout: 90000 }).should( + "have.text", + "up", + ); + + //check if embedded entities are present and relations are assigned correctly + cy.get('[aria-label="instance-details-link"]', { timeout: 20000 }) + .first() + .click(); + + //check if embedded entities are present and relations are assigned correctly + cy.get('[aria-label="Expand row 27"]').click(); //toggle extra_embedded + cy.get('[aria-label="Expand row 28"]').click(); //toggle fist entity of extra_embedded + + cy.get('[aria-label="parent_service_value"]') + .invoke("text") + .then((text) => { + expect(text).to.match(uuidRegex); + }); + + cy.get('[aria-label="embedded.parent_service_value"]') + .invoke("text") + .then((text) => { + expect(text).to.match(uuidRegex); + }); + cy.get('[aria-label="extra_embedded.0.parent_service_value"]').should( + "have.text", + "null", + ); + }); + + it("8.3 composer edit view can perform it's required functions and deploy edited instance", () => { + // Select 'test' environment + cy.visit("/console/"); + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") + .click(); + // click on Show Inventory of many-defaults service, expect no instances + cy.get("#many-defaults", { timeout: 60000 }) + .contains("Show inventory") + .click(); + + cy.get('[aria-label="ServiceInventory-Success"]').should("to.be.visible"); + cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); + // click on edit instance with composer + cy.get('[aria-label="row actions toggle"]').click(); + cy.get("button").contains("Edit in Composer").click(); + + // Expect Canvas to be visible + cy.get(".canvas").should("be.visible"); + + //assert if default entities are present, on init on the canvas we should have already basic required structure for the service instance + cy.get('[data-type="app.ServiceEntityBlock"]').should("have.length", 5); + cy.get('[data-type="Link"').should("have.length", 4); + + cy.get('[data-type="app.ServiceEntityBlock"]') + .contains("embedded") + .should("be.visible"); + cy.get('[data-type="app.ServiceEntityBlock"]') + .contains("extra_embedded") + .should("be.visible"); + cy.get('[data-type="app.ServiceEntityBlock"]') + .contains("many-defaults") + .should("be.visible"); + + //related Services should be disabled from removing + cy.get('[data-testid="header-parent-service"]').should("have.length", 2); + + cy.get('[data-testid="header-parent-service"]').eq(0).click(); + cy.get("button") + .contains("span", "Remove") + .parent() + .should("be.disabled"); + + cy.get('[data-testid="header-parent-service"]').eq(1).click(); + cy.get("button") + .contains("span", "Remove") + .parent() + .should("be.disabled"); + + //edit some of core attributes + cy.get('[data-type="app.ServiceEntityBlock"]') + .contains("many-defaults") + .click(); + + cy.get('[aria-label="TextInput-default_string"]').type( + "{selectAll}{backspace}updated_string", + ); + cy.get('[aria-label="TextInput-default_int"]').type("{backspace}2"); + cy.get('[aria-label="TextInput-default_float"]').type("{backspace}2"); + cy.get('[aria-label="BooleanToggleInput-default_bool"]').within(() => { + cy.get(".pf-v6-c-switch").click(); + }); + cy.get('[aria-label="TextInput-default_empty_dict"]').type( + '{selectAll}{backspace}{{}"test2":"value2"{}}', + ); + + //edit some of embedded attributes + cy.get('[data-type="app.ServiceEntityBlock"]') + .contains("embedded") + .click(); + cy.get("button") + .contains("span", "Remove") + .parent() + .should("be.disabled"); + + cy.get('[aria-label="TextInput-default_string"]').type( + "{selectAll}{backspace}updated_string", + ); + cy.get('[aria-label="TextInput-default_int"]').type("{backspace}2"); + cy.get('[aria-label="TextInput-default_float"]').type("{backspace}2"); + cy.get('[aria-label="BooleanToggleInput-default_bool"]').within(() => { + cy.get(".pf-v6-c-switch").click(); + }); + cy.get('[aria-label="TextInput-default_empty_dict"]').type( + '{selectAll}{backspace}{{}"test2":"value2"{}}', + ); + + //remove extra_embedded instance + cy.get('[data-type="app.ServiceEntityBlock"]') + .contains("extra_embedded") + .click(); + cy.get("button").contains("Remove").click(); + + cy.get("button").contains("Deploy").click(); + + //assert that many-defaults is deployed and up + cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); + // await until parent_service is deployed and up + cy.get('[data-label="State"]', { timeout: 90000 }).should( + "have.text", + "up", + ); + + cy.get('[aria-label="ServiceInventory-Success"]').should("to.be.visible"); + //assert that many-defaults is deployed and up + cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); + // await until parent_service is deployed and up + cy.get('[data-label="State"]', { timeout: 90000 }).should( + "have.text", + "up", + ); + + //check if core attributes and embedded entities are updated + cy.get('[aria-label="instance-details-link"]', { timeout: 20000 }) + .first() + .click(); + + //Go to candidate attributes and assert that they are updated + cy.get('[aria-label="Select-AttributeSet"]').select( + "candidate_attributes", + ); + cy.get('[aria-label="default_int_value"]').should("have.text", "22"); + + cy.get('[aria-label="default_string_value"]').should( + "have.text", + "updated_string", + ); + + cy.get('[aria-label="Expand row 1"]').click(); //toggle embedded + + cy.get('[aria-label="embedded.default_int_value"]').should( + "have.text", + "22", + ); + cy.get('[aria-label="embedded.default_float_value"]').should( + "have.text", + "2", + ); + cy.get('[aria-label="embedded.default_string_value"]').should( + "have.text", + "updated_string", + ); + + cy.get('[aria-label="extra_embedded_value"]').should("have.text", "[]"); + }); + + it("8.4 composer edit view is able to edit instances relations", () => { + // Select 'test' environment + cy.visit("/console/"); + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") + .click(); + // click on Show Inventory of many-defaults service, expect no instances + cy.get("#child-service", { timeout: 60000 }) + .contains("Show inventory") + .click(); + cy.get('[aria-label="ServiceInventory-Empty"]').should("to.be.visible"); + // click on add instance with composer + cy.get('[aria-label="AddInstanceToggle"]').click(); + cy.get("#add-instance-composer-button").click(); + + // Expect Canvas to be visible + cy.get(".canvas").should("be.visible"); + + //Assert error message is visible as there is missing relation + cy.get('[data-testid="Error-container"]').should("be.visible"); + cy.get('[data-testid="Error-container"]').should( + "have.text", + "Danger alert:Errors found: 1", + ); + cy.get('[aria-label="Danger alert details"]').click(); + cy.get( + '[aria-label="missingRelationsParagraph-child-service_parent-service_0"]', + ).should( + "have.text", + "Expected at least 1 parent-service inter-service relation(s) for child-service", + ); + + //assert if default entities are present, on init on the canvas we should have already basic required structure for the service instance + cy.get('[data-type="app.ServiceEntityBlock"').should("have.length", 1); + + cy.get('[data-type="app.ServiceEntityBlock"]').click(); + cy.get("button") + .contains("span", "Remove") + .parent() + .should("be.disabled"); + + cy.get('[aria-label="TextInput-name"]').type("test_child"); + cy.get('[aria-label="TextInput-service_id"]').type("test_child_id"); + + //add parent instance to the canvas and connect it to the core instance + cy.get("#inventory-tab").click(); + + cy.get('[aria-labelledby="bodyTwo_test_name"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 800, + clientY: 600, + }) + .trigger("mouseup"); + + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("child-service") + .click(); + cy.get('[data-action="link"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 800, + clientY: 600, + }) + .trigger("mouseup"); + + cy.get('[data-type="Link"]').should("have.length", 1); + + cy.get("button").contains("Deploy").click(); + cy.get('[aria-label="InstanceRow-Intro"]').should("have.length", 1); + // await until parent_service is deployed and up + cy.get('[data-label="State"]', { timeout: 90000 }).should( + "have.text", + "up", + ); + + //check if relation is assigned correctly + cy.get('[aria-label="instance-details-link"]', { timeout: 20000 }) + .first() + .click(); + + cy.get('[aria-label="parent_entity_value"]') + .invoke("text") + .then((text) => { + expect(text).to.match(uuidRegex); + }); + + // click on edit instance with composer + cy.get('[aria-label="Actions-Toggle"]').click(); + cy.get("button").contains("Edit in Composer").click(); + + // Expect Canvas to be visible + cy.get(".canvas").should("be.visible"); + + //assert if default entities are present, on init on the canvas we should have already basic required structure for the service instance + cy.get('[data-type="app.ServiceEntityBlock"]').should("have.length", 2); + cy.get('[data-type="Link"').should("have.length", 1); + + cy.get('[data-type="app.ServiceEntityBlock"]') + .contains("parent-service") + .click(); + cy.get("button").contains("Remove").click(); + + //add parent instance to the canvas and connect it to the core instance + cy.get("#inventory-tab").click(); + + //click on core instance to focus canvas view near it + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("child-service") + .click(); + + cy.get('[aria-labelledby="bodyTwo_test_name2"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 800, + clientY: 500, + }) + .trigger("mouseup"); + + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("child-service") + .click(); + cy.get('[data-action="link"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 800, + clientY: 500, + }) + .trigger("mouseup"); + + cy.get('[data-testid="Error-container"]').should("not.exist"); + + cy.get("button").contains("Deploy").click(); + + // await until parent_service is deployed and up + cy.get('[data-label="State"]', { timeout: 90000 }).should( + "have.text", + "up", + ); + + //check if relation is updated correctly + cy.get('[aria-label="instance-details-link"]', { timeout: 20000 }) + .first() + .click(); + + //Make sure we are at the active attirubtes + cy.get('[aria-label="Select-AttributeSet"]').select("active_attributes"); + + cy.get('[aria-label="parent_entity_value"]') + .invoke("text") + .then((text) => { + expect(text).to.match(uuidRegex); + }); + + cy.get("#Compare").click(); + + cy.get('[aria-label="left-side-attribute-set-select"]').select( + "active_attributes", + ); + cy.get('[aria-label="right-side-version-select"]').select("4"); + cy.wait(500); + cy.get(".editor.original").within(() => { + cy.get(".mtk5") + .invoke("text") + .then((text) => { + cy.wrap(text).as("newUuid"); + }); + }); + + cy.get(".editor.modified").within(() => { + cy.get(".mtk5") + .invoke("text") + .then((text) => { + cy.wrap(text).as("oldUuid"); + }); + }); + + cy.then(function () { + expect(this.oldUuid).to.not.be.equal(this.newUuid); + }); + }); + + it("8.5 composer edit view is able to remove inter-service relation from instance", () => { + // Select 'test' environment + cy.visit("/console/"); + + cy.get(`[aria-label="Select-environment-test"]`).click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Service Catalog") + .click(); + + // click on Show Inventory of many-defaults service, expect no instances + cy.get("#child-with-many-parents-service", { timeout: 60000 }) + .contains("Show inventory") + .click(); + cy.get('[aria-label="ServiceInventory-Empty"]').should("to.be.visible"); + // click on add instance with composer + cy.get('[aria-label="AddInstanceToggle"]').click(); + cy.get("#add-instance-composer-button").click(); + + //fill child attributes + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("child-with-many") + .click(); + cy.get('[aria-label="TextInput-name"]').type("child_test"); + cy.get('[aria-label="TextInput-service_id"]').type("child_test_id"); + + //add parent instances to the canvas and connect them to the core instance + cy.get("#inventory-tab").click(); + + //add first inter-service relation + cy.get('[aria-labelledby="bodyTwo_test_name"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 800, + clientY: 500, + }) + .trigger("mouseup"); + + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("child-with-many") + .click(); + cy.get('[data-action="link"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 800, + clientY: 600, + }) + .trigger("mouseup"); + + //add second inter-service relation + cy.get('[aria-labelledby="bodyTwo_test_name2"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 800, + clientY: 700, + }) + .trigger("mouseup"); + + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("child-with-many") + .click(); + cy.get('[data-action="link"]') + .trigger("mouseover") + .trigger("mousedown") + .trigger("mousemove", { + clientX: 800, + clientY: 800, + }) + .trigger("mouseup"); + + cy.get('[data-type="Link"]').should("have.length", 2); + + cy.get("button").contains("Deploy").click(); + + cy.get('[data-label="State"]', { timeout: 90000 }) + .eq(0, { timeout: 90000 }) + .should("have.text", "up", { timeout: 90000 }); + + // click on edit instance with composer + cy.get('[aria-label="row actions toggle"]').click(); + cy.get("button").contains("Edit in Composer").click(); + + cy.get('[data-type="app.ServiceEntityBlock"') + .contains("test_name2") + .click(); + + cy.get("button").contains("Remove").click(); + cy.get("button").contains("Deploy").click(); + + cy.get('[data-label="State"]', { timeout: 90000 }) + .eq(0, { timeout: 90000 }) + .should("have.text", "up", { timeout: 90000 }); + + //go to details view + cy.get('[aria-label="instance-details-link"]', { timeout: 20000 }) + .first() + .click(); + + //assert that in Active attribute we have only 1 relation + cy.get('[aria-label="Expand row 2"]').click(); + + cy.get('[data-testid="0"]') + .invoke("text") + .then((text) => { + expect(text).to.match(uuidRegex); + }); + + cy.get('[data-testid="1"]').should("not.exist"); + + //assert that in Rollback attribute we have 2 relations + cy.get('[aria-label="Select-AttributeSet"]').select( + "rollback_attributes", + ); + + cy.get('[data-testid="0"]') + .invoke("text") + .then((text) => { + expect(text).to.match(uuidRegex); + }); + + cy.get('[data-testid="1"]') + .invoke("text") + .then((text) => { + expect(text).to.match(uuidRegex); + }); + }); + }); +} diff --git a/cypress/e2e/scenario-9-orders.cy.js b/cypress/e2e/scenario-9-orders.cy.js index f05e4edfc..df42f404d 100644 --- a/cypress/e2e/scenario-9-orders.cy.js +++ b/cypress/e2e/scenario-9-orders.cy.js @@ -4,9 +4,9 @@ * * @param {string} nameEnvironment */ -const clearEnvironment = (nameEnvironment = "lsm-frontend") => { +const clearEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -45,9 +45,9 @@ const checkStatusCompile = (id) => { * * @param {string} nameEnvironment */ -const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { +const forceUpdateEnvironment = (nameEnvironment = "test") => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); + cy.get(`[aria-label="Select-environment-${nameEnvironment}"]`).click(); cy.url().then((url) => { const location = new URL(url); const id = location.searchParams.get("env"); @@ -71,12 +71,12 @@ if (Cypress.env("edition") === "iso") { it("Displays a Partial order with multiple dependencies correctly", () => { cy.visit("/console/"); - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") - .click(); + cy.get(`[aria-label="Select-environment-test"]`).click(); // go to the Orders page - cy.get(".pf-v5-c-nav__link").contains("Orders").click(); + cy.get('[aria-label="Sidebar-Navigation-Item"]') + .contains("Orders") + .click(); // it shouldn't have any orders yet cy.get('[aria-label="ServiceOrderRow"]').should("not.to.exist"); diff --git a/cypress/e2e/scenario-Z-update.cy.js b/cypress/e2e/scenario-Z-update.cy.js deleted file mode 100644 index a372b67b0..000000000 --- a/cypress/e2e/scenario-Z-update.cy.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Shorthand method to clear the environment being passed. - * By default, if no arguments are passed it will target the 'lsm-frontend' environment. - * - * @param {string} nameEnvironment - */ -const clearEnvironment = (nameEnvironment = "lsm-frontend") => { - cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); - cy.url().then((url) => { - const location = new URL(url); - const id = location.searchParams.get("env"); - - cy.request("DELETE", `/api/v1/decommission/${id}`); - }); -}; - -/** - * based on the environment id, it will recursively check if a compile is pending. - * It will continue the recursion as long as the statusCode is equal to 200 - * - * @param {string} id - */ -const checkStatusCompile = (id) => { - let statusCodeCompile = 200; - - if (statusCodeCompile === 200) { - cy.intercept(`/api/v1/notify/${id}`).as("IsCompiling"); - // the timeout is necessary to avoid errors. - // Cypress doesn't support while loops and this was the only workaround to wait till the statuscode is not 200 anymore. - // the default timeout in cypress is 5000, but since we have recursion it goes into timeout for the nested awaits because of the recursion. - cy.wait("@IsCompiling").then((req) => { - statusCodeCompile = req.response.statusCode; - - if (statusCodeCompile === 200) { - checkStatusCompile(id); - } - }); - } -}; - -/** - * Will by default execute the force update on the 'lsm-frontend' environment if no argumenst are being passed. - * This method can be executed standalone, but is part of the cleanup cycle that is needed before running a scenario. - * - * @param {string} nameEnvironment - */ -const forceUpdateEnvironment = (nameEnvironment = "lsm-frontend") => { - cy.visit("/console/"); - cy.get('[aria-label="Environment card"]').contains(nameEnvironment).click(); - cy.url().then((url) => { - const location = new URL(url); - const id = location.searchParams.get("env"); - - cy.request({ - method: "POST", - url: `/lsm/v1/exporter/export_service_definition`, - headers: { "X-Inmanta-Tid": id }, - body: { force_update: true }, - }); - checkStatusCompile(id); - }); -}; - -if (Cypress.env("edition") === "iso") { - describe("Scenario 2.4 Service Catalog - update", () => { - before(() => { - clearEnvironment(); - forceUpdateEnvironment(); - }); - it("2.4.1 Add Instance Cancel form", () => { - // Go from Home page to Service Inventory of Basic-service - cy.visit("/console/"); - //open Environment - cy.get('[aria-label="Environment card"]') - .contains("lsm-frontend") - .click(); - cy.get(".pf-v5-c-nav__item").contains("Service Catalog").click(); - cy.get('[aria-label="ServiceCatalog-Success"]').should("to.be.visible"); - //click update button then cancel popup - cy.get("button").contains("Update Service Catalog").click(); - cy.get(".pf-v5-c-modal-box").should("to.be.visible"); - cy.get("#cancel").click(); - cy.get(".pf-v5-c-alert").should("to.not.exist"); - //click update button and confirm the popup - cy.get("button").contains("Update Service Catalog").click(); - cy.get("#submit").click(); - cy.get(".pf-v5-c-alert") - .contains("The update has been requested") - .should("to.be.visible"); - //find newest compile report - cy.get(".pf-v5-c-nav__link").contains("Compile Reports").click(); - cy.get('[aria-label="Compile Reports Table Row"]') - .eq(0) - .find('[data-label="Message"]') - .should("have.text", "Recompile model to export service definition"); - }); - }); -} diff --git a/package.json b/package.json index 56abb99c1..718e9f31f 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,11 @@ { "name": "@inmanta/web-console", - "version": "2.0.0", + "version": "2.1.0", "description": "Web Console for Inmanta Orchestrator", "exports": "./dist/index.js", "repository": "https://github.com/inmanta/web-console.git", "license": "Apache-2.0", "scripts": { - "prepare": "husky install", "prebuild": "yarn clean", "build": "webpack --config webpack.prod.cjs", "start": "webpack serve --hot --color --progress --config webpack.dev.cjs", @@ -21,10 +20,10 @@ "clean": "rimraf dist", "delete:reports": "rm -r cypress/reports/* || true", "precypress-test": "yarn run delete:reports", - "cypress-test:oss": "yarn cypress run --env edition=oss,project=oss-frontend", + "cypress-test:oss": "yarn cypress run --env edition=oss", "cypress-test:iso": "yarn cypress run", - "cypress-test:keycloak": "yarn cypress run --env edition=oss,project=oss-frontend,keycloak=true --config baseUrl='https://127.0.0.1:8888' --spec 'cypress/e2e/scenario-7.1-keycloak.cy.js'", - "cypress-test:local-auth": "yarn cypress run --env edition=oss,project=oss-frontend,local-auth=true --config baseUrl='https://127.0.0.1:8888' --spec 'cypress/e2e/scenario-7.2-local-auth.cy.js,cypress/e2e/scenario-7.3-user-management.cy.js'", + "cypress-test:keycloak": "yarn cypress run --env edition=oss,keycloak=true --config baseUrl='https://127.0.0.1:8888' --spec 'cypress/e2e/scenario-7.1-keycloak.cy.js'", + "cypress-test:local-auth": "yarn cypress run --env edition=oss,local-auth=true --config baseUrl='https://127.0.0.1:8888' --spec 'cypress/e2e/scenario-7.2-local-auth.cy.js,cypress/e2e/scenario-7.3-user-management.cy.js'", "package-cleanup": "node clean_up_packages", "check-circular-deps": "madge --circular ./src/index.tsx", "install:orchestrator:keycloak": "sh ./shell-scripts/setup-auth.sh version='oss' release='dev' branch='oss_case_1'", @@ -34,7 +33,7 @@ "install:orchestrator:oss": "sh ./shell-scripts/setup-orchestrator.sh flag='-ti' version='oss' release='dev' branch='oss_case_1'", "install:orchestrator:ci": "sh ./shell-scripts/setup-orchestrator.sh", "kill-server": "sh ./shell-scripts/clear-server.sh", - "update:dist": "docker exec inmanta_orchestrator rm -rf /usr/share/inmanta/web-console && docker cp dist inmanta_orchestrator:/usr/share/inmanta/web-console" + "update:dist": "docker exec -u root inmanta_orchestrator rm -rf /usr/share/inmanta/web-console && docker cp dist inmanta_orchestrator:/usr/share/inmanta/web-console" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ @@ -50,37 +49,38 @@ ], "type": "module", "devDependencies": { - "@babel/core": "^7.25.2", - "@babel/plugin-transform-typescript": "^7.25.2", - "@eslint/compat": "^1.1.1", + "@babel/core": "^7.26.0", + "@babel/plugin-transform-typescript": "^7.26.3", + "@eslint/compat": "^1.2.2", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.10.0", + "@eslint/js": "^9.14.0", "@stylistic/eslint-plugin-ts": "^2.6.1", "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.4.8", - "@testing-library/react": "^15.0.7", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", - "@types/backbone": "^1.4.19", + "@types/backbone": "^1.4.22", "@types/fetch-mock": "^7.3.8", "@types/file-saver": "^2.0.7", "@types/jest": "^29.5.12", "@types/jest-axe": "^3.5.9", "@types/json-bigint": "^1.0.4", "@types/loadable__component": "^5.13.9", - "@types/lodash": "^4.17.7", + "@types/lodash": "^4.17.13", "@types/lodash-es": "^4.17.12", + "@types/markdown-it-emoji": "^3", "@types/node": "^22.5.0", - "@types/qs": "^6.9.15", - "@types/react-dom": "^18.3.0", + "@types/qs": "^6.9.17", + "@types/react-dom": "^18.3.5", "@types/react-router-dom": "^5.3.3", "@types/react-syntax-highlighter": "^15.5.13", - "@types/react-test-renderer": "^18.3.0", + "@types/react-test-renderer": "^18.3.1", "@types/styled-components": "^5.1.26", - "@types/uuid": "^9", + "@types/uuid": "^10", "@types/webpack": "^5.28.5", - "@typescript-eslint/eslint-plugin": "^8.1.0", - "@typescript-eslint/parser": "^8.2.0", - "babel-loader": "^9.1.3", + "@typescript-eslint/eslint-plugin": "^8.13.0", + "@typescript-eslint/parser": "^8.13.0", + "babel-loader": "^9.2.1", "clean-css": "^5.3.3", "copy-webpack-plugin": "^12.0.2", "css-loader": "^7.1.2", @@ -89,22 +89,21 @@ "cypress-file-upload": "^5.0.8", "cypress-multi-reporters": "^1.6.4", "dotenv-webpack": "^8.1.0", - "eslint": "^9.0.0", + "eslint": "^9.14.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.3", - "eslint-import-resolver-webpack": "^0.13.8", - "eslint-plugin-import": "^2.29.1", + "eslint-import-resolver-webpack": "^0.13.9", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest-dom": "^5.4.0", "eslint-plugin-prettier": "5.2.1", - "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-testing-library": "^6.3.0", - "fetch-mock": "^11.1.3", + "eslint-plugin-testing-library": "^6.4.0", + "fetch-mock": "^11.1.5", "file-loader": "^6.2.0", "git-revision-webpack-plugin": "^5.0.0", "globals": "^15.9.0", "html-webpack-plugin": "^5.6.0", - "husky": "^9.1.4", "imagemin": "^9.0.0", "jest": "^29.7.0", "jest-axe": "^9.0.0", @@ -113,8 +112,9 @@ "jest-junit": "^16.0.0", "lint-staged": "^15.2.10", "madge": "^6.1.0", + "markdown-it-emoji": "^3.0.0", "mini-css-extract-plugin": "^2.9.0", - "mocha": "^10.7.3", + "mocha": "^10.8.2", "mocha-junit-reporter": "^2.2.1", "mochawesome": "^7.1.3", "mochawesome-report-generator": "^6.2.0", @@ -132,12 +132,12 @@ "ts-prune": "^0.10.3", "tsconfig-paths-webpack-plugin": "^4.1.0", "typescript": "^5.4.5", - "typescript-eslint": "^8.0.1", + "typescript-eslint": "^8.13.0", "url-loader": "^4.1.1", "webpack": "^5.94.0", "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.1.0", + "webpack-dev-server": "^5.2.0", "webpack-merge": "^6.0.1", "webpack-version-file": "^0.1.7" }, @@ -149,12 +149,12 @@ "@joint/layout-directed-graph": "^4.0.1", "@loadable/component": "^5.16.3", "@monaco-editor/react": "^4.6.0", - "@patternfly/react-charts": "7.3.0", - "@patternfly/react-core": "^5.2.2", - "@patternfly/react-icons": "^5.4.0", - "@patternfly/react-styles": "^5.3.1", - "@patternfly/react-table": "^5.4.0", - "@patternfly/react-tokens": "^5.3.1", + "@patternfly/react-charts": "^7.2.2", + "@patternfly/react-core": "^6.0.0", + "@patternfly/react-icons": "^6.0.0", + "@patternfly/react-styles": "^6.0.0", + "@patternfly/react-table": "^6.0.0", + "@patternfly/react-tokens": "^6.0.0", "@react-keycloak/web": "^3.4.0", "@tanstack/react-query": "^5.37.1", "@tanstack/react-query-devtools": "^5.56.2", @@ -200,10 +200,10 @@ "loader-utils": "2.0.4 || 1.4.2", "micromatch": "^4.0.8", "minimist": "^1.2.6", + "nanoid": "^3.3.8", "node-forge": "^1.0.0", "nth-check": "^2.0.1", "path-to-regexp@^6.2.0": "8.0.0", - "path-to-regexp@0.1.7": "0.1.10", "prismjs": "^1.25.0", "simple-git": "^3.5.0", "terser-webpack-plugin": "^1.4.5", diff --git a/public/images/inmanta-wings.svg b/public/images/inmanta-wings.svg new file mode 100644 index 000000000..162bbd972 --- /dev/null +++ b/public/images/inmanta-wings.svg @@ -0,0 +1,109 @@ + + + +image/svg+xml + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/logo.svg b/public/images/logo.svg index 2f7cb355b..230d7aa42 100644 --- a/public/images/logo.svg +++ b/public/images/logo.svg @@ -1,9 +1,103 @@ - - - - - - - - - \ No newline at end of file + + + +image/svg+xml \ No newline at end of file diff --git a/shell-scripts/setup-orchestrator.sh b/shell-scripts/setup-orchestrator.sh index 82f1e6969..b2fa8c390 100644 --- a/shell-scripts/setup-orchestrator.sh +++ b/shell-scripts/setup-orchestrator.sh @@ -70,7 +70,7 @@ cd ../.. echo "Replace dist folder on the orchestrator with current build..." # remove old build output and copy dist to the the orchestrator -docker exec inmanta_orchestrator rm -rf /usr/share/inmanta/web-console +docker exec -u root inmanta_orchestrator rm -rf /usr/share/inmanta/web-console docker cp dist inmanta_orchestrator:/usr/share/inmanta/web-console diff --git a/src/Core/Command/Command.ts b/src/Core/Command/Command.ts index 1828ebb6c..9da15ed7b 100644 --- a/src/Core/Command/Command.ts +++ b/src/Core/Command/Command.ts @@ -10,10 +10,6 @@ import { DeleteService, DeleteServiceManifest, } from "@/Data/Managers/DeleteService/interface"; -import { - DeleteVersion, - DeleteVersionManifest, -} from "@/Data/Managers/DeleteVersion/interface"; import { Deploy, DeployManifest } from "@/Data/Managers/Deploy/interface"; import { DestroyInstance, @@ -47,10 +43,6 @@ import { ModifyEnvironment, ModifyEnvironmentManifest, } from "@/Data/Managers/ModifyEnvironment/interface"; -import { - PromoteVersion, - PromoteVersionManifest, -} from "@/Data/Managers/PromoteVersion/interface"; import { Repair, RepairManifest } from "@/Data/Managers/Repair/interface"; import { ResumeEnvironment, @@ -107,13 +99,11 @@ export type Command = | DeleteInstance | DestroyInstance | DeleteService - | DeleteVersion | Deploy | GenerateToken | GetSupportArchive | HaltEnvironment | ModifyEnvironment - | PromoteVersion | UpdateCatalog | Repair | ResetEnvironmentSetting @@ -147,13 +137,11 @@ interface Manifest { DeleteInstance: DeleteInstanceManifest; DestroyInstance: DestroyInstanceManifest; DeleteService: DeleteServiceManifest; - DeleteVersion: DeleteVersionManifest; Deploy: DeployManifest; GenerateToken: GenerateTokenManifest; GetSupportArchive: GetSupportArchiveManifest; HaltEnvironment: HaltEnvironmentManifest; ModifyEnvironment: ModifyEnvironmentManifest; - PromoteVersion: PromoteVersionManifest; UpdateCatalog: UpdateCatalogManifest; Repair: RepairManifest; ResetEnvironmentSetting: ResetEnvironmentSettingManifest; diff --git a/src/Core/Domain/AttributeValidation.ts b/src/Core/Domain/AttributeValidation.ts index e7d1dd191..93c78a9e8 100644 --- a/src/Core/Domain/AttributeValidation.ts +++ b/src/Core/Domain/AttributeValidation.ts @@ -41,10 +41,14 @@ interface IntValidation { interface IpValidation { validation_type: + | "pydantic.IPvAnyInterface" + | "pydantic.IPvAnyAddress" | "ipaddress.IPv4Address" | "ipaddress.IPv4Address?" | "ipaddress.IPv4Interface" | "pydantic.constr[]" - | "ipaddress.IPv4Network"; + | "ipaddress.IPv4Network" + | "pydantic.PositiveInt?" + | "pydantic.PositiveInt"; validation_parameters: { [key: string]: string } | null; } diff --git a/src/Core/Domain/Field.ts b/src/Core/Domain/Field.ts index c7fb491ef..73e88f3ad 100644 --- a/src/Core/Domain/Field.ts +++ b/src/Core/Domain/Field.ts @@ -22,7 +22,8 @@ export type FieldLikeWithFormState = Field; interface BaseField { name: string; - description?: string; + description?: string | null; + id?: string; isOptional: boolean; isDisabled: boolean; suggestion?: FormSuggestion | null; @@ -71,14 +72,14 @@ export interface DictListField extends BaseField { kind: "DictList"; fields: Field[]; min: ParsedNumber; - max?: ParsedNumber; + max?: ParsedNumber | null; } export interface RelationListField extends BaseField { kind: "RelationList"; serviceEntity: string; min: ParsedNumber; - max?: ParsedNumber; + max?: ParsedNumber | null; } export interface InterServiceRelationField extends BaseField { diff --git a/src/Slices/ServiceInstanceHistory/Core/Domain.ts b/src/Core/Domain/HistoryLog.ts similarity index 78% rename from src/Slices/ServiceInstanceHistory/Core/Domain.ts rename to src/Core/Domain/HistoryLog.ts index 9d5208d20..d3123852f 100644 --- a/src/Slices/ServiceInstanceHistory/Core/Domain.ts +++ b/src/Core/Domain/HistoryLog.ts @@ -1,6 +1,6 @@ -import { InstanceEvent } from "@/Core/Domain/EventModel"; -import { InstanceAttributeModel } from "@/Core/Domain/ServiceInstanceModel"; -import { ParsedNumber } from "@/Core/Language"; +import { ParsedNumber } from "../Language"; +import { InstanceEvent } from "./EventModel"; +import { InstanceAttributeModel } from "./ServiceInstanceModel"; export interface InstanceLog { service_instance_id: string; diff --git a/src/Core/Domain/InventoryTable.ts b/src/Core/Domain/InventoryTable.ts index ded02bf05..65ed2ee20 100644 --- a/src/Core/Domain/InventoryTable.ts +++ b/src/Core/Domain/InventoryTable.ts @@ -5,6 +5,7 @@ import { Uuid } from "./Uuid"; export interface DateInfo { full: string; relative: string; + dateTimeMilliseconds: string; } export interface AttributesSummary { @@ -23,18 +24,17 @@ export interface Attributes { export interface Row { id: Uuid; - attributesSummary?: AttributesSummary; - attributes: Attributes; createdAt: string; updatedAt: string; version: ParsedNumber; - instanceSetStateTargets: string[]; service_entity: string; + state: string; environment: string; deploymentProgress?: DeploymentProgress | null; serviceIdentityValue?: string; deleted: boolean; - configDisabled?: boolean; + editDisabled: boolean; + deleteDisabled: boolean; } export interface State { diff --git a/src/Core/Domain/Route.ts b/src/Core/Domain/Route.ts index 4941ff122..6cfdc7d21 100644 --- a/src/Core/Domain/Route.ts +++ b/src/Core/Domain/Route.ts @@ -19,7 +19,6 @@ const kinds = [ "DuplicateInstance", "EditInstance", "Events", - "History", "InstanceDetails", "InstanceComposer", "InstanceComposerEditor", @@ -32,7 +31,6 @@ const kinds = [ /** * Resource Manager */ - "AgentProcess", "Agents", "DiscoveredResources", "Facts", @@ -81,7 +79,6 @@ export interface RouteKindWithId { * Only contains routes that have parameters (environment not included) */ interface RouteParamKeysManifest { - AgentProcess: "id"; CompileDetails: "id"; ComplianceCheck: "version"; CreateInstance: "service"; @@ -92,7 +89,6 @@ interface RouteParamKeysManifest { DuplicateInstance: "service" | "instance"; EditInstance: "service" | "instance"; Events: "service" | "instance"; - History: "service" | "instance"; InstanceDetails: "service" | "instance" | "instanceId"; InstanceComposer: "service"; InstanceComposerEditor: "service" | "instance"; diff --git a/src/Core/Domain/ServiceInstanceModel.ts b/src/Core/Domain/ServiceInstanceModel.ts index 9a7af34c4..6b78d566f 100644 --- a/src/Core/Domain/ServiceInstanceModel.ts +++ b/src/Core/Domain/ServiceInstanceModel.ts @@ -64,7 +64,7 @@ export interface ServiceInstanceModel service_entity_version?: ParsedNumber; desired_state_version?: ParsedNumber; transfer_context?: string; - metadata?: { [key: string]: string }; + metadata?: Record; } /** diff --git a/src/Core/Domain/ServiceModel.ts b/src/Core/Domain/ServiceModel.ts index f393f9278..e5bfa0f18 100644 --- a/src/Core/Domain/ServiceModel.ts +++ b/src/Core/Domain/ServiceModel.ts @@ -10,7 +10,7 @@ import { FormSuggestion } from "./ServiceInstanceModel"; export type AttributeModel = AttributeValidation & { name: string; type: string; - description?: string; + description?: string | null; modifier: string; default_value: | string @@ -108,15 +108,16 @@ export interface InstanceSummary { */ export interface ServiceModel extends ServiceIdentifier { environment: string; - description?: string; + description?: string | null; lifecycle: LifecycleModel; attributes: AttributeModel[]; service_identity?: string; - service_identity_display_name?: string; + service_identity_display_name?: string | null; + entity_annotations?: Record; config: Config; instance_summary?: InstanceSummary | null; embedded_entities: EmbeddedEntity[]; - inter_service_relations?: InterServiceRelation[]; + inter_service_relations: InterServiceRelation[]; strict_modifier_enforcement?: boolean; key_attributes?: string[] | null; owner: null | string; @@ -130,7 +131,7 @@ export interface ServiceModel extends ServiceIdentifier { */ export interface RelationAttribute { lower_limit: ParsedNumber; - upper_limit?: ParsedNumber; + upper_limit?: ParsedNumber | null; modifier: string; } @@ -139,7 +140,8 @@ export interface RelationAttribute { */ export interface InterServiceRelation extends RelationAttribute { name: string; - description?: string; + attribute_annotations?: Record; + description?: string | null; entity_type: string; } @@ -148,12 +150,13 @@ export interface InterServiceRelation extends RelationAttribute { */ export interface EmbeddedEntity extends RelationAttribute { name: string; - description?: string; + description?: string | null; attributes: AttributeModel[]; embedded_entities: EmbeddedEntity[]; - inter_service_relations?: InterServiceRelation[]; + inter_service_relations: InterServiceRelation[]; key_attributes?: string[] | null; attribute_annotations?: AttributeAnnotations; + entity_annotations?: Record; } /** @@ -161,7 +164,7 @@ export interface EmbeddedEntity extends RelationAttribute { */ interface MinimalEmbeddedEntity { name: string; - description?: string; + description?: string | null; attributes: Pick[]; inter_service_relations?: Pick< InterServiceRelation, diff --git a/src/Core/Query/Query.ts b/src/Core/Query/Query.ts index a97b559b9..eb570bca4 100644 --- a/src/Core/Query/Query.ts +++ b/src/Core/Query/Query.ts @@ -59,7 +59,6 @@ import { GetServices, GetServicesManifest, } from "@/Data/Managers/Services/interface"; -import * as GetAgentProcess from "@S/AgentProcess/Core/Query"; import * as GetAgents from "@S/Agents/Core/Query"; import * as GetCompileDetails from "@S/CompileDetails/Core/Query"; import * as GetCompileReports from "@S/CompileReports/Core/Query"; @@ -84,7 +83,6 @@ import * as GetResourceHistory from "@S/ResourceDetails/Core/GetResourceHistoryQ import * as GetResourceLogs from "@S/ResourceDetails/Core/GetResourceLogsQuery"; import * as GetDiscoveredResources from "@S/ResourceDiscovery/Core/Query"; import * as GetCallbacks from "@S/ServiceDetails/Core/GetCallbacksQuery"; -import * as GetInstanceLogs from "@S/ServiceInstanceHistory/Core/Query"; import * as GetEnvironmentDetails from "@S/Settings/Core/GetEnvironmentDetailsQuery"; import * as GetProjects from "@S/Settings/Core/GetProjectsQuery"; @@ -96,7 +94,6 @@ export type Query = | GetServiceConfig | GetInstanceResources | GetInstanceEvents.Query - | GetInstanceLogs.Query | GetInstanceConfig | GetMetrics.Query | GetDiagnostics.Query @@ -120,7 +117,6 @@ export type Query = | GetFacts.Query | GetResourceFacts.Query | GetAgents.Query - | GetAgentProcess.Query | GetDesiredStates.Query | GetVersionResources.Query | GetCompilerStatus @@ -147,7 +143,6 @@ interface Manifest { GetServiceConfig: GetServiceConfigManifest; GetInstanceResources: GetInstanceResourcesManifest; GetInstanceEvents: GetInstanceEvents.Manifest; - GetInstanceLogs: GetInstanceLogs.Manifest; GetInstanceConfig: GetInstanceConfigManifest; GetDiagnostics: GetDiagnostics.Manifest; GetDiscoveredResources: GetDiscoveredResources.Manifest; @@ -171,7 +166,6 @@ interface Manifest { GetEnvironmentsContinuous: GetEnvironmentsContinuousManifest; GetResourceFacts: GetResourceFacts.Manifest; GetAgents: GetAgents.Manifest; - GetAgentProcess: GetAgentProcess.Manifest; GetDesiredStates: GetDesiredStates.Manifest; GetVersionResources: GetVersionResources.Manifest; GetCompilerStatus: GetCompilerStatusManifest; diff --git a/src/Data/Common/utils.ts b/src/Data/Common/getConfigFromService.ts similarity index 74% rename from src/Data/Common/utils.ts rename to src/Data/Common/getConfigFromService.ts index c7146865e..edf144dfe 100644 --- a/src/Data/Common/utils.ts +++ b/src/Data/Common/getConfigFromService.ts @@ -1,7 +1,7 @@ import { uniq } from "lodash-es"; import { isNotNull, ServiceModel } from "@/Core"; -export function getOptionsFromService(service: ServiceModel): string[] { +export function getConfigFromService(service: ServiceModel): string[] { return uniq( service.lifecycle.transfers .map((transfer) => transfer.config_name) diff --git a/src/Data/Common/index.ts b/src/Data/Common/index.ts index 472a59f90..83ad85897 100644 --- a/src/Data/Common/index.ts +++ b/src/Data/Common/index.ts @@ -13,4 +13,4 @@ export * from "./CommandManagerWithEnv"; export * from "./UpdaterWithEnv"; export * from "./VoidLogger"; export * from "./PrimaryLogger"; -export * from "./utils"; +export * from "./getConfigFromService"; diff --git a/src/Data/Managers/DeleteVersion/DeleteVersion.ts b/src/Data/Managers/DeleteVersion/DeleteVersion.ts deleted file mode 100644 index 431bd013c..000000000 --- a/src/Data/Managers/DeleteVersion/DeleteVersion.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiHelper } from "@/Core"; -import { CommandManagerWithEnv } from "@/Data/Common"; - -export function DeleteVersionCommandManager(apiHelper: ApiHelper) { - return CommandManagerWithEnv<"DeleteVersion">( - "DeleteVersion", - ({ version }, environment) => { - return () => apiHelper.delete(`/api/v1/version/${version}`, environment); - }, - ); -} diff --git a/src/Data/Managers/DeleteVersion/index.ts b/src/Data/Managers/DeleteVersion/index.ts deleted file mode 100644 index 6d9da3623..000000000 --- a/src/Data/Managers/DeleteVersion/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./DeleteVersion"; diff --git a/src/Data/Managers/DeleteVersion/interface.ts b/src/Data/Managers/DeleteVersion/interface.ts deleted file mode 100644 index 185144465..000000000 --- a/src/Data/Managers/DeleteVersion/interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Maybe, ParsedNumber } from "@/Core/Language"; -import { Query } from "../../../Core/Query"; - -export interface DeleteVersion { - kind: "DeleteVersion"; - version: ParsedNumber; -} - -export interface DeleteVersionManifest { - error: string; - apiData: undefined; - body: null; - command: DeleteVersion; - trigger: ( - query: Query.SubQuery<"GetDesiredStates">, - ) => Promise>; -} diff --git a/src/Data/Managers/Helpers/QueryManager/ContinuousWithEnv.test.tsx b/src/Data/Managers/Helpers/QueryManager/ContinuousWithEnv.test.tsx index 8c496b7f9..31b8e4269 100644 --- a/src/Data/Managers/Helpers/QueryManager/ContinuousWithEnv.test.tsx +++ b/src/Data/Managers/Helpers/QueryManager/ContinuousWithEnv.test.tsx @@ -85,9 +85,7 @@ test("GIVEN QueryManager.ContinuousWithEnv WHEN environment changes THEN the api const button = screen.getByRole("button", { name: "change-env" }); - await act(async () => { - await userEvent.click(button); - }); + await userEvent.click(button); expect(apiHelper.pendingRequests[0]).toEqual({ method: "GET", diff --git a/src/Data/Managers/PromoteVersion/CommandManager.ts b/src/Data/Managers/PromoteVersion/CommandManager.ts deleted file mode 100644 index 47622207b..000000000 --- a/src/Data/Managers/PromoteVersion/CommandManager.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ApiHelper, UpdaterWithEnv } from "@/Core"; -import { CommandManagerWithEnv } from "@/Data/Common"; - -export function PromoteVersionCommandManager( - apiHelper: ApiHelper, - updater: UpdaterWithEnv<"GetDesiredStates">, -) { - return CommandManagerWithEnv<"PromoteVersion">( - "PromoteVersion", - ({ version }, environment) => - async (query) => { - const result = await apiHelper.postWithoutResponse( - `/api/v2/desiredstate/${version}/promote`, - environment, - null, - ); - - await updater.update(query, environment); - - return result; - }, - ); -} diff --git a/src/Data/Managers/PromoteVersion/Updater.ts b/src/Data/Managers/PromoteVersion/Updater.ts deleted file mode 100644 index 4aab1aa24..000000000 --- a/src/Data/Managers/PromoteVersion/Updater.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - ApiHelper, - Query, - RemoteData, - StateHelperWithEnv, - UpdaterWithEnv, -} from "@/Core"; -import { getUrl } from "@S/DesiredState/Data/getUrl"; - -export class DesiredStatesUpdater - implements UpdaterWithEnv<"GetDesiredStates"> -{ - constructor( - private readonly stateHelper: StateHelperWithEnv<"GetDesiredStates">, - private readonly apiHelper: ApiHelper, - ) {} - - async update( - query: Query.SubQuery<"GetDesiredStates">, - environment: string, - ): Promise { - this.stateHelper.set( - RemoteData.fromEither( - await this.apiHelper.get(getUrl(query), environment), - ), - query, - environment, - ); - } -} diff --git a/src/Data/Managers/PromoteVersion/index.ts b/src/Data/Managers/PromoteVersion/index.ts deleted file mode 100644 index 3e3d66bc0..000000000 --- a/src/Data/Managers/PromoteVersion/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./CommandManager"; -export * from "./Updater"; diff --git a/src/Data/Managers/PromoteVersion/interface.ts b/src/Data/Managers/PromoteVersion/interface.ts deleted file mode 100644 index f2089a04d..000000000 --- a/src/Data/Managers/PromoteVersion/interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Maybe, ParsedNumber } from "@/Core/Language"; -import { Query } from "@/Core/Query"; - -export interface PromoteVersion { - kind: "PromoteVersion"; - version: ParsedNumber; -} - -export interface PromoteVersionManifest { - error: string; - apiData: undefined; - body: null; - command: PromoteVersion; - trigger: ( - query: Query.SubQuery<"GetDesiredStates">, - ) => Promise>; -} diff --git a/src/Data/Managers/ServiceConfig/ConfigFinalizer.ts b/src/Data/Managers/ServiceConfig/ConfigFinalizer.ts index 51f5573a6..c3055d83d 100644 --- a/src/Data/Managers/ServiceConfig/ConfigFinalizer.ts +++ b/src/Data/Managers/ServiceConfig/ConfigFinalizer.ts @@ -4,7 +4,7 @@ import { ConfigFinalizer, StateHelperWithEnv, } from "@/Core"; -import { getOptionsFromService } from "@/Data/Common"; +import { getConfigFromService } from "@/Data/Common"; export class ServiceConfigFinalizer implements ConfigFinalizer<"GetServiceConfig"> @@ -30,7 +30,7 @@ export class ServiceConfigFinalizer if (!RemoteData.isSuccess(serviceData)) return serviceData; const config = configData.value; const service = serviceData.value; - const options = getOptionsFromService(service); + const options = getConfigFromService(service); const fullConfig: Config = options.reduce((acc, option) => { acc[option] = typeof config[option] !== "undefined" ? config[option] : false; diff --git a/src/Data/Managers/V2/DELETE/DeleteDesiredStateVersion/index.ts b/src/Data/Managers/V2/DELETE/DeleteDesiredStateVersion/index.ts new file mode 100644 index 000000000..b3793ee77 --- /dev/null +++ b/src/Data/Managers/V2/DELETE/DeleteDesiredStateVersion/index.ts @@ -0,0 +1 @@ +export * from "./useDeleteDesiredStateVersion"; diff --git a/src/Data/Managers/V2/DELETE/DeleteDesiredStateVersion/useDeleteDesiredStateVersion.ts b/src/Data/Managers/V2/DELETE/DeleteDesiredStateVersion/useDeleteDesiredStateVersion.ts new file mode 100644 index 000000000..701e5feb7 --- /dev/null +++ b/src/Data/Managers/V2/DELETE/DeleteDesiredStateVersion/useDeleteDesiredStateVersion.ts @@ -0,0 +1,56 @@ +import { + UseMutationResult, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; +import { PrimaryBaseUrlManager } from "@/UI"; +import { useFetchHelpers } from "../../helpers"; + +/** + * React Query hook for deleting version of Desired State + * + * @param {string} env - The environment in which we are trying to remove the version. + * @returns {Mutation} - The mutation object provided by `useMutation` hook. + */ +export const useDeleteDesiredStateVersion = ( + env: string, +): UseMutationResult => { + const client = useQueryClient(); + const { createHeaders, handleErrors } = useFetchHelpers(); + const baseUrlManager = new PrimaryBaseUrlManager( + globalThis.location.origin, + globalThis.location.pathname, + ); + + const baseUrl = baseUrlManager.getBaseUrl(process.env.API_BASEURL); + + /** + * Deletes a version of the desired state. + * + * @param {string} version - The version of the desired state to be removed. + * @returns {Promise} - A promise that resolves when the version is successfully removed. + * @throws {Error} If the response is not successful, an error with the error message is thrown. + */ + const deleteOrder = async (version: string): Promise => { + const response = await fetch(baseUrl + `/api/v1/version/${version}`, { + method: "DELETE", + headers: createHeaders(env), + }); + + await handleErrors(response); + }; + + return useMutation({ + mutationFn: deleteOrder, + mutationKey: ["delete_desired_state_version"], + onSuccess: () => { + //invalidate the desired state queries to update the list + client.invalidateQueries({ + queryKey: ["get_desired_states-continuous"], + }); + client.invalidateQueries({ + queryKey: ["get_desired_states-one_time"], + }); + }, + }); +}; diff --git a/src/Data/Managers/V2/GETTERS/GetAllServiceModels/index.ts b/src/Data/Managers/V2/GETTERS/GetAllServiceModels/index.ts new file mode 100644 index 000000000..dafc8d005 --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetAllServiceModels/index.ts @@ -0,0 +1 @@ +export * from "./useGetAllServiceModels"; diff --git a/src/Data/Managers/V2/GETTERS/GetAllServiceModels/useGetAllServiceModels.ts b/src/Data/Managers/V2/GETTERS/GetAllServiceModels/useGetAllServiceModels.ts new file mode 100644 index 000000000..5595518ee --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetAllServiceModels/useGetAllServiceModels.ts @@ -0,0 +1,67 @@ +import { UseQueryResult, useQuery } from "@tanstack/react-query"; +import { ServiceModel } from "@/Core"; +import { PrimaryBaseUrlManager } from "@/UI"; +import { useFetchHelpers } from "../../helpers"; + +/** + * Return Signature of the useGetAllServiceModels React Query + */ +interface useGetAllServiceModels { + useOneTime: () => UseQueryResult; + useContinuous: () => UseQueryResult; +} + +/** + * React Query hook to fetch all the service models available in the given environment. + * + * @param environment {string} - the environment in which the instance belongs + * + * @returns {useGetAllServiceModels} An object containing the different available queries. + * @returns {UseQueryResult} returns.useOneTime - Fetch the service models with a single query. + * @returns {UseQueryResult} returns.useContinuous - Fetch the service models with a recursive query with an interval of 5s. + */ +export const useGetAllServiceModels = ( + environment: string, +): useGetAllServiceModels => { + const { createHeaders, handleErrors } = useFetchHelpers(); + const headers = createHeaders(environment); + + const baseUrlManager = new PrimaryBaseUrlManager( + globalThis.location.origin, + globalThis.location.pathname, + ); + const baseUrl = baseUrlManager.getBaseUrl(process.env.API_BASEURL); + + /** + * Fetches all service models from the service catalog. + * + * @returns {Promise<{ data: ServiceModel[] }>} A promise that resolves to an object containing an array of service models. + * @throws Will throw an error if the fetch operation fails. + */ + const fetchServices = async (): Promise<{ data: ServiceModel[] }> => { + const response = await fetch(`${baseUrl}/lsm/v1/service_catalog`, { + headers, + }); + + await handleErrors(response, `Failed to fetch Service Models`); + + return response.json(); + }; + + return { + useOneTime: (): UseQueryResult => + useQuery({ + queryKey: ["get_all_service_models-one_time"], + queryFn: fetchServices, + retry: false, + select: (data) => data.data, + }), + useContinuous: (): UseQueryResult => + useQuery({ + queryKey: ["get_all_service_models-continuous"], + queryFn: fetchServices, + refetchInterval: 5000, + select: (data) => data.data, + }), + }; +}; diff --git a/src/Data/Managers/V2/GETTERS/GetDesiredStates/getUrl.ts b/src/Data/Managers/V2/GETTERS/GetDesiredStates/getUrl.ts new file mode 100644 index 000000000..aad4ec03e --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetDesiredStates/getUrl.ts @@ -0,0 +1,53 @@ +import moment from "moment-timezone"; +import qs from "qs"; +import { Query, RangeOperator } from "@/Core"; + +/** + * Constructs the URL for fetching desired states based on the provided query parameters. + * @note This function is imported from V1 of Query Manager of the same url query manager, as filters are out of scope of the query manager upgrade/migration - https://github.com/inmanta/web-console/issues/5973 + * + * @param query - The query parameters for fetching desired states. + * @param timezone - The timezone to use for date conversions (default: guessed timezone). + * @returns The constructed URL for fetching desired states. + */ +export function getUrl( + { pageSize, filter, currentPage }: Query.SubQuery<"GetDesiredStates">, + timezone = moment.tz.guess(), +): string { + const defaultFilter = { status: ["active", "candidate", "retired"] }; + const filterWithDefaults = + filter && filter.status && filter.status?.length > 0 + ? filter + : { ...filter, ...defaultFilter }; + const filterParam = + filterWithDefaults && Object.keys(filterWithDefaults).length > 0 + ? `&${qs.stringify( + { + filter: { + status: filterWithDefaults.status, + date: filterWithDefaults.date?.map( + (timestampWithOperator) => + `${RangeOperator.serializeOperator( + timestampWithOperator.operator, + )}:${moment + .tz(timestampWithOperator.date, timezone) + .utc() + .format("YYYY-MM-DD+HH:mm:ss")}`, + ), + version: filterWithDefaults.version?.map( + ({ value, operator }) => + `${RangeOperator.serializeOperator(operator)}:${value}`, + ), + }, + }, + { allowDots: true, arrayFormat: "repeat" }, + )}` + : ""; + const sortParam = `&sort=version.desc`; + + return `/api/v2/desiredstate?limit=${ + pageSize.value + }${sortParam}${filterParam}${ + currentPage.value ? `&${currentPage.value}` : "" + }`; +} diff --git a/src/Data/Managers/V2/GETTERS/GetDesiredStates/index.ts b/src/Data/Managers/V2/GETTERS/GetDesiredStates/index.ts new file mode 100644 index 000000000..811a935a0 --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetDesiredStates/index.ts @@ -0,0 +1 @@ +export * from "./useGetDesiredStates"; diff --git a/src/Data/Managers/V2/GETTERS/GetDesiredStates/useGetDesiredStates.ts b/src/Data/Managers/V2/GETTERS/GetDesiredStates/useGetDesiredStates.ts new file mode 100644 index 000000000..7bcd663a1 --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetDesiredStates/useGetDesiredStates.ts @@ -0,0 +1,123 @@ +import { UseQueryResult, useQuery } from "@tanstack/react-query"; +import { DateRange, IntRange, PageSize, Pagination } from "@/Core"; +import { CurrentPage } from "@/Data/Common/UrlState/useUrlStateWithCurrentPage"; +import { + DesiredStateVersion, + DesiredStateVersionStatus, +} from "@/Slices/DesiredState/Core/Domain"; +import { PrimaryBaseUrlManager } from "@/UI"; +import { useFetchHelpers } from "../../helpers"; +import { getUrl } from "./getUrl"; + +/** + * interface of filter object for desired states + */ +export interface Filter { + version?: IntRange.IntRange[]; + date?: DateRange.DateRange[]; + status?: DesiredStateVersionStatus[]; +} + +/** + * interface of Result of the useGetDesiredStates React Query + */ +interface Result { + data: DesiredStateVersion[]; + links: Pagination.Links; + metadata: Pagination.Metadata; +} + +/** + * Return Signature of the useGetDesiredStates React Query + */ +interface GetDesiredStates { + useOneTime: ( + pageSize: PageSize.PageSize, + filter: Filter, + currentPage: CurrentPage, + ) => UseQueryResult; + useContinuous: ( + pageSize: PageSize.PageSize, + filter: Filter, + currentPage: CurrentPage, + ) => UseQueryResult; +} + +/** + * React Query hook to fetch a list of desired States + * @param environment {string} - the environment in which the instance belongs + * + * @returns {GetDesiredStates} An object containing the available queries. + * @returns {UseQueryResult} returns.useOneTime - Fetch the desired states with a single query. + * @returns {UseQueryResult} returns.useContinuous - Fetch the desired states with a recurrent query with an interval of 5s. + */ +export const useGetDesiredStates = (environment: string): GetDesiredStates => { + const { createHeaders, handleErrors } = useFetchHelpers(); + const headers = createHeaders(environment); + + const baseUrlManager = new PrimaryBaseUrlManager( + globalThis.location.origin, + globalThis.location.pathname, + ); + const baseUrl = baseUrlManager.getBaseUrl(process.env.API_BASEURL); + + /** + * Fetches the desired states from the API. + * + * @param {PageSize.PageSize} pageSize - The number of desired states to fetch per page. + * @param {Filter} filter - The filter to apply to the desired states. + * @param {CurrentPage} currentPage - The current page of desired states to fetch. + * @returns {Promise} - A promise that resolves with the fetched desired states. + * @throws {Error} If the response is not successful, an error with the error message is thrown. + */ + const fetchDesiredStates = async ( + pageSize: PageSize.PageSize, + filter: Filter, + currentPage: CurrentPage, + ): Promise => { + const response = await fetch( + baseUrl + + getUrl({ pageSize, filter, currentPage, kind: "GetDesiredStates" }), + { + headers, + }, + ); + + await handleErrors(response, `Failed to fetch desired states`); + + return response.json(); + }; + + return { + useOneTime: ( + pageSize: PageSize.PageSize, + filter: Filter, + currentPage: CurrentPage, + ): UseQueryResult => + useQuery({ + queryKey: [ + "get_desired_states-one_time", + pageSize, + filter, + currentPage, + ], + queryFn: () => fetchDesiredStates(pageSize, filter, currentPage), + retry: false, + }), + useContinuous: ( + pageSize: PageSize.PageSize, + filter: Filter, + currentPage: CurrentPage, + ): UseQueryResult => + useQuery({ + queryKey: [ + "get_desired_states-continuous", + pageSize, + filter, + currentPage, + ], + queryFn: () => fetchDesiredStates(pageSize, filter, currentPage), + refetchInterval: 5000, + }), + }; +}; diff --git a/src/Data/Managers/V2/GETTERS/GetDiagnostics/index.ts b/src/Data/Managers/V2/GETTERS/GetDiagnostics/index.ts new file mode 100644 index 000000000..13c084f90 --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetDiagnostics/index.ts @@ -0,0 +1 @@ +export * from "./useGetDiagnostics"; diff --git a/src/Data/Managers/V2/GETTERS/GetDiagnostics/useGetDiagnostics.tsx b/src/Data/Managers/V2/GETTERS/GetDiagnostics/useGetDiagnostics.tsx new file mode 100644 index 000000000..173bbd888 --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetDiagnostics/useGetDiagnostics.tsx @@ -0,0 +1,64 @@ +import { useQuery, UseQueryResult } from "@tanstack/react-query"; +import { RawDiagnostics } from "@/Slices/Diagnose/Core/Domain"; +import { PrimaryBaseUrlManager } from "@/UI"; +import { useFetchHelpers } from "../../helpers"; + +/** + * Return Signature of the useGetDiagnostics React Query + */ +interface GetDiagnostics { + useOneTime: (lookBehind: string) => UseQueryResult; +} + +/** + * React Query hook to fetch a diagnostics for a given instance + * + * @param service {string} - the service entity + * @param instanceId {string} - the instance ID for which the data needs to be fetched. + * @param environment {string} - the environment in which the instance belongs + * + * @returns {GetInstance} An object containing the different available queries. + * @returns {UseQueryResult} returns.useOneTime - Fetch the diagnose report with a single query. + */ +export const useGetDiagnostics = ( + service: string, + instanceId: string, + environment: string, +): GetDiagnostics => { + const { createHeaders, handleErrors } = useFetchHelpers(); + const headers = createHeaders(environment); + + const baseUrlManager = new PrimaryBaseUrlManager( + globalThis.location.origin, + globalThis.location.pathname, + ); + const baseUrl = baseUrlManager.getBaseUrl(process.env.API_BASEURL); + + const fetchDiagnostics = async ( + lookBehind: string, + ): Promise<{ data: RawDiagnostics }> => { + const response = await fetch( + `${baseUrl}/lsm/v1/service_inventory/${service}/${instanceId}/diagnose?rejection_lookbehind=${lookBehind}&failure_lookbehind=${lookBehind}`, + { + headers, + }, + ); + + await handleErrors( + response, + `Failed to fetch diagnostics for id: ${instanceId}`, + ); + + return response.json(); + }; + + return { + useOneTime: (lookBehind: string): UseQueryResult => + useQuery({ + queryKey: ["get_diagnostics-one_time", service, instanceId, lookBehind], + queryFn: () => fetchDiagnostics(lookBehind), + retry: false, + select: (data) => data.data, + }), + }; +}; diff --git a/src/Data/Managers/V2/GETTERS/GetInfiniteInstanceLogs/index.ts b/src/Data/Managers/V2/GETTERS/GetInfiniteInstanceLogs/index.ts new file mode 100644 index 000000000..3e19edf4e --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetInfiniteInstanceLogs/index.ts @@ -0,0 +1 @@ +export * from "./useGetInfiniteInstanceLogs"; diff --git a/src/Data/Managers/V2/GETTERS/GetInfiniteInstanceLogs/useGetInfiniteInstanceLogs.ts b/src/Data/Managers/V2/GETTERS/GetInfiniteInstanceLogs/useGetInfiniteInstanceLogs.ts new file mode 100644 index 000000000..eb550ba73 --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetInfiniteInstanceLogs/useGetInfiniteInstanceLogs.ts @@ -0,0 +1,98 @@ +import { + UseInfiniteQueryResult, + useInfiniteQuery, +} from "@tanstack/react-query"; +import { Pagination } from "@/Core"; +import { InstanceLog } from "@/Core/Domain/HistoryLog"; +import { PrimaryBaseUrlManager } from "@/UI"; +import { useFetchHelpers } from "../../helpers"; + +export interface LogsResponse { + data: InstanceLog[]; + links: Pagination.Links; + metadata: Pagination.Metadata; +} + +/** + * Return Signature of the useGetInfiniteInstanceLogs React Query + */ +interface GetInfiniteInstanceLogs { + useContinuous: ( + selectedVersion: string, + ) => UseInfiniteQueryResult; +} + +/** + * React Infinite Query hook to fetch a the history logs for an instance + * + * @param service {string} - the service entity + * @param instanceId {string} - the instance ID for which the data needs to be fetched. + * @param environment {string} - the environment in which the instance belongs + * + * @returns {GetInfiniteInstanceLogs} An object containing the different available queries. + * @returns {UseInfiniteQueryResult} returns.useOneTime - Fetch the logs with a single query. + * @returns {UseInfiniteQueryResult} returns.useContinuous - Fetch the logs with a recursive query with an interval of 5s. + */ +export const useGetInfiniteInstanceLogs = ( + service: string, + instance: string, + environment: string, +): GetInfiniteInstanceLogs => { + const { createHeaders, handleErrors } = useFetchHelpers(); + + const headers = createHeaders(environment); + + const baseUrlManager = new PrimaryBaseUrlManager( + globalThis.location.origin, + globalThis.location.pathname, + ); + const baseUrl = baseUrlManager.getBaseUrl(process.env.API_BASEURL); + const fetchInstance = async ( + { pageParam }, + selectedVersion, + ): Promise => { + const initialParameters = selectedVersion + ? `limit=50&end=${Number(selectedVersion) + 1}` + : "limit=50"; + + const response = await fetch( + `${baseUrl}/lsm/v1/service_inventory/${service}/${instance}/log?${pageParam ? pageParam : initialParameters}`, + { + headers, + }, + ); + + await handleErrors(response, `Failed to fetch logs for: ${instance}`); + + return response.json(); + }; + + return { + useContinuous: ( + selectedVersion: string, + ): UseInfiniteQueryResult => + useInfiniteQuery({ + queryKey: ["get_instance_logs-continuous", service, instance], + queryFn: (query) => fetchInstance(query, selectedVersion), + refetchInterval: 5000, + select: (data) => { + return data.pages.flatMap((page) => page.data); + }, + getPreviousPageParam: (lastPage, _pages) => { + if (!lastPage.links.prev) { + return undefined; + } + + return lastPage.links.prev.split("?")[1]; + }, + getNextPageParam: (lastPage, _pages) => { + if (!lastPage.links.next) { + return undefined; + } + + return lastPage.links.next.split("?")[1]; + }, + initialPageParam: "", + }), + }; +}; diff --git a/src/Data/Managers/V2/GETTERS/GetInstance/useGetInstance.ts b/src/Data/Managers/V2/GETTERS/GetInstance/useGetInstance.ts index 91248302f..29f060338 100644 --- a/src/Data/Managers/V2/GETTERS/GetInstance/useGetInstance.ts +++ b/src/Data/Managers/V2/GETTERS/GetInstance/useGetInstance.ts @@ -38,7 +38,7 @@ export const useGetInstance = ( const fetchInstance = async (): Promise<{ data: ServiceInstanceModel }> => { const response = await fetch( - `${baseUrl}/lsm/v1/service_inventory/${service}/${instanceId}`, + `${baseUrl}/lsm/v1/service_inventory/${service}/${instanceId}?include_deployment_progress=true`, { headers, }, diff --git a/src/Data/Managers/V2/GETTERS/GetInstanceLogs/useGetInstanceLogs.ts b/src/Data/Managers/V2/GETTERS/GetInstanceLogs/useGetInstanceLogs.ts index 82dbccd2b..524d801f2 100644 --- a/src/Data/Managers/V2/GETTERS/GetInstanceLogs/useGetInstanceLogs.ts +++ b/src/Data/Managers/V2/GETTERS/GetInstanceLogs/useGetInstanceLogs.ts @@ -1,5 +1,5 @@ import { UseQueryResult, useQuery } from "@tanstack/react-query"; -import { InstanceLog } from "@/Slices/ServiceInstanceHistory/Core/Domain"; +import { InstanceLog } from "@/Core/Domain/HistoryLog"; import { PrimaryBaseUrlManager } from "@/UI"; import { useFetchHelpers } from "../../helpers"; diff --git a/src/Data/Managers/V2/GETTERS/GetInstanceResources/index.ts b/src/Data/Managers/V2/GETTERS/GetInstanceResources/index.ts new file mode 100644 index 000000000..e8929d4f0 --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetInstanceResources/index.ts @@ -0,0 +1 @@ +export * from "./useGetInstanceResources"; diff --git a/src/Data/Managers/V2/GETTERS/GetInstanceResources/useGetInstanceResources.tsx b/src/Data/Managers/V2/GETTERS/GetInstanceResources/useGetInstanceResources.tsx new file mode 100644 index 000000000..7207bbbfd --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetInstanceResources/useGetInstanceResources.tsx @@ -0,0 +1,80 @@ +import { UseQueryResult, useQuery } from "@tanstack/react-query"; +import { InstanceResourceModel } from "@/Core"; +import { PrimaryBaseUrlManager } from "@/UI"; +import { useFetchHelpers } from "../../helpers"; + +/** + * Return Signature of the useGetInstanceResources React Query + */ +interface getInstanceResources { + useOneTime: () => UseQueryResult; + useContinuous: () => UseQueryResult; +} + +/** + * React Query hook to fetch the resources for given service instance + * + * @param {string} id - the service instance id + * @param {string} serviceName - the service name + * @param environment {string} - the environment in which the instance belongs + * + * @returns {getInstanceResources} An object containing the different available queries. + * @returns {UseQueryResult} returns.useOneTime - Fetch the service instance resources ies as a single query. + * @returns {UseQueryResult} returns.useContinuous - Fetch the service instance resources with a recursive query with an interval of 5s. + */ +export const useGetInstanceResources = ( + id: string, + service_entity: string, + version: string, + environment: string, +): getInstanceResources => { + const { createHeaders, handleErrors } = useFetchHelpers(); + const headers = createHeaders(environment); + + const baseUrlManager = new PrimaryBaseUrlManager( + globalThis.location.origin, + globalThis.location.pathname, + ); + const baseUrl = baseUrlManager.getBaseUrl(process.env.API_BASEURL); + + /** + * Fetches the all the resources for a given service instance. + * + * @returns A promise that resolves to an object containing an array of service instance resources + * @throws Will throw an error if the fetch operation fails. + */ + const fetchResources = async (): Promise<{ + data: InstanceResourceModel[]; + }> => { + const response = await fetch( + `${baseUrl}/lsm/v1/service_inventory/${service_entity}/${id}/resources?current_version=${version}`, + { + headers, + }, + ); + + await handleErrors( + response, + `Failed to fetch service instance resources for instance of id: ${id}`, + ); + + return response.json(); + }; + + return { + useOneTime: (): UseQueryResult => + useQuery({ + queryKey: ["get_instance_resources-one_time", id], + queryFn: fetchResources, + retry: false, + }), + useContinuous: (): UseQueryResult => + useQuery({ + queryKey: ["get_instance_resources-continuous", id], + queryFn: fetchResources, + refetchInterval: 5000, + select: (data): InstanceResourceModel[] => data.data, + retry: false, + }), + }; +}; diff --git a/src/Data/Managers/V2/GETTERS/GetInstanceWithRelations/useGetInstanceWithRelations.test.tsx b/src/Data/Managers/V2/GETTERS/GetInstanceWithRelations/useGetInstanceWithRelations.test.tsx index 5e387e718..e938e678d 100644 --- a/src/Data/Managers/V2/GETTERS/GetInstanceWithRelations/useGetInstanceWithRelations.test.tsx +++ b/src/Data/Managers/V2/GETTERS/GetInstanceWithRelations/useGetInstanceWithRelations.test.tsx @@ -4,7 +4,11 @@ import { renderHook, waitFor } from "@testing-library/react"; import { HttpResponse, http } from "msw"; import { setupServer } from "msw/node"; import { ServiceInstanceModel } from "@/Core"; -import { testInstance } from "@/UI/Components/Diagram/Mock"; +import { + childModel, + testInstance, + testService, +} from "@/UI/Components/Diagram/Mocks"; import { useGetInstanceWithRelations } from "./useGetInstanceWithRelations"; export const server = setupServer( @@ -19,6 +23,34 @@ export const server = setupServer( }); } + if (params.request.url.match(/child_id/)) { + return HttpResponse.json({ + data: { + id: "child_id", + environment: "env", + service_entity: "child-service", + version: 4, + config: {}, + state: "up", + candidate_attributes: null, + active_attributes: { + name: "child-test", + service_id: "123523534623", + parent_entity: "test_mpn_id", + should_deploy_fail: false, + }, + rollback_attributes: null, + created_at: "2023-09-19T14:40:08.999123", + last_updated: "2023-09-19T14:40:36.178723", + callback: [], + deleted: false, + deployment_progress: null, + service_identity_attribute_value: "child-test", + referenced_by: [], + }, + }); + } + return HttpResponse.json({ data: { ...testInstance, id: "test_mpn_id" }, }); @@ -50,7 +82,13 @@ const createWrapper = () => { test("if the fetched instance has referenced instance(s), then query will return the given instance with that related instance(s)", async () => { const { result } = renderHook( - () => useGetInstanceWithRelations("test_id", "env").useOneTime(), + () => + useGetInstanceWithRelations( + "test_id", + "env", + false, + testService, + ).useOneTime(), { wrapper: createWrapper(), }, @@ -60,15 +98,117 @@ test("if the fetched instance has referenced instance(s), then query will return expect(result.current.data).toBeDefined(); expect(result.current.data?.instance.id).toEqual("test_id"); - expect(result.current.data?.relatedInstances).toHaveLength(1); + expect(result.current.data?.interServiceRelations).toHaveLength(1); + expect( + (result.current.data?.interServiceRelations as ServiceInstanceModel[])[0] + .id, + ).toEqual("test_mpn_id"); +}); + +test("if the fetched instance has inter-service relation(s) in the model, then query will return the given instance with that related instance(s)", async () => { + const { result } = renderHook( + () => + useGetInstanceWithRelations( + "child_id", + "env", + false, + childModel, + ).useOneTime(), + { + wrapper: createWrapper(), + }, + ); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toBeDefined(); + expect(result.current.data?.instance.id).toEqual("child_id"); + expect(result.current.data?.interServiceRelations).toHaveLength(1); + expect( + (result.current.data?.interServiceRelations as ServiceInstanceModel[])[0] + .id, + ).toEqual("test_mpn_id"); +}); + +test("if the fetched instance has inter-service relation(s) in the model, and they are stored in the array in the instance, then query will return the given instance with that related instance(s)", async () => { + server.use( + http.get("/lsm/v1/service_inventory", async (params) => { + if (params.request.url.match(/test_id/)) { + return HttpResponse.json({ + data: { + ...testInstance, + id: "test_id", + referenced_by: ["test_mpn_id"], + }, + }); + } + + if (params.request.url.match(/child_id/)) { + return HttpResponse.json({ + data: { + id: "child_id", + environment: "env", + service_entity: "child-service", + version: 4, + config: {}, + state: "up", + candidate_attributes: null, + active_attributes: { + name: "child-test", + service_id: "123523534623", + parent_entity: ["test_mpn_id"], + should_deploy_fail: false, + }, + rollback_attributes: null, + created_at: "2023-09-19T14:40:08.999123", + last_updated: "2023-09-19T14:40:36.178723", + callback: [], + deleted: false, + deployment_progress: null, + service_identity_attribute_value: "child-test", + referenced_by: [], + }, + }); + } + + return HttpResponse.json({ + data: { ...testInstance, id: "test_mpn_id" }, + }); + }), + ); + const { result } = renderHook( + () => + useGetInstanceWithRelations( + "child_id", + "env", + false, + childModel, + ).useOneTime(), + { + wrapper: createWrapper(), + }, + ); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toBeDefined(); + expect(result.current.data?.instance.id).toEqual("child_id"); + expect(result.current.data?.interServiceRelations).toHaveLength(1); expect( - (result.current.data?.relatedInstances as ServiceInstanceModel[])[0].id, + (result.current.data?.interServiceRelations as ServiceInstanceModel[])[0] + .id, ).toEqual("test_mpn_id"); }); -test("when instance returned has not referenced instance(s), then the query will return the given instance without relatedInstances", async () => { +test("when instance returned has not referenced instance(s), then the query will return the given instance without interServiceRelations", async () => { const { result } = renderHook( - () => useGetInstanceWithRelations("test_mpn_id", "env").useOneTime(), + () => + useGetInstanceWithRelations( + "test_mpn_id", + "env", + false, + testService, + ).useOneTime(), { wrapper: createWrapper(), }, @@ -78,5 +218,5 @@ test("when instance returned has not referenced instance(s), then the query will expect(result.current.data).toBeDefined(); expect(result.current.data?.instance.id).toEqual("test_mpn_id"); - expect(result.current.data?.relatedInstances).toHaveLength(0); + expect(result.current.data?.interServiceRelations).toHaveLength(0); }); diff --git a/src/Data/Managers/V2/GETTERS/GetInstanceWithRelations/useGetInstanceWithRelations.ts b/src/Data/Managers/V2/GETTERS/GetInstanceWithRelations/useGetInstanceWithRelations.ts index 060c1542f..dc6e5c3f2 100644 --- a/src/Data/Managers/V2/GETTERS/GetInstanceWithRelations/useGetInstanceWithRelations.ts +++ b/src/Data/Managers/V2/GETTERS/GetInstanceWithRelations/useGetInstanceWithRelations.ts @@ -1,5 +1,10 @@ import { UseQueryResult, useQuery } from "@tanstack/react-query"; -import { ServiceInstanceModel } from "@/Core"; +import { + EmbeddedEntity, + InstanceAttributeModel, + ServiceInstanceModel, + ServiceModel, +} from "@/Core"; import { PrimaryBaseUrlManager } from "@/UI"; import { useFetchHelpers } from "../../helpers"; @@ -8,7 +13,7 @@ import { useFetchHelpers } from "../../helpers"; */ export interface InstanceWithRelations { instance: ServiceInstanceModel; - relatedInstances?: ServiceInstanceModel[]; + interServiceRelations: ServiceInstanceModel[]; coordinates?: string; } @@ -17,17 +22,22 @@ export interface InstanceWithRelations { */ interface GetInstanceWithRelationsHook { useOneTime: () => UseQueryResult; + useContinuous: () => UseQueryResult; } /** - * React Query hook to fetch an instance with its related instances from the API. + * React Query hook to fetch an instance with its related instances from the API. The related instances are all instances connected with given instance by inter-service relation, both, as a parent and as a child. * @param {string} id - The ID of the instance to fetch. * @param {string} environment - The environment in which we are looking for instances. + * @param {boolean} includesReferencedBy - A flag indicating if we should fetch instances that reference our main instance - defaults to false. + * @param {ServiceModel} [serviceModel] - The service Model of the instance (optional as it can be undefined at the init of the component that use the hook) * @returns {GetInstanceWithRelationsHook} An object containing a custom hook to fetch the instance with its related instances. */ export const useGetInstanceWithRelations = ( instanceId: string, environment: string, + includesReferencedBy: boolean = false, + serviceModel?: ServiceModel, ): GetInstanceWithRelationsHook => { //extracted headers to avoid breaking rules of Hooks const { createHeaders, handleErrors } = useFetchHelpers(); @@ -44,12 +54,12 @@ export const useGetInstanceWithRelations = ( * @returns {Promise<{data: ServiceInstanceModel}>} An object containing the fetched instance. * @throws Error if the instance fails to fetch. */ - const fetchInstance = async ( + const fetchSingleInstance = async ( id: string, ): Promise<{ data: ServiceInstanceModel }> => { //we use this endpoint instead /lsm/v1/service_inventory/{service_entity}/{service_id} because referenced_by property includes only ids, without information about service_entity for given ids const response = await fetch( - `${baseUrl}/lsm/v1/service_inventory?service_id=${id}&include_deployment_progress=false&exclude_read_only_attributes=false&include_referenced_by=true&include_metadata=true`, + `${baseUrl}/lsm/v1/service_inventory?service_id=${id}&include_deployment_progress=false&exclude_read_only_attributes=false&include_referenced_by=${includesReferencedBy}&include_metadata=true`, { headers, }, @@ -61,32 +71,142 @@ export const useGetInstanceWithRelations = ( }; /** - * Fetches a service instance with its related instances. + * This function is responsible for extracting the names of all inter-service relations from the provided service model or embedded entity. + * It also recursively extracts the names of all relations from any embedded entities within the provided service model or embedded entity. + * + * @param {ServiceModel | EmbeddedEntity} serviceModel - The service model or embedded entity from which to extract the relations. + * @returns {string[]} An array of the names of all relations. + */ + const getAllRelationNames = ( + serviceModel: ServiceModel | EmbeddedEntity, + ): string[] => { + const relations = + serviceModel.inter_service_relations.map((relation) => relation.name) || + []; + + const nestedRelations = serviceModel.embedded_entities.flatMap((entity) => + getAllRelationNames(entity), + ); + + return [...relations, ...nestedRelations]; + }; + + /** + * This function is responsible for extracting the names of all embedded entities from the provided service model or embedded entity. + * It also recursively extracts the names of all embedded entities from any embedded entities within the provided service model or embedded entity. + * + * @param {ServiceModel | EmbeddedEntity} serviceModel - The service model or embedded entity from which to extract the embedded entities. + * @returns {string[]} An array of the names of all embedded entities. + */ + const getAllEmbeddedEntityNames = ( + serviceModel: ServiceModel | EmbeddedEntity, + ): string[] => { + const embeddedEntities = serviceModel.embedded_entities.map( + (entity) => entity.name, + ); + const nestedEmbeddedEntities = serviceModel.embedded_entities.flatMap( + (entity) => getAllEmbeddedEntityNames(entity), + ); + + return [...embeddedEntities, ...nestedEmbeddedEntities]; + }; + + /** + * This function extracts all the inter-service-relation ids from the provided attributes. + * It does this by mapping over the provided relation names and extracting the corresponding Ids from the attributes. + * It also recursively extracts the Ids of all related instances from any embedded entities within the provided attributes. + * + * @param {InstanceAttributeModel} attributes - The attributes from which to extract the related Ids. + * @param {string[]} relationNames - The names of the relations to extract. + * @param {string[]} embeddedNames - The names of the embedded entities that can have relations. + * + * @returns {string[]} An array of the Ids of all related instances. + */ + const getInterServiceRelationIds = ( + attributes: InstanceAttributeModel, + relationNames: string[], + embeddedNames: string[], + ): string[] => { + // Map relation names to corresponding IDs from attributes + const relationIds = relationNames + .flatMap((relationName) => attributes[relationName]) //relations can be in the array of strings so we need to flatten it just in case + .filter((id): id is string => typeof id === "string"); // Filter to ensure only you only keep strings + + // Extract IDs from embedded relations recursively + const embeddedRelationsIds = embeddedNames.flatMap((embeddedName) => { + const embeddedAttributes = attributes[embeddedName]; + + if (!embeddedAttributes) { + return []; + } + + if (Array.isArray(embeddedAttributes)) { + // Recursively collect IDs from an array of embedded attributes + return embeddedAttributes.flatMap((embedded) => + getInterServiceRelationIds(embedded, relationNames, embeddedNames), + ); + } else { + // Recursively collect IDs from a single embedded attribute + return getInterServiceRelationIds( + embeddedAttributes as InstanceAttributeModel, //InstanceAttributeModel is a Record so casting is required here + relationNames, + embeddedNames, + ); + } + }); + + // Combine and filter out falsy values (undefined, null, "") + const ids = [...relationIds, ...embeddedRelationsIds].filter(Boolean); + + return ids; + }; + + /** + * For a given instance Id, fetches the root instance with its corresponding inter-service-relation instances. * @param {string} id - The ID of the instance to fetch. * @returns {Promise} An object containing the fetched instance and its related instances. * @throws Error if the instance fails to fetch. */ - const fetchInstances = async (id: string): Promise => { - const relatedInstances: ServiceInstanceModel[] = []; - const instance = (await fetchInstance(id)).data; - - if (instance.referenced_by !== null) { - await Promise.all( - instance.referenced_by.map(async (relatedId) => { - const relatedInstance = await fetchInstance(relatedId); + const fetchInstanceWithRelations = async ( + id: string, + ): Promise => { + const interServiceRelations: ServiceInstanceModel[] = []; + const { data: instance } = await fetchSingleInstance(id); + let serviceIds: string[] = []; - if (relatedInstance) { - relatedInstances.push(relatedInstance.data); - } + if (serviceModel) { + const attributesToFetch = getAllRelationNames(serviceModel); + const uniqueAttributes = [...new Set(attributesToFetch)]; + const allEmbedded = getAllEmbeddedEntityNames(serviceModel); + const attributes = + instance.active_attributes || instance.candidate_attributes || {}; //we don't operate on rollback attributes - return relatedInstance; - }), + serviceIds = getInterServiceRelationIds( + attributes, + uniqueAttributes, + allEmbedded, ); } + const allIds = [ + ...new Set([...serviceIds, ...(instance.referenced_by || [])]), + ]; + + await Promise.all( + allIds.map(async (relatedId) => { + const interServiceRelation = await fetchSingleInstance(relatedId); + + if (interServiceRelation) { + interServiceRelations.push(interServiceRelation.data); + } + + return interServiceRelation; + }), + ); + return { instance, - relatedInstances, + interServiceRelations, }; }; @@ -102,8 +222,22 @@ export const useGetInstanceWithRelations = ( instanceId, environment, ], - queryFn: () => fetchInstances(instanceId), + queryFn: () => fetchInstanceWithRelations(instanceId), + retry: false, + enabled: serviceModel !== undefined, + gcTime: 0, + }), + useContinuous: (): UseQueryResult => + useQuery({ + queryKey: [ + "get_instance_with_relations-continuous", + instanceId, + environment, + ], + queryFn: () => fetchInstanceWithRelations(instanceId), retry: false, + refetchInterval: 5000, + enabled: serviceModel !== undefined, }), }; }; diff --git a/src/Data/Managers/V2/GETTERS/GetInventoryList/index.ts b/src/Data/Managers/V2/GETTERS/GetInventoryList/index.ts new file mode 100644 index 000000000..ca293fe29 --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetInventoryList/index.ts @@ -0,0 +1 @@ +export * from "./useGetInventoryList"; diff --git a/src/Data/Managers/V2/GETTERS/GetInventoryList/useGetInventoryList.ts b/src/Data/Managers/V2/GETTERS/GetInventoryList/useGetInventoryList.ts new file mode 100644 index 000000000..1a6a10793 --- /dev/null +++ b/src/Data/Managers/V2/GETTERS/GetInventoryList/useGetInventoryList.ts @@ -0,0 +1,102 @@ +import { UseQueryResult, useQuery } from "@tanstack/react-query"; +import { ServiceInstanceModel } from "@/Core"; +import { PrimaryBaseUrlManager } from "@/UI"; +import { useFetchHelpers } from "../../helpers"; + +/** + * Inventories interface + * + * It is used to group service instances by a service name + */ +export interface Inventories { + [serviceName: string]: ServiceInstanceModel[]; +} + +/** + * Return Signature of the useGetInventoryList React Query + */ +interface GetInventoryList { + useOneTime: () => UseQueryResult; + useContinuous: () => UseQueryResult; +} + +/** + * React Query hook to fetch the service inventory of each service in the list of service names. + * + * @param {string[]} serviceNames - the array of service names + * @param environment {string} - the environment in which the instance belongs + * + * @returns {GetInventoryList} An object containing the different available queries. + * @returns {UseQueryResult} returns.useOneTime - Fetch the service inventories as a single query. + * @returns {UseQueryResult} returns.useContinuous - Fetch the service inventories with a recursive query with an interval of 5s. + */ +export const useGetInventoryList = ( + serviceNames: string[], + environment: string, +): GetInventoryList => { + const { createHeaders, handleErrors } = useFetchHelpers(); + const headers = createHeaders(environment); + + const baseUrlManager = new PrimaryBaseUrlManager( + globalThis.location.origin, + globalThis.location.pathname, + ); + const baseUrl = baseUrlManager.getBaseUrl(process.env.API_BASEURL); + + /** + * Fetches the inventory for a single service. + * + * @param service - The name of the service to fetch the inventory for. + * @returns A promise that resolves to an object containing an array of service instance models. + * @throws Will throw an error if the fetch operation fails. + */ + const fetchSingleService = async ( + service: string, + ): Promise<{ data: ServiceInstanceModel[] }> => { + const response = await fetch( + `${baseUrl}/lsm/v1/service_inventory/${service}?limit=1000`, + { + headers, + }, + ); + + await handleErrors( + response, + `Failed to fetch service inventory for name: ${service}`, + ); + + return response.json(); + }; + + /** + * Fetches the inventory for all services. + * + * @returns A promise that resolves to an object mapping service names to arrays of service instances. + * @throws Will throw an error if the fetch operation for any service fails. + */ + const fetchAllServices = async (): Promise => { + const responses = await Promise.all( + serviceNames.map(async (serviceName) => fetchSingleService(serviceName)), + ); + + // Map the responses to an object of service names and arrays of service instances for each service + return Object.fromEntries( + responses.map((response, index) => [serviceNames[index], response.data]), + ); + }; + + return { + useOneTime: (): UseQueryResult => + useQuery({ + queryKey: ["get_inventory_list-one_time", serviceNames], + queryFn: fetchAllServices, + retry: false, + }), + useContinuous: (): UseQueryResult => + useQuery({ + queryKey: ["get_inventory_list-continuous", serviceNames], + queryFn: fetchAllServices, + refetchInterval: 5000, + }), + }; +}; diff --git a/src/Data/Managers/V2/GETTERS/GetServiceModel/UseGetServiceModel.ts b/src/Data/Managers/V2/GETTERS/GetServiceModel/UseGetServiceModel.ts index 396dc1601..f93a8fb72 100644 --- a/src/Data/Managers/V2/GETTERS/GetServiceModel/UseGetServiceModel.ts +++ b/src/Data/Managers/V2/GETTERS/GetServiceModel/UseGetServiceModel.ts @@ -4,7 +4,7 @@ import { PrimaryBaseUrlManager } from "@/UI"; import { useFetchHelpers } from "../../helpers"; /** - * Return Signature of the useServiceModel React Query + * Return Signature of the useGetServiceModel React Query */ interface GetServiceModel { useOneTime: () => UseQueryResult; diff --git a/src/Data/Managers/V2/POST/PostOrder/usePostOrder.ts b/src/Data/Managers/V2/POST/PostOrder/usePostOrder.ts index 85bd3c9e1..5fae08fcb 100644 --- a/src/Data/Managers/V2/POST/PostOrder/usePostOrder.ts +++ b/src/Data/Managers/V2/POST/PostOrder/usePostOrder.ts @@ -31,7 +31,7 @@ export const usePostOrder = ( method: "POST", body: JSON.stringify({ service_order_items: serviceOrderItems, - description: words("inventory.instanceComposer.orderDescription"), + description: words("instanceComposer.orderDescription"), }), headers, }); diff --git a/src/Data/Managers/V2/POST/PromoteDesiredStateVersion/index.ts b/src/Data/Managers/V2/POST/PromoteDesiredStateVersion/index.ts new file mode 100644 index 000000000..50d1e3417 --- /dev/null +++ b/src/Data/Managers/V2/POST/PromoteDesiredStateVersion/index.ts @@ -0,0 +1 @@ +export * from "./usePromoteDesiredStateVersion"; diff --git a/src/Data/Managers/V2/POST/PromoteDesiredStateVersion/usePromoteDesiredStateVersion.ts b/src/Data/Managers/V2/POST/PromoteDesiredStateVersion/usePromoteDesiredStateVersion.ts new file mode 100644 index 000000000..171699335 --- /dev/null +++ b/src/Data/Managers/V2/POST/PromoteDesiredStateVersion/usePromoteDesiredStateVersion.ts @@ -0,0 +1,58 @@ +import { + UseMutationResult, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; +import { PrimaryBaseUrlManager } from "@/UI"; + +import { useFetchHelpers } from "../../helpers"; + +/** + * React Query hook for promoting a version of desired state + * + * @param {string} env - The environment in which we are trying to promote the version. + * @returns {Mutation} The mutation object for sending the request. + */ +export const usePromoteDesiredStateVersion = ( + environment: string, +): UseMutationResult => { + const client = useQueryClient(); + const { createHeaders, handleErrors } = useFetchHelpers(); + const headers = createHeaders(environment); + const baseUrlManager = new PrimaryBaseUrlManager( + globalThis.location.origin, + globalThis.location.pathname, + ); + const baseUrl = baseUrlManager.getBaseUrl(process.env.API_BASEURL); + + /** + * Sends a request to promote a version of desired state + * @param {string} version - the stringified version of desired state. + * @throws {Error} If the response is not successful, an error with the error message is thrown. + */ + const promoteDesiredStateVersion = async (version: string): Promise => { + const response = await fetch( + baseUrl + `/api/v2/desiredstate/${version}/promote`, + { + method: "POST", + headers, + }, + ); + + await handleErrors(response); + }; + + return useMutation({ + mutationFn: promoteDesiredStateVersion, + mutationKey: ["promote_version"], + onSuccess: () => { + // Refetch the desired state queries to update the list + client.invalidateQueries({ + queryKey: ["get_desired_states-continuous"], + }); + client.invalidateQueries({ + queryKey: ["get_desired_states-one_time"], + }); + }, + }); +}; diff --git a/src/Data/Managers/V2/POST/UpdateEnvConfig/index.ts b/src/Data/Managers/V2/POST/UpdateEnvConfig/index.ts new file mode 100644 index 000000000..068295aa9 --- /dev/null +++ b/src/Data/Managers/V2/POST/UpdateEnvConfig/index.ts @@ -0,0 +1 @@ +export * from "./useUpdateEnvConfig"; diff --git a/src/Data/Managers/V2/POST/UpdateEnvConfig/useUpdateEnvConfig.ts b/src/Data/Managers/V2/POST/UpdateEnvConfig/useUpdateEnvConfig.ts new file mode 100644 index 000000000..035eba88f --- /dev/null +++ b/src/Data/Managers/V2/POST/UpdateEnvConfig/useUpdateEnvConfig.ts @@ -0,0 +1,71 @@ +import { + UseMutationResult, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; +import { ParsedNumber } from "@/Core"; +import { PrimaryBaseUrlManager } from "@/UI"; +import { Dict } from "@/UI/Components"; +import { useFetchHelpers } from "../../helpers"; + +interface ConfigUpdate { + id: string; + value: string | boolean | ParsedNumber | Dict; +} + +/** + * React Query hook for updating environment configuration settings. + * + * @param {string} environment - The environment to use for creating headers. + * @returns {UseMutationResult}- The mutation object from `useMutation` hook. + */ +export const useUpdateEnvConfig = ( + environment: string, +): UseMutationResult => { + const client = useQueryClient(); + + const baseUrlManager = new PrimaryBaseUrlManager( + globalThis.location.origin, + globalThis.location.pathname, + ); + const { createHeaders, handleErrors } = useFetchHelpers(); + const headers = createHeaders(environment); + const baseUrl = baseUrlManager.getBaseUrl(process.env.API_BASEURL); + + /** + * Update the environment configuration setting. + * + * @param {ConfigUpdate} configUpdate - The info about the config setting to update + * + * @returns {Promise} - The promise object of the fetch request. + * @throws {Error} If the response is not successful, an error with the error message is thrown. + */ + const updateConfig = async (configUpdate: ConfigUpdate): Promise => { + const { id, value } = configUpdate; + + const response = await fetch( + baseUrl + `/api/v2/environment_settings/${id}`, + { + method: "POST", + body: JSON.stringify({ value }), + headers, + }, + ); + + await handleErrors(response); + }; + + return useMutation({ + mutationFn: updateConfig, + mutationKey: ["update_env_config"], + onSuccess: () => { + client.invalidateQueries({ + queryKey: ["get_env_config"], //for the future rework of the env getter + }); + client.invalidateQueries({ + queryKey: ["get_env_details"], //for the future rework of the env getter + }); + document.dispatchEvent(new Event("settings-update")); + }, + }); +}; diff --git a/src/Data/Managers/index.ts b/src/Data/Managers/index.ts index 722eda54e..5fa2b293a 100644 --- a/src/Data/Managers/index.ts +++ b/src/Data/Managers/index.ts @@ -24,7 +24,6 @@ export { TriggerSetStateCommandManager } from "./TriggerSetState"; export * from "./TriggerForceState"; export * from "./Deploy"; export * from "./Repair"; -export * from "./PromoteVersion"; export * from "./ControlAgent"; export * from "./GetCompilerStatus"; export * from "./GetCompilationState"; diff --git a/src/Data/Resolvers/CommandManagerResolverImpl.ts b/src/Data/Resolvers/CommandManagerResolverImpl.ts index 1c73ac01e..7845749c1 100644 --- a/src/Data/Resolvers/CommandManagerResolverImpl.ts +++ b/src/Data/Resolvers/CommandManagerResolverImpl.ts @@ -21,8 +21,6 @@ import { RepairCommandManager, DeployCommandManager, GetSupportArchiveCommandManager, - PromoteVersionCommandManager, - DesiredStatesUpdater, ControlAgentCommandManager, TriggerCompileCommandManager, TriggerDryRun, @@ -40,7 +38,6 @@ import { CreateProjectCommandManager, } from "@S/CreateEnvironment/Data"; import { CreateInstanceCommandManager } from "@S/CreateInstance/Data"; -import { GetDesiredStatesStateHelper } from "@S/DesiredState/Data"; import { TriggerInstanceUpdateCommandManager } from "@S/EditInstance/Data"; import { DeleteEnvironmentCommandManager, ProjectsUpdater } from "@S/Home/Data"; import { UpdateNotificationCommandManager } from "@S/Notification/Data/CommandManager"; @@ -52,7 +49,6 @@ import { } from "@S/ServiceDetails/Data"; import { ClearEnvironmentCommandManager } from "@S/Settings/Data/ClearEnvironmentCommandManager"; import { AuthContextInterface } from "../Auth"; -import { DeleteVersionCommandManager } from "../Managers/DeleteVersion"; import { UpdateCatalogCommandManager } from "../Managers/UpdateCatalog/CommandManager"; import { UpdateInstanceAttributeCommandManager } from "../Managers/UpdateInstanceAttribute"; @@ -79,11 +75,6 @@ export class CommandManagerResolverImpl implements CommandManagerResolver { this.apiHelper, GetEnvironmentSettingStateHelper(this.store), ); - const getDesiredStatesStateHelper = GetDesiredStatesStateHelper(this.store); - const desiredStatesUpdater = new DesiredStatesUpdater( - getDesiredStatesStateHelper, - this.apiHelper, - ); const callbacksUpdater = new CallbacksUpdater( CallbacksStateHelper(this.store), this.apiHelper, @@ -163,8 +154,6 @@ export class CommandManagerResolverImpl implements CommandManagerResolver { GenerateTokenCommandManager(this.apiHelper), DeployCommandManager(this.apiHelper), RepairCommandManager(this.apiHelper), - PromoteVersionCommandManager(this.apiHelper, desiredStatesUpdater), - DeleteVersionCommandManager(this.apiHelper), ControlAgentCommandManager( this.apiHelper, new GetAgentsUpdater(this.store, this.apiHelper), diff --git a/src/Data/Resolvers/QueryManagerResolverImpl.ts b/src/Data/Resolvers/QueryManagerResolverImpl.ts index 4c4f1fae2..9f78dfc89 100644 --- a/src/Data/Resolvers/QueryManagerResolverImpl.ts +++ b/src/Data/Resolvers/QueryManagerResolverImpl.ts @@ -44,7 +44,6 @@ import { EnvironmentDetailsOneTimeQueryManager, } from "@/Slices/Settings/Data/GetEnvironmentDetails"; import { GetProjectsQueryManager } from "@/Slices/Settings/Data/GetProjects"; -import { GetAgentProcessQueryManager } from "@S/AgentProcess/Data"; import { GetAgentsQueryManager } from "@S/Agents/Data"; import { CompileDetailsQueryManager } from "@S/CompileDetails/Data"; import { CompileReportsQueryManager } from "@S/CompileReports/Data"; @@ -52,10 +51,6 @@ import { GetDryRunReportQueryManager, GetDryRunsQueryManager, } from "@S/ComplianceCheck/Data"; -import { - GetDesiredStatesQueryManager, - GetDesiredStatesStateHelper, -} from "@S/DesiredState/Data"; import { GetDesiredStateDiffQueryManager, GetDesiredStateDiffStateHelper, @@ -65,10 +60,6 @@ import { GetVersionResourcesStateHelper, } from "@S/DesiredStateDetails/Data"; import { GetDesiredStateResourceDetailsQueryManager } from "@S/DesiredStateResourceDetails/Data"; -import { - DiagnosticsQueryManager, - DiagnosticsStateHelper, -} from "@S/Diagnose/Data"; import { EventsQueryManager, EventsStateHelper } from "@S/Events/Data"; import { GetFactsQueryManager } from "@S/Facts/Data"; import { @@ -96,10 +87,6 @@ import { CallbacksQueryManager, CallbacksStateHelper, } from "@S/ServiceDetails/Data"; -import { - GetInstanceLogsQueryManager, - GetInstanceLogsStateHelper, -} from "@S/ServiceInstanceHistory/Data"; import { GetEnvironmentsContinuousQueryManager, GetEnvironmentsContinuousStateHelper, @@ -207,21 +194,11 @@ export class QueryManagerResolverImpl implements QueryManagerResolver { EventsStateHelper(this.store), this.scheduler, ), - GetInstanceLogsQueryManager( - this.apiHelper, - GetInstanceLogsStateHelper(this.store), - this.scheduler, - ), InstanceConfigQueryManager( this.apiHelper, InstanceConfigStateHelper(this.store), new InstanceConfigFinalizer(serviceStateHelper), ), - DiagnosticsQueryManager( - this.apiHelper, - DiagnosticsStateHelper(this.store), - this.scheduler, - ), GetDiscoveredResourcesQueryManager( this.apiHelper, GetDiscoveredResourcesStateHelper(this.store), @@ -263,12 +240,6 @@ export class QueryManagerResolverImpl implements QueryManagerResolver { this.scheduler, ), GetAgentsQueryManager(this.store, this.apiHelper, this.scheduler), - GetAgentProcessQueryManager(this.store, this.apiHelper), - GetDesiredStatesQueryManager( - this.apiHelper, - GetDesiredStatesStateHelper(this.store), - this.scheduler, - ), GetVersionResourcesQueryManager( this.apiHelper, GetVersionResourcesStateHelper(this.store), diff --git a/src/Data/Store/ServicesSlice.test.ts b/src/Data/Store/ServicesSlice.test.ts index b70bc2a91..9b52f394e 100644 --- a/src/Data/Store/ServicesSlice.test.ts +++ b/src/Data/Store/ServicesSlice.test.ts @@ -6,6 +6,7 @@ describe("ServicesSlice", () => { const serviceModels: ServiceModel[] = [ { attributes: [], + inter_service_relations: [], environment: "env-id", lifecycle: { initial_state: "", states: [], transfers: [] }, name: "test_service", @@ -16,6 +17,7 @@ describe("ServicesSlice", () => { }, { attributes: [], + inter_service_relations: [], environment: "env-id", lifecycle: { initial_state: "", states: [], transfers: [] }, name: "another_test_service", diff --git a/src/Data/Store/Store.ts b/src/Data/Store/Store.ts index 635d05fe6..8815b56c4 100644 --- a/src/Data/Store/Store.ts +++ b/src/Data/Store/Store.ts @@ -8,10 +8,6 @@ import { DiscoveredResourcesSlice, discoveredResourcesSlice, } from "@/Slices/ResourceDiscovery/Data/Store"; -import { - agentProcessSlice, - AgentProcessSlice, -} from "@S/AgentProcess/Data/Store"; import { agentsSlice, AgentsSlice } from "@S/Agents/Data/Store"; import { compileDetailsSlice, @@ -29,10 +25,6 @@ import { dryRunsSlice, DryRunsSlice, } from "@S/ComplianceCheck/Data/DryRunsSlice"; -import { - DesiredStatesSlice, - desiredStatesSlice, -} from "@S/DesiredState/Data/Store"; import { desiredStateDiffSlice, DesiredStateDiffSlice, @@ -45,7 +37,6 @@ import { versionedResourceDetailsSlice, VersionedResourceDetailsSlice, } from "@S/DesiredStateResourceDetails/Data/Store"; -import { diagnosticsSlice, DiagnosticsSlice } from "@S/Diagnose/Data/Store"; import { factsSlice, FactsSlice } from "@S/Facts/Data/Store"; import { notificationSlice, @@ -72,10 +63,6 @@ import { CallbacksSlice, callbacksSlice, } from "@S/ServiceDetails/Data/CallbacksSlice"; -import { - InstanceLogsSlice, - instanceLogsSlice, -} from "@S/ServiceInstanceHistory/Data/Store"; import { serverStatusSlice, ServerStatusSlice } from "@S/Status/Data/Store"; import { environmentSlice, EnvironmentSlice } from "./EnvironmentSlice"; import { @@ -100,14 +87,11 @@ import { import { servicesSlice, ServicesSlice } from "./ServicesSlice"; export interface StoreModel { - agentProcess: AgentProcessSlice; agents: AgentsSlice; callbacks: CallbacksSlice; compileDetails: CompileDetailsSlice; compileReports: CompileReportsSlice; desiredStateDiff: DesiredStateDiffSlice; - desiredStates: DesiredStatesSlice; - diagnostics: DiagnosticsSlice; discoveredResources: DiscoveredResourcesSlice; dryRunReport: DryRunReportSlice; dryRuns: DryRunsSlice; @@ -115,7 +99,6 @@ export interface StoreModel { events: EventsSlice; facts: FactsSlice; instanceConfig: InstanceConfigSlice; - instanceLogs: InstanceLogsSlice; instanceResources: InstanceResourcesSlice; notification: NotificationSlice; parameters: ParametersSlice; @@ -137,14 +120,11 @@ export interface StoreModel { } export const storeModel: StoreModel = { - agentProcess: agentProcessSlice, agents: agentsSlice, callbacks: callbacksSlice, compileDetails: compileDetailsSlice, compileReports: compileReportsSlice, desiredStateDiff: desiredStateDiffSlice, - desiredStates: desiredStatesSlice, - diagnostics: diagnosticsSlice, discoveredResources: discoveredResourcesSlice, dryRunReport: dryRunReportSlice, dryRuns: dryRunsSlice, @@ -152,7 +132,6 @@ export const storeModel: StoreModel = { events: eventsSlice, facts: factsSlice, instanceConfig: instanceConfigSlice, - instanceLogs: instanceLogsSlice, instanceResources: instanceResourcesSlice, notification: notificationSlice, parameters: parametersSlice, diff --git a/src/Injector.tsx b/src/Injector.tsx index 9a13a60eb..19c5999c5 100644 --- a/src/Injector.tsx +++ b/src/Injector.tsx @@ -25,11 +25,21 @@ import { } from "@/UI"; import { AuthContext } from "./Data/Auth/"; import { UpdateBanner } from "./UI/Components/UpdateBanner"; +import { ModalProvider } from "./UI/Root/Components/ModalProvider"; interface Props { store: Store; } +/** + * This component creates instances of managers, helpers, and resolvers, and provides them through a `DependencyProvider`. + * It also contains `ModalProvider` and an `UpdateBanner`. + * + * @props {Props} props - The properties passed to the component. + * @prop {Store} store - The store to be used by the managers and resolvers. + * @prop {React.ReactNode} children - The children to be rendered within the Injector. + * @returns {React.FC>} A `DependencyProvider` that wraps a `ModalProvider`, an `UpdateBanner`, and the children. + */ export const Injector: React.FC> = ({ store, children, @@ -84,8 +94,10 @@ export const Injector: React.FC> = ({ authHelper, }} > - - {children} + + + {children} + ); }; diff --git a/src/Slices/AgentProcess/Core/Domain.ts b/src/Slices/AgentProcess/Core/Domain.ts deleted file mode 100644 index be2ee843a..000000000 --- a/src/Slices/AgentProcess/Core/Domain.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface AgentProcess { - sid: string; - hostname: string; - environment: string; - first_seen?: string; - last_seen?: string; - expired?: string; - state?: Record; -} diff --git a/src/Slices/AgentProcess/Core/Mock.ts b/src/Slices/AgentProcess/Core/Mock.ts deleted file mode 100644 index 38ca4d337..000000000 --- a/src/Slices/AgentProcess/Core/Mock.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { AgentProcess } from "./Domain"; - -export const data: AgentProcess = { - sid: "b2c8937e-5359-11ec-9c3f-98e743b19755", - hostname: "hostname1", - environment: "13963724-e4e8-4818-b019-d130daabfdd9", - first_seen: "2021-12-02T10:22:05.839054", - last_seen: "2021-12-02T10:39:09.640165", - state: { - environment: "13963724-e4e8-4818-b019-d130daabfdd9", - platform: "Linux-5.4.0-90-generic-x86_64-with-glibc2.27", - hostname: "hostname1", - ips: { - v4: ["127.0.0.1", "192.168.0.1"], - v6: ["::1", "2a02:1911:c95:9c00:6456:d690:521d:1c0f"], - }, - python: "CPython 3.8.0 ('default', 'Feb 25 2021 22:10:10')", - pid: "4989", - resources: { - utime: 15.475143, - stime: 1.433864, - maxrss: 2989796, - ixrss: 0, - idrss: 0, - isrss: 0, - minflt: 63511, - majflt: 0, - nswap: 0, - inblock: 0, - oublock: 24, - msgsnd: 0, - msgrcv: 0, - nsignals: 0, - nvcsw: 21305, - nivcsw: 558, - }, - }, -}; diff --git a/src/Slices/AgentProcess/Core/Query.ts b/src/Slices/AgentProcess/Core/Query.ts deleted file mode 100644 index 949e49493..000000000 --- a/src/Slices/AgentProcess/Core/Query.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AgentProcess } from "./Domain"; - -export interface Query { - kind: "GetAgentProcess"; - id: string; -} - -export interface Manifest { - error: string; - apiResponse: { - data: AgentProcess; - }; - data: AgentProcess; - usedData: AgentProcess; - query: Query; -} diff --git a/src/Slices/AgentProcess/Core/Route.ts b/src/Slices/AgentProcess/Core/Route.ts deleted file mode 100644 index aea3c52f7..000000000 --- a/src/Slices/AgentProcess/Core/Route.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Route } from "@/Core"; - -export const path = "/agents/:id"; - -export const route = (base: string): Route<"AgentProcess"> => ({ - kind: "AgentProcess", - parent: "Agents", - path: `${base}${path}`, - generateLabel: () => "Agent Process", - environmentRole: "Required", -}); diff --git a/src/Slices/AgentProcess/Data/QueryManager.ts b/src/Slices/AgentProcess/Data/QueryManager.ts deleted file mode 100644 index e77c49348..000000000 --- a/src/Slices/AgentProcess/Data/QueryManager.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { identity } from "lodash-es"; -import { ApiHelper } from "@/Core"; -import { QueryManager } from "@/Data/Managers/Helpers"; -import { Store } from "@/Data/Store"; -import { StateHelper } from "./StateHelper"; - -export function GetAgentProcessQueryManager( - store: Store, - apiHelper: ApiHelper, -) { - return QueryManager.OneTimeWithEnv<"GetAgentProcess">( - apiHelper, - StateHelper(store), - ({ id }) => [id], - "GetAgentProcess", - ({ id }) => `/api/v2/agents/process/${id}?report=True`, - identity, - ); -} diff --git a/src/Slices/AgentProcess/Data/StateHelper.ts b/src/Slices/AgentProcess/Data/StateHelper.ts deleted file mode 100644 index c30e42cd6..000000000 --- a/src/Slices/AgentProcess/Data/StateHelper.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { RemoteData } from "@/Core"; -import { PrimaryStateHelper } from "@/Data/Common"; -import { Store } from "@/Data/Store"; - -export function StateHelper(store: Store) { - return PrimaryStateHelper<"GetAgentProcess">( - store, - (data, { id }) => { - const value = RemoteData.mapSuccess((data) => data.data, data); - - store.dispatch.agentProcess.setData({ id, value }); - }, - (state, { id }) => state.agentProcess.byId[id], - ); -} diff --git a/src/Slices/AgentProcess/Data/Store.ts b/src/Slices/AgentProcess/Data/Store.ts deleted file mode 100644 index 7371ffcdc..000000000 --- a/src/Slices/AgentProcess/Data/Store.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Action, action } from "easy-peasy"; -import { RemoteData } from "@/Core"; -import { AgentProcess } from "@S/AgentProcess/Core/Domain"; - -export interface AgentProcessSlice { - byId: Record>; - setData: Action< - AgentProcessSlice, - { id: string; value: RemoteData.Type } - >; -} - -export const agentProcessSlice: AgentProcessSlice = { - byId: {}, - setData: action((state, payload) => { - state.byId[payload.id] = payload.value; - }), -}; diff --git a/src/Slices/AgentProcess/Data/index.ts b/src/Slices/AgentProcess/Data/index.ts deleted file mode 100644 index 03002773c..000000000 --- a/src/Slices/AgentProcess/Data/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./QueryManager"; diff --git a/src/Slices/AgentProcess/UI/AgentProcessDetails.tsx b/src/Slices/AgentProcess/UI/AgentProcessDetails.tsx deleted file mode 100644 index e430fc9b4..000000000 --- a/src/Slices/AgentProcess/UI/AgentProcessDetails.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from "react"; -import { JsonFormatter, XmlFormatter } from "@/Data"; -import { MomentDatePresenter, words } from "@/UI"; -import { - AttributeClassifier, - AttributeList, - PagePadder, - PageTitle, -} from "@/UI/Components"; -import { AgentProcess } from "@S/AgentProcess/Core/Domain"; - -interface Props { - agentProcess: AgentProcess; -} - -export const AgentProcessDetails: React.FC = ({ - agentProcess, - ...props -}) => { - const classifier = new AttributeClassifier( - new JsonFormatter(), - new XmlFormatter(), - ); - - const classifiedReport = classifier.classify( - agentProcess.state ? agentProcess.state : {}, - ); - - // Add the dates to the top - classifiedReport.unshift( - { - kind: "SingleLine", - key: words("agentProcess.firstSeen"), - value: getFormattedDate(agentProcess.first_seen), - }, - { - kind: "SingleLine", - key: words("agentProcess.lastSeen"), - value: getFormattedDate(agentProcess.last_seen), - }, - { - kind: "SingleLine", - key: words("agentProcess.expired"), - value: getFormattedDate(agentProcess.expired), - }, - ); - - return ( - - {`${words("agentProcess.title")} ${ - agentProcess.hostname - }`} - - - ); -}; - -function getFormattedDate(date?: string): string { - const datePresenter = new MomentDatePresenter(); - - return date ? datePresenter.getFull(date) : ""; -} diff --git a/src/Slices/AgentProcess/UI/Page.test.tsx b/src/Slices/AgentProcess/UI/Page.test.tsx deleted file mode 100644 index 94fa568cc..000000000 --- a/src/Slices/AgentProcess/UI/Page.test.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { act } from "react"; -import { MemoryRouter } from "react-router-dom"; -import { render, screen } from "@testing-library/react"; -import { StoreProvider } from "easy-peasy"; -import { configureAxe, toHaveNoViolations } from "jest-axe"; -import { Either } from "@/Core"; -import { - getStoreInstance, - QueryResolverImpl, - QueryManagerResolverImpl, -} from "@/Data"; -import { DeferredApiHelper, dependencies, StaticScheduler } from "@/Test"; -import { DependencyProvider } from "@/UI/Dependency"; -import * as AgentProcessMock from "@S/AgentProcess/Core/Mock"; -import { Page } from "./Page"; -expect.extend(toHaveNoViolations); - -const axe = configureAxe({ - rules: { - // disable landmark rules when testing isolated components. - region: { enabled: false }, - }, -}); - -function setup() { - const store = getStoreInstance(); - const scheduler = new StaticScheduler(); - const apiHelper = new DeferredApiHelper(); - const queryResolver = new QueryResolverImpl( - new QueryManagerResolverImpl(store, apiHelper, scheduler, scheduler), - ); - - const component = ( - - - - - - - - ); - - return { component, apiHelper, scheduler }; -} - -test("Agent Process Page shows failed view", async () => { - const { component, apiHelper } = setup(); - - render(component); - - expect( - await screen.findByRole("region", { name: "AgentProcessView-Loading" }), - ).toBeInTheDocument(); - - apiHelper.resolve(Either.left("error")); - - expect( - await screen.findByRole("region", { name: "AgentProcessView-Failed" }), - ).toBeInTheDocument(); - - await act(async () => { - const results = await axe(document.body); - - expect(results).toHaveNoViolations(); - }); -}); - -test("Agent Process Page shows success view", async () => { - const { component, apiHelper } = setup(); - - render(component); - - expect( - await screen.findByRole("region", { name: "AgentProcessView-Loading" }), - ).toBeInTheDocument(); - - apiHelper.resolve(Either.right({ data: AgentProcessMock.data })); - - expect( - await screen.findByRole("generic", { name: "AgentProcessView-Success" }), - ).toBeInTheDocument(); - - await act(async () => { - const results = await axe(document.body); - - expect(results).toHaveNoViolations(); - }); -}); diff --git a/src/Slices/AgentProcess/UI/Page.tsx b/src/Slices/AgentProcess/UI/Page.tsx deleted file mode 100644 index 28409cdf9..000000000 --- a/src/Slices/AgentProcess/UI/Page.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { useContext } from "react"; -import { PageSection } from "@patternfly/react-core"; -import { RemoteDataView } from "@/UI/Components"; -import { DependencyContext } from "@/UI/Dependency"; -import { useRouteParams } from "@/UI/Routing"; -import { AgentProcessDetails } from "./AgentProcessDetails"; - -export const Page: React.FC = () => { - const { id } = useRouteParams<"AgentProcess">(); - const { queryResolver } = useContext(DependencyContext); - - const [data] = queryResolver.useOneTime<"GetAgentProcess">({ - kind: "GetAgentProcess", - id, - }); - - return ( - - ( - - )} - /> - - ); -}; diff --git a/src/Slices/AgentProcess/UI/index.ts b/src/Slices/AgentProcess/UI/index.ts deleted file mode 100644 index f5c3104ff..000000000 --- a/src/Slices/AgentProcess/UI/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Page as AgentProcessPage } from "./Page"; diff --git a/src/Slices/AgentProcess/index.ts b/src/Slices/AgentProcess/index.ts deleted file mode 100644 index 81bf47ea7..000000000 --- a/src/Slices/AgentProcess/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as AgentProcess from "./Core/Route"; diff --git a/src/Slices/Agents/UI/Agents.test.tsx b/src/Slices/Agents/UI/Agents.test.tsx index 598a1c9b1..732f364a3 100644 --- a/src/Slices/Agents/UI/Agents.test.tsx +++ b/src/Slices/Agents/UI/Agents.test.tsx @@ -152,32 +152,22 @@ test("When using the name filter then only the matching agents should be fetched expect(initialRows).toHaveLength(6); - await act(async () => { - await userEvent.click( - within(screen.getByRole("toolbar", { name: "FilterBar" })).getByRole( - "button", - { name: "FilterPicker" }, - ), - ); - }); + await userEvent.click( + within(screen.getByRole("toolbar", { name: "FilterBar" })).getByRole( + "button", + { name: "FilterPicker" }, + ), + ); - await act(async () => { - await userEvent.click( - screen.getByRole("option", { name: words("attribute.name") }), - ); - }); + await userEvent.click( + screen.getByRole("option", { name: words("attribute.name") }), + ); const input = screen.getByPlaceholderText( words("home.filters.env.placeholder"), ); - await act(async () => { - await userEvent.click(input); - }); - - await act(async () => { - await userEvent.type(input, "internal{enter}"); - }); + await userEvent.type(input, "internal{enter}"); expect(apiHelper.pendingRequests[0].url).toEqual( `/api/v2/agents?limit=20&filter.name=internal&sort=name.asc`, @@ -205,7 +195,7 @@ test("When using the name filter then only the matching agents should be fetched }); }); -test("When using the process name filter then only the matching agents should be fetched and shown", async () => { +test("When using the status filter with the 'up' option then the agents in the 'up' state should be fetched and shown", async () => { const { component, apiHelper } = setup(); render(component); @@ -220,94 +210,22 @@ test("When using the process name filter then only the matching agents should be expect(initialRows).toHaveLength(6); - await act(async () => { - await userEvent.click( - within(screen.getByRole("toolbar", { name: "FilterBar" })).getByRole( - "button", - { name: "FilterPicker" }, - ), - ); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("option", { name: words("agent.tests.processName") }), - ); - }); - - const input = screen.getByPlaceholderText( - words("agents.filters.processName.placeholder"), + await userEvent.click( + within(screen.getByRole("toolbar", { name: "FilterBar" })).getByRole( + "button", + { name: "FilterPicker" }, + ), ); - await act(async () => { - await userEvent.click(input); - }); - - await act(async () => { - await userEvent.type(input, "internal{enter}"); - }); - - expect(apiHelper.pendingRequests[0].url).toEqual( - `/api/v2/agents?limit=20&filter.process_name=internal&sort=name.asc`, + await userEvent.click( + screen.getByRole("option", { name: words("agent.tests.status") }), ); - await act(async () => { - await apiHelper.resolve( - Either.right({ - ...AgentsMock.response, - data: AgentsMock.response.data.slice(0, 3), - }), - ); - }); - - const rowsAfter = await screen.findAllByRole("row", { - name: "Agents Table Row", - }); - - expect(rowsAfter).toHaveLength(3); - - await act(async () => { - const results = await axe(document.body); - - expect(results).toHaveNoViolations(); - }); -}); - -test("When using the status filter with the 'up' option then the agents in the 'up' state should be fetched and shown", async () => { - const { component, apiHelper } = setup(); - - render(component); - - await act(async () => { - await apiHelper.resolve(Either.right(AgentsMock.response)); - }); - - const initialRows = await screen.findAllByRole("row", { - name: "Agents Table Row", - }); - - expect(initialRows).toHaveLength(6); - - await act(async () => { - await userEvent.click( - within(screen.getByRole("toolbar", { name: "FilterBar" })).getByRole( - "button", - { name: "FilterPicker" }, - ), - ); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("option", { name: words("agent.tests.status") }), - ); - }); - const input = screen.getByPlaceholderText( words("agents.filters.status.placeholder"), ); - await act(async () => { - await userEvent.click(input); - }); + await userEvent.click(input); await act(async () => { const results = await axe(document.body); @@ -319,9 +237,7 @@ test("When using the status filter with the 'up' option then the agents in the ' name: words("agent.tests.up"), }); - await act(async () => { - await userEvent.click(option); - }); + await userEvent.click(option); expect(apiHelper.pendingRequests[0].url).toEqual( `/api/v2/agents?limit=20&filter.status=up&sort=name.asc`, @@ -356,12 +272,7 @@ test("Given the Agents view with filters, When pausing an agent, then the correc words("agents.filters.name.placeholder"), ); - await act(async () => { - await userEvent.click(input); - }); - await act(async () => { - await userEvent.type(input, "aws{enter}"); - }); + await userEvent.type(input, "aws{enter}"); await act(async () => { await apiHelper.resolve(Either.right(AgentsMock.response)); @@ -373,9 +284,7 @@ test("Given the Agents view with filters, When pausing an agent, then the correc name: words("agents.actions.pause"), }); - await act(async () => { - await userEvent.click(pauseAgentButton); - }); + await userEvent.click(pauseAgentButton); expect(apiHelper.pendingRequests).toHaveLength(1); const request = apiHelper.pendingRequests[0]; @@ -422,12 +331,7 @@ test("Given the Agents view with filters, When unpausing an agent, then the corr words("agents.filters.name.placeholder"), ); - await act(async () => { - await userEvent.click(input); - }); - await act(async () => { - await userEvent.type(input, "bru{enter}"); - }); + await userEvent.type(input, "bru{enter}"); await act(async () => { await apiHelper.resolve(Either.right(AgentsMock.response)); @@ -439,9 +343,7 @@ test("Given the Agents view with filters, When unpausing an agent, then the corr name: words("agents.actions.unpause"), }); - await act(async () => { - await userEvent.click(unpauseAgentButton); - }); + await userEvent.click(unpauseAgentButton); await act(async () => { const results = await axe(document.body); @@ -496,9 +398,8 @@ test("Given the Agents view When pausing an agent results in an error, then the expect(results).toHaveNoViolations(); }); - await act(async () => { - await userEvent.click(pauseAgentButton); - }); + await userEvent.click(pauseAgentButton); + expect(apiHelper.pendingRequests).toHaveLength(1); const request = apiHelper.pendingRequests[0]; @@ -528,15 +429,14 @@ test("Given the Agents view with the environment halted, When setting keep_pause await apiHelper.resolve(Either.right(AgentsMock.response)); }); - const onResumeToggle = await screen.findByRole("checkbox", { + const onResumeToggle = await screen.findByRole("switch", { name: "aws-on-resume-toggle", }); expect(onResumeToggle).toBeVisible(); expect(onResumeToggle).toBeChecked(); - await act(async () => { - await userEvent.click(onResumeToggle); - }); + + await userEvent.click(onResumeToggle); expect(apiHelper.pendingRequests[0]).toEqual({ method: "POST", @@ -586,15 +486,14 @@ test("Given the Agents view with the environment halted, When setting unpause_on await apiHelper.resolve(Either.right(AgentsMock.response)); }); - const onResumeToggle = await screen.findByRole("checkbox", { + const onResumeToggle = await screen.findByRole("switch", { name: "ecx-on-resume-toggle", }); expect(onResumeToggle).toBeVisible(); expect(onResumeToggle).not.toBeChecked(); - await act(async () => { - await userEvent.click(onResumeToggle); - }); + + await userEvent.click(onResumeToggle); expect(apiHelper.pendingRequests[0]).toEqual({ method: "POST", @@ -628,7 +527,7 @@ test("Given the Agents view with the environment NOT halted, THEN the on resume const tableHeaders = await screen.findAllByRole("columnheader"); - expect(tableHeaders).toHaveLength(4); + expect(tableHeaders).toHaveLength(2); const onResumeColumnHeader = tableHeaders.find( (header) => header.textContent === "On resume", @@ -658,7 +557,7 @@ test("Given the Agents view with the environment halted, THEN the on resume colu const tableHeaders = await screen.findAllByRole("columnheader"); - expect(tableHeaders).toHaveLength(5); + expect(tableHeaders).toHaveLength(3); const onResumeColumnHeader = tableHeaders.find( (header) => header.textContent === "On resume", @@ -699,9 +598,7 @@ test("GIVEN AgentsView WHEN sorting changes AND we are not on the first page THE expect(screen.getByLabelText("Go to next page")).toBeEnabled(); - await act(async () => { - await userEvent.click(screen.getByLabelText("Go to next page")); - }); + await userEvent.click(screen.getByLabelText("Go to next page")); //expect the api url to contain start and end keywords that are used for pagination when we are moving to the next page expect(apiHelper.pendingRequests[0].url).toMatch(/(&start=|&end=)/); @@ -726,9 +623,7 @@ test("GIVEN AgentsView WHEN sorting changes AND we are not on the first page THE }); //sort on the second page - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "Name" })); - }); + await userEvent.click(screen.getByRole("button", { name: "Name" })); // expect the api url to not contain start and end keywords that are used for pagination to assert we are back on the first page. // we are asserting on the second request as the first request is for the updated sorting event, and second is chained to back to the first page with still correct sorting diff --git a/src/Slices/Agents/UI/AgentsTable.tsx b/src/Slices/Agents/UI/AgentsTable.tsx index bd005fea4..2f7d4385d 100644 --- a/src/Slices/Agents/UI/AgentsTable.tsx +++ b/src/Slices/Agents/UI/AgentsTable.tsx @@ -1,7 +1,7 @@ import React from "react"; import { OnSort, - Table /* data-codemods */, + Table, TableVariant, Th, Thead, diff --git a/src/Slices/Agents/UI/AgentsTablePresenter.ts b/src/Slices/Agents/UI/AgentsTablePresenter.ts index 1cfeaf656..bbd691e2a 100644 --- a/src/Slices/Agents/UI/AgentsTablePresenter.ts +++ b/src/Slices/Agents/UI/AgentsTablePresenter.ts @@ -10,12 +10,7 @@ export class AgentsTablePresenter implements TablePresenter { constructor(isHalted: boolean) { this.columnHeads = [ { displayName: words("agents.columns.name"), apiName: "name" }, - { displayName: words("agents.columns.process"), apiName: "process_name" }, { displayName: words("agents.columns.status"), apiName: "status" }, - { - displayName: words("agents.columns.failover"), - apiName: "last_failover", - }, { displayName: words("agents.columns.unpause"), apiName: "unpause_on_resume", @@ -57,7 +52,7 @@ export class AgentsTablePresenter implements TablePresenter { } getSortableColumnNames(): string[] { - const sortableColumns = ["name", "process_name", "status", "last_failover"]; + const sortableColumns = ["name", "status"]; return sortableColumns; } diff --git a/src/Slices/Agents/UI/AgentsTableRow.tsx b/src/Slices/Agents/UI/AgentsTableRow.tsx index 0accfb99b..1cc21704a 100644 --- a/src/Slices/Agents/UI/AgentsTableRow.tsx +++ b/src/Slices/Agents/UI/AgentsTableRow.tsx @@ -1,13 +1,11 @@ import React, { useContext } from "react"; -import { Button } from "@patternfly/react-core"; import { Tbody, Td, Tr } from "@patternfly/react-table"; -import { DateWithTooltip, Link } from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; import { words } from "@/UI/words"; import { AgentRow } from "@S/Agents/Core/Domain"; import { ActionButton, - StatusLabel, + AgentStatusLabel, KebabDropdown, OnResumeToggle, } from "./Components"; @@ -17,41 +15,25 @@ interface Props { } export const AgentsTableRow: React.FC = ({ row }) => { - const { routeManager, environmentModifier } = useContext(DependencyContext); + const { environmentModifier } = useContext(DependencyContext); const isHalted = environmentModifier.useIsHalted(); return ( {row.name} - - {row.process_id && ( - - - - )} - - - - - - {row.last_failover && ( - - )} + + {isHalted && ( - + )} - + diff --git a/src/Slices/Agents/UI/Components/ActionButton.tsx b/src/Slices/Agents/UI/Components/ActionButton.tsx index 76847b9ff..e72e66868 100644 --- a/src/Slices/Agents/UI/Components/ActionButton.tsx +++ b/src/Slices/Agents/UI/Components/ActionButton.tsx @@ -44,6 +44,7 @@ export const ActionButton: React.FC = ({ name, paused }) => { + + + ); +}; diff --git a/src/Slices/Diagnose/UI/Page.tsx b/src/Slices/Diagnose/UI/Page.tsx index b987b37c8..e7ca5e29f 100644 --- a/src/Slices/Diagnose/UI/Page.tsx +++ b/src/Slices/Diagnose/UI/Page.tsx @@ -1,42 +1,66 @@ import React, { useContext } from "react"; -import { RemoteData } from "@/Core"; -import { PageContainer, ServiceInstanceDescription } from "@/UI/Components"; + +import { useUrlStateWithString } from "@/Data"; +import { useGetInstance } from "@/Data/Managers/V2/GETTERS/GetInstance"; +import { + Description, + ErrorView, + LoadingView, + PageContainer, +} from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; import { useRouteParams } from "@/UI/Routing"; import { words } from "@/UI/words"; import { Diagnose } from "./Diagnose"; +import { LookBackSlider } from "./LookBackSlider"; export const Page: React.FC = () => { const { service, instance } = useRouteParams<"Diagnose">(); - const { queryResolver } = useContext(DependencyContext); - - const [data] = queryResolver.useContinuous<"GetServiceInstance">({ - kind: "GetServiceInstance", - service_entity: service, - id: instance, - }); - - return ( - - - - instanceData.service_identity_attribute_value || instanceData.id, - data, - ), - instance, - )} - /> - - ); + const { environmentHandler } = useContext(DependencyContext); + + const [amountOfVersionToLookBehind, setAmountOfVersionToLookBehind] = + useUrlStateWithString({ + default: "1", + key: `lookBehind`, + route: "Diagnose", + }); + const { data, error, isError, isSuccess } = useGetInstance( + service, + instance, + environmentHandler.useId(), + ).useContinuous(); + + const handleSliding = (value: number) => { + setAmountOfVersionToLookBehind(value.toString()); + }; + + if (isError) { + return ; + } + + if (isSuccess) { + const id = data.service_identity_attribute_value || instance; + const versionAsNumber = Number(data.version); + + return ( + + + + {words("diagnose.main.subtitle")(id)} + + + + ); + } + + return ; }; diff --git a/src/Slices/Diagnose/UI/RejectionCard.tsx b/src/Slices/Diagnose/UI/RejectionCard.tsx index 9fff7985a..1ae3cd9b2 100644 --- a/src/Slices/Diagnose/UI/RejectionCard.tsx +++ b/src/Slices/Diagnose/UI/RejectionCard.tsx @@ -11,10 +11,8 @@ import { MenuToggleElement, } from "@patternfly/react-core"; import { EllipsisVIcon } from "@patternfly/react-icons"; -import styled from "styled-components"; import { Link } from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; -import { greyText } from "@/UI/Styles"; import { words } from "@/UI/words"; import { Rejection } from "@S/Diagnose/Core/Domain"; import { Pre } from "./Pre"; @@ -73,9 +71,8 @@ export const RejectionCard: React.FC = ({ variant="plain" onClick={onToggleClick} isExpanded={isOpen} - > - - + icon={} + /> )} isOpen={isOpen} isPlain @@ -92,7 +89,7 @@ export const RejectionCard: React.FC = ({ > {words("diagnose.rejection.title")} - {error && {error.type}} + {error && {error.type}} {error &&
{error.message}
} {trace && } @@ -100,7 +97,3 @@ export const RejectionCard: React.FC = ({ ); }; - -const StyledCardTitle = styled(CardTitle)` - ${greyText} -`; diff --git a/src/Slices/DuplicateInstance/UI/DuplicateInstancePage.test.tsx b/src/Slices/DuplicateInstance/UI/DuplicateInstancePage.test.tsx index a062c46d5..ff732ee24 100644 --- a/src/Slices/DuplicateInstance/UI/DuplicateInstancePage.test.tsx +++ b/src/Slices/DuplicateInstance/UI/DuplicateInstancePage.test.tsx @@ -120,12 +120,9 @@ test("DuplicateInstance View shows success form", async () => { expect(bandwidthField).toBeVisible(); - await act(async () => { - await userEvent.type(bandwidthField, "3"); - }); - await act(async () => { - await userEvent.click(screen.getByText(words("confirm"))); - }); + await userEvent.type(bandwidthField, "3"); + + await userEvent.click(screen.getByText(words("confirm"))); await act(async () => { const results = await axe(document.body); @@ -209,16 +206,11 @@ test("Given the DuplicateInstance View When changing a embedded entity Then the expect(results).toHaveNoViolations(); }); - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "circuits" })); - }); - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "1" })); - }); + await userEvent.click(screen.getByRole("button", { name: "circuits" })); - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "csp_endpoint" })); - }); + await userEvent.click(screen.getByRole("button", { name: "1" })); + + await userEvent.click(screen.getByRole("button", { name: "csp_endpoint" })); const bandwidthField = screen.getByText("bandwidth"); @@ -228,16 +220,11 @@ test("Given the DuplicateInstance View When changing a embedded entity Then the "cloud_service_provider", )[0]; - await act(async () => { - await userEvent.type(firstCloudServiceProviderField, "2"); - }); + await userEvent.type(firstCloudServiceProviderField, "2"); - await act(async () => { - await userEvent.type(bandwidthField, "22"); - }); - await act(async () => { - await userEvent.click(screen.getByText(words("confirm"))); - }); + await userEvent.type(bandwidthField, "22"); + + await userEvent.click(screen.getByText(words("confirm"))); expect(apiHelper.pendingRequests).toHaveLength(1); @@ -334,49 +321,57 @@ test("Given the DuplicateInstance View When changing an embedded entity Then the "DictListFieldInput-editableOptionalEmbedded_base", ); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "embedded_base" }), - ); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "editableEmbedded_base" }), - ); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "optionalEmbedded_base" }), - ); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "editableOptionalEmbedded_base" }), - ); - }); + await userEvent.click(screen.getByRole("button", { name: "embedded_base" })); - expect(within(embedded_base).queryByText("Add")).toBeEnabled(); - expect(within(embedded_base).queryByText("Delete")).toBeDisabled(); + await userEvent.click( + screen.getByRole("button", { name: "editableEmbedded_base" }), + ); - expect(within(editableEmbedded_base).queryByText("Add")).toBeEnabled(); - expect(within(editableEmbedded_base).queryByText("Delete")).toBeDisabled(); + await userEvent.click( + screen.getByRole("button", { name: "optionalEmbedded_base" }), + ); - expect(within(optionalEmbedded_base).queryByText("Add")).toBeEnabled(); - expect(within(optionalEmbedded_base).queryByText("Delete")).toBeEnabled(); + await userEvent.click( + screen.getByRole("button", { name: "editableOptionalEmbedded_base" }), + ); expect( - within(editableOptionalEmbedded_base).queryByText("Add"), + within(embedded_base).queryByRole("button", { name: "Add" }), ).toBeEnabled(); expect( - within(editableOptionalEmbedded_base).queryByText("Delete"), + within(embedded_base).queryByRole("button", { name: "Delete" }), + ).toBeDisabled(); + + expect( + within(editableEmbedded_base).queryByRole("button", { name: "Add" }), + ).toBeEnabled(); + expect( + within(editableEmbedded_base).queryByRole("button", { name: "Delete" }), + ).toBeDisabled(); + + expect( + within(optionalEmbedded_base).queryByRole("button", { name: "Add" }), + ).toBeEnabled(); + expect( + within(optionalEmbedded_base).queryByRole("button", { name: "Delete" }), + ).toBeEnabled(); + + expect( + within(editableOptionalEmbedded_base).queryByRole("button", { + name: "Add", + }), + ).toBeEnabled(); + expect( + within(editableOptionalEmbedded_base).queryByRole("button", { + name: "Delete", + }), ).toBeEnabled(); //check if direct attributes for embedded entities are correctly displayed - await act(async () => { - await userEvent.click( - within(embedded_base).getByRole("button", { name: "0" }), - ); - }); + await userEvent.click( + within(embedded_base).getByRole("button", { name: "0" }), + ); + expect(within(embedded_base).queryByDisplayValue("string")).toBeEnabled(); expect( within(embedded_base).queryByDisplayValue("editableString"), @@ -408,31 +403,31 @@ test("Given the DuplicateInstance View When changing an embedded entity Then the expect( within(embedded_base).queryByLabelText("TextFieldInput-string[]"), - ).not.toHaveClass("is-disabled"); + ).toBeEnabled(); expect( within(embedded_base).queryByLabelText("TextFieldInput-editableString[]"), - ).not.toHaveClass("is-disabled"); + ).toBeEnabled(); expect( within(embedded_base).queryByLabelText("TextFieldInput-string[]?"), - ).not.toHaveClass("is-disabled"); + ).toBeEnabled(); expect( within(embedded_base).queryByLabelText("TextFieldInput-editableString[]?"), - ).not.toHaveClass("is-disabled"); + ).toBeEnabled(); expect( within(embedded_base).queryByTestId("enum-select-toggle"), - ).not.toHaveClass("pf-m-disabled"); + ).toBeEnabled(); expect( within(embedded_base).queryByTestId("editableEnum-select-toggle"), - ).not.toHaveClass("pf-m-disabled"); + ).toBeEnabled(); expect( within(embedded_base).queryByTestId("enum?-select-toggle"), - ).not.toHaveClass("pf-m-disabled"); + ).toBeEnabled(); expect( within(embedded_base).queryByTestId("editableEnum?-select-toggle"), - ).not.toHaveClass("pf-m-disabled"); + ).toBeEnabled(); expect( within(embedded_base).queryByLabelText("TextInput-dict"), @@ -463,53 +458,61 @@ test("Given the DuplicateInstance View When changing an embedded entity Then the "DictListFieldInput-embedded_base.0.editableEmbedded?", ); - await act(async () => { - await userEvent.click( - within(embedded_base).getByRole("button", { name: "embedded" }), - ); - }); + await userEvent.click( + within(embedded_base).getByRole("button", { name: "embedded" }), + ); - await act(async () => { - await userEvent.click( - within(embedded_base).getByRole("button", { - name: "editableEmbedded", - }), - ); - }); + await userEvent.click( + within(embedded_base).getByRole("button", { + name: "editableEmbedded", + }), + ); - await act(async () => { - await userEvent.click( - within(embedded_base).getByRole("button", { - name: "embedded?", - }), - ); - }); + await userEvent.click( + within(embedded_base).getByRole("button", { + name: "embedded?", + }), + ); - await act(async () => { - await userEvent.click( - within(embedded_base).getByRole("button", { - name: "editableEmbedded?", - }), - ); - }); + await userEvent.click( + within(embedded_base).getByRole("button", { + name: "editableEmbedded?", + }), + ); - expect(within(nested_embedded_base).queryByText("Add")).toBeEnabled(); - expect(within(nested_embedded_base).queryByText("Delete")).toBeDisabled(); + expect( + within(nested_embedded_base).queryByRole("button", { name: "Add" }), + ).toBeEnabled(); + expect( + within(nested_embedded_base).queryByRole("button", { name: "Delete" }), + ).toBeDisabled(); - expect(within(nested_editableEmbedded_base).queryByText("Add")).toBeEnabled(); expect( - within(nested_editableEmbedded_base).queryByText("Delete"), + within(nested_editableEmbedded_base).queryByRole("button", { name: "Add" }), + ).toBeEnabled(); + expect( + within(nested_editableEmbedded_base).queryByRole("button", { + name: "Delete", + }), ).toBeDisabled(); - expect(within(nested_optionalEmbedded_base).queryByText("Add")).toBeEnabled(); expect( - within(nested_optionalEmbedded_base).queryByText("Delete"), + within(nested_optionalEmbedded_base).queryByRole("button", { name: "Add" }), + ).toBeEnabled(); + expect( + within(nested_optionalEmbedded_base).queryByRole("button", { + name: "Delete", + }), ).toBeEnabled(); expect( - within(nested_editableOptionalEmbedded_base).queryByText("Add"), + within(nested_editableOptionalEmbedded_base).queryByRole("button", { + name: "Add", + }), ).toBeEnabled(); expect( - within(nested_editableOptionalEmbedded_base).queryByText("Delete"), + within(nested_editableOptionalEmbedded_base).queryByRole("button", { + name: "Delete", + }), ).toBeEnabled(); }); diff --git a/src/Slices/EditInstance/UI/EditInstancePage.test.tsx b/src/Slices/EditInstance/UI/EditInstancePage.test.tsx index 412a63c84..f59e09391 100644 --- a/src/Slices/EditInstance/UI/EditInstancePage.test.tsx +++ b/src/Slices/EditInstance/UI/EditInstancePage.test.tsx @@ -142,12 +142,9 @@ test("EditInstance View shows success form", async () => { expect(bandwidthField).toBeVisible(); - await act(async () => { - await userEvent.type(bandwidthField, "2"); - }); - await act(async () => { - await userEvent.click(screen.getByText(words("confirm"))); - }); + await userEvent.type(bandwidthField, "2"); + + await userEvent.click(screen.getByText(words("confirm"))); expect(apiHelper.pendingRequests).toHaveLength(1); expect(apiHelper.pendingRequests[0]).toEqual({ @@ -184,12 +181,9 @@ test("Given the EditInstance View When changing a v1 embedded entity Then the co await screen.findByRole("generic", { name: "EditInstance-Success" }), ).toBeInTheDocument(); - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "circuits" })); - }); - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "0" })); - }); + await userEvent.click(screen.getByRole("button", { name: "circuits" })); + + await userEvent.click(screen.getByRole("button", { name: "0" })); expect(screen.getByLabelText("TextInput-service_id")).toBeDisabled(); @@ -197,12 +191,9 @@ test("Given the EditInstance View When changing a v1 embedded entity Then the co expect(bandwidthField).toBeVisible(); - await act(async () => { - await userEvent.type(bandwidthField, "22"); - }); - await act(async () => { - await userEvent.click(screen.getByText(words("confirm"))); - }); + await userEvent.type(bandwidthField, "22"); + + await userEvent.click(screen.getByText(words("confirm"))); expect(apiHelper.pendingRequests).toHaveLength(1); @@ -240,12 +231,9 @@ test("Given the EditInstance View When changing a v2 embedded entity Then the co await screen.findByRole("generic", { name: "EditInstance-Success" }), ).toBeInTheDocument(); - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "circuits" })); - }); - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "0" })); - }); + await userEvent.click(screen.getByRole("button", { name: "circuits" })); + + await userEvent.click(screen.getByRole("button", { name: "0" })); expect(screen.getByLabelText("TextInput-service_id")).toBeDisabled(); @@ -253,12 +241,9 @@ test("Given the EditInstance View When changing a v2 embedded entity Then the co expect(bandwidthField).toBeVisible(); - await act(async () => { - await userEvent.type(bandwidthField, "24"); - }); - await act(async () => { - await userEvent.click(screen.getByText(words("confirm"))); - }); + await userEvent.type(bandwidthField, "24"); + + await userEvent.click(screen.getByText(words("confirm"))); expect(apiHelper.pendingRequests).toHaveLength(1); @@ -365,50 +350,57 @@ test("Given the EditInstance View When changing an embedded entity Then the inpu "DictListFieldInput-editableOptionalEmbedded_base", ); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "embedded_base" }), - ); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "editableEmbedded_base" }), - ); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "optionalEmbedded_base" }), - ); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "editableOptionalEmbedded_base" }), - ); - }); + await userEvent.click(screen.getByRole("button", { name: "embedded_base" })); + + await userEvent.click( + screen.getByRole("button", { name: "editableEmbedded_base" }), + ); - expect(within(embedded_base).queryByText("Add")).toBeDisabled(); - expect(within(embedded_base).queryByText("Delete")).toBeDisabled(); + await userEvent.click( + screen.getByRole("button", { name: "optionalEmbedded_base" }), + ); - expect(within(editableEmbedded_base).queryByText("Add")).toBeEnabled(); - expect(within(editableEmbedded_base).queryByText("Delete")).toBeDisabled(); + await userEvent.click( + screen.getByRole("button", { name: "editableOptionalEmbedded_base" }), + ); - expect(within(optionalEmbedded_base).queryByText("Add")).toBeDisabled(); - expect(within(optionalEmbedded_base).queryByText("Delete")).toBeDisabled(); + expect( + within(embedded_base).queryByRole("button", { name: "Add" }), + ).toBeDisabled(); + expect( + within(embedded_base).queryByRole("button", { name: "Delete" }), + ).toBeDisabled(); + + expect( + within(editableEmbedded_base).queryByRole("button", { name: "Add" }), + ).toBeEnabled(); + expect( + within(editableEmbedded_base).queryByRole("button", { name: "Delete" }), + ).toBeDisabled(); + + expect( + within(optionalEmbedded_base).queryByRole("button", { name: "Add" }), + ).toBeDisabled(); + expect( + within(optionalEmbedded_base).queryByRole("button", { name: "Delete" }), + ).toBeDisabled(); expect( - within(editableOptionalEmbedded_base).queryByText("Add"), + within(editableOptionalEmbedded_base).queryByRole("button", { + name: "Add", + }), ).toBeEnabled(); expect( - within(editableOptionalEmbedded_base).queryByText("Delete"), + within(editableOptionalEmbedded_base).queryByRole("button", { + name: "Delete", + }), ).toBeEnabled(); //check if direct attributes for embedded entities are correctly displayed + await userEvent.click( + within(embedded_base).getByRole("button", { name: "0" }), + ); - await act(async () => { - await userEvent.click( - within(embedded_base).getByRole("button", { name: "0" }), - ); - }); expect(within(embedded_base).queryByDisplayValue("string")).toBeDisabled(); expect( within(embedded_base).queryByDisplayValue("editableString"), @@ -495,56 +487,62 @@ test("Given the EditInstance View When changing an embedded entity Then the inpu "DictListFieldInput-embedded_base.0.editableEmbedded?", ); - await act(async () => { - await userEvent.click( - within(embedded_base).getByRole("button", { name: "embedded" }), - ); - }); + await userEvent.click( + within(embedded_base).getByRole("button", { name: "embedded" }), + ); - await act(async () => { - await userEvent.click( - within(embedded_base).getByRole("button", { - name: "editableEmbedded", - }), - ); - }); + await userEvent.click( + within(embedded_base).getByRole("button", { + name: "editableEmbedded", + }), + ); - await act(async () => { - await userEvent.click( - within(embedded_base).getByRole("button", { - name: "embedded?", - }), - ); - }); + await userEvent.click( + within(embedded_base).getByRole("button", { + name: "embedded?", + }), + ); - await act(async () => { - await userEvent.click( - within(embedded_base).getByRole("button", { - name: "editableEmbedded?", - }), - ); - }); + await userEvent.click( + within(embedded_base).getByRole("button", { + name: "editableEmbedded?", + }), + ); - expect(within(nested_embedded_base).queryByText("Add")).toBeDisabled(); - expect(within(nested_embedded_base).queryByText("Delete")).toBeDisabled(); + expect( + within(nested_embedded_base).queryByRole("button", { name: "Add" }), + ).toBeDisabled(); + expect( + within(nested_embedded_base).queryByRole("button", { name: "Delete" }), + ).toBeDisabled(); - expect(within(nested_editableEmbedded_base).queryByText("Add")).toBeEnabled(); expect( - within(nested_editableEmbedded_base).queryByText("Delete"), + within(nested_editableEmbedded_base).queryByRole("button", { name: "Add" }), + ).toBeEnabled(); + expect( + within(nested_editableEmbedded_base).queryByRole("button", { + name: "Delete", + }), ).toBeDisabled(); expect( - within(nested_optionalEmbedded_base).queryByText("Add"), + within(nested_optionalEmbedded_base).queryByRole("button", { name: "Add" }), ).toBeDisabled(); expect( - within(nested_optionalEmbedded_base).queryByText("Delete"), + within(nested_optionalEmbedded_base).queryByRole("button", { + name: "Delete", + }), ).toBeDisabled(); expect( - within(nested_editableOptionalEmbedded_base).queryByText("Add"), + within(nested_editableOptionalEmbedded_base).queryByRole("button", { + name: "Add", + }), ).toBeEnabled(); expect( - within(nested_editableOptionalEmbedded_base).queryByText("Delete"), + within(nested_editableOptionalEmbedded_base).queryByRole("button", { + name: "Delete", + }), ).toBeEnabled(); await act(async () => { @@ -573,23 +571,16 @@ test("Given the EditInstance View When adding new nested embedded entity Then th "DictListFieldInput-editableOptionalEmbedded_base", ); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "editableOptionalEmbedded_base" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "editableOptionalEmbedded_base" }), + ); - await act(async () => { - await userEvent.click( - within(editableOptionalEmbedded_base).getByText("Add"), - ); - }); + await userEvent.click(within(editableOptionalEmbedded_base).getByText("Add")); + + await userEvent.click( + within(editableOptionalEmbedded_base).getByRole("button", { name: "1" }), + ); - await act(async () => { - await userEvent.click( - within(editableOptionalEmbedded_base).getByRole("button", { name: "1" }), - ); - }); const addedOptionalEmbedded = screen.getByLabelText( "DictListFieldInputItem-editableOptionalEmbedded_base.1", ); @@ -662,65 +653,70 @@ test("Given the EditInstance View When adding new nested embedded entity Then th "DictListFieldInput-editableOptionalEmbedded_base.1.editableEmbedded?", ); - await act(async () => { - await userEvent.click( - within(addedOptionalEmbedded).getByRole("button", { name: "embedded" }), - ); - }); + await userEvent.click( + within(addedOptionalEmbedded).getByRole("button", { name: "embedded" }), + ); - await act(async () => { - await userEvent.click( - within(addedOptionalEmbedded).getByRole("button", { - name: "editableEmbedded", - }), - ); - }); + await userEvent.click( + within(addedOptionalEmbedded).getByRole("button", { + name: "editableEmbedded", + }), + ); - await act(async () => { - await userEvent.click( - within(addedOptionalEmbedded).getByRole("button", { - name: "embedded?", - }), - ); - }); + await userEvent.click( + within(addedOptionalEmbedded).getByRole("button", { + name: "embedded?", + }), + ); - await act(async () => { - await userEvent.click( - within(addedOptionalEmbedded).getByRole("button", { - name: "editableEmbedded?", - }), - ); - }); + await userEvent.click( + within(addedOptionalEmbedded).getByRole("button", { + name: "editableEmbedded?", + }), + ); - expect(within(nested_embedded_base).queryByText("Add")).toBeEnabled(); - expect(within(nested_embedded_base).queryByText("Delete")).toBeDisabled(); + expect( + within(nested_embedded_base).queryByRole("button", { name: "Add" }), + ).toBeEnabled(); + expect( + within(nested_embedded_base).queryByRole("button", { name: "Delete" }), + ).toBeDisabled(); - expect(within(nested_editableEmbedded_base).queryByText("Add")).toBeEnabled(); expect( - within(nested_editableEmbedded_base).queryByText("Delete"), + within(nested_editableEmbedded_base).queryByRole("button", { name: "Add" }), + ).toBeEnabled(); + expect( + within(nested_editableEmbedded_base).queryByRole("button", { + name: "Delete", + }), ).toBeDisabled(); - expect(within(nested_optionalEmbedded_base).queryByText("Add")).toBeEnabled(); - await act(async () => { - await userEvent.click( - within(nested_optionalEmbedded_base).getByText("Add"), - ); - }); expect( - within(nested_optionalEmbedded_base).queryByText("Delete"), + within(nested_optionalEmbedded_base).queryByRole("button", { name: "Add" }), + ).toBeEnabled(); + + await userEvent.click(within(nested_optionalEmbedded_base).getByText("Add")); + + expect( + within(nested_optionalEmbedded_base).queryByRole("button", { + name: "Delete", + }), ).toBeEnabled(); expect( - within(nested_editableOptionalEmbedded_base).queryByText("Add"), + within(nested_editableOptionalEmbedded_base).queryByRole("button", { + name: "Add", + }), ).toBeEnabled(); - await act(async () => { - await userEvent.click( - within(nested_editableOptionalEmbedded_base).getByText("Add"), - ); - }); + + await userEvent.click( + within(nested_editableOptionalEmbedded_base).getByText("Add"), + ); expect( - within(nested_editableOptionalEmbedded_base).queryByText("Delete"), + within(nested_editableOptionalEmbedded_base).queryByRole("button", { + name: "Delete", + }), ).toBeEnabled(); await act(async () => { @@ -743,67 +739,45 @@ test("GIVEN the EditInstance View WHEN changing an embedded entity with nested e await apiHelper.resolve(Either.right({ data: ServiceInstance.a })); }); - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "embedded" })); - }); + await userEvent.click(screen.getByRole("button", { name: "embedded" })); - await act(async () => { - await userEvent.click(screen.getByText("Add")); - }); + await userEvent.click(screen.getByText("Add")); - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "0" })); - }); + await userEvent.click(screen.getByRole("button", { name: "0" })); - await act(async () => { - await userEvent.click(screen.getAllByText("Add")[1]); - }); + await userEvent.click(screen.getAllByText("Add")[1]); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "embedded_single" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "embedded_single" }), + ); - await act(async () => { - await userEvent.click(screen.getAllByText("Add")[2]); - }); + await userEvent.click(screen.getAllByText("Add")[2]); const another_embedded_group = screen.getByLabelText( "DictListFieldInput-embedded.0.embedded_single.another_embedded", ); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "another_embedded" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "another_embedded" }), + ); - await act(async () => { - await userEvent.click( - within(another_embedded_group).getByRole("button", { name: "0" }), - ); - }); + await userEvent.click( + within(another_embedded_group).getByRole("button", { name: "0" }), + ); const deep_nested_group = screen.getByLabelText( "DictListFieldInput-embedded.0.embedded_single.another_embedded.0.another_deeper_embedded", ); - await act(async () => { - await userEvent.click(within(deep_nested_group).getByText("Add")); - }); + await userEvent.click(within(deep_nested_group).getByText("Add")); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "another_deeper_embedded" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "another_deeper_embedded" }), + ); - await act(async () => { - await userEvent.click( - within(deep_nested_group).getByRole("button", { name: "0" }), - ); - }); + await userEvent.click( + within(deep_nested_group).getByRole("button", { name: "0" }), + ); // expect all fields in deep_nested_group to be enabled const deep_nested_group_fields = diff --git a/src/Slices/Events/UI/Events.test.tsx b/src/Slices/Events/UI/Events.test.tsx index aea02963a..dd510ff3c 100644 --- a/src/Slices/Events/UI/Events.test.tsx +++ b/src/Slices/Events/UI/Events.test.tsx @@ -250,9 +250,8 @@ test("GIVEN EventsView WHEN sorting changes AND we are not on the first page THE expect(nextPageButton).toBeEnabled(); - await act(async () => { - await userEvent.click(nextPageButton); - }); + await userEvent.click(nextPageButton); + //expect the api url to contain start and end keywords that are used for pagination when we are moving to the next page expect(apiHelper.pendingRequests[0].url).toMatch(/(&start=|&end=)/); expect(apiHelper.pendingRequests[0].url).toMatch(/(&sort=timestamp.desc)/); @@ -262,9 +261,7 @@ test("GIVEN EventsView WHEN sorting changes AND we are not on the first page THE }); //sort on the second page - await act(async () => { - await userEvent.click(screen.getByText("Date")); - }); + await userEvent.click(screen.getByText("Date")); // expect the api url to not contain start and end keywords that are used for pagination to assert we are back on the first page. // we are asserting on the second request as the first request is for the updated sorting event, and second is chained to back to the first page with still correct sorting diff --git a/src/Slices/Events/UI/FilterWidget/VersionFilter.tsx b/src/Slices/Events/UI/FilterWidget/VersionFilter.tsx index 18591b698..cf692d1f8 100644 --- a/src/Slices/Events/UI/FilterWidget/VersionFilter.tsx +++ b/src/Slices/Events/UI/FilterWidget/VersionFilter.tsx @@ -35,8 +35,8 @@ export const VersionFilter: React.FC = ({ return ( @@ -55,12 +55,11 @@ export const VersionFilter: React.FC = ({ + > diff --git a/src/Slices/Events/UI/Spec/EventsPageIntegration.test.tsx b/src/Slices/Events/UI/Spec/EventsPageIntegration.test.tsx index f88c9a178..20f96575a 100644 --- a/src/Slices/Events/UI/Spec/EventsPageIntegration.test.tsx +++ b/src/Slices/Events/UI/Spec/EventsPageIntegration.test.tsx @@ -45,33 +45,25 @@ describe("Given the Events Page", () => { expect(initialRows).toHaveLength(14); - await act(async () => { - await userEvent.click( - within(screen.getByRole("toolbar", { name: "FilterBar" })).getByRole( - "button", - { name: "FilterPicker" }, - ), - ); - }); - await act(async () => { - await userEvent.click(screen.getByRole("option", { name: filterName })); - }); + await userEvent.click( + within(screen.getByRole("toolbar", { name: "FilterBar" })).getByRole( + "button", + { name: "FilterPicker" }, + ), + ); + + await userEvent.click(screen.getByRole("option", { name: filterName })); const input = await screen.findByPlaceholderText(placeholderText); - await act(async () => { - await userEvent.click(input); - }); + await userEvent.click(input); + if (filterType === "select") { const option = await screen.findByRole("option", { name: filterValue }); - await act(async () => { - await userEvent.click(option); - }); + await userEvent.click(option); } else { - await act(async () => { - await userEvent.type(input, `${filterValue}{enter}`); - }); + await userEvent.type(input, `${filterValue}{enter}`); } expect(apiHelper.pendingRequests[0].url).toEqual( @@ -119,38 +111,25 @@ describe("Given the Events Page", () => { expect(initialRows).toHaveLength(14); - await act(async () => { - await userEvent.click( - within(screen.getByRole("toolbar", { name: "FilterBar" })).getByRole( - "button", - { name: "FilterPicker" }, - ), - ); - }); - await act(async () => { - await userEvent.click(screen.getByRole("option", { name: "Date" })); - }); + await userEvent.click( + within(screen.getByRole("toolbar", { name: "FilterBar" })).getByRole( + "button", + { name: "FilterPicker" }, + ), + ); + + await userEvent.click(screen.getByRole("option", { name: "Date" })); const fromDatePicker = await screen.findByLabelText("From Date Picker"); - await act(async () => { - await userEvent.click(fromDatePicker); - }); - await act(async () => { - await userEvent.type(fromDatePicker, `2021-04-28`); - }); + await userEvent.type(fromDatePicker, `2021-04-28`); + const toDatePicker = await screen.findByLabelText("To Date Picker"); - await act(async () => { - await userEvent.click(toDatePicker); - }); - await act(async () => { - await userEvent.type(toDatePicker, `2021-04-30`); - }); + await userEvent.type(toDatePicker, `2021-04-30`); + + await userEvent.click(await screen.findByLabelText("Apply date filter")); - await act(async () => { - await userEvent.click(await screen.findByLabelText("Apply date filter")); - }); expect(apiHelper.pendingRequests[0].url).toMatch( `/lsm/v1/service_inventory/${Service.a.name}/id1/events?limit=20&sort=timestamp.desc&filter.timestamp=ge%3A2021-04-`, ); @@ -213,33 +192,22 @@ describe("Given the Events Page", () => { expect(initialRows).toHaveLength(14); - await act(async () => { - await userEvent.click( - within(screen.getByRole("toolbar", { name: "FilterBar" })).getByRole( - "button", - { name: "FilterPicker" }, - ), - ); - }); - await act(async () => { - await userEvent.click(screen.getByRole("option", { name: "Date" })); - }); + await userEvent.click( + within(screen.getByRole("toolbar", { name: "FilterBar" })).getByRole( + "button", + { name: "FilterPicker" }, + ), + ); + + await userEvent.click(screen.getByRole("option", { name: "Date" })); const toDatePicker = await screen.findByLabelText( `${filterType} Date Picker`, ); - await act(async () => { - await userEvent.click(toDatePicker); - }); - await act(async () => { - await userEvent.type(toDatePicker, value); - }); - await act(async () => { - await userEvent.click( - await screen.findByLabelText("Apply date filter"), - ); - }); + await userEvent.type(toDatePicker, value); + + await userEvent.click(await screen.findByLabelText("Apply date filter")); expect(apiHelper.pendingRequests[0].url).toMatch( `/lsm/v1/service_inventory/${Service.a.name}/id1/events?limit=20&sort=timestamp.desc&filter.timestamp=${operator}%3A2021-05-`, @@ -268,9 +236,13 @@ describe("Given the Events Page", () => { }); expect(await screen.findByText(chip, { exact: false })).toBeVisible(); - await act(async () => { - await userEvent.click(await screen.findByLabelText("close")); - }); + + await userEvent.click( + screen.getByRole("button", { + name: `Close ${chip}`, + }), + ); + expect(apiHelper.pendingRequests[0].url).toMatch( `/lsm/v1/service_inventory/${Service.a.name}/id1/events?limit=20&sort=timestamp.desc`, ); diff --git a/src/Slices/Facts/UI/FactsRow.tsx b/src/Slices/Facts/UI/FactsRow.tsx index e2ed11943..2eafa2dab 100644 --- a/src/Slices/Facts/UI/FactsRow.tsx +++ b/src/Slices/Facts/UI/FactsRow.tsx @@ -21,7 +21,7 @@ export const FactsRow: React.FC = ({ row }) => { {row.value} - + = ({ : {}; return ( - + {displayName} - + ); }); @@ -74,21 +68,3 @@ export const FactsTable: React.FC = ({ ); }; - -interface HeaderProps { - $characters: number; - $hasSort: boolean; -} - -const getWidth = ({ $characters, $hasSort }: HeaderProps) => { - const base = `${$characters}ch`; - const extra = $hasSort ? "60px" : "16px"; - - return `calc(${base} + ${extra})`; -}; - -const StyledTh = styled(Th)` - &&& { - min-width: ${getWidth}; - } -`; diff --git a/src/Slices/Facts/UI/Page.test.tsx b/src/Slices/Facts/UI/Page.test.tsx index cfade5614..41f8f0207 100644 --- a/src/Slices/Facts/UI/Page.test.tsx +++ b/src/Slices/Facts/UI/Page.test.tsx @@ -107,9 +107,9 @@ test("GIVEN Facts page THEN sets sorting parameters correctly on click", async ( }); expect(resourceIdButton).toBeVisible(); - await act(async () => { - await userEvent.click(resourceIdButton); - }); + + await userEvent.click(resourceIdButton); + expect(apiHelper.pendingRequests[0].url).toContain("&sort=resource_id.asc"); await act(async () => { @@ -149,13 +149,7 @@ test.each` const input = await screen.findByPlaceholderText(placeholderText); - await act(async () => { - await userEvent.click(input); - }); - - await act(async () => { - await userEvent.type(input, `${filterValue}{enter}`); - }); + await userEvent.type(input, `${filterValue}{enter}`); expect(apiHelper.pendingRequests[0].url).toEqual( `/api/v2/facts?limit=20&filter.${filterUrlName}=${filterValue}&sort=name.asc`, @@ -212,9 +206,7 @@ test("GIVEN FactsView WHEN sorting changes AND we are not on the first page THEN expect(screen.getByLabelText("Go to next page")).toBeEnabled(); - await act(async () => { - await userEvent.click(screen.getByLabelText("Go to next page")); - }); + await userEvent.click(screen.getByLabelText("Go to next page")); //expect the api url to contain start and end keywords that are used for pagination when we are moving to the next page expect(apiHelper.pendingRequests[0].url).toMatch(/(&start=|&end=)/); @@ -243,9 +235,7 @@ test("GIVEN FactsView WHEN sorting changes AND we are not on the first page THEN expect(resourceIdButton).toBeVisible(); - await act(async () => { - await userEvent.click(resourceIdButton); - }); + await userEvent.click(resourceIdButton); // expect the api url to not contain start and end keywords that are used for pagination to assert we are back on the first page. // we are asserting on the second request as the first request is for the updated sorting event, and second is chained to back to the first page with still correct sorting diff --git a/src/Slices/Home/UI/CardView.tsx b/src/Slices/Home/UI/CardView.tsx index 69b54af0a..69de9e234 100644 --- a/src/Slices/Home/UI/CardView.tsx +++ b/src/Slices/Home/UI/CardView.tsx @@ -1,23 +1,25 @@ import React, { useContext } from "react"; import { + Brand, Bullseye, Card, CardBody, CardFooter, + CardHeader, CardTitle, + Content, EmptyState, - EmptyStateIcon, EmptyStateVariant, Gallery, + Label, PageSection, Title, } from "@patternfly/react-core"; import { PlusCircleIcon } from "@patternfly/react-icons"; -import styled from "styled-components"; import { FlatEnvironment } from "@/Core"; import { DependencyContext } from "@/UI"; -import { Link } from "@/UI/Components"; import { words } from "@/UI/words"; +import fallBackImage from "@images/inmanta-wings.svg"; interface Props { environments: FlatEnvironment[]; @@ -28,17 +30,8 @@ export const CardView: React.FC = ({ environments, ...props }) => { const pathname = routeManager.getUrl("Dashboard", undefined); return ( - - + + @@ -55,20 +48,24 @@ export const CardView: React.FC = ({ environments, ...props }) => { }; const CreateNewEnvironmentCard: React.FC<{ url: string }> = ({ url }) => ( - + + - - - - - - {words("home.create.env.desciption")} - - - - + + + + {words("home.create.env.desciption")} + + + - + ); interface EnvironmentCardProps { @@ -81,87 +78,33 @@ const EnvironmentCard: React.FC = ({ pathname, }) => { return ( - - + - - {environment.icon ? ( - - ) : ( - - {environment.name[0].toUpperCase()} - - )} - {environment.name} - - - {environment.description} - - - {environment.projectName} - - - + + + {environment.name} + + + + {environment.description} + + + + + ); }; - -const StyledLink = styled(Link)` - text-decoration: auto; - color: var(--pf-v5-global--Color--100); -`; - -const StyledCardContent = styled.div` - white-space: pre-wrap; - height: 20ch; -`; - -const StyledFooterDiv = styled.div` - color: var(--pf-v5-global--secondary-color--100); -`; - -const StyledIcon = styled.img` - width: 40px; - height: 40px; - object-fit: contain; - display: inline-block; -`; - -const FillerIcon = styled.div` - display: inline-block; - width: 40px; - height: 40px; - line-height: 40px; - text-align: center; - color: white; - background-color: var(--pf-v5-global--custom-color--100); - border-radius: 50%; -`; - -const StyledTitle = styled(CardTitle)` - display: flex; - gap: 10px; - flex-direction: row; - flex-wrap: nowrap; - align-items: center; - font-weight: var(--pf-v5-global--FontWeight--bold); -} -`; - -const StyledCard = styled(Card)` - height: 30ch; - &.pf-m-clickable:hover { - box-shadow: var(--pf-v5-global--BoxShadow--lg); - } -`; - -const AlignedEmptyState = styled(EmptyState)` - margin-top: 54px; -`; diff --git a/src/Slices/Home/UI/EnvironmentsOverview.test.tsx b/src/Slices/Home/UI/EnvironmentsOverview.test.tsx index 1cb84a6e5..1aa36a3a0 100644 --- a/src/Slices/Home/UI/EnvironmentsOverview.test.tsx +++ b/src/Slices/Home/UI/EnvironmentsOverview.test.tsx @@ -1,4 +1,4 @@ -import React, { act } from "react"; +import React from "react"; import { MemoryRouter } from "react-router-dom"; import { render, screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; @@ -37,12 +37,7 @@ it.each` words("home.filters.env.placeholder"), ); - await act(async () => { - await userEvent.click(input); - }); - await act(async () => { - await userEvent.type(input, filterValue); - }); + await userEvent.type(input, filterValue); expect(screen.queryAllByTestId("Environment card")).toHaveLength( numberOfResults, @@ -62,15 +57,11 @@ test("Given environments overview When filtering by project Then only the matchi words("home.filters.project.placeholder"), ); - await act(async () => { - await userEvent.click(input); - }); + await userEvent.click(input); const option = await screen.findByRole("option", { name: "default" }); - await act(async () => { - await userEvent.click(option); - }); + await userEvent.click(option); expect(screen.queryAllByTestId("Environment card")).toHaveLength(2); }); @@ -86,39 +77,35 @@ test("Given environments overview When filtering by name and project Then only t words("home.filters.project.placeholder"), ); - await act(async () => { - await userEvent.click(projectInput); - }); + await userEvent.click(projectInput); const option = await screen.findByRole("option", { name: "default" }); - await act(async () => { - await userEvent.click(option); - }); + await userEvent.click(option); + const nameInput = await screen.findByPlaceholderText( words("home.filters.env.placeholder"), ); - await act(async () => { - await userEvent.click(nameInput); - }); - await act(async () => { - await userEvent.type(nameInput, "test"); - }); + await userEvent.type(nameInput, "test"); expect(await screen.findByTestId("Environment card")).toBeVisible(); }); -test("Given environments overview When rendering environment with icon Then the icon is shown", async () => { +test("Given environments overview When rendering environment with icon Then the icon is shown, otherwise, show default icon", async () => { const { component } = setup(); render(component); - const cardWithIcon = await screen.findByRole("img", { - name: "test-env1-icon", + + const cardWithIcon = screen.getByRole("img", { + name: "test-env1-environment-logo", }); expect(cardWithIcon).toBeVisible(); - const cardWithoutIcon = screen.queryByRole("img", { name: "dev-env2-icon" }); - expect(cardWithoutIcon).not.toBeInTheDocument(); + const cardWithDefaultIcon = screen.getByRole("img", { + name: "dev-env2-environment-logo", + }); + + expect(cardWithDefaultIcon).toBeVisible(); }); diff --git a/src/Slices/Home/UI/EnvironmentsOverview.tsx b/src/Slices/Home/UI/EnvironmentsOverview.tsx index b1d71a1b2..a3c37d651 100644 --- a/src/Slices/Home/UI/EnvironmentsOverview.tsx +++ b/src/Slices/Home/UI/EnvironmentsOverview.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { PageSection } from "@patternfly/react-core"; import { FlatEnvironment } from "@/Core"; import { useUrlStateWithFilter } from "@/Data"; import { CardView } from "./CardView"; @@ -42,16 +43,22 @@ export const EnvironmentsOverview: React.FC = ({ return ( <> - - setFilter({ projectFilter: undefined, environmentFilter: undefined }) - } - /> + + + setFilter({ + projectFilter: undefined, + environmentFilter: undefined, + }) + } + /> + + ); diff --git a/src/Slices/Home/UI/FilterToolbar.tsx b/src/Slices/Home/UI/FilterToolbar.tsx index f4a49bfdd..39192ac86 100644 --- a/src/Slices/Home/UI/FilterToolbar.tsx +++ b/src/Slices/Home/UI/FilterToolbar.tsx @@ -33,7 +33,7 @@ export const FilterToolbar: React.FC = ({ aria-label="FilterBar" role="toolbar" > - + = ({ searchEntry={environmentFilter} /> - + { loading: () => ( <> - + @@ -30,7 +30,7 @@ export const Page: React.FC = () => { failed: (error) => ( <> - + { - const { service: serviceName } = useRouteParams<"InstanceComposer">(); - const { featureManager } = useContext(DependencyContext); - - return featureManager.isComposerEnabled() ? ( - ( - - - - )} - /> - ) : ( - - ); -}; - -/** - * Wrapper component for the Page component. - * Renders the PageContainer component with the provided props and children. - */ -const PageWrapper: React.FC> = ({ - children, - ...props -}) => ( - - {children} - -); diff --git a/src/Slices/InstanceComposer/Core/Route.ts b/src/Slices/InstanceComposerCreator/Core/Route.ts similarity index 100% rename from src/Slices/InstanceComposer/Core/Route.ts rename to src/Slices/InstanceComposerCreator/Core/Route.ts diff --git a/src/Slices/InstanceComposerCreator/UI/Page.tsx b/src/Slices/InstanceComposerCreator/UI/Page.tsx new file mode 100644 index 000000000..a47939ede --- /dev/null +++ b/src/Slices/InstanceComposerCreator/UI/Page.tsx @@ -0,0 +1,25 @@ +import React, { useContext } from "react"; +import { DependencyContext, useRouteParams, words } from "@/UI"; +import { EmptyView } from "@/UI/Components"; +import { ComposerCreatorProvider } from "@/UI/Components/Diagram/Context/ComposerCreatorProvider"; + +/** + * Renders the Page component for the Instance Composer Creator Page. + * If the composer feature is enabled, it renders the Canvas. + * If the composer feature is disabled, it renders an EmptyView component with a message indicating that the composer is disabled. + * + * @returns {React.FC} The Page component. + */ +export const Page: React.FC = () => { + const { service: serviceName } = useRouteParams<"InstanceComposer">(); + const { featureManager } = useContext(DependencyContext); + + if (!featureManager.isComposerEnabled()) { + ; + } + + return ; +}; diff --git a/src/Slices/InstanceComposer/UI/index.ts b/src/Slices/InstanceComposerCreator/UI/index.ts similarity index 100% rename from src/Slices/InstanceComposer/UI/index.ts rename to src/Slices/InstanceComposerCreator/UI/index.ts diff --git a/src/Slices/InstanceComposer/index.ts b/src/Slices/InstanceComposerCreator/index.ts similarity index 100% rename from src/Slices/InstanceComposer/index.ts rename to src/Slices/InstanceComposerCreator/index.ts diff --git a/src/Slices/InstanceComposerEditor/UI/Page.tsx b/src/Slices/InstanceComposerEditor/UI/Page.tsx index 2ff6ccaf2..0c0940011 100644 --- a/src/Slices/InstanceComposerEditor/UI/Page.tsx +++ b/src/Slices/InstanceComposerEditor/UI/Page.tsx @@ -1,54 +1,34 @@ import React, { useContext } from "react"; import { DependencyContext, useRouteParams, words } from "@/UI"; -import { EmptyView, PageContainer, ServicesProvider } from "@/UI/Components"; -import { InstanceProvider } from "@/UI/Components/InstanceProvider"; +import { EmptyView } from "@/UI/Components"; +import { ComposerEditorProvider } from "@/UI/Components/Diagram/Context/ComposerEditorProvider"; /** * Renders the Page component for the Instance Composer Editor Page. * If the composer feature is enabled, it renders the Canvas component wrapped in a ServicesProvider. * If the composer feature is disabled, it renders an EmptyView component with a message indicating that the composer is disabled. + * + * @returns {React.FC} The Page component. */ -export const Page = () => { +export const Page: React.FC = () => { const { service: serviceName, instance } = useRouteParams<"InstanceComposerEditor">(); const { featureManager } = useContext(DependencyContext); - return featureManager.isComposerEnabled() ? ( - + ); + } + + return ( + ( - - - - )} - /> - ) : ( - ); }; - -/** - * PageWrapper component. - * Wraps the content of the Page component with a PageContainer. - */ -const PageWrapper: React.FC> = ({ - children, - ...props -}) => ( - - {children} - -); diff --git a/src/Slices/InstanceComposerViewer/UI/Page.tsx b/src/Slices/InstanceComposerViewer/UI/Page.tsx index 802446e13..1a9e78679 100644 --- a/src/Slices/InstanceComposerViewer/UI/Page.tsx +++ b/src/Slices/InstanceComposerViewer/UI/Page.tsx @@ -1,53 +1,34 @@ import React, { useContext } from "react"; import { DependencyContext, useRouteParams, words } from "@/UI"; -import { EmptyView, PageContainer, ServicesProvider } from "@/UI/Components"; -import { InstanceProvider } from "@/UI/Components/InstanceProvider"; +import { EmptyView } from "@/UI/Components"; +import { ComposerEditorProvider } from "@/UI/Components/Diagram/Context/ComposerEditorProvider"; /** * Renders the Page component for the Instance Composer Viewer Page. * If the composer feature is enabled, it renders the Canvas component wrapped in a ServicesProvider. * If the composer feature is disabled, it renders an EmptyView component with a message indicating that the composer is disabled. + * + * @returns {React.FC} The Page component. */ -export const Page = () => { +export const Page: React.FC = () => { const { service: serviceName, instance } = useRouteParams<"InstanceComposerViewer">(); const { featureManager } = useContext(DependencyContext); - return featureManager.isComposerEnabled() ? ( - + ); + } + + return ( + ( - - - - )} - /> - ) : ( - ); }; - -/** - * PageWrapper component. - * Wraps the content of the Page component with a PageContainer. - */ -const PageWrapper: React.FC> = ({ - children, - ...props -}) => ( - - {children} - -); diff --git a/src/Slices/Login/Page.test.tsx b/src/Slices/Login/Page.test.tsx index 610ac98d9..0fc5354b7 100644 --- a/src/Slices/Login/Page.test.tsx +++ b/src/Slices/Login/Page.test.tsx @@ -82,20 +82,15 @@ describe("Login", () => { const usernameInput = screen.getByLabelText("input-username"); - await act(async () => { - await userEvent.type(usernameInput, "test_user"); - }); + await userEvent.type(usernameInput, "test_user"); + const passwordInput = screen.getByLabelText("input-password"); - await act(async () => { - await userEvent.type(passwordInput, "test_password"); - }); + await userEvent.type(passwordInput, "test_password"); const showPasswordButton = screen.getByLabelText("show-password"); - await act(async () => { - await userEvent.click(showPasswordButton); - }); + await userEvent.click(showPasswordButton); const passwordInput1 = screen.getByLabelText("input-password"); @@ -103,9 +98,7 @@ describe("Login", () => { const hidePasswordButton = screen.getByLabelText("hide-password"); - await act(async () => { - await userEvent.click(hidePasswordButton); - }); + await userEvent.click(hidePasswordButton); const passwordInput2 = screen.getByLabelText("input-password"); @@ -113,9 +106,7 @@ describe("Login", () => { const logInButton = screen.getByLabelText("login-button"); - await act(async () => { - await userEvent.click(logInButton); - }); + await userEvent.click(logInButton); await waitFor(() => expect(spiedCreateCookie).toHaveBeenCalledWith( @@ -164,20 +155,15 @@ describe("Login", () => { const usernameInput = screen.getByLabelText("input-username"); - await act(async () => { - await userEvent.type(usernameInput, "test_user"); - }); + await userEvent.type(usernameInput, "test_user"); + const passwordInput = screen.getByLabelText("input-password"); - await act(async () => { - await userEvent.type(passwordInput, "test_password"); - }); + await userEvent.type(passwordInput, "test_password"); const logInButton = screen.getByLabelText("login-button"); - await act(async () => { - await userEvent.click(logInButton); - }); + await userEvent.click(logInButton); await waitFor(() => { expect(screen.getByLabelText("error-message")).toHaveTextContent( @@ -223,9 +209,7 @@ describe("Login", () => { const logInButton = screen.getByLabelText("login-button"); - await act(async () => { - await userEvent.click(logInButton); - }); + await userEvent.click(logInButton); await waitFor(() => { expect(screen.getByLabelText("error-message")).toHaveTextContent( diff --git a/src/Slices/Login/Page.tsx b/src/Slices/Login/Page.tsx index 99b43cb9c..f3145e57f 100644 --- a/src/Slices/Login/Page.tsx +++ b/src/Slices/Login/Page.tsx @@ -1,48 +1,30 @@ -import React, { useContext, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; +import React from "react"; import { LoginPage, ListVariant } from "@patternfly/react-core"; import styled from "styled-components"; -import { useLogin } from "@/Data/Managers/V2/POST/Login"; -import { DependencyContext, words } from "@/UI"; -import { UserCredentialsForm } from "@/UI/Components/UserCredentialsForm"; +import { words } from "@/UI"; import logo from "@images/logo.svg"; +import { LoginForm } from "./UI/LoginForm"; /** + * PF-MIGRATION TODO : UPDATE based on new guidelines + * * Login component. * This component is responsible for rendering the login page. * @note This is being used only when database authentication is enabled. - * @returns {React.FunctionComponent} The rendered component. + * @returns {React.FC} The rendered component. */ -export const Login: React.FunctionComponent = () => { - const { authHelper } = useContext(DependencyContext); - const navigate = useNavigate(); - - const { mutate, isError, error, isSuccess, isPending, data } = useLogin(); - - useEffect(() => { - if (isSuccess) { - authHelper.updateUser(data.data.user.username, data.data.token); - navigate("/"); - } - }, [data, isSuccess, authHelper, navigate]); - +export const Login: React.FC = () => { return ( - - mutate({ username, password })} - submitButtonText={words("login.login")} - /> - + + ); }; @@ -55,15 +37,3 @@ const Wrapper = styled.div` top: 0; left: 0; `; -const StyledLogin = styled(LoginPage)` - background-color: var(--pf-v5-global--BackgroundColor--dark-300); - .pf-v5-c-login__container { - @media (min-width: 1200px) { - width: 100%; - max-width: 500px !important; - display: block !important; - padding-inline-start: 0px; - padding-inline-end: 0px; - } - } -`; diff --git a/src/UI/Components/UserCredentialsForm/UserCredentialsForm.tsx b/src/Slices/Login/UI/LoginForm.tsx similarity index 58% rename from src/UI/Components/UserCredentialsForm/UserCredentialsForm.tsx rename to src/Slices/Login/UI/LoginForm.tsx index 0d6697b3f..560b9ec63 100644 --- a/src/UI/Components/UserCredentialsForm/UserCredentialsForm.tsx +++ b/src/Slices/Login/UI/LoginForm.tsx @@ -1,4 +1,5 @@ -import React, { useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; import { FormHelperText, HelperText, @@ -12,36 +13,38 @@ import { ActionGroup, ValidatedOptions, Spinner, - Text, + Content, } from "@patternfly/react-core"; import { ExclamationCircleIcon, EyeIcon, EyeSlashIcon, } from "@patternfly/react-icons"; +import { useLogin } from "@/Data/Managers/V2/POST/Login"; +import { DependencyContext, words } from "@/UI"; -interface UserCredentialsFormProps { - onSubmit: (username: string, password: string) => void; - isPending: boolean; - isError: boolean; - error: Error | null; +interface Props { submitButtonText: string; submitButtonLabel?: string; } /** - * UserCredentialsForm component. - * @param {UserCredentialsFormProps} props - The component props. - * @returns {JSX.Element} The rendered component. + * LoginForm component. + * @props {Props} props - The component + * @prop {string} submitButtonText - The text to display on the submit button. + * @prop {string} submitButtonLabel - The aria-label for the submit button. + * + * @returns {React.FC} The rendered component. */ -export const UserCredentialsForm: React.FC = ({ - onSubmit, - isPending, - isError, - error, +export const LoginForm: React.FC = ({ submitButtonText, submitButtonLabel = "login-button", }) => { + const { authHelper } = useContext(DependencyContext); + const navigate = useNavigate(); + + const { data, mutate, isSuccess, isError, error, isPending } = useLogin(); + const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [isPasswordHidden, setIsPasswordHidden] = useState(true); @@ -50,11 +53,13 @@ export const UserCredentialsForm: React.FC = ({ * Handle the change of the username input field. * @param {React.FormEvent} _event - The event object. * @param {string} value - The current value of the input field. + * + * @returns {void} */ const handleUsernameChange = ( _event: React.FormEvent, value: string, - ) => { + ): void => { setUsername(value); }; @@ -62,21 +67,47 @@ export const UserCredentialsForm: React.FC = ({ * Handle the change of the password input field. * @param {React.FormEvent} _event The event object. * @param {string} value The current value of the input field. + * + * @returns {void} */ const handlePasswordChange = ( _event: React.FormEvent, value: string, - ) => { + ): void => { setPassword(value); }; + /** + * Handles the submission of the login form. + * + * This function is responsible for preventing the default form submission behavior and then calling the mutate function with the current username and password. + * @param {React.FormEvent | React.MouseEvent} event - The event that triggered the form submission. + * + * @returns {void} A Promise that resolves when the operation is complete. + */ + const handleSubmit = ( + event: + | React.FormEvent + | React.MouseEvent, + ): void => { + event.preventDefault(); + mutate({ username, password }); + }; + + useEffect(() => { + if (isSuccess) { + authHelper.updateUser(data.data.user.username, data.data.token); + navigate("/"); + } + }, [isSuccess, navigate, data, authHelper]); + return ( -
+ {isError && error && ( } aria-label="error-message" > @@ -85,7 +116,11 @@ export const UserCredentialsForm: React.FC = ({ )} - + = ({ onChange={handleUsernameChange} /> - + { @@ -131,14 +170,15 @@ export const UserCredentialsForm: React.FC = ({ aria-label={submitButtonLabel} variant="primary" type="submit" - onClick={(event) => { - event.preventDefault(); - onSubmit(username, password); - }} + onClick={handleSubmit} isBlock isDisabled={isPending} > - {isPending ? : {submitButtonText}} + {isPending ? ( + + ) : ( + {submitButtonText} + )} diff --git a/src/Slices/Login/UI/index.ts b/src/Slices/Login/UI/index.ts new file mode 100644 index 000000000..1cad6f418 --- /dev/null +++ b/src/Slices/Login/UI/index.ts @@ -0,0 +1 @@ +export * from "./LoginForm"; diff --git a/src/Slices/NotFound/UI/Page.tsx b/src/Slices/NotFound/UI/Page.tsx index 7c5419fe8..daf83f73e 100644 --- a/src/Slices/NotFound/UI/Page.tsx +++ b/src/Slices/NotFound/UI/Page.tsx @@ -2,9 +2,7 @@ import * as React from "react"; import { Button, EmptyState, - EmptyStateIcon, PageSection, - EmptyStateHeader, EmptyStateFooter, } from "@patternfly/react-core"; import { ExclamationTriangleIcon } from "@patternfly/react-icons"; @@ -18,13 +16,12 @@ export const Page: React.FC = () => { const { routeManager } = React.useContext(DependencyContext); return ( - - - {words("notFound.title")}} - icon={} - headingLevel="h3" - /> + + {words("notFound.title")}} + > diff --git a/src/Slices/Notification/UI/Badge/Badge.tsx b/src/Slices/Notification/UI/Badge/Badge.tsx index 21604d681..675a744e6 100644 --- a/src/Slices/Notification/UI/Badge/Badge.tsx +++ b/src/Slices/Notification/UI/Badge/Badge.tsx @@ -3,7 +3,6 @@ import { NotificationBadge, NotificationBadgeVariant, } from "@patternfly/react-core"; -import styled from "styled-components"; import { RemoteData } from "@/Core"; import { ToastAlert } from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; @@ -34,14 +33,14 @@ export const View: React.FC = ({ data, onClick }) => { return RemoteData.fold( { notAsked: () => ( - ), loading: () => ( - = ({ data, onClick }) => { title={words("error")} setMessage={setError} /> - notification.severity === "error"; const isUnread = (notification: Notification) => notification.read === false; - -const PlainBadge = styled(NotificationBadge)` - --pf-v5-c-button--m-plain--hover--Color: var(--pf-v5-global--Color--200); - --pf-v5-c-button--m-plain--focus--Color: var(--pf-v5-global--Color--200); - --pf-v5-c-button--m-plain--active--Color: var(--pf-v5-global--Color--200); -`; diff --git a/src/Slices/Notification/UI/Center/ActionList.tsx b/src/Slices/Notification/UI/Center/ActionList.tsx index 499f22464..0fb04e347 100644 --- a/src/Slices/Notification/UI/Center/ActionList.tsx +++ b/src/Slices/Notification/UI/Center/ActionList.tsx @@ -43,9 +43,8 @@ export const ActionList: React.FC = ({ read, id, onUpdate }) => { variant="plain" onClick={onToggleClick} isExpanded={isOpen} - > - - + icon={} + /> )} > = ({ notification, onUpdate }) => {

{notification.message}

- + , ]} /> @@ -49,7 +49,6 @@ export const Item: React.FC = ({ notification, onUpdate }) => { aria-labelledby="multi-actions-item1 multi-actions-action1" id="multi-actions-action1" aria-label="Actions" - isPlainButtonAction > @@ -84,17 +83,6 @@ interface CustomItemProps { } const CustomItem = styled(DataListItem)` - --pf-v5-c-data-list__item-row--PaddingRight: 24px; - --pf-v5-c-data-list__item-row--PaddingLeft: 24px; - --pf-v5-c-data-list__item-content--md--PaddingBottom: 16px; - --pf-v5-c-data-list__cell--PaddingTop: 16px; - --pf-v5-c-data-list__item-action--PaddingBottom: 16px; - --pf-v5-c-data-list__item-action--PaddingTop: 16px; - --pf-v5-c-data-list__item--before--BackgroundColor: ${(p) => + --pf-v6-c-data-list__item--before--BackgroundColor: ${(p) => p.$read ? "transparent" : getColorForVisualSeverity(p.$severity)}; `; - -const CustomDateWithTooltip = styled(DateWithTooltip)` - color: var(--pf-v5-global--Color--200); - font-size: var(--pf-v5-global--FontSize--sm); -`; diff --git a/src/Slices/Notification/UI/Center/Page.test.tsx b/src/Slices/Notification/UI/Center/Page.test.tsx index 8dca8e29e..f47ef666a 100644 --- a/src/Slices/Notification/UI/Center/Page.test.tsx +++ b/src/Slices/Notification/UI/Center/Page.test.tsx @@ -4,7 +4,7 @@ import { Page } from "@patternfly/react-core"; import { render, screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { StoreProvider } from "easy-peasy"; -import { axe, toHaveNoViolations } from "jest-axe"; +import { configureAxe, toHaveNoViolations } from "jest-axe"; import { Either } from "@/Core"; import { CommandManagerResolverImpl, @@ -21,6 +21,13 @@ import { NotificationCenterPage } from "."; expect.extend(toHaveNoViolations); +const axe = configureAxe({ + rules: { + // disable landmark rules when testing isolated components. + region: { enabled: false }, + }, +}); + const setup = (entries?: string[]) => { const apiHelper = new DeferredApiHelper(); const scheduler = new StaticScheduler(); @@ -83,14 +90,11 @@ test("Given Notification Center page When user filters on severity Then executes await apiHelper.resolve(Either.right(Mock.response)); }); - await act(async () => { - await userEvent.click( - screen.getByRole("combobox", { name: "SeverityFilterInput" }), - ); - }); - await act(async () => { - await userEvent.click(screen.getByRole("option", { name: "message" })); - }); + await userEvent.click( + screen.getByRole("combobox", { name: "SeverityFilterInput" }), + ); + + await userEvent.click(screen.getByRole("option", { name: "message" })); expect(apiHelper.pendingRequests).toEqual([ request("?limit=20&filter.severity=message"), @@ -106,16 +110,13 @@ test("Given Notification Center page When user filters on severity Then executes screen.getAllByRole("listitem", { name: "NotificationItem" }), ).toHaveLength(2); - await act(async () => { - await userEvent.click( - screen.getByRole("combobox", { name: "SeverityFilterInput" }), - ); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Clear input value" }), - ); - }); + await userEvent.click( + screen.getByRole("combobox", { name: "SeverityFilterInput" }), + ); + + await userEvent.click( + screen.getByRole("button", { name: "Clear input value" }), + ); expect(apiHelper.pendingRequests).toEqual([request("?limit=20")]); @@ -134,14 +135,11 @@ test("Given Notification Center page When user filters on read Then executes cor await apiHelper.resolve(Either.right(Mock.response)); }); - await act(async () => { - await userEvent.click( - screen.getByRole("combobox", { name: "ReadFilterInput" }), - ); - }); - await act(async () => { - await userEvent.click(screen.getByRole("option", { name: "read" })); - }); + await userEvent.click( + screen.getByRole("combobox", { name: "ReadFilterInput" }), + ); + + await userEvent.click(screen.getByRole("option", { name: "read" })); expect(apiHelper.pendingRequests).toEqual([ request("?limit=20&filter.read=true"), @@ -159,16 +157,14 @@ test("Given Notification Center page When user filters on read Then executes cor }), ).toHaveLength(2); - await act(async () => { - await userEvent.click( - screen.getByRole("combobox", { name: "ReadFilterInput" }), - ); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Clear input value" }), - ); - }); + await userEvent.click( + screen.getByRole("combobox", { name: "ReadFilterInput" }), + ); + + await userEvent.click( + screen.getByRole("button", { name: "Clear input value" }), + ); + expect(apiHelper.pendingRequests).toEqual([request("?limit=20")]); await act(async () => { @@ -186,12 +182,10 @@ test("Given Notification Center page When user filters on message Then executes await apiHelper.resolve(Either.right(Mock.response)); }); - await act(async () => { - await userEvent.type( - screen.getByRole("searchbox", { name: "MessageFilter" }), - "abc{enter}", - ); - }); + await userEvent.type( + screen.getByRole("textbox", { name: "MessageFilter" }), + "abc{enter}", + ); expect(apiHelper.pendingRequests).toEqual([ request("?limit=20&filter.message=abc"), @@ -209,9 +203,8 @@ test("Given Notification Center page When user filters on message Then executes }), ).toHaveLength(2); - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "close abc" })); - }); + await userEvent.click(screen.getByRole("button", { name: /close abc/i })); + expect(apiHelper.pendingRequests).toEqual([request("?limit=20")]); await act(async () => { @@ -229,12 +222,10 @@ test("Given Notification Center page When user filters on title Then executes co await apiHelper.resolve(Either.right(Mock.response)); }); - await act(async () => { - await userEvent.type( - screen.getByRole("searchbox", { name: "TitleFilter" }), - "abc{enter}", - ); - }); + await userEvent.type( + screen.getByRole("textbox", { name: "TitleFilter" }), + "abc{enter}", + ); expect(apiHelper.pendingRequests).toEqual([ request("?limit=20&filter.title=abc"), @@ -251,9 +242,9 @@ test("Given Notification Center page When user filters on title Then executes co name: "NotificationItem", }), ).toHaveLength(2); - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "close abc" })); - }); + + await userEvent.click(screen.getByRole("button", { name: /close abc/i })); + expect(apiHelper.pendingRequests).toEqual([request("?limit=20")]); await act(async () => { @@ -276,9 +267,7 @@ test("Given Notification Center page When user clicks next page Then fetches nex expect(button).toBeEnabled(); - await act(async () => { - await userEvent.click(button); - }); + await userEvent.click(button); expect(apiHelper.pendingRequests).toEqual([ { diff --git a/src/Slices/Notification/UI/Center/ReadFilter.tsx b/src/Slices/Notification/UI/Center/ReadFilter.tsx index ddb735ad1..826bdabc3 100644 --- a/src/Slices/Notification/UI/Center/ReadFilter.tsx +++ b/src/Slices/Notification/UI/Center/ReadFilter.tsx @@ -31,8 +31,8 @@ export const ReadFilter: React.FC = ({ filter, setFilter }) => { return ( = ({ filter, setFilter }) => { return ( } isNotificationDrawerExpanded={true} - header={ + masthead={ @@ -130,15 +130,11 @@ test("Given Drawer When clicking on 'Clear all' Then all notifications are clear await apiHelper.resolve(Either.right(Mock.response)); }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "NotificationListActions" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "NotificationListActions" }), + ); - await act(async () => { - await userEvent.click(screen.getByRole("menuitem", { name: "Clear all" })); - }); + await userEvent.click(screen.getByRole("menuitem", { name: "Clear all" })); expect(apiHelper.pendingRequests).toEqual([ updateRequest("abcdefgh01", { read: true, cleared: true }), @@ -179,16 +175,14 @@ test("Given Drawer When user clicks on 'Read all' Then all notifications are rea await act(async () => { await apiHelper.resolve(Either.right(Mock.response)); }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "NotificationListActions" }), - ); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("menuitem", { name: "Mark all as read" }), - ); - }); + + await userEvent.click( + screen.getByRole("button", { name: "NotificationListActions" }), + ); + + await userEvent.click( + screen.getByRole("menuitem", { name: "Mark all as read" }), + ); expect(apiHelper.pendingRequests).toEqual([ updateRequest("abcdefgh01", { read: true }), @@ -238,9 +232,7 @@ test("Given Drawer When user clicks a notification Then it becomes read", async const items = screen.getAllByRole("listitem", { name: "NotificationItem" }); - await act(async () => { - await userEvent.click(items[0]); - }); + await userEvent.click(items[0]); expect(apiHelper.pendingRequests).toEqual([ updateRequest("abcdefgh01", { read: true }), @@ -286,9 +278,7 @@ test("Given Drawer When user clicks a notification with an uri then go to the ur const items = screen.getAllByRole("listitem", { name: "NotificationItem" }); - await act(async () => { - await userEvent.click(items[0]); - }); + await userEvent.click(items[0]); expect(history.location.pathname).toBe( "/compilereports/f2c68117-24bd-43cf-a9dc-ce42b934a614", @@ -311,9 +301,7 @@ test("Given Drawer When user clicks a notification without an uri then nothing h expect(results).toHaveNoViolations(); }); - await act(async () => { - await userEvent.click(items[3]); - }); + await userEvent.click(items[3]); expect(history.location.pathname).toBe("/"); }); @@ -336,9 +324,7 @@ test("Given Drawer When user clicks a notification toggle with an uri then do no expect(results).toHaveNoViolations(); }); - await act(async () => { - await userEvent.click(items[0]); - }); + await userEvent.click(items[0]); expect(history.location.pathname).toBe("/"); }); @@ -356,14 +342,11 @@ test("Given Drawer When user clicks on 'unread' for 1 notification Then it becom name: "NotificationListItemActions", }); - await act(async () => { - await userEvent.click(actions); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("menuitem", { name: "Mark as unread" }), - ); - }); + await userEvent.click(actions); + + await userEvent.click( + screen.getByRole("menuitem", { name: "Mark as unread" }), + ); expect(apiHelper.pendingRequests).toEqual([ updateRequest("abcdefgh03", { read: false }), @@ -408,12 +391,9 @@ test("Given Drawer When user clicks on 'Clear' for 1 notification Then it is cle name: "NotificationListItemActions", }); - await act(async () => { - await userEvent.click(actions); - }); - await act(async () => { - await userEvent.click(screen.getByRole("menuitem", { name: "Clear" })); - }); + await userEvent.click(actions); + + await userEvent.click(screen.getByRole("menuitem", { name: "Clear" })); expect(apiHelper.pendingRequests).toEqual([ updateRequest("abcdefgh03", { read: true, cleared: true }), diff --git a/src/Slices/Notification/UI/Drawer/Drawer.tsx b/src/Slices/Notification/UI/Drawer/Drawer.tsx index af65974c2..6249fcd11 100644 --- a/src/Slices/Notification/UI/Drawer/Drawer.tsx +++ b/src/Slices/Notification/UI/Drawer/Drawer.tsx @@ -16,7 +16,6 @@ import { NotificationDrawerList, } from "@patternfly/react-core"; import { EllipsisVIcon } from "@patternfly/react-icons"; -import styled from "styled-components"; import { RemoteData } from "@/Core"; import { DependencyContext } from "@/UI/Dependency"; import { useNavigateTo } from "@/UI/Routing"; @@ -115,8 +114,8 @@ export const View: React.FC = ({ onClose()}> - - + + {RemoteData.fold( { notAsked: () => null, @@ -134,21 +133,12 @@ export const View: React.FC = ({ }, data, )} - - + + ); }; -const CustomNotificationDrawerBody = styled(NotificationDrawerBody)` - padding-bottom: 300px; - background-color: var(--pf-v5-global--BackgroundColor--200); -`; - -const CustomNotificationDrawerList = styled(NotificationDrawerList)` - overflow-y: visible; -`; - interface ActionListProps { onClearAll(): void; onReadAll(): void; @@ -182,9 +172,8 @@ const ActionList: React.FC = ({ variant="plain" onClick={onToggleClick} isExpanded={isOpen} - > - - + icon={} + /> )} isOpen={isOpen} onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)} diff --git a/src/Slices/Notification/UI/Drawer/Item.tsx b/src/Slices/Notification/UI/Drawer/Item.tsx index 96df4833a..c3543037f 100644 --- a/src/Slices/Notification/UI/Drawer/Item.tsx +++ b/src/Slices/Notification/UI/Drawer/Item.tsx @@ -77,9 +77,8 @@ const ActionList: React.FC = ({ notification, onUpdate }) => { variant="plain" isExpanded={isOpen} onClick={onToggle} - > - - + icon={} + /> )} onSelect={() => setIsOpen(false)} popperProps={{ position: "right" }} @@ -110,44 +109,4 @@ const ActionList: React.FC = ({ notification, onUpdate }) => { ); - - // return ( - // setIsOpen(false)} - // toggle={ - // , value: boolean) => - // onToggle(value, e) - // } - // aria-label="NotificationItemActions" - // /> - // } - // isOpen={isOpen} - // isPlain - // dropdownItems={[ - // { - // event.stopPropagation(); - // onUpdate({ read: false }); - // }} - // isDisabled={!notification.read} - // > - // {words("notification.unread")} - // , - // { - // event.stopPropagation(); - // onUpdate({ read: true, cleared: true }); - // }} - // > - // {words("notification.drawer.clear")} - // , - // ]} - // /> - // ); }; diff --git a/src/Slices/Notification/UI/Utils.ts b/src/Slices/Notification/UI/Utils.ts index 972f44dea..5ffc3d7f4 100644 --- a/src/Slices/Notification/UI/Utils.ts +++ b/src/Slices/Notification/UI/Utils.ts @@ -1,9 +1,9 @@ import { - global_danger_color_100, - global_Color_200, - global_info_color_100, - global_success_color_100, - global_warning_color_100, + t_global_icon_color_status_danger_default, + t_global_icon_color_status_custom_default, + t_global_color_brand_default, + t_global_icon_color_status_success_default, + t_global_icon_color_status_warning_default, } from "@patternfly/react-tokens"; import { Severity } from "@S/Notification/Core/Domain"; @@ -30,14 +30,14 @@ export type VisualSeverity = export const getColorForVisualSeverity = (severity: VisualSeverity): string => { switch (severity) { case "danger": - return global_danger_color_100.var; + return t_global_icon_color_status_danger_default.var; case "warning": - return global_warning_color_100.var; + return t_global_icon_color_status_warning_default.var; case "custom": - return global_Color_200.var; + return t_global_icon_color_status_custom_default.var; case "info": - return global_info_color_100.var; + return t_global_color_brand_default.var; case "success": - return global_success_color_100.var; + return t_global_icon_color_status_success_default.var; } }; diff --git a/src/Slices/OrderDetails/UI/OrderDependencies.tsx b/src/Slices/OrderDetails/UI/OrderDependencies.tsx index 3b6ea73f5..f67eac507 100644 --- a/src/Slices/OrderDetails/UI/OrderDependencies.tsx +++ b/src/Slices/OrderDetails/UI/OrderDependencies.tsx @@ -1,8 +1,7 @@ import React from "react"; import { Card, Label } from "@patternfly/react-core"; -import { ExclamationCircleIcon } from "@patternfly/react-icons"; +import { InfoAltIcon } from "@patternfly/react-icons"; import { Table, Tbody, Td, Tr } from "@patternfly/react-table"; -import styled from "styled-components"; import { ServiceOrderItemDependencies } from "@/Slices/Orders/Core/Query"; import { OrderStatusLabel } from "@/Slices/Orders/UI/OrderStatusLabel"; import { words } from "@/UI"; @@ -25,7 +24,7 @@ interface Props { export const OrderDependencies: React.FC = ({ dependencies }) => { if (!Object.keys(dependencies).length) { return ( -
)} diff --git a/src/Slices/ServiceInventory/UI/Components/FilterWidget/DeletedFilter.tsx b/src/Slices/ServiceInventory/UI/Components/FilterWidget/DeletedFilter.tsx index f6e2dbc85..f73aebf77 100644 --- a/src/Slices/ServiceInventory/UI/Components/FilterWidget/DeletedFilter.tsx +++ b/src/Slices/ServiceInventory/UI/Components/FilterWidget/DeletedFilter.tsx @@ -50,8 +50,8 @@ export const DeletedFilter: React.FC = ({ return ( diff --git a/src/Slices/ServiceInventory/UI/Components/FilterWidget/IdFilter.tsx b/src/Slices/ServiceInventory/UI/Components/FilterWidget/IdFilter.tsx index bcc6493c3..31712c23c 100644 --- a/src/Slices/ServiceInventory/UI/Components/FilterWidget/IdFilter.tsx +++ b/src/Slices/ServiceInventory/UI/Components/FilterWidget/IdFilter.tsx @@ -30,8 +30,8 @@ export const IdFilter: React.FC = ({ id, isVisible, update }) => { return ( @@ -51,12 +51,11 @@ export const IdFilter: React.FC = ({ id, isVisible, update }) => { + > diff --git a/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DeleteAction/DeleteAction.test.tsx b/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DeleteAction/DeleteAction.test.tsx index 2f2f7b1fa..2b4b7215c 100644 --- a/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DeleteAction/DeleteAction.test.tsx +++ b/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DeleteAction/DeleteAction.test.tsx @@ -13,6 +13,7 @@ import { ServiceInventoryContext } from "@/Slices/ServiceInventory/UI/ServiceInv import { DeferredApiHelper, dependencies, ServiceInstance } from "@/Test"; import { words } from "@/UI"; import { DependencyProvider } from "@/UI/Dependency"; +import { ModalProvider } from "@/UI/Root/Components/ModalProvider"; import { DeleteAction } from "./DeleteAction"; function setup() { @@ -47,31 +48,33 @@ function setup() { commandResolver, }} > - - - + + + + + ), @@ -88,9 +91,7 @@ describe("DeleteModal ", () => { render(component()); const modalButton = await screen.findByText(words("delete")); - await act(async () => { - await userEvent.click(modalButton); - }); + await userEvent.click(modalButton); expect(await screen.findByText(words("yes"))).toBeVisible(); expect(await screen.findByText(words("no"))).toBeVisible(); @@ -102,15 +103,11 @@ describe("DeleteModal ", () => { const modalButton = await screen.findByText(words("delete")); - await act(async () => { - await userEvent.click(modalButton); - }); + await userEvent.click(modalButton); const noButton = await screen.findByText(words("no")); - await act(async () => { - await userEvent.click(noButton); - }); + await userEvent.click(noButton); expect(screen.queryByText(words("yes"))).not.toBeInTheDocument(); }); @@ -121,15 +118,11 @@ describe("DeleteModal ", () => { const modalButton = await screen.findByText(words("delete")); - await act(async () => { - await userEvent.click(modalButton); - }); + await userEvent.click(modalButton); const yesButton = await screen.findByText(words("yes")); - await act(async () => { - await userEvent.click(yesButton); - }); + await userEvent.click(yesButton); expect(screen.queryByText(words("yes"))).not.toBeInTheDocument(); expect(apiHelper.pendingRequests[0]).toEqual({ diff --git a/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DeleteAction/DeleteAction.tsx b/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DeleteAction/DeleteAction.tsx index 6fb3bbc34..7b703e332 100644 --- a/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DeleteAction/DeleteAction.tsx +++ b/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DeleteAction/DeleteAction.tsx @@ -1,5 +1,5 @@ import React, { useContext, useState } from "react"; -import { MenuItem, Modal } from "@patternfly/react-core"; +import { MenuItem } from "@patternfly/react-core"; import { TrashAltIcon } from "@patternfly/react-icons"; import { Maybe, VersionedServiceInstanceIdentifier } from "@/Core"; import { ServiceInventoryContext } from "@/Slices/ServiceInventory/UI/ServiceInventory"; @@ -9,6 +9,7 @@ import { ConfirmUserActionForm, } from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; +import { ModalContext } from "@/UI/Root/Components/ModalProvider"; import { words } from "@/UI/words"; interface Props extends VersionedServiceInstanceIdentifier { @@ -23,10 +24,7 @@ export const DeleteAction: React.FC = ({ version, service_entity, }) => { - const [isOpen, setIsOpen] = useState(false); - const handleModalToggle = () => { - setIsOpen(!isOpen); - }; + const { triggerModal, closeModal } = useContext(ModalContext); const [errorMessage, setErrorMessage] = useState(""); const { commandResolver, environmentModifier } = useContext(DependencyContext); @@ -39,8 +37,35 @@ export const DeleteAction: React.FC = ({ version, }); const isHalted = environmentModifier.useIsHalted(); - const onSubmit = async () => { - setIsOpen(false); + + /** + * Opens a modal with a confirmation form. + * + * @returns {void} + */ + const handleModalToggle = (): void => { + triggerModal({ + title: words("inventory.deleteInstance.title"), + content: ( + <> + {words("inventory.deleteInstance.header")( + instance_identity, + service_entity, + )} + + + ), + }); + }; + + /** + * async method that is closing modal and sending out the request to delete the instance + * if there is an error, it will set the error message accordingly + * + * @returns {Promise} A Promise that resolves when the operation is complete. + */ + const onSubmit = async (): Promise => { + closeModal(); const result = await trigger(refetch); if (Maybe.isSome(result)) { @@ -75,22 +100,6 @@ export const DeleteAction: React.FC = ({ {words("inventory.deleteInstance.button")} - - {words("inventory.deleteInstance.header")( - instance_identity, - service_entity, - )} - setIsOpen(false)} - /> - ); }; diff --git a/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DestroyAction/DestroyAction.test.tsx b/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DestroyAction/DestroyAction.test.tsx index 3322a1d7c..d770a1f0c 100644 --- a/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DestroyAction/DestroyAction.test.tsx +++ b/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DestroyAction/DestroyAction.test.tsx @@ -13,6 +13,7 @@ import { ServiceInventoryContext } from "@/Slices/ServiceInventory/UI/ServiceInv import { DeferredApiHelper, dependencies, ServiceInstance } from "@/Test"; import { words } from "@/UI"; import { DependencyProvider } from "@/UI/Dependency"; +import { ModalProvider } from "@/UI/Root/Components/ModalProvider"; import { DestroyAction } from "./DestroyAction"; function setup() { @@ -47,30 +48,32 @@ function setup() { commandResolver, }} > - - - + + + + + ), @@ -89,9 +92,8 @@ describe("DeleteModal ", () => { words("inventory.destroyInstance.button"), ); - await act(async () => { - await userEvent.click(modalButton); - }); + await userEvent.click(modalButton); + expect(await screen.findByText(words("yes"))).toBeVisible(); expect(await screen.findByText(words("no"))).toBeVisible(); }); @@ -104,14 +106,12 @@ describe("DeleteModal ", () => { words("inventory.destroyInstance.button"), ); - await act(async () => { - await userEvent.click(modalButton); - }); + await userEvent.click(modalButton); + const noButton = await screen.findByText(words("no")); - await act(async () => { - await userEvent.click(noButton); - }); + await userEvent.click(noButton); + expect(screen.queryByText(words("yes"))).not.toBeInTheDocument(); }); @@ -123,14 +123,12 @@ describe("DeleteModal ", () => { words("inventory.destroyInstance.button"), ); - await act(async () => { - await userEvent.click(modalButton); - }); + await userEvent.click(modalButton); + const yesButton = await screen.findByText(words("yes")); - await act(async () => { - await userEvent.click(yesButton); - }); + await userEvent.click(yesButton); + expect(screen.queryByText(words("yes"))).not.toBeInTheDocument(); expect(apiHelper.pendingRequests[0]).toEqual({ environment: "env", diff --git a/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DestroyAction/DestroyAction.tsx b/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DestroyAction/DestroyAction.tsx index 90fd6c9f0..23cf335bb 100644 --- a/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DestroyAction/DestroyAction.tsx +++ b/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/DestroyAction/DestroyAction.tsx @@ -1,26 +1,35 @@ import React, { useContext, useState } from "react"; -import { MenuItem, Modal, Text } from "@patternfly/react-core"; +import { MenuItem, Content } from "@patternfly/react-core"; import { WarningTriangleIcon } from "@patternfly/react-icons"; import { Maybe, VersionedServiceInstanceIdentifier } from "@/Core"; import { ServiceInventoryContext } from "@/Slices/ServiceInventory/UI/ServiceInventory"; import { ToastAlert, ConfirmUserActionForm } from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; +import { ModalContext } from "@/UI/Root/Components/ModalProvider"; import { words } from "@/UI/words"; interface Props extends VersionedServiceInstanceIdentifier { instance_identity: string; } +/** + * DestroyAction is a component that allows the user to destroy a service instance. + * + * @props {Props} props - The props of the component. + * @prop {string} service_entity - The service entity of the service instance. + * @prop {string} id - The id of the service instance. + * @prop {string} instance_identity - The instance identity of the service instance. + * @prop {string} version - The version of the service instance. + * + * @returns {React.FC} A React component that allows the user to destroy a service instance. + */ export const DestroyAction: React.FC = ({ id, instance_identity, version, service_entity, }) => { - const [isOpen, setIsOpen] = useState(false); - const handleModalToggle = () => { - setIsOpen(!isOpen); - }; + const { triggerModal, closeModal } = useContext(ModalContext); const [errorMessage, setErrorMessage] = useState(""); const { commandResolver } = useContext(DependencyContext); const { refetch } = useContext(ServiceInventoryContext); @@ -31,8 +40,15 @@ export const DestroyAction: React.FC = ({ id, version, }); - const onSubmit = async () => { - setIsOpen(false); + + /** + * async method that is closing modal and sending out the request to destroy the instance + * if there is an error, it will set the error message + * + * @returns {Promise} A Promise that resolves when the operation is complete. + */ + const onSubmit = async (): Promise => { + closeModal(); const result = await trigger(refetch); if (Maybe.isSome(result)) { @@ -40,6 +56,33 @@ export const DestroyAction: React.FC = ({ } }; + /** + * Opens a modal with a confirmation form. + * + * @returns {void} + */ + const openModal = (): void => { + triggerModal({ + title: words("inventory.destroyInstance.title"), + iconVariant: "danger", + content: ( + <> + + {words("inventory.destroyInstance.header")( + instance_identity, + service_entity, + )} + +
+ + {words("inventory.destroyInstance.text")} + + + + ), + }); + }; + return ( <> = ({ } > {words("inventory.destroyInstance.button")} - - - {words("inventory.destroyInstance.header")( - instance_identity, - service_entity, - )} - -
- {words("inventory.destroyInstance.text")} - -
); }; diff --git a/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/ForceStateAction/ForceStateAction.tsx b/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/ForceStateAction/ForceStateAction.tsx index c8e5931a2..910ab1c53 100644 --- a/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/ForceStateAction/ForceStateAction.tsx +++ b/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/ForceStateAction/ForceStateAction.tsx @@ -1,11 +1,17 @@ import React, { useContext, useState } from "react"; -import { Divider, DrilldownMenu, MenuItem, Text } from "@patternfly/react-core"; +import { + Button, + Divider, + DrilldownMenu, + MenuItem, + Content, +} from "@patternfly/react-core"; import { WarningTriangleIcon } from "@patternfly/react-icons"; import { Maybe, VersionedServiceInstanceIdentifier } from "@/Core"; import { ActionDisabledTooltip } from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; +import { ModalContext } from "@/UI/Root/Components/ModalProvider"; import { words } from "@/UI/words"; -import ConfirmationModal from "../../ConfirmationModal"; import { ToastAlertMessage } from "../../ToastAlertMessage"; interface Props extends VersionedServiceInstanceIdentifier { @@ -23,7 +29,7 @@ interface Props extends VersionedServiceInstanceIdentifier { * @prop {string[]} availableStates - The available states of the service instance. * @prop {string} insetHeight - The inset height of the service instance. * - * @returns {React.FunctionComponent} A React component that allows the user to force a state on a service instance. + * @returns {React.FC} A React component that allows the user to force a state on a service instance. */ export const ForceStateAction: React.FC = ({ service_entity, @@ -32,15 +38,9 @@ export const ForceStateAction: React.FC = ({ version, availableStates, }) => { - const [isModalOpen, setIsModalOpen] = useState(false); - const [confirmationText, setConfirmationText] = useState(""); - const [targetState, setTargetState] = useState(""); + const { triggerModal, closeModal } = useContext(ModalContext); const [stateErrorMessage, setStateErrorMessage] = useState(""); - const handleModalToggle = () => { - setIsModalOpen(!isModalOpen); - }; - const menuItems = availableStates.sort().map((target) => ( = ({ const isHalted = environmentModifier.useIsHalted(); - const onSubmit = async (targetState: string) => { - const result = await trigger(targetState); + /** + * Opens a modal with confirmation buttons. + * @param {string} targetState - The target state to be used in the operation. + + * + * @returns {void} + */ + const openModal = (targetState: string): void => { + /** + * Handles the submission of the force state action. + * + * @returns {Promise} A Promise that resolves when the operation is complete. + */ + const onSubmit = async () => { + const result = await trigger(targetState); + + if (Maybe.isSome(result)) { + setStateErrorMessage(result.value); + } + closeModal(); + }; - if (Maybe.isSome(result)) { - setStateErrorMessage(result.value); - } + triggerModal({ + title: words("inventory.statustab.forceState.confirmTitle"), + iconVariant: "danger", + actions: [ + , + , + ], + content: ( + <> + + {words("inventory.statustab.forceState.message")( + instance_identity, + targetState, + )} + +
+ + {words("inventory.statustab.forceState.confirmMessage")} + + + {words("inventory.statustab.forceState.confirmQuestion")} + + + ), + }); }; + /** + * Handles the selection of the state. + * @param {string} value - The target state to be used in the operation. + * + * @returns {Promise} A Promise that resolves when the operation is complete. + */ const onSelect = (value: string) => { - setTargetState(value); - setConfirmationText( - words("inventory.statustab.forceState.message")(instance_identity, value), - ); - handleModalToggle(); + openModal(value); }; return ( @@ -104,7 +161,8 @@ export const ForceStateAction: React.FC = ({ icon={} direction="down" style={{ - backgroundColor: "var(--pf-v5-global--palette--red-50)", + backgroundColor: + "var(--pf-t--global--color--nonstatus--red--default)", }} drilldownMenu={ = ({ > } itemId="group:expertstate_breadcrumb" direction="up" aria-hidden + isDanger > Force state to: @@ -130,20 +190,6 @@ export const ForceStateAction: React.FC = ({ Force State
- - {confirmationText} -
- {words("inventory.statustab.forceState.confirmMessage")} - {words("inventory.statustab.forceState.confirmQuestion")} -
); }; diff --git a/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/RowActions.tsx b/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/RowActions.tsx index 723f09551..311ec89c2 100644 --- a/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/RowActions.tsx +++ b/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/RowActions.tsx @@ -1,335 +1,160 @@ -import React, { useContext, useEffect } from "react"; +import React, { useContext } from "react"; import { Divider, MenuToggle, - MenuList, - MenuItem, - Menu, - MenuContainer, - MenuContent, - MenuGroup, - DrilldownMenu, + Dropdown, + MenuToggleElement, + DropdownList, + DropdownItem, } from "@patternfly/react-core"; import { CopyIcon, EllipsisVIcon, EyeIcon, FileMedicalAltIcon, - HistoryIcon, - InfoAltIcon, - PortIcon, ToolsIcon, } from "@patternfly/react-icons"; +import { ParsedNumber } from "@/Core"; import { DependencyContext, words } from "@/UI"; import { Link } from "@/UI/Components"; -import { ServiceInstanceForAction } from "@/UI/Presenters"; import { DeleteAction } from "./DeleteAction"; -import { DestroyAction } from "./DestroyAction"; -import { ForceStateAction } from "./ForceStateAction/ForceStateAction"; -import { SetStateSection } from "./SetStateSection/SetStateSection"; - -interface MenuHeightsType { - [id: string]: number; -} export interface InstanceActionsProps { - instance: ServiceInstanceForAction; + instanceId: string; + service_identity_attribute_value: string | undefined; + entity: string; editDisabled: boolean; deleteDisabled: boolean; diagnoseDisabled: boolean; - composerAvailable: boolean; - availableStates: string[]; + version: ParsedNumber; } export const RowActions: React.FunctionComponent = ({ - instance, + instanceId, + service_identity_attribute_value, + entity, editDisabled, deleteDisabled, diagnoseDisabled, - composerAvailable, - availableStates, + version, }) => { - const { routeManager, environmentModifier, featureManager } = - useContext(DependencyContext); - - const [activeMenu, setActiveMenu] = React.useState("rootMenu"); - const [menuDrilledIn, setMenuDrilledIn] = React.useState([]); - const [drilldownPath, setDrilldownPath] = React.useState([]); - const [menuHeights, setMenuHeights] = React.useState({}); + const { routeManager, featureManager } = useContext(DependencyContext); - const toggleRef = React.useRef(null); - const menuRef = React.useRef(null); const [isOpen, setIsOpen] = React.useState(false); - const composerEnabled = - featureManager.isComposerEnabled() && composerAvailable; + const composerEnabled = featureManager.isComposerEnabled(); const onToggleClick = () => { setIsOpen(!isOpen); - setMenuDrilledIn([]); - setDrilldownPath([]); - setActiveMenu("rootMenu"); - }; - - const updateHeights = (menuId: string, height: number) => { - if ( - !menuHeights[menuId] && - menuId !== "rootMenu" && - menuHeights[menuId] !== height - ) { - if (isOpen && menuId === activeMenu) { - setMenuHeights({ - ...menuHeights, - [menuId]: height, - }); - } - } }; - // reset the height of the container when the state is updated. - useEffect(() => { - setMenuHeights({}); - }, [instance.state]); - - const drillIn = ( - _event: React.KeyboardEvent | React.MouseEvent, - fromMenuId: string, - toMenuId: string, - pathId: string, - ) => { - setMenuDrilledIn([...menuDrilledIn, fromMenuId]); - setDrilldownPath([...drilldownPath, pathId]); - setActiveMenu(toMenuId); - }; - - const drillOut = ( - _event: React.KeyboardEvent | React.MouseEvent, - toMenuId: string, - ) => { - setMenuDrilledIn(menuDrilledIn.slice(0, menuDrilledIn.length - 1)); - setDrilldownPath(drilldownPath.slice(0, drilldownPath.length - 1)); - setActiveMenu(toMenuId); - }; - - const toggle = ( + const toggle = (toggleRef: React.Ref) => ( - - + icon={} + /> ); - const menu = ( - setIsOpen(isOpen)} + onSelect={() => setIsOpen(false)} + isOpen={isOpen} + popperProps={{ position: "right" }} > - - - }> - - {words("instanceDetails.button")} - - - + + } > - - {words("inventory.statustab.diagnose")} - - - {composerEnabled && ( - + + {composerEnabled && ( + + } > - - {words("inventory.instanceComposer.editButton")} - - - )} - {featureManager.isComposerEnabled() && ( - }> - - {words("inventory.instanceComposer.showButton")} - - - )} - - + + )} + {featureManager.isComposerEnabled() && ( + + }> + {words("instanceComposer.showButton")} + + + )} + + } > - - {words("inventory.editInstance.button")} - - - - }> - - {words("inventory.duplicateInstance.button")} - - - - - - {words("back")} - - - }> - - {words("inventory.statusTab.history")} - - - }> - - {words("inventory.statusTab.events")} - - - - {environmentModifier.useIsExpertModeEnabled() && ( - - )} - - } - > - {words("inventory.actions.drilldown")} - - {environmentModifier.useIsExpertModeEnabled() && ( - - )} - - - setIsOpen(false)} - /> - - - - - ); - - return ( - setIsOpen(isOpen)} - menu={menu} - menuRef={menuRef} - toggle={toggle} - toggleRef={toggleRef} - popperProps={{ position: "right" }} - /> + {words("inventory.editInstance.button")} + + + + + }> + {words("inventory.duplicateInstance.button")} + + + + + + + ); }; diff --git a/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/SetStateSection/SetStateSection.tsx b/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/SetStateSection/SetStateSection.tsx index 91e477af9..cae383341 100644 --- a/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/SetStateSection/SetStateSection.tsx +++ b/src/Slices/ServiceInventory/UI/Components/RowActionsMenu/SetStateSection/SetStateSection.tsx @@ -1,10 +1,10 @@ import React, { useContext, useState } from "react"; -import { MenuItem, Text } from "@patternfly/react-core"; +import { Button, MenuItem, Content } from "@patternfly/react-core"; import { Maybe, VersionedServiceInstanceIdentifier } from "@/Core"; import { ActionDisabledTooltip } from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; +import { ModalContext } from "@/UI/Root/Components/ModalProvider"; import { words } from "@/UI/words"; -import ConfirmationModal from "../../ConfirmationModal"; import { ToastAlertMessage } from "../../ToastAlertMessage"; interface Props extends VersionedServiceInstanceIdentifier { @@ -13,27 +13,30 @@ interface Props extends VersionedServiceInstanceIdentifier { onClose: () => void; } -export const SetStateSection: React.FunctionComponent = ({ +/** + * SetStateSection is a component that allows the user to set a state on a service instance. + * + * @props {Props} props - The props of the component. + * @prop {string} service_entity - The service entity of the service instance. + * @prop {string} id - The id of the service instance. + * @prop {string} instance_identity - The instance identity of the service instance. + * @prop {string} version - The version of the service instance. + * @prop {string[]} targets - The available states of the service instance. + * + * @returns {React.FC} A React component that allows the user to set a state on a service instance. + */ +export const SetStateSection: React.FC = ({ service_entity, id, instance_identity, version, targets, }) => { - const [isModalOpen, setIsModalOpen] = useState(false); - const [confirmationText, setConfirmationText] = useState(""); - const [targetState, setTargetState] = useState(""); + const { triggerModal, closeModal } = useContext(ModalContext); const [stateErrorMessage, setStateErrorMessage] = useState(""); - const handleModalToggle = () => { - setIsModalOpen(!isModalOpen); - }; const onSelect = (value: string) => { - setTargetState(value); - setConfirmationText( - words("inventory.statustab.confirmMessage")(instance_identity, value), - ); - handleModalToggle(); + openModal(value); }; const isDisabled = !targets || targets.length === 0; @@ -47,12 +50,58 @@ export const SetStateSection: React.FunctionComponent = ({ }); const isHalted = environmentModifier.useIsHalted(); - const onSubmit = async (targetState: string) => { - const result = await trigger(targetState); + /** + * Opens a modal with a confirmation buttons. + * @param {string} targetState - The target state to be used in the operation. + * + * @returns {void} + */ + const openModal = (targetState: string): void => { + /** + * Handles the submission of the form. + * + * @param {string} targetState - The target state to be used in the operation. + * + * @returns {Promise} A Promise that resolves when the operation is complete. + */ + const onSubmit = async () => { + const result = await trigger(targetState); + + if (Maybe.isSome(result)) { + setStateErrorMessage(result.value); + } + closeModal(); + }; - if (Maybe.isSome(result)) { - setStateErrorMessage(result.value); - } + triggerModal({ + title: words("inventory.statustab.confirmTitle"), + actions: [ + , + , + ], + content: ( + + {words("inventory.statustab.confirmMessage")( + instance_identity, + targetState, + )} + + ), + }); }; return ( @@ -96,17 +145,6 @@ export const SetStateSection: React.FunctionComponent = ({ None available )} - - {confirmationText} - ); }; diff --git a/src/Slices/ServiceInventory/UI/Components/TableControls/TableControls.tsx b/src/Slices/ServiceInventory/UI/Components/TableControls/TableControls.tsx index 16dfa595f..332d8b8a9 100644 --- a/src/Slices/ServiceInventory/UI/Components/TableControls/TableControls.tsx +++ b/src/Slices/ServiceInventory/UI/Components/TableControls/TableControls.tsx @@ -37,8 +37,7 @@ export const TableControls: React.FC = ({ const [isOpen, setIsOpen] = useState(false); const { routeManager, featureManager } = useContext(DependencyContext); - const composerEnabled = - service.owner === null && featureManager.isComposerEnabled(); + const composerEnabled = featureManager.isComposerEnabled(); const states = service.lifecycle.states.map((state) => state.name).sort(); @@ -52,26 +51,23 @@ export const TableControls: React.FC = ({ variant="secondary" isExpanded={isOpen} onClick={onToggleClick} - splitButtonOptions={{ - variant: "action", - items: [ - + - - {words("inventory.addInstance.button")} - - , - ], - }} + {words("inventory.addInstance.button")} + + , + ]} aria-label="AddInstanceToggle" /> ); @@ -80,7 +76,7 @@ export const TableControls: React.FC = ({ setFilter({})}> - + {composerEnabled ? ( = ({ })} search={location.search} > - - + } + > {words("inventory.addInstance.composerButton")} @@ -113,8 +111,8 @@ export const TableControls: React.FC = ({ })} search={location.search} > - diff --git a/src/Slices/ServiceInventory/UI/InstanceRow.tsx b/src/Slices/ServiceInventory/UI/InstanceRow.tsx index cba7a2450..db0e3361b 100644 --- a/src/Slices/ServiceInventory/UI/InstanceRow.tsx +++ b/src/Slices/ServiceInventory/UI/InstanceRow.tsx @@ -1,68 +1,46 @@ -import React, { useRef, useState } from "react"; - -import { Tbody, Tr, Td, ExpandableRowContent } from "@patternfly/react-table"; +import React, { useContext } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { Button, Flex, FlexItem } from "@patternfly/react-core"; +import { Tbody, Tr, Td } from "@patternfly/react-table"; import styled from "styled-components"; -import { Row, ServiceModel, VersionedServiceInstanceIdentifier } from "@/Core"; +import { Row, ServiceModel } from "@/Core"; +import { StateLabel } from "@/Slices/ServiceInstanceDetails/UI/Components/Sections"; +import { DependencyContext } from "@/UI"; import { DateWithTooltip } from "@/UI/Components"; import { CopyMultiOptions } from "@/UI/Components/CopyMultiOptions"; -import { scrollRowIntoView } from "@/UI/Utils"; import { words } from "@/UI/words"; import { DeploymentProgressBar, IdWithCopy } from "./Components"; - -import { Tabs, TabKey } from "./Tabs"; +import { RowActions } from "./Components/RowActionsMenu/RowActions"; interface Props { row: Row; - index: number; - isExpanded: boolean; - onToggle: () => void; - numberOfColumns: number; - rowActions: React.ReactElement | null; - state: React.ReactElement | null; - service?: ServiceModel; - serviceInstanceIdentifier: VersionedServiceInstanceIdentifier; shouldUseServiceIdentity?: boolean; idDataLabel: string; + service: ServiceModel; } export const InstanceRow: React.FC = ({ row, - index, - isExpanded, - onToggle, - numberOfColumns, - rowActions, - state, - serviceInstanceIdentifier, shouldUseServiceIdentity, idDataLabel, service, }) => { - const [activeTab, setActiveTab] = useState(TabKey.Status); - const rowRef = useRef(null); - const openTabAndScrollTo = (tab: TabKey) => () => { - setActiveTab(tab); - if (!isExpanded) { - onToggle(); - } - scrollRowIntoView(rowRef); - }; + const { routeManager } = useContext(DependencyContext); + + const instanceDetailsUrl = routeManager.useUrl("InstanceDetails", { + service: service.name, + instance: row.serviceIdentityValue || row.id.full, + instanceId: row.id.full, + }); + const navigate = useNavigate(); return ( - - + - {shouldUseServiceIdentity && row.serviceIdentityValue ? ( = ({ )} - {state} + + + + navigate( + `${instanceDetailsUrl}&state.InstanceDetails.tab=Resources`, + ) + } > @@ -93,24 +78,37 @@ export const InstanceRow: React.FC = ({ - {rowActions} - - - - - - + + + + + + + + + + + @@ -120,8 +118,3 @@ export const InstanceRow: React.FC = ({ const ActionWrapper = styled.span` cursor: pointer; `; - -const StyledRow = styled(Tr)<{ $deleted: boolean }>` - ${(p) => - p.$deleted ? "background-color: var(--pf-v5-global--palette--red-50);" : ""} -`; diff --git a/src/Slices/ServiceInventory/UI/InventoryTable.test.tsx b/src/Slices/ServiceInventory/UI/InventoryTable.test.tsx index eb71b3d7d..543953017 100644 --- a/src/Slices/ServiceInventory/UI/InventoryTable.test.tsx +++ b/src/Slices/ServiceInventory/UI/InventoryTable.test.tsx @@ -1,13 +1,12 @@ -import React, { act } from "react"; +import React from "react"; import { MemoryRouter, useLocation } from "react-router"; -import { render, screen, within } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { StoreProvider } from "easy-peasy"; -import { Either, RemoteData } from "@/Core"; +import { RemoteData } from "@/Core"; import { QueryResolverImpl, getStoreInstance, - QueryManagerResolverImpl, CommandResolverImpl, BaseApiHelper, DeleteInstanceCommandManager, @@ -23,167 +22,28 @@ import { defaultAuthContext } from "@/Data/Auth/AuthContext"; import { TriggerInstanceUpdateCommandManager } from "@/Slices/EditInstance/Data"; import { Row, - tablePresenter, - tablePresenterWithIdentity, StaticScheduler, dependencies, DeferredApiHelper, - ServiceInstance, DynamicCommandManagerResolverImpl, DynamicQueryManagerResolverImpl, + Service, } from "@/Test"; -import { withIdentity } from "@/Test/Data/Service"; -import { words } from "@/UI"; import { DependencyProvider, EnvironmentHandlerImpl, EnvironmentModifierImpl, } from "@/UI/Dependency"; +import { ModalProvider } from "@/UI/Root/Components/ModalProvider"; import { InventoryTable } from "./InventoryTable"; -import { InstanceActionPresenter } from "./Presenters"; +import { InventoryTablePresenter } from "./Presenters"; const dummySetter = () => { return; }; -test("InventoryTable can be expanded", async () => { - const store = getStoreInstance(); - const queryResolver = new QueryResolverImpl( - new QueryManagerResolverImpl( - store, - new DeferredApiHelper(), - new StaticScheduler(), - new StaticScheduler(), - ), - ); - const environmentHandler = EnvironmentHandlerImpl( - useLocation, - dependencies.routeManager, - ); - - store.dispatch.environment.setEnvironments( - RemoteData.success([ - { - id: "aaa", - name: "env-a", - project_id: "ppp", - repo_branch: "branch", - repo_url: "repo", - projectName: "project", - settings: { - enable_lsm_expert_mode: false, - }, - }, - ]), - ); - - render( - - - - - - - , - ); - const testid = `details_${Row.a.id.short}`; - - // Act - const expandCell = screen.getByLabelText(`expand-button-${Row.a.id.short}`); - - await act(async () => { - await userEvent.click(within(expandCell).getByRole("button")); - }); - // Assert - expect(await screen.findByTestId(testid)).toBeVisible(); -}); - -test("ServiceInventory can show resources for instance", async () => { - const store = getStoreInstance(); - const apiHelper = new DeferredApiHelper(); - const queryResolver = new QueryResolverImpl( - new QueryManagerResolverImpl( - store, - apiHelper, - new StaticScheduler(), - new StaticScheduler(), - ), - ); - const environmentHandler = EnvironmentHandlerImpl( - useLocation, - dependencies.routeManager, - ); - - store.dispatch.environment.setEnvironments( - RemoteData.success([ - { - id: "aaa", - name: "env-a", - project_id: "ppp", - repo_branch: "branch", - repo_url: "repo", - projectName: "project", - settings: { - enable_lsm_expert_mode: false, - }, - }, - ]), - ); - - render( - - - - - - - , - ); - - const expandCell = screen.getByLabelText(`expand-button-${Row.a.id.short}`); - - await act(async () => { - await userEvent.click(within(expandCell).getByRole("button")); - }); - - await act(async () => { - await userEvent.click( - screen.getByRole("tab", { name: words("inventory.tabs.resources") }), - ); - }); - await act(async () => { - apiHelper.resolve( - Either.right({ - data: [ - { - resource_id: "[resource_id_1],v=1", - resource_state: "resource_state", - }, - ], - }), - ); - }); - - expect( - await screen.findByRole("grid", { name: "ResourceTable-Success" }), - ).toBeInTheDocument(); - - expect(screen.getByText("[resource_id_1]")).toBeInTheDocument(); -}); +const tablePresenterWithIdentity = () => + new InventoryTablePresenter("service_id", "Service ID"); function setup(expertMode = false, setSortFn: (props) => void = dummySetter) { const store = getStoreInstance(); @@ -283,9 +143,6 @@ function setup(expertMode = false, setSortFn: (props) => void = dummySetter) { ); const environmentModifier = EnvironmentModifierImpl(); - const instances = [ServiceInstance.a]; - const actionPresenter = new InstanceActionPresenter(instances, withIdentity); - environmentModifier.setEnvironment("aaa"); const component = ( @@ -299,12 +156,15 @@ function setup(expertMode = false, setSortFn: (props) => void = dummySetter) { }} > - + + + @@ -318,7 +178,7 @@ test("ServiceInventory shows service identity if it's defined", async () => { render(component); - expect(await screen.findByText("Order ID")).toBeVisible(); + expect(await screen.findByText("Service ID")).toBeVisible(); expect(await screen.findByText("instance1")).toBeVisible(); }); @@ -344,41 +204,15 @@ test("ServiceInventory sets sorting parameters correctly on click", async () => const stateButton = await screen.findByRole("button", { name: /state/i }); expect(stateButton).toBeVisible(); - await act(async () => { - await userEvent.click(stateButton); - }); + + await userEvent.click(stateButton); + expect(sort.name).toEqual("state"); expect(sort.order).toEqual("asc"); }); describe("Actions", () => { - it("Should have expert options in expert-mode in dropdown and trigger dialog when forcing state", async () => { - const expertMode = true; - const component = setup(expertMode); - - render(component); - - const menuToggle = await screen.findByRole("button", { - name: "row actions toggle", - }); - - await act(async () => { - await userEvent.click(menuToggle); - }); - - const options = await screen.findAllByRole("menuitem"); - - expect(options).toHaveLength(19); - - await act(async () => { - await userEvent.click(await screen.findByTestId("forceState")); - await userEvent.click(screen.getByText("rejected")); - }); - - expect(await screen.findByRole("dialog")).toBeVisible(); - }); - - it("Shouldn't have expert options if not in expert-mode in dropdown", async () => { + it("Should have 6 options in total", async () => { const component = setup(); render(component); @@ -387,12 +221,10 @@ describe("Actions", () => { name: "row actions toggle", }); - await act(async () => { - await userEvent.click(menuToggle); - }); + await userEvent.click(menuToggle); const options = await screen.findAllByRole("menuitem"); - expect(options).toHaveLength(12); + expect(options).toHaveLength(6); }); }); diff --git a/src/Slices/ServiceInventory/UI/InventoryTable.tsx b/src/Slices/ServiceInventory/UI/InventoryTable.tsx index 0a34c2098..8a6462103 100644 --- a/src/Slices/ServiceInventory/UI/InventoryTable.tsx +++ b/src/Slices/ServiceInventory/UI/InventoryTable.tsx @@ -1,19 +1,12 @@ import React from "react"; -import { - Table /* data-codemods */, - Thead, - Tr, - Th, - OnSort, -} from "@patternfly/react-table"; +import { Table, Thead, Tr, Th, OnSort } from "@patternfly/react-table"; import { Row, ServiceModel, Sort } from "@/Core"; -import { useUrlStateWithExpansion } from "@/Data"; import { InstanceRow } from "./InstanceRow"; import { InventoryTablePresenter } from "./Presenters"; interface Props { rows: Row[]; - service?: ServiceModel; + service: ServiceModel; tablePresenter: InventoryTablePresenter; sort: Sort.Type; setSort: (sort: Sort.Type) => void; @@ -27,9 +20,6 @@ export const InventoryTable: React.FC = ({ setSort, ...props }) => { - const [isExpanded, onExpansion] = useUrlStateWithExpansion({ - route: "Inventory", - }); const onSort: OnSort = (_event, index, order) => { setSort({ name: tablePresenter.getColumnNameForIndex(index) as string, @@ -69,9 +59,8 @@ export const InventoryTable: React.FC = ({ width={getColumnWidth(column.apiName)} key={column.displayName} {...sortParams} - {...(column.apiName === "actions" && { "aria-hidden": true })} > - {column.apiName === "actions" ? "" : column.displayName} + {column.displayName} ); }); @@ -79,26 +68,12 @@ export const InventoryTable: React.FC = ({ return ( - - + {heads} - {rows.map((row, index) => ( + {rows.map((row) => ( = ({
- {heads} -
); }; - -const getIdentityForRow = (row: Row): string => row.id.full; diff --git a/src/Slices/ServiceInventory/UI/Presenters/InstanceActionPresenter.test.ts b/src/Slices/ServiceInventory/UI/Presenters/InstanceActionPresenter.test.ts deleted file mode 100644 index bfe76cef0..000000000 --- a/src/Slices/ServiceInventory/UI/Presenters/InstanceActionPresenter.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { ServiceModel } from "@/Core"; -import { ServiceInstance } from "@/Test"; -import { InstanceActionPresenter } from "./InstanceActionPresenter"; - -const instances = [ServiceInstance.a]; - -describe("InstanceActionPresenter ", () => { - it("returns disabled for transfers which are not in the lifecycle", () => { - const partialEntity = { - name: "cloudconnectv2", - lifecycle: { - states: [{ name: "creating", label: "info" }], - transfers: [{}], - }, - } as ServiceModel; - const actionPresenter = new InstanceActionPresenter( - instances, - partialEntity, - ); - const editDisabled = actionPresenter.isTransferDisabled( - instances[0].id, - "on_update", - ); - - expect(editDisabled).toBeTruthy(); - - const deleteDisabled = actionPresenter.isTransferDisabled( - instances[0].id, - "on_delete", - ); - - expect(deleteDisabled).toBeTruthy(); - }); - - it("returns enabled for update transfers which are in the lifecycle", () => { - const partialEntity = { - name: "cloudconnectv2", - lifecycle: { - states: [{ name: "creating", label: "info" }], - transfers: [{ source: "creating", on_update: true }], - }, - } as ServiceModel; - const actionPresenter = new InstanceActionPresenter( - instances, - partialEntity, - ); - const editDisabled = actionPresenter.isTransferDisabled( - instances[0].id, - "on_update", - ); - - expect(editDisabled).toBeFalsy(); - - const deleteDisabled = actionPresenter.isTransferDisabled( - instances[0].id, - "on_delete", - ); - - expect(deleteDisabled).toBeTruthy(); - }); - - it("returns enabled for delete transfers which are in the lifecycle", () => { - const partialEntity = { - name: "cloudconnectv2", - lifecycle: { - states: [{ name: "creating", label: "info" }], - transfers: [{ source: "creating", on_delete: true }], - }, - } as ServiceModel; - const actionPresenter = new InstanceActionPresenter( - instances, - partialEntity, - ); - const editDisabled = actionPresenter.isTransferDisabled( - instances[0].id, - "on_update", - ); - - expect(editDisabled).toBeTruthy(); - - const deleteDisabled = actionPresenter.isTransferDisabled( - instances[0].id, - "on_delete", - ); - - expect(deleteDisabled).toBeFalsy(); - }); -}); diff --git a/src/Slices/ServiceInventory/UI/Presenters/InstanceActionPresenter.ts b/src/Slices/ServiceInventory/UI/Presenters/InstanceActionPresenter.ts deleted file mode 100644 index 95634f9cc..000000000 --- a/src/Slices/ServiceInventory/UI/Presenters/InstanceActionPresenter.ts +++ /dev/null @@ -1,54 +0,0 @@ -import React, { ReactElement } from "react"; -import { ServiceModel } from "@/Core"; -import { ActionPresenter, ServiceInstanceForAction } from "@/UI/Presenters"; -import { RowActions } from "../Components/RowActionsMenu/RowActions"; - -export class InstanceActionPresenter implements ActionPresenter { - constructor( - private readonly instances: ServiceInstanceForAction[], - private readonly serviceEntity: ServiceModel, - ) {} - - private getInstanceForId(id: string): ServiceInstanceForAction | undefined { - return this.instances.find((instance) => instance.id === id); - } - - getForId(id: string): ReactElement | null { - const instance = this.getInstanceForId(id); - - if (typeof instance === "undefined") return null; - - return React.createElement(RowActions, { - instance, - editDisabled: this.isTransferDisabled(id, "on_update"), - deleteDisabled: this.isTransferDisabled(id, "on_delete"), - diagnoseDisabled: instance.deleted, - composerAvailable: this.serviceEntity.owner === null, - availableStates: this.getAvailableStates(), - }); - } - - getAvailableStates(): string[] { - return this.serviceEntity.lifecycle.states.map((state) => state.name); - } - - isTransferDisabled( - id: string, - transferType: "on_update" | "on_delete", - ): boolean { - const instance = this.getInstanceForId(id); - - if (typeof instance === "undefined") { - return false; - } - // If the action is allowed, there is a corresponding transfer in the lifecycle, - // where the source state is the current state - const transfersFromCurrentSource = - this.serviceEntity.lifecycle.transfers.filter( - (transfer) => - transfer.source === instance.state && transfer[transferType], - ); - - return transfersFromCurrentSource.length === 0; - } -} diff --git a/src/Slices/ServiceInventory/UI/Presenters/InstanceStatePresenter.test.ts b/src/Slices/ServiceInventory/UI/Presenters/InstanceStatePresenter.test.ts deleted file mode 100644 index bc8b60595..000000000 --- a/src/Slices/ServiceInventory/UI/Presenters/InstanceStatePresenter.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ServiceModel } from "@/Core"; -import { ServiceInstance } from "@/Test"; -import { InstanceStatePresenter } from "./InstanceStatePresenter"; - -const instances = [ServiceInstance.a]; - -test("InstanceStatePresenter returns correct InstanceState when every input is correct", () => { - const partialEntity = { - name: "cloudconnectv2", - lifecycle: { states: [{ name: "creating", label: "info" }] }, - } as ServiceModel; - - const statePresenter = new InstanceStatePresenter(instances, partialEntity); - const stateLabel = statePresenter.getForId(instances[0].id); - - expect(stateLabel).toBeTruthy(); - expect(stateLabel?.props.color).toEqual("blue"); - expect(stateLabel?.props.icon.type.displayName).toEqual("InfoCircleIcon"); -}); - -test("InstanceStatePresenter returns null when the instance can't be found", () => { - const partialEntity = { - name: "cloudconnectv2", - lifecycle: { states: [{ name: "creating", label: "info" }] }, - } as ServiceModel; - - const statePresenter = new InstanceStatePresenter(instances, partialEntity); - const stateLabel = statePresenter.getForId("id"); - - expect(stateLabel).toBeFalsy(); -}); - -test("InstanceStatePresenter returns null when the state can't be found in the lifecycle", () => { - const partialEntity = { - name: "cloudconnectv2", - lifecycle: { states: [{ name: "up", label: "success" }] }, - } as ServiceModel; - - const statePresenter = new InstanceStatePresenter(instances, partialEntity); - const stateLabel = statePresenter.getForId(instances[0].id); - - expect(stateLabel).toBeFalsy(); -}); diff --git a/src/Slices/ServiceInventory/UI/Presenters/InstanceStatePresenter.ts b/src/Slices/ServiceInventory/UI/Presenters/InstanceStatePresenter.ts deleted file mode 100644 index 6baaafe38..000000000 --- a/src/Slices/ServiceInventory/UI/Presenters/InstanceStatePresenter.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ReactElement } from "react"; -import { ServiceInstanceModelWithTargetStates, ServiceModel } from "@/Core"; -import { InstanceState } from "@/UI/Components"; -import { StatePresenter } from "./StatePresenter"; - -export class InstanceStatePresenter implements StatePresenter { - constructor( - private readonly instances: ServiceInstanceModelWithTargetStates[], - private readonly serviceEntity: ServiceModel, - ) {} - - private getInstanceForId( - id: string, - ): ServiceInstanceModelWithTargetStates | undefined { - return this.instances.find((instance) => instance.id === id); - } - - getForId(id: string): ReactElement | null { - const instance = this.getInstanceForId(id); - - if (typeof instance === "undefined") { - return null; - } - // The service entity lifecycle contains all of the states an instance of that entity can reach - const lifecycleState = this.serviceEntity.lifecycle.states.find( - (state) => state.name === instance.state, - ); - - if (!lifecycleState) { - return null; - } - - return InstanceState({ - name: lifecycleState.name, - label: lifecycleState.label, - }) as ReactElement; - } -} diff --git a/src/Slices/ServiceInventory/UI/Presenters/InventoryTablePresenter.test.ts b/src/Slices/ServiceInventory/UI/Presenters/InventoryTablePresenter.test.ts index 6dfc57e11..051dfa0b9 100644 --- a/src/Slices/ServiceInventory/UI/Presenters/InventoryTablePresenter.test.ts +++ b/src/Slices/ServiceInventory/UI/Presenters/InventoryTablePresenter.test.ts @@ -1,22 +1,13 @@ -import { ServiceModel } from "@/Core"; -import { - ServiceInstance, - DummyActionPresenter, - DummyDatePresenter, - tablePresenter, -} from "@/Test"; -import { DummyStatePresenter } from "@/Test/Mock/DummyStatePresenter"; -import { AttributesPresenter } from "./AttributesPresenter"; -import { InstanceActionPresenter } from "./InstanceActionPresenter"; +import { Service, ServiceInstance } from "@/Test"; import { InventoryTablePresenter } from "./InventoryTablePresenter"; -const presenter = new InventoryTablePresenter( - new DummyDatePresenter(), - new AttributesPresenter(), - new DummyActionPresenter(), - new DummyStatePresenter(), -); -const rows = presenter.createRows([ServiceInstance.a]); +const tablePresenter = () => new InventoryTablePresenter(); + +const tablePresenterWithIdentity = () => + new InventoryTablePresenter("service_id", "Service ID"); + +const presenter = new InventoryTablePresenter(); +const rows = presenter.createRows([ServiceInstance.a], Service.a); test("TablePresenter short id", () => { expect(rows[0].id.short.length).toBe(4); @@ -45,14 +36,7 @@ test("TablePresenter returns sortable columns correctly", () => { }); describe("TablePresenter with identity ", () => { - const presenterWithIdentity = new InventoryTablePresenter( - new DummyDatePresenter(), - new AttributesPresenter(), - new DummyActionPresenter(), - new DummyStatePresenter(), - "service_id", - "Service ID", - ); + const presenterWithIdentity = tablePresenterWithIdentity(); test("returns sortable columns correctly", () => { expect(presenterWithIdentity.getSortableColumnNames()).toEqual([ @@ -83,20 +67,7 @@ describe("TablePresenter with identity ", () => { }); describe("TablePresenter with Actions", () => { - const instances = [ServiceInstance.a]; - const partialEntity = { - name: "cloudconnectv2", - lifecycle: { - states: [{ name: "creating", label: "info" }], - transfers: [{}], - }, - } as ServiceModel; - const actionPresenter = new InstanceActionPresenter(instances, partialEntity); const presenterWithActions = new InventoryTablePresenter( - new DummyDatePresenter(), - new AttributesPresenter(), - actionPresenter, - new DummyStatePresenter(), "service_id", "Service ID", ); @@ -104,7 +75,6 @@ describe("TablePresenter with Actions", () => { test("TablePresenter converts column name to index correctly", () => { expect(presenterWithActions.getIndexForColumnName("id")).toEqual(-1); expect(presenterWithActions.getIndexForColumnName("state")).toEqual(1); - expect(presenterWithActions.getIndexForColumnName("actions")).toEqual(5); expect(presenterWithActions.getIndexForColumnName("history")).toEqual(-1); expect(presenterWithActions.getIndexForColumnName(undefined)).toEqual(-1); }); diff --git a/src/Slices/ServiceInventory/UI/Presenters/InventoryTablePresenter.ts b/src/Slices/ServiceInventory/UI/Presenters/InventoryTablePresenter.ts index 0d7fc7e2b..82e4e9a1f 100644 --- a/src/Slices/ServiceInventory/UI/Presenters/InventoryTablePresenter.ts +++ b/src/Slices/ServiceInventory/UI/Presenters/InventoryTablePresenter.ts @@ -1,18 +1,13 @@ -import { ReactElement } from "react"; import { Row, + ServiceInstanceModel, ServiceInstanceModelWithTargetStates, + ServiceModel, getUuidFromRaw, } from "@/Core"; -import { - ActionPresenter, - TablePresenter, - DatePresenter, - ColumnHead, -} from "@/UI/Presenters"; +import { isTransferDisabled } from "@/Slices/ServiceInstanceDetails/Utils"; +import { TablePresenter, ColumnHead } from "@/UI/Presenters"; import { words } from "@/UI/words"; -import { AttributesPresenter } from "./AttributesPresenter"; -import { StatePresenter } from "./StatePresenter"; /** * The TablePresenter is responsible for formatting the domain data. @@ -20,19 +15,14 @@ import { StatePresenter } from "./StatePresenter"; * This class should not hold state as state should be kept in the Redux Store. */ export class InventoryTablePresenter - implements TablePresenter + implements TablePresenter { readonly columnHeads: ColumnHead[]; readonly numberOfColumns: number; constructor( - private _datePresenter: DatePresenter, - private _attributesPresenter: AttributesPresenter, - private actionPresenter: ActionPresenter, - private statePresenter: StatePresenter, private serviceIdentity?: string, - private serviceIdentityDisplayName?: string, - private isConfigDisabled?: boolean, + private serviceIdentityDisplayName?: string | null, ) { this.columnHeads = [ { @@ -52,17 +42,15 @@ export class InventoryTablePresenter displayName: words("inventory.column.updatedAt"), apiName: "last_updated", }, - { displayName: words("inventory.column.actions"), apiName: "actions" }, ]; this.numberOfColumns = this.columnHeads.length + 1; } - public getActionsFor(id: string): ReactElement | null { - return this.actionPresenter.getForId(id); - } - - public createRows(instances: ServiceInstanceModelWithTargetStates[]): Row[] { - return instances.map((instance) => this.instanceToRow(instance)); + public createRows( + instances: ServiceInstanceModelWithTargetStates[], + service: ServiceModel, + ): Row[] { + return instances.map((instance) => this.instanceToRow(instance, service)); } public getColumnHeadDisplayNames(): string[] { @@ -123,22 +111,18 @@ export class InventoryTablePresenter return this.numberOfColumns; } - public getStateFor(id: string): ReactElement | null { - return this.statePresenter.getForId(id); - } - - private instanceToRow(instance: ServiceInstanceModelWithTargetStates): Row { + private instanceToRow( + instance: ServiceInstanceModel, + service: ServiceModel, + ): Row { const { id, - candidate_attributes, - active_attributes, - rollback_attributes, created_at, last_updated, version, - instanceSetStateTargets, environment, service_entity, + state, deployment_progress, service_identity_attribute_value, deleted, @@ -146,21 +130,17 @@ export class InventoryTablePresenter return { id: getUuidFromRaw(id), - attributes: { - candidate: candidate_attributes, - active: active_attributes, - rollback: rollback_attributes, - }, + state: state, createdAt: created_at, updatedAt: last_updated, version: version, - instanceSetStateTargets, environment, service_entity, deploymentProgress: deployment_progress, serviceIdentityValue: service_identity_attribute_value, deleted, - configDisabled: this.isConfigDisabled, + editDisabled: isTransferDisabled(instance, "on_update", service), + deleteDisabled: isTransferDisabled(instance, "on_delete", service), }; } } diff --git a/src/Slices/ServiceInventory/UI/Presenters/StatePresenter.ts b/src/Slices/ServiceInventory/UI/Presenters/StatePresenter.ts deleted file mode 100644 index 95a017295..000000000 --- a/src/Slices/ServiceInventory/UI/Presenters/StatePresenter.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ReactElement } from "react"; - -/** - * A StatePresenter is responsible for presenting the states of a service instance. - */ -export interface StatePresenter { - getForId(id: string): ReactElement | null; -} diff --git a/src/Slices/ServiceInventory/UI/Presenters/index.ts b/src/Slices/ServiceInventory/UI/Presenters/index.ts index 069bef06f..86811dfb5 100644 --- a/src/Slices/ServiceInventory/UI/Presenters/index.ts +++ b/src/Slices/ServiceInventory/UI/Presenters/index.ts @@ -1,5 +1,2 @@ export * from "./AttributesPresenter"; -export * from "./InstanceActionPresenter"; export * from "./InventoryTablePresenter"; -export * from "./StatePresenter"; -export * from "./InstanceStatePresenter"; diff --git a/src/Slices/ServiceInventory/UI/ServiceInventory.test.tsx b/src/Slices/ServiceInventory/UI/ServiceInventory.test.tsx index 2c8102590..c1bcf2995 100644 --- a/src/Slices/ServiceInventory/UI/ServiceInventory.test.tsx +++ b/src/Slices/ServiceInventory/UI/ServiceInventory.test.tsx @@ -1,7 +1,7 @@ import React, { act } from "react"; import { MemoryRouter, useLocation } from "react-router-dom"; import { Page } from "@patternfly/react-core"; -import { fireEvent, render, screen } from "@testing-library/react"; +import { fireEvent, render, screen, within } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { StoreProvider } from "easy-peasy"; import { axe, toHaveNoViolations } from "jest-axe"; @@ -24,7 +24,6 @@ import { defaultAuthContext } from "@/Data/Auth/AuthContext"; import { Service, ServiceInstance, - InstanceResource, Pagination, StaticScheduler, DynamicQueryManagerResolverImpl, @@ -35,6 +34,7 @@ import { } from "@/Test"; import { words } from "@/UI"; import { DependencyProvider, EnvironmentHandlerImpl } from "@/UI/Dependency"; +import { ModalProvider } from "@/UI/Root/Components/ModalProvider"; import { TriggerInstanceUpdateCommandManager } from "@S/EditInstance/Data"; import { Chart } from "./Components"; import { ServiceInventory } from "./ServiceInventory"; @@ -123,16 +123,17 @@ function setup(service = Service.a, pageSize = "") { }} > - - } - /> - + + + } + /> + + - {/* */} ); @@ -237,11 +238,9 @@ test("ServiceInventory shows next page of instances", async () => { await screen.findByRole("cell", { name: "IdCell-a" }), ).toBeInTheDocument(); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Go to next page" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Go to next page" }), + ); apiHelper.resolve( Either.right({ @@ -262,85 +261,6 @@ test("ServiceInventory shows next page of instances", async () => { }); }); -test("GIVEN ResourcesView fetches resources for new instance after instance update", async () => { - const { component, apiHelper, scheduler } = setup(); - - render(component); - - await act(async () => { - await apiHelper.resolve( - Either.right({ - data: [ServiceInstance.a], - links: Pagination.links, - metadata: Pagination.metadata, - }), - ); - }); - - expect( - await screen.findByRole("grid", { name: "ServiceInventory-Success" }), - ).toBeInTheDocument(); - - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "Details" })); - }); - await act(async () => { - await userEvent.click( - await screen.findByRole("tab", { - name: words("inventory.tabs.resources"), - }), - ); - }); - - await act(async () => { - await apiHelper.resolve(Either.right({ data: InstanceResource.listA })); - }); - - expect( - screen.getByRole("cell", { name: "[resource_id_a]" }), - ).toBeInTheDocument(); - - scheduler.executeAll(); - - await act(async () => { - await apiHelper.resolve( - Either.right({ - data: [{ ...ServiceInstance.a, version: 4 }], - links: Pagination.links, - metadata: Pagination.metadata, - }), - ); - }); - - expect(apiHelper.pendingRequests[0].url).toMatch( - `/lsm/v1/service_inventory/${ServiceInstance.a.service_entity}/${ServiceInstance.a.id}/resources?current_version=3`, - ); - await act(async () => { - await apiHelper.resolve(Either.left({ message: "Conflict", status: 409 })); - }); - - expect(apiHelper.pendingRequests[0].url).toMatch( - "/lsm/v1/service_inventory/service_name_a/service_instance_id_a", - ); - await act(async () => { - await apiHelper.resolve( - Either.right({ - data: { ...ServiceInstance.a, version: 4 }, - }), - ); - }); - - expect(apiHelper.pendingRequests[0].url).toMatch( - `/lsm/v1/service_inventory/${ServiceInstance.a.service_entity}/${ServiceInstance.a.id}/resources?current_version=4`, - ); - - await act(async () => { - const results = await axe(document.body); - - expect(results).toHaveNoViolations(); - }); -}); - test("ServiceInventory shows instance summary chart", async () => { const { component } = setup(Service.withInstanceSummary); @@ -351,38 +271,6 @@ test("ServiceInventory shows instance summary chart", async () => { ).toBeInTheDocument(); }); -test("ServiceInventory shows disabled composer buttons for non-root instances ", async () => { - const { component, apiHelper } = setup({ ...Service.a, owner: "owner" }); - - render(component); - - await act(async () => { - apiHelper.resolve( - Either.right({ - data: [{ ...ServiceInstance.a, id: "a" }], - links: { ...Pagination.links }, - metadata: Pagination.metadata, - }), - ); - }); - - expect(screen.queryByRole("Add in Composer")).not.toBeInTheDocument(); - - const menuToggle = await screen.findByRole("button", { - name: "row actions toggle", - }); - - await act(async () => { - await userEvent.click(menuToggle); - }); - - const button = screen.queryByRole("menuitem", { - name: "Edit in Composer", - }); - - expect(button).not.toBeInTheDocument(); -}); - test("ServiceInventory shows enabled composer buttons for root instances ", async () => { const { component, apiHelper } = setup(Service.a); @@ -398,11 +286,9 @@ test("ServiceInventory shows enabled composer buttons for root instances ", asyn ); }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "AddInstanceToggle" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "AddInstanceToggle" }), + ); expect(await screen.findByText("Add in Composer")).toBeEnabled(); @@ -410,9 +296,7 @@ test("ServiceInventory shows enabled composer buttons for root instances ", asyn name: "row actions toggle", }); - await act(async () => { - await userEvent.click(menuToggle); - }); + await userEvent.click(menuToggle); expect(await screen.findByText("Edit in Composer")).toBeEnabled(); @@ -434,19 +318,21 @@ test("ServiceInventory shows only button to display instance in the composer for ); }); - expect(screen.queryByText("Add in Composer")).not.toBeInTheDocument(); + await userEvent.click( + screen.getByRole("button", { name: "AddInstanceToggle" }), + ); + + expect(screen.getByText("Add in Composer")).toBeInTheDocument(); const menuToggle = await screen.findByRole("button", { name: "row actions toggle", }); - await act(async () => { - await userEvent.click(menuToggle); - }); + await userEvent.click(menuToggle); expect(await screen.findByText("Show in Composer")).toBeEnabled(); - expect(screen.queryByText("Edit in Composer")).not.toBeInTheDocument(); + expect(screen.getByText("Edit in Composer")).toBeInTheDocument(); }); test("GIVEN ServiceInventory WHEN sorting changes AND we are not on the first page THEN we are sent back to the first page", async () => { @@ -474,9 +360,8 @@ test("GIVEN ServiceInventory WHEN sorting changes AND we are not on the first pa expect(nextPageButton).toBeEnabled(); - await act(async () => { - await userEvent.click(nextPageButton); - }); + await userEvent.click(nextPageButton); + //expect the api url to contain start and end keywords that are used for pagination when we are moving to the next page expect(apiHelper.pendingRequests[0].url).toMatch(/(&start=|&end=)/); expect(apiHelper.pendingRequests[0].url).toMatch(/(&sort=created_at.desc)/); @@ -497,10 +382,16 @@ test("GIVEN ServiceInventory WHEN sorting changes AND we are not on the first pa }); //sort on the second page - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "State" })); + const columnheader = screen.getByRole("columnheader", { + name: /state/i, }); + await userEvent.click( + within(columnheader).getByRole("button", { + name: /state/i, + }), + ); + // expect the api url to not contain start and end keywords that are used for pagination to assert we are back on the first page. // we are asserting on the second request as the first request is for the updated sorting event, and second is chained to back to the first page with still correct sorting expect(apiHelper.pendingRequests[1].url).not.toMatch(/(&start=|&end=)/); diff --git a/src/Slices/ServiceInventory/UI/ServiceInventory.tsx b/src/Slices/ServiceInventory/UI/ServiceInventory.tsx index 916f2ada4..2965f6527 100644 --- a/src/Slices/ServiceInventory/UI/ServiceInventory.tsx +++ b/src/Slices/ServiceInventory/UI/ServiceInventory.tsx @@ -16,45 +16,13 @@ import { ErrorView, LoadingView, PaginationWidget, - ServiceProvider, } from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; -import { useRouteParams } from "@/UI/Routing"; import { words } from "@/UI/words"; -import { Chart, TableControls } from "./Components"; +import { TableControls } from "./Components"; import { TableProvider } from "./TableProvider"; import { Wrapper } from "./Wrapper"; -/** - * The main page component for the Service Inventory. - */ -export const Page: React.FC = () => { - const { service: serviceName } = useRouteParams<"Inventory">(); - - return ( - - ); -}; - -/** - * A prepped version of the ServiceInventory component. - * @param {ServiceModel} service - Service Model. - * @returns {JSX.Element} - The rendered ServiceInventory component. - */ -const PreppedServiceInventory: React.FC<{ service: ServiceModel }> = ({ - service, -}) => ( - } - /> -); - interface Props { labelFiltering: { danger: string[]; diff --git a/src/Slices/ServiceInventory/UI/Spec/AttributesFilter.spec.ts b/src/Slices/ServiceInventory/UI/Spec/AttributesFilter.spec.ts deleted file mode 100644 index bde2445f2..000000000 --- a/src/Slices/ServiceInventory/UI/Spec/AttributesFilter.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { act } from "react"; -import { render, screen, within } from "@testing-library/react"; -import { userEvent } from "@testing-library/user-event"; -import { Either } from "@/Core"; -import { Service, ServiceInstance, Pagination } from "@/Test"; -import { ServiceInventoryPrepper } from "./ServiceInventoryPrepper"; - -test("GIVEN The Service Inventory WHEN the user filters on AttributeSet ('Active', 'Not Empty') THEN only instances which have active attributes are shown", async () => { - const { component, apiHelper } = new ServiceInventoryPrepper().prep(); - - render(component); - - await act(async () => { - await apiHelper.resolve( - Either.right({ - data: [ServiceInstance.a, ServiceInstance.b], - links: Pagination.links, - metadata: Pagination.metadata, - }), - ); - }); - - await act(async () => { - await userEvent.click( - within(screen.getByRole("toolbar", { name: "FilterBar" })).getByRole( - "button", - { name: "FilterPicker" }, - ), - ); - }); - await act(async () => { - await userEvent.click(screen.getByRole("option", { name: "AttributeSet" })); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Select AttributeSet" }), - ); - }); - await act(async () => { - await userEvent.click(screen.getByRole("option", { name: "Active" })); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Select Quality" }), - ); - }); - await act(async () => { - await userEvent.click(screen.getByRole("option", { name: "Not Empty" })); - }); - - expect(apiHelper.pendingRequests[0].url).toEqual( - `/lsm/v1/service_inventory/${Service.a.name}?include_deployment_progress=True&limit=20&filter.attribute_set_not_empty=active_attributes&sort=created_at.desc`, - ); - - await act(async () => { - await apiHelper.resolve( - Either.right({ - data: [ServiceInstance.a], - links: Pagination.links, - metadata: Pagination.metadata, - }), - ); - }); - - const rowsAfter = await screen.findAllByRole("row", { - name: "InstanceRow-Intro", - }); - - expect(rowsAfter.length).toEqual(1); -}); diff --git a/src/Slices/ServiceInventory/UI/Spec/DeletedFilter.spec.ts b/src/Slices/ServiceInventory/UI/Spec/DeletedFilter.spec.ts index 7b923425d..43baf60b7 100644 --- a/src/Slices/ServiceInventory/UI/Spec/DeletedFilter.spec.ts +++ b/src/Slices/ServiceInventory/UI/Spec/DeletedFilter.spec.ts @@ -26,26 +26,22 @@ test("GIVEN The Service Inventory WHEN the user filters on deleted ('Only') THEN name: "FilterPicker", }); - await act(async () => { - await userEvent.click(picker); - }); + await userEvent.click(picker); + const id = screen.getByRole("option", { name: "Deleted" }); - await act(async () => { - await userEvent.click(id); - }); + await userEvent.click(id); + const rule = within(filterBar).getByRole("button", { name: "Select Deleted", }); - await act(async () => { - await userEvent.click(rule); - }); + await userEvent.click(rule); + const only = screen.getByRole("option", { name: "Only" }); - await act(async () => { - await userEvent.click(only); - }); + await userEvent.click(only); + expect(apiHelper.pendingRequests[0].url).toEqual( `/lsm/v1/service_inventory/${Service.a.name}?include_deployment_progress=True&limit=20&filter.deleted=true&sort=created_at.desc`, ); diff --git a/src/Slices/ServiceInventory/UI/Spec/Filter.spec.ts b/src/Slices/ServiceInventory/UI/Spec/Filter.spec.ts index 9b323b69d..b95d2c23d 100644 --- a/src/Slices/ServiceInventory/UI/Spec/Filter.spec.ts +++ b/src/Slices/ServiceInventory/UI/Spec/Filter.spec.ts @@ -31,17 +31,13 @@ test("GIVEN The Service Inventory WHEN the user filters on something THEN a data words("inventory.filters.state.placeholder"), ); - await act(async () => { - await userEvent.click(input); - }); + await userEvent.click(input); const option = await screen.findByRole("option", { name: `${words("inventory.test.creating")}`, }); - await act(async () => { - await userEvent.click(option); - }); + await userEvent.click(option); expect( await screen.findByRole("region", { name: "ServiceInventory-Loading" }), diff --git a/src/Slices/ServiceInventory/UI/Spec/IdFilter.spec.ts b/src/Slices/ServiceInventory/UI/Spec/IdFilter.spec.ts index 74830a818..7406d6373 100644 --- a/src/Slices/ServiceInventory/UI/Spec/IdFilter.spec.ts +++ b/src/Slices/ServiceInventory/UI/Spec/IdFilter.spec.ts @@ -26,21 +26,15 @@ test("GIVEN The Service Inventory WHEN the user filters on id ('a') THEN only 1 name: "FilterPicker", }); - await act(async () => { - await userEvent.click(picker); - }); + await userEvent.click(picker); const id = screen.getByRole("option", { name: "Id" }); - await act(async () => { - await userEvent.click(id); - }); + await userEvent.click(id); const input = screen.getByRole("searchbox", { name: "IdFilter" }); - await act(async () => { - await userEvent.type(input, `${ServiceInstance.a.id}{enter}`); - }); + await userEvent.type(input, `${ServiceInstance.a.id}{enter}`); expect(apiHelper.pendingRequests[0].url).toEqual( `/lsm/v1/service_inventory/${Service.a.name}?include_deployment_progress=True&limit=20&filter.id_or_service_identity=${ServiceInstance.a.id}&sort=created_at.desc`, diff --git a/src/Slices/ServiceInventory/UI/Spec/Pagination.spec.ts b/src/Slices/ServiceInventory/UI/Spec/Pagination.spec.ts index fe0d9c943..718dae438 100644 --- a/src/Slices/ServiceInventory/UI/Spec/Pagination.spec.ts +++ b/src/Slices/ServiceInventory/UI/Spec/Pagination.spec.ts @@ -35,9 +35,7 @@ test("GIVEN ServiceInventory WHEN on 2nd page with outdated 1st page and user cl expect(button).toBeEnabled(); - await act(async () => { - await userEvent.click(button); - }); + await userEvent.click(button); expect(apiHelper.pendingRequests).toEqual([]); }); diff --git a/src/Slices/ServiceInventory/UI/Spec/ResourcesTab.spec.tsx b/src/Slices/ServiceInventory/UI/Spec/ResourcesTab.spec.tsx deleted file mode 100644 index cd1cf7b18..000000000 --- a/src/Slices/ServiceInventory/UI/Spec/ResourcesTab.spec.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { act } from "react"; -import { render, screen } from "@testing-library/react"; -import { userEvent } from "@testing-library/user-event"; -import { Either, Maybe } from "@/Core"; -import { ServiceInstance, Pagination, InstanceResource } from "@/Test"; -import { ServiceInventoryPrepper } from "./ServiceInventoryPrepper"; - -const jestOptions = { legacyFakeTimers: false }; - -jest.useFakeTimers(jestOptions); - -test("GIVEN The Service Inventory WHEN the user clicks on the resourcesTab THEN data is fetched immediately", async () => { - const user = userEvent.setup({ delay: null }); - const { component, scheduler, apiHelper } = - new ServiceInventoryPrepper().prep(); - - render(component); - - await act(async () => { - await apiHelper.resolve( - Either.right({ - data: [ServiceInstance.a, ServiceInstance.b], - links: Pagination.links, - metadata: Pagination.metadata, - }), - ); - }); - - await act(async () => { - await user.click(screen.getAllByRole("button", { name: "Details" })[0]); - }); - await act(async () => { - await user.click(screen.getAllByRole("tab", { name: "Resources" })[0]); - }); - - expect(apiHelper.pendingRequests).toHaveLength(1); - expect(apiHelper.pendingRequests[0].url).toEqual( - "/lsm/v1/service_inventory/service_name_a/service_instance_id_a/resources?current_version=3", - ); - - await act(async () => { - await apiHelper.resolve(Either.right({ data: InstanceResource.listA })); - }); - - const serviceInstancesTask = Maybe.orNull( - scheduler.tasks.get( - `GetServiceInstances_${ServiceInstance.a.service_entity}`, - ), - ); - const resourcesTask = Maybe.orNull( - scheduler.tasks.get(`GetInstanceResources_${ServiceInstance.a.id}`), - ); - - expect(serviceInstancesTask?.effect).not.toBeCalled(); - expect(resourcesTask?.effect).not.toBeCalled(); -}); - -test("GIVEN The Service Inventory WHEN the user clicks on the resourcesTab THEN the Resources auto-update happens in sync with the ServiceInstances", async () => { - const user = userEvent.setup({ delay: null }); - const prepper = new ServiceInventoryPrepper(); - const { component, scheduler, apiHelper } = prepper.prep(); - - render(component); - - await act(async () => { - await apiHelper.resolve( - Either.right({ - data: [ServiceInstance.a, ServiceInstance.b], - links: Pagination.links, - metadata: Pagination.metadata, - }), - ); - }); - - await act(async () => { - await user.click(screen.getAllByRole("button", { name: "Details" })[0]); - }); - await act(async () => { - await user.click(screen.getAllByRole("tab", { name: "Resources" })[0]); - }); - - await act(async () => { - await apiHelper.resolve(Either.right({ data: InstanceResource.listA })); - }); - - const serviceInstancesTask = Maybe.orNull( - scheduler.tasks.get( - `GetServiceInstances_${ServiceInstance.a.service_entity}`, - ), - ); - const resourcesTask = Maybe.orNull( - scheduler.tasks.get(`GetInstanceResources_${ServiceInstance.a.id}`), - ); - - await act(async () => { - await jest.advanceTimersByTime(5000); - }); - - await act(async () => { - await apiHelper.resolve( - Either.right({ - data: [ServiceInstance.a, ServiceInstance.b], - links: Pagination.links, - metadata: Pagination.metadata, - }), - ); - }); - await act(async () => { - await apiHelper.resolve(Either.right({ data: InstanceResource.listA })); - }); - - expect(serviceInstancesTask?.effect).toHaveBeenCalledTimes(1); - expect(serviceInstancesTask?.update).toHaveBeenCalledTimes(1); - expect(resourcesTask?.effect).toHaveBeenCalledTimes(1); - expect(resourcesTask?.update).toHaveBeenCalledTimes(1); -}); diff --git a/src/Slices/ServiceInventory/UI/Spec/StateFilter.spec.ts b/src/Slices/ServiceInventory/UI/Spec/StateFilter.spec.ts index 7a03b5d59..014516796 100644 --- a/src/Slices/ServiceInventory/UI/Spec/StateFilter.spec.ts +++ b/src/Slices/ServiceInventory/UI/Spec/StateFilter.spec.ts @@ -28,15 +28,11 @@ test("GIVEN The Service Inventory WHEN the user filters on state ('creating') TH const input = await screen.findByPlaceholderText("Select a state..."); - await act(async () => { - await userEvent.click(input); - }); + await userEvent.click(input); const option = await screen.findByRole("option", { name: "creating" }); - await act(async () => { - await userEvent.click(option); - }); + await userEvent.click(option); expect(apiHelper.pendingRequests[0].url).toEqual( `/lsm/v1/service_inventory/${Service.a.name}?include_deployment_progress=True&limit=20&filter.state=creating&sort=created_at.desc`, diff --git a/src/Slices/ServiceInventory/UI/TableProvider.tsx b/src/Slices/ServiceInventory/UI/TableProvider.tsx index d3237d772..d1545b10a 100644 --- a/src/Slices/ServiceInventory/UI/TableProvider.tsx +++ b/src/Slices/ServiceInventory/UI/TableProvider.tsx @@ -4,15 +4,8 @@ import { ServiceInstanceModelWithTargetStates, Sort, } from "@/Core"; -import { getOptionsFromService } from "@/Data"; -import { MomentDatePresenter } from "@/UI/Utils"; import { InventoryTable } from "./InventoryTable"; -import { - AttributesPresenter, - InstanceActionPresenter, - InstanceStatePresenter, - InventoryTablePresenter, -} from "./Presenters"; +import { InventoryTablePresenter } from "./Presenters"; export interface Props { instances: ServiceInstanceModelWithTargetStates[]; @@ -28,20 +21,11 @@ export const TableProvider: React.FC = ({ setSort, ...props }) => { - const datePresenter = new MomentDatePresenter(); - const attributesPresenter = new AttributesPresenter(); - const actionPresenter = new InstanceActionPresenter(instances, serviceEntity); - const statePresenter = new InstanceStatePresenter(instances, serviceEntity); const tablePresenter = new InventoryTablePresenter( - datePresenter, - attributesPresenter, - actionPresenter, - statePresenter, serviceEntity.service_identity, serviceEntity.service_identity_display_name, - getOptionsFromService(serviceEntity).length === 0, ); - const rows = tablePresenter.createRows(instances); + const rows = tablePresenter.createRows(instances, serviceEntity); return ( void; -} - -export const AttributesTab: React.FC = ({ - attributes, - id, - service, - version, - setTab = () => {}, -}) => { - const navigate = useNavigateTo(); - - return ( - { - navigate( - "Inventory", - { service: serviceName as string }, - `?env=${service?.environment}&state.Inventory.filter.id_or_service_identity[0]=${value}`, - ); - }, - }} - > - - - ); -}; diff --git a/src/Slices/ServiceInventory/UI/Tabs/ConfigDetails.test.tsx b/src/Slices/ServiceInventory/UI/Tabs/ConfigDetails.test.tsx index 1fc320ffb..56a115758 100644 --- a/src/Slices/ServiceInventory/UI/Tabs/ConfigDetails.test.tsx +++ b/src/Slices/ServiceInventory/UI/Tabs/ConfigDetails.test.tsx @@ -75,6 +75,6 @@ it("Config Details takes environment halted status in account", async () => { }); rerender(component({ enabled: true })); expect( - await screen.findByRole("checkbox", { name: "enabled-True" }), + await screen.findByRole("switch", { name: "enabled-True" }), ).toBeDisabled(); }); diff --git a/src/Slices/ServiceInventory/UI/Tabs/ConfigSectionContent.test.tsx b/src/Slices/ServiceInventory/UI/Tabs/ConfigSectionContent.test.tsx index 2b5079ce0..fa89a6606 100644 --- a/src/Slices/ServiceInventory/UI/Tabs/ConfigSectionContent.test.tsx +++ b/src/Slices/ServiceInventory/UI/Tabs/ConfigSectionContent.test.tsx @@ -119,19 +119,17 @@ test("ConfigTab can reset all settings", async () => { expect(resetButton).toBeVisible(); expect( - screen.getByRole("checkbox", { name: "auto_creating-False" }), + screen.getByRole("switch", { name: "auto_creating-False" }), ).toBeVisible(); - await act(async () => { - await userEvent.click(resetButton, { skipHover: true }); - }); + await userEvent.click(resetButton, { skipHover: true }); await act(async () => { await apiHelper.resolve(Either.right({ data: {} })); }); expect( - await screen.findByRole("checkbox", { name: "auto_creating-True" }), + await screen.findByRole("switch", { name: "auto_creating-True" }), ).toBeVisible(); }); @@ -140,15 +138,13 @@ test("ConfigTab can change 1 toggle", async () => { render(component); - const toggle = await screen.findByRole("checkbox", { + const toggle = await screen.findByRole("switch", { name: "auto_designed-True", }); expect(toggle).toBeVisible(); - await act(async () => { - await userEvent.click(toggle, { skipHover: true }); - }); + await userEvent.click(toggle, { skipHover: true }); await act(async () => { await apiHelper.resolve( @@ -157,11 +153,11 @@ test("ConfigTab can change 1 toggle", async () => { }); expect( - screen.getByRole("checkbox", { name: "auto_creating-False" }), + screen.getByRole("switch", { name: "auto_creating-False" }), ).toBeVisible(); expect( - await screen.findByRole("checkbox", { name: "auto_designed-False" }), + await screen.findByRole("switch", { name: "auto_designed-False" }), ).toBeVisible(); }); @@ -177,7 +173,7 @@ test("ConfigTab handles hooks with environment modifier correctly", async () => }); render(component); - const toggle = await screen.findByRole("checkbox", { + const toggle = await screen.findByRole("switch", { name: "auto_designed-True", }); diff --git a/src/Slices/ServiceInventory/UI/Tabs/MarkdownCard.tsx b/src/Slices/ServiceInventory/UI/Tabs/MarkdownCard.tsx index b26557ee6..791fcf157 100644 --- a/src/Slices/ServiceInventory/UI/Tabs/MarkdownCard.tsx +++ b/src/Slices/ServiceInventory/UI/Tabs/MarkdownCard.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Card, CardBody } from "@patternfly/react-core"; +import { Panel } from "@patternfly/react-core"; import { MarkdownContainer } from "@/UI/Components/MarkdownContainer"; interface Props { @@ -23,10 +23,8 @@ export const MarkdownCard = ({ attributeValue, web_title }: Props) => { : JSON.stringify(attributeValue); return ( - - - - - + + + ); }; diff --git a/src/Slices/ServiceInventory/UI/Tabs/ResourcesTab.test.tsx b/src/Slices/ServiceInventory/UI/Tabs/ResourcesTab.test.tsx deleted file mode 100644 index 053034f8a..000000000 --- a/src/Slices/ServiceInventory/UI/Tabs/ResourcesTab.test.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { act } from "react"; -import { MemoryRouter } from "react-router"; -import { render, screen } from "@testing-library/react"; -import { StoreProvider } from "easy-peasy"; -import { Either } from "@/Core"; -import { - QueryResolverImpl, - getStoreInstance, - QueryManagerResolverImpl, -} from "@/Data"; -import { DeferredApiHelper, dependencies, StaticScheduler } from "@/Test"; -import { DependencyProvider } from "@/UI/Dependency"; -import { ResourcesTab } from "./ResourcesTab"; - -function setup() { - const store = getStoreInstance(); - const scheduler = new StaticScheduler(); - const apiHelper = new DeferredApiHelper(); - const queryResolver = new QueryResolverImpl( - new QueryManagerResolverImpl(store, apiHelper, scheduler, scheduler), - ); - - const component = ( - - - - - - - - ); - - return { component, apiHelper, scheduler }; -} - -test("ResourcesView shows empty table", async () => { - const { component, apiHelper } = setup(); - - render(component); - - expect( - await screen.findByRole("grid", { name: "ResourceTable-Loading" }), - ).toBeInTheDocument(); - - await act(async () => { - await apiHelper.resolve(Either.right({ data: [] })); - }); - - expect( - await screen.findByRole("grid", { name: "ResourceTable-Empty" }), - ).toBeInTheDocument(); -}); - -test("ResourcesView shows failed table", async () => { - const { component, apiHelper } = setup(); - - render(component); - - expect( - await screen.findByRole("grid", { name: "ResourceTable-Loading" }), - ).toBeInTheDocument(); - - await act(async () => { - await apiHelper.resolve(Either.left("error")); - }); - - expect( - await screen.findByRole("grid", { name: "ResourceTable-Failed" }), - ).toBeInTheDocument(); -}); - -test("ResourcesView shows success table", async () => { - const { component, apiHelper } = setup(); - - render(component); - - expect( - await screen.findByRole("grid", { name: "ResourceTable-Loading" }), - ).toBeInTheDocument(); - - await act(async () => { - await apiHelper.resolve( - Either.right({ - data: [{ resource_id: "abc123,v=3", resource_state: "deployed" }], - }), - ); - }); - - expect( - await screen.findByRole("grid", { name: "ResourceTable-Success" }), - ).toBeInTheDocument(); -}); - -test("ResourcesView shows updated table", async () => { - const { component, apiHelper, scheduler } = setup(); - - render(component); - - expect( - await screen.findByRole("grid", { name: "ResourceTable-Loading" }), - ).toBeInTheDocument(); - - await act(async () => { - await apiHelper.resolve(Either.right({ data: [] })); - }); - - expect( - await screen.findByRole("grid", { name: "ResourceTable-Empty" }), - ).toBeInTheDocument(); - - scheduler.executeAll(); - - await act(async () => { - await apiHelper.resolve( - Either.right({ - data: [{ resource_id: "abc123,v=4", resource_state: "deployed" }], - }), - ); - }); - - expect( - await screen.findByRole("grid", { name: "ResourceTable-Success" }), - ).toBeInTheDocument(); -}); diff --git a/src/Slices/ServiceInventory/UI/Tabs/ResourcesTab.tsx b/src/Slices/ServiceInventory/UI/Tabs/ResourcesTab.tsx deleted file mode 100644 index afe05e671..000000000 --- a/src/Slices/ServiceInventory/UI/Tabs/ResourcesTab.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useContext } from "react"; -import { RemoteData, VersionedServiceInstanceIdentifier } from "@/Core"; -import { - ResourceTable, - ResourceTableWrapper, - EmptyView, - LoadingView, - ErrorView, -} from "@/UI/Components"; -import { DependencyContext } from "@/UI/Dependency"; -import { words } from "@/UI/words"; - -interface Props { - serviceInstanceIdentifier: VersionedServiceInstanceIdentifier; -} - -export const ResourcesTab: React.FC = ({ - serviceInstanceIdentifier, -}) => { - const { queryResolver } = useContext(DependencyContext); - const { id } = serviceInstanceIdentifier; - - const [data] = queryResolver.useContinuous<"GetInstanceResources">({ - kind: "GetInstanceResources", - ...serviceInstanceIdentifier, - }); - - return RemoteData.fold( - { - notAsked: () => null, - loading: () => ( - - - - ), - failed: (error) => ( - - - - ), - success: (resources) => - resources.length === 0 ? ( - - - - ) : ( - - ), - }, - data, - ); -}; diff --git a/src/Slices/ServiceInventory/UI/Tabs/StatusTab.tsx b/src/Slices/ServiceInventory/UI/Tabs/StatusTab.tsx deleted file mode 100644 index 78507cde9..000000000 --- a/src/Slices/ServiceInventory/UI/Tabs/StatusTab.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from "react"; -import { - Card, - CardBody, - DescriptionList, - DescriptionListGroup, - DescriptionListTerm, - DescriptionListDescription, - Title, - Flex, - FlexItem, -} from "@patternfly/react-core"; -import { ParsedNumber } from "@/Core"; -import { TextWithCopy } from "@/UI/Components"; -import { words } from "@/UI/words"; - -interface StatusInfo { - instanceId: string; - state: React.ReactElement | null; - version: ParsedNumber; - createdAt: string; - updatedAt: string; -} - -interface Props { - statusInfo: StatusInfo; -} - -export const StatusTab: React.FC = ({ statusInfo }) => { - return ( - - - - - - - {words("inventory.statustab.details")} - - - - - - - {words("inventory.column.id")} - - - - - - - - {words("inventory.column.state")} - - - {statusInfo.state} - - - - - {words("inventory.statustab.version")} - - - {statusInfo.version.toString()} - - - - - {words("inventory.column.createdAt")} - - - {statusInfo.createdAt} - - - - - {words("inventory.column.updatedAt")} - - - {statusInfo.updatedAt} - - - - - - - - - ); -}; diff --git a/src/Slices/ServiceInventory/UI/Tabs/Tabs.tsx b/src/Slices/ServiceInventory/UI/Tabs/Tabs.tsx deleted file mode 100644 index a10d844b0..000000000 --- a/src/Slices/ServiceInventory/UI/Tabs/Tabs.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import React, { useRef } from "react"; -import { Tooltip } from "@patternfly/react-core"; -import { - AutomationIcon, - CogIcon, - InfoCircleIcon, - ListIcon, -} from "@patternfly/react-icons"; -import { Row, ServiceModel, VersionedServiceInstanceIdentifier } from "@/Core"; -import { IconTabs, TabDescriptor } from "@/UI/Components"; -import { DynamicFAIcon } from "@/UI/Components/FaIcon"; -import { MomentDatePresenter } from "@/UI/Utils"; -import { words } from "@/UI/words"; -import { AttributesTab } from "./AttributesTab"; -import { ConfigSectionContent } from "./ConfigSectionContent"; -import { MarkdownCard } from "./MarkdownCard"; -import { ResourcesTab } from "./ResourcesTab"; -import { StatusTab } from "./StatusTab"; - -/** - * Enum representing the available tab keys. - */ -export enum TabKey { - Status = "Status", - Attributes = "Attributes", - Resources = "Resources", - Events = "Events", - Config = "Config", -} - -interface Props { - activeTab: TabKey | string; - setActiveTab: (tabKey: TabKey | string) => void; - row: Row; - state: React.ReactElement | null; - service?: ServiceModel; - serviceInstanceIdentifier: VersionedServiceInstanceIdentifier; -} - -/** - * Component that renders the tabs for the service inventory. - * - * @props Props - The component props. - * @prop {TabKey | string} activeTab - The active tab. - * @note TabKey is for known tabs, while the string is for custom tabs such as documentation. - * The documentation tabs are generated based on the service model. Thus making them unpredictable. - * @prop {function} setActiveTab - The callback for setting the active tab. - * @prop {Row} row - The row object. - * @prop {React.ReactElement | null} state - The state element. - * @prop {ServiceModel} service - The service model. - * @prop {VersionedServiceInstanceIdentifier} serviceInstanceIdentifier - The service instance identifier. - * - * @returns The tabs component. - */ -export const Tabs: React.FC = ({ - activeTab, - setActiveTab, - row, - state, - service, - serviceInstanceIdentifier, -}) => { - const configTooltipRef = useRef(); - const configTabDisabled = row.deleted || !!row.configDisabled; - - return ( - <> - - {configTabDisabled && ( - - )} - - ); -}; - -const datePresenter = new MomentDatePresenter(); - -/** - * Creates the status tab descriptor. - * - * @param row - The row object. - * @param state - The state element. - * @returns The status tab descriptor. - */ -const statusTab = ( - row: Row, - state: React.ReactElement | null, -): TabDescriptor => ({ - id: TabKey.Status, - title: words("inventory.tabs.status"), - icon: , - view: ( - - ), -}); - -/** - * Creates the attributes tab descriptor. - * - * @param row - The row object. - * @param service - The service model. - * @returns The attributes tab descriptor. - */ -const attributesTab = ( - row: Row, - setActiveTab: (tabKey: TabKey | string) => void, - service?: ServiceModel, -): TabDescriptor => ({ - id: TabKey.Attributes, - title: words("inventory.tabs.attributes"), - icon: , - view: ( - - ), -}); - -/** - * Creates the resources tab descriptor. - * - * @param serviceInstanceIdentifier - The service instance identifier. - * @returns The resources tab descriptor. - */ -const resourcesTab = ( - serviceInstanceIdentifier: VersionedServiceInstanceIdentifier, -): TabDescriptor => ({ - id: TabKey.Resources, - title: words("inventory.tabs.resources"), - icon: , - view: , -}); - -/** - * Creates the config tab descriptor. - * - * @param isDisabled - Indicates if the config tab is disabled. - * @param serviceInstanceIdentifier - The service instance identifier. - * @param ref - The ref object. - * @returns The config tab descriptor. - */ -const configTab = ( - isDisabled: boolean, - serviceInstanceIdentifier: VersionedServiceInstanceIdentifier, - ref: React.MutableRefObject, -): TabDescriptor => ({ - id: TabKey.Config, - title: words("config.title"), - icon: , - view: ( - - ), - isDisabled, - ref, -}); - -/** - * Creates an array of documentation tab descriptors. - * - * @param row - The row object. - * @param service - The service model. - * @returns An array of documentation tab descriptors. - */ -const documentationTab = ( - row: Row, - service?: ServiceModel, -): TabDescriptor[] => { - // check in the row if there are web_presentation attributes and if they are set to documentation. - const webPresentationAttributes: TabDescriptor[] = []; - - if (service && service.attributes) { - for (const attribute of service.attributes) { - if ( - attribute.attribute_annotations && - attribute.attribute_annotations.web_title && - attribute.attribute_annotations.web_presentation === "documentation" - ) { - const attributeValue = getAttributeValue(attribute.name, row); - - webPresentationAttributes.push({ - id: attribute.attribute_annotations.web_title, - icon: ( - - ), - view: ( - - ), - title: attribute.attribute_annotations.web_title, - }); - } - } - } - - return webPresentationAttributes; -}; - -/** - * Gets the attribute value from the row object. - * - * @param attributeName - The name of the attribute. - * @param row - The row object. - * @returns The attribute value. - */ -const getAttributeValue = (attributeName: string, row: Row) => { - if ( - row.attributes && - row.attributes.candidate && - row.attributes.candidate[attributeName] - ) { - return row.attributes.candidate[attributeName]; - } - if ( - row.attributes && - row.attributes.active && - row.attributes.active[attributeName] - ) { - return row.attributes.active[attributeName]; - } - if ( - row.attributes && - row.attributes.rollback && - row.attributes.rollback[attributeName] - ) { - return row.attributes.rollback[attributeName]; - } - - return "No data available for this attribute."; -}; diff --git a/src/Slices/ServiceInventory/UI/Tabs/index.ts b/src/Slices/ServiceInventory/UI/Tabs/index.ts index 5de2df695..22f62967e 100644 --- a/src/Slices/ServiceInventory/UI/Tabs/index.ts +++ b/src/Slices/ServiceInventory/UI/Tabs/index.ts @@ -1 +1 @@ -export * from "./Tabs"; +export * from "../Tabs"; diff --git a/src/Slices/Settings/UI/Tabs/Configuration/Components/BooleanInput.tsx b/src/Slices/Settings/UI/Tabs/Configuration/Components/BooleanInput.tsx index bc734d1fb..3471915b5 100644 --- a/src/Slices/Settings/UI/Tabs/Configuration/Components/BooleanInput.tsx +++ b/src/Slices/Settings/UI/Tabs/Configuration/Components/BooleanInput.tsx @@ -1,6 +1,5 @@ import React from "react"; -import { Switch } from "@patternfly/react-core"; -import styled from "styled-components"; +import { Flex, FlexItem, Switch } from "@patternfly/react-core"; import { EnvironmentSettings } from "@/Core"; import { Warning } from "./Warning"; @@ -9,22 +8,17 @@ interface Props { } export const BooleanInput: React.FC = ({ info }) => ( - - info.set(value)} - aria-label={`Toggle-${info.name}`} - /> - {info.isUpdateable(info) && } - -); - -const StyledWarning = styled(Warning)` - margin-left: 16px; -`; + + + info.set(value)} + aria-label={`Toggle-${info.name}`} + /> + -const Container = styled.div` - display: inline-flex; - align-items: center; - vertical-align: bottom; -`; + + {info.isUpdateable(info) && } + + +); diff --git a/src/Slices/Settings/UI/Tabs/Configuration/Components/DictInput.tsx b/src/Slices/Settings/UI/Tabs/Configuration/Components/DictInput.tsx index 7e31879e8..0de8532e0 100644 --- a/src/Slices/Settings/UI/Tabs/Configuration/Components/DictInput.tsx +++ b/src/Slices/Settings/UI/Tabs/Configuration/Components/DictInput.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import styled from "styled-components"; +import { Flex, FlexItem } from "@patternfly/react-core"; import { EnvironmentSettings, Maybe } from "@/Core"; import { DictEditor, Entry, Dict } from "@/UI/Components"; import { Row } from "./Row"; @@ -32,34 +32,30 @@ export const DictInputWithRow: React.FC = ({ info }) => { }, isUpdateable: () => info.isUpdateable(info) || newEntry[0].length > 0, }; - const isDeleteEntryAllowed = (value: Dict, key: string) => + const isDeleteEntryAllowed = (_value: Dict, key: string) => !Object.keys(info.default).includes(key); return ( - - - {customInfo.isUpdateable() && } - + + + + + + + {customInfo.isUpdateable() && } + + ); }; -const StyledWarning = styled(Warning)` - height: 36px; -`; - -const Container = styled.div<{ hasWarning: boolean }>` - display: flex; - margin-right: ${(p) => (p.hasWarning ? "0" : "16px")}; -`; - const getSanitizedNewEntry = ([key, value]: Entry) => { if (key.length <= 0) return {}; diff --git a/src/Slices/Settings/UI/Tabs/Configuration/Components/EnumInput.tsx b/src/Slices/Settings/UI/Tabs/Configuration/Components/EnumInput.tsx index 2fae71df9..3ab398d96 100644 --- a/src/Slices/Settings/UI/Tabs/Configuration/Components/EnumInput.tsx +++ b/src/Slices/Settings/UI/Tabs/Configuration/Components/EnumInput.tsx @@ -1,6 +1,5 @@ import React from "react"; -import { SelectOptionProps } from "@patternfly/react-core"; -import styled from "styled-components"; +import { Flex, FlexItem, SelectOptionProps } from "@patternfly/react-core"; import { EnvironmentSettings } from "@/Core"; import { SingleTextSelect } from "@/UI/Components"; import { Warning } from "./Warning"; @@ -16,23 +15,19 @@ export const EnumInput: React.FC = ({ info }) => { }); return ( - <> - - {info.isUpdateable(info) && } - + + + + + + + {info.isUpdateable(info) && } + + ); }; - -const StyledWarning = styled(Warning)` - height: 36px; - margin-left: 16px; -`; - -const StyledSingleTextSelect = styled(SingleTextSelect)` - width: 300px; -`; diff --git a/src/Slices/Settings/UI/Tabs/Configuration/Components/InputActions.tsx b/src/Slices/Settings/UI/Tabs/Configuration/Components/InputActions.tsx index 45354a6da..a3220309a 100644 --- a/src/Slices/Settings/UI/Tabs/Configuration/Components/InputActions.tsx +++ b/src/Slices/Settings/UI/Tabs/Configuration/Components/InputActions.tsx @@ -40,10 +40,11 @@ const InputResetAction: React.FC<{ info: Info<"reset">; }> = ({ info }) => ( ); diff --git a/src/Slices/Settings/UI/Tabs/Configuration/Components/IntInput.tsx b/src/Slices/Settings/UI/Tabs/Configuration/Components/IntInput.tsx index f41b0381d..59987a8ed 100644 --- a/src/Slices/Settings/UI/Tabs/Configuration/Components/IntInput.tsx +++ b/src/Slices/Settings/UI/Tabs/Configuration/Components/IntInput.tsx @@ -1,6 +1,5 @@ import React from "react"; -import { NumberInput } from "@patternfly/react-core"; -import styled from "styled-components"; +import { Flex, FlexItem, NumberInput } from "@patternfly/react-core"; import { EnvironmentSettings } from "@/Core"; import { Warning } from "./Warning"; @@ -16,24 +15,24 @@ export const IntInput: React.FC = ({ info }) => { const onPlus = () => info.set(Number(info.value) + 1); return ( - <> - - {info.isUpdateable(info) && } - + + + + + + + {info.isUpdateable(info) && } + + ); }; - -const StyledWarning = styled(Warning)` - height: 36px; - margin-left: 16px; -`; diff --git a/src/Slices/Settings/UI/Tabs/Configuration/Components/PositiveFloatInput.tsx b/src/Slices/Settings/UI/Tabs/Configuration/Components/PositiveFloatInput.tsx index 5908e22d4..77965d38b 100644 --- a/src/Slices/Settings/UI/Tabs/Configuration/Components/PositiveFloatInput.tsx +++ b/src/Slices/Settings/UI/Tabs/Configuration/Components/PositiveFloatInput.tsx @@ -1,6 +1,5 @@ import React from "react"; -import { NumberInput } from "@patternfly/react-core"; -import styled from "styled-components"; +import { Flex, FlexItem, NumberInput } from "@patternfly/react-core"; import { EnvironmentSettings } from "@/Core"; import { Warning } from "./Warning"; @@ -21,25 +20,25 @@ export const PositiveFloatInput: React.FC = ({ info }) => { const onPlus = () => info.set(Number(info.value) + 1); return ( - <> - - {info.isUpdateable(info) && } - + + + + + + + {info.isUpdateable(info) && } + + ); }; - -const StyledWarning = styled(Warning)` - height: 36px; - margin-left: 16px; -`; diff --git a/src/Slices/Settings/UI/Tabs/Configuration/Components/Row.tsx b/src/Slices/Settings/UI/Tabs/Configuration/Components/Row.tsx index 040c0f2da..cfd44187f 100644 --- a/src/Slices/Settings/UI/Tabs/Configuration/Components/Row.tsx +++ b/src/Slices/Settings/UI/Tabs/Configuration/Components/Row.tsx @@ -2,7 +2,6 @@ import React from "react"; import { Tooltip } from "@patternfly/react-core"; import { OutlinedQuestionCircleIcon } from "@patternfly/react-icons"; import { Td, Tr } from "@patternfly/react-table"; -import styled from "styled-components"; import { EnvironmentSettings } from "@/Core"; import { InputActions } from "./InputActions"; @@ -17,11 +16,11 @@ export const Row: React.FC> = ({ {info.name}{" "} - + - + - {children} + {children} @@ -37,7 +36,3 @@ const getDescription = ( return `${info.doc}\ndefault: ${info.default}`; }; - -const StyledTooltip = styled(Tooltip)` - white-space: pre-line; -`; diff --git a/src/Slices/Settings/UI/Tabs/Configuration/Components/StringInput.tsx b/src/Slices/Settings/UI/Tabs/Configuration/Components/StringInput.tsx index 4a1a6f255..9f204ed72 100644 --- a/src/Slices/Settings/UI/Tabs/Configuration/Components/StringInput.tsx +++ b/src/Slices/Settings/UI/Tabs/Configuration/Components/StringInput.tsx @@ -1,6 +1,5 @@ import React from "react"; -import { TextInput } from "@patternfly/react-core"; -import styled from "styled-components"; +import { Flex, FlexItem, TextInput } from "@patternfly/react-core"; import { EnvironmentSettings } from "@/Core"; import { Warning } from "./Warning"; @@ -10,23 +9,19 @@ interface Props { export const StringInput: React.FC = ({ info }) => { return ( - - info.set(value)} - aria-label="string input" - type="text" - /> - {info.isUpdateable(info) && } - + + + info.set(value)} + aria-label="string input" + type="text" + /> + + + + {info.isUpdateable(info) && } + + ); }; - -const StyledWarning = styled(Warning)` - height: 36px; - margin-left: 16px; -`; -const Container = styled.div<{ hasWarning: boolean }>` - display: flex; - margin-right: ${(p) => (p.hasWarning ? "0" : "16px")}; -`; diff --git a/src/Slices/Settings/UI/Tabs/Configuration/Components/Warning.tsx b/src/Slices/Settings/UI/Tabs/Configuration/Components/Warning.tsx index e513132ec..f6b1f7a42 100644 --- a/src/Slices/Settings/UI/Tabs/Configuration/Components/Warning.tsx +++ b/src/Slices/Settings/UI/Tabs/Configuration/Components/Warning.tsx @@ -1,22 +1,12 @@ import React from "react"; import { Icon, Tooltip } from "@patternfly/react-core"; import { ExclamationTriangleIcon } from "@patternfly/react-icons"; -import { global_warning_color_100 } from "@patternfly/react-tokens"; -import styled from "styled-components"; +import { words } from "@/UI"; -export const Warning: React.FC<{ className?: string }> = ({ className }) => ( - - - - - - - +export const Warning: React.FC = () => ( + + + + + ); - -const IconWrapper = styled.span` - font-size: 16px; - display: inline-flex; - align-items: center; - vertical-align: bottom; -`; diff --git a/src/Slices/Settings/UI/Tabs/Configuration/Container.tsx b/src/Slices/Settings/UI/Tabs/Configuration/Container.tsx index 8075e3840..ef534caf4 100644 --- a/src/Slices/Settings/UI/Tabs/Configuration/Container.tsx +++ b/src/Slices/Settings/UI/Tabs/Configuration/Container.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from "react"; -import { Alert, AlertActionCloseButton } from "@patternfly/react-core"; +import { Alert, AlertActionCloseButton, Stack } from "@patternfly/react-core"; import { Tbody, Table } from "@patternfly/react-table"; -import styled from "styled-components"; import { EnvironmentSettings } from "@/Core"; import { words } from "@/UI"; import { InputRow } from "./Components"; @@ -37,9 +36,9 @@ export const Container: React.FC = ({ }, [setShowUpdateBanner]); return ( - + {errorMessage && ( - = ({ /> )} {showUpdateBanner && ( - = ({ isInline /> )} - + {infos.map((info) => ( ))} - - +
+
); }; - -const StyledAlert = styled(Alert)` - margin-bottom: 1rem; -`; - -const Wrapper = styled.div` - padding-top: 1rem; - overflow-x: auto; -`; - -const StyledTable = styled(Table)` - width: auto; -`; diff --git a/src/Slices/Settings/UI/Tabs/Configuration/Tab.test.tsx b/src/Slices/Settings/UI/Tabs/Configuration/Tab.test.tsx index f14290f93..230e1e2ba 100644 --- a/src/Slices/Settings/UI/Tabs/Configuration/Tab.test.tsx +++ b/src/Slices/Settings/UI/Tabs/Configuration/Tab.test.tsx @@ -121,13 +121,17 @@ test("GIVEN ConfigurationTab WHEN editing a dict field THEN shows warning icon", const newKeyInput = within(newEntryRow).getByRole("textbox", { name: "editEntryKey", }); + const newValueInput = within(newEntryRow).getByRole("textbox", { + name: /editentryvalue/i, + }); expect( within(row).queryByRole("generic", { name: "Warning" }), ).not.toBeInTheDocument(); - await act(async () => { - await userEvent.type(newKeyInput, "testKey"); - }); + + await userEvent.type(newKeyInput, "testKey"); + await userEvent.type(newValueInput, "testValue"); + expect(within(row).getByTestId("Warning")).toBeInTheDocument(); await act(async () => { @@ -150,23 +154,22 @@ test("GIVEN ConfigurationTab WHEN editing an enum field THEN shows warning icon" name: "Row-agent_trigger_method_on_auto_deploy", }); - await act(async () => { - await userEvent.click( - within(row).getByRole("combobox", { - name: "EnumInput-agent_trigger_method_on_auto_deployFilterInput", - }), - ); - }); + await userEvent.click( + within(row).getByRole("combobox", { + name: "EnumInput-agent_trigger_method_on_auto_deployFilterInput", + }), + ); expect( within(row).queryByRole("generic", { name: "Warning" }), ).not.toBeInTheDocument(); - await act(async () => { - await userEvent.click( - within(row).getByRole("option", { name: "push_full_deploy" }), - ); - }); + await userEvent.click( + screen.getByRole("option", { + name: /push_full_deploy/i, + }), + ); + expect(within(row).getByTestId("Warning")).toBeInTheDocument(); await act(async () => { @@ -193,13 +196,11 @@ test("GIVEN ConfigurationTab WHEN editing a boolean field THEN shows warning ico within(row).queryByRole("generic", { name: "Warning" }), ).not.toBeInTheDocument(); - await act(async () => { - await userEvent.click( - within(row).getByRole("checkbox", { - name: "Toggle-auto_deploy", - }), - ); - }); + await userEvent.click( + within(row).getByRole("switch", { + name: "Toggle-auto_deploy", + }), + ); expect(within(row).getByTestId("Warning")).toBeInTheDocument(); @@ -227,9 +228,7 @@ test("GIVEN ConfigurationTab WHEN editing a number field THEN shows warning icon within(row).queryByRole("generic", { name: "Warning" }), ).not.toBeInTheDocument(); - await act(async () => { - await userEvent.click(within(row).getByRole("button", { name: "plus" })); - }); + await userEvent.click(within(row).getByRole("button", { name: "plus" })); expect(within(row).getByTestId("Warning")).toBeInTheDocument(); @@ -257,9 +256,7 @@ test("GIVEN ConfigurationTab WHEN editing a positiveFloat field THEN shows warni within(row).queryByRole("generic", { name: "Warning" }), ).not.toBeInTheDocument(); - await act(async () => { - await userEvent.click(within(row).getByRole("button", { name: "plus" })); - }); + await userEvent.click(within(row).getByRole("button", { name: "plus" })); expect(within(row).getByTestId("Warning")).toBeInTheDocument(); @@ -290,9 +287,7 @@ test("GIVEN ConfigurationTab WHEN editing a string field THEN shows warning icon within(row).queryByRole("generic", { name: "Warning" }), ).not.toBeInTheDocument(); - await act(async () => { - await userEvent.type(textbox, "testString"); - }); + await userEvent.type(textbox, "testString"); expect(within(row).getByTestId("Warning")).toBeInTheDocument(); @@ -341,26 +336,23 @@ test("GIVEN ConfigurationTab and boolean input WHEN changing boolean value and s name: "Row-auto_deploy", }); - const toggle = within(row).getByRole("checkbox", { + const toggle = within(row).getByRole("switch", { name: "Toggle-auto_deploy", }); expect(toggle).not.toBeChecked(); - await act(async () => { - await userEvent.click(toggle); - }); + + await userEvent.click(toggle); expect(toggle).toBeChecked(); expect(apiHelper.resolvedRequests).toHaveLength(1); - await act(async () => { - await userEvent.click( - within(row).getByRole("button", { name: "SaveAction" }), - { - skipHover: true, - }, - ); - }); + await userEvent.click( + within(row).getByRole("button", { name: "SaveAction" }), + { + skipHover: true, + }, + ); expect(apiHelper.pendingRequests).toHaveLength(1); expect(apiHelper.pendingRequests[0]).toEqual({ @@ -415,20 +407,18 @@ test("GIVEN ConfigurationTab and boolean input WHEN clicking reset THEN delete i name: "Row-auto_deploy", }); - const toggle = within(row).getByRole("checkbox", { + const toggle = within(row).getByRole("switch", { name: "Toggle-auto_deploy", }); expect(toggle).not.toBeChecked(); - await act(async () => { - await userEvent.click( - within(row).getByRole("button", { name: "ResetAction" }), - { - skipHover: true, - }, - ); - }); + await userEvent.click( + within(row).getByRole("button", { name: "ResetAction" }), + { + skipHover: true, + }, + ); expect(apiHelper.pendingRequests).toHaveLength(1); expect(apiHelper.pendingRequests[0]).toEqual({ @@ -481,25 +471,20 @@ test("GIVEN ConfigurationTab and dict input WHEN adding an entry and saving THEN name: "editEntryKey", }); - await act(async () => { - await userEvent.type(newKeyInput, "testKey"); - }); + await userEvent.type(newKeyInput, "testKey"); + const newValueInput = within(newEntryRow).getByRole("textbox", { name: "editEntryValue", }); - await act(async () => { - await userEvent.type(newValueInput, "testValue"); - }); + await userEvent.type(newValueInput, "testValue"); - await act(async () => { - await userEvent.click( - within(row).getByRole("button", { name: "SaveAction" }), - { - skipHover: true, - }, - ); - }); + await userEvent.click( + within(row).getByRole("button", { name: "SaveAction" }), + { + skipHover: true, + }, + ); expect(apiHelper.pendingRequests).toHaveLength(1); expect(apiHelper.pendingRequests[0]).toEqual({ diff --git a/src/Slices/Settings/UI/Tabs/Environment/Components/Actions.test.tsx b/src/Slices/Settings/UI/Tabs/Environment/Components/Actions.test.tsx index b59edc0f5..8fafe09e4 100644 --- a/src/Slices/Settings/UI/Tabs/Environment/Components/Actions.test.tsx +++ b/src/Slices/Settings/UI/Tabs/Environment/Components/Actions.test.tsx @@ -21,6 +21,7 @@ import { StaticScheduler, } from "@/Test"; import { DependencyProvider } from "@/UI"; +import { ModalProvider } from "@/UI/Root/Components/ModalProvider"; import { Actions } from "./Actions"; function setup() { @@ -35,8 +36,6 @@ function setup() { new CommandManagerResolverImpl(store, apiHelper, defaultAuthContext), ); - const onClose = jest.fn(); - dependencies.environmentModifier.setEnvironment("env"); const component = ( @@ -45,13 +44,15 @@ function setup() { - + + + ); - return { component, apiHelper, onClose, store }; + return { component, apiHelper, store }; } test("GIVEN Environment Actions and delete modal WHEN empty or wrong env THEN delete disabled", async () => { @@ -59,11 +60,15 @@ test("GIVEN Environment Actions and delete modal WHEN empty or wrong env THEN de render(component); - await act(async () => { - await userEvent.click( - await screen.findByRole("button", { name: "Delete environment" }), - ); - }); + await userEvent.click( + await screen.findByRole("button", { name: "Delete environment" }), + ); + + expect( + screen.getByRole("textbox", { + name: "delete environment check", + }), + ).toHaveFocus(); const input = screen.getByRole("textbox", { name: "delete environment check", @@ -73,9 +78,7 @@ test("GIVEN Environment Actions and delete modal WHEN empty or wrong env THEN de expect(input.value).toHaveLength(0); expect(deleteButton).toBeDisabled(); - await act(async () => { - await userEvent.type(input, "wrong"); - }); + await userEvent.type(input, "wrong"); expect(input.value).toMatch("wrong"); expect(deleteButton).toBeDisabled(); @@ -86,11 +89,9 @@ test("GIVEN Environment Actions and delete modal WHEN correct env THEN delete en render(component); - await act(async () => { - await userEvent.click( - await screen.findByRole("button", { name: "Delete environment" }), - ); - }); + await userEvent.click( + await screen.findByRole("button", { name: "Delete environment" }), + ); const input = screen.getByRole("textbox", { name: "delete environment check", @@ -100,9 +101,8 @@ test("GIVEN Environment Actions and delete modal WHEN correct env THEN delete en expect(input.value).toHaveLength(0); expect(deleteButton).toBeDisabled(); - await act(async () => { - await userEvent.type(input, "connect"); - }); + await userEvent.type(input, "connect"); + expect(input.value).toMatch("connect"); expect(deleteButton).toBeEnabled(); }); @@ -112,11 +112,9 @@ test("GIVEN Environment Actions and delete modal WHEN correct env & delete butto render(component); - await act(async () => { - await userEvent.click( - await screen.findByRole("button", { name: "Delete environment" }), - ); - }); + await userEvent.click( + await screen.findByRole("button", { name: "Delete environment" }), + ); const input = screen.getByRole("textbox", { name: "delete environment check", @@ -124,12 +122,9 @@ test("GIVEN Environment Actions and delete modal WHEN correct env & delete butto const deleteButton = screen.getByRole("button", { name: "delete" }); - await act(async () => { - await userEvent.type(input, "connect"); - }); - await act(async () => { - await userEvent.click(deleteButton); - }); + await userEvent.type(input, "connect"); + + await userEvent.click(deleteButton); expect(apiHelper.pendingRequests).toHaveLength(1); const request = apiHelper.pendingRequests[0]; @@ -160,23 +155,18 @@ test("GIVEN Environment Actions and delete modal WHEN delete executed and error render(component); - await act(async () => { - await userEvent.click( - await screen.findByRole("button", { name: "Delete environment" }), - ); - }); + await userEvent.click( + await screen.findByRole("button", { name: "Delete environment" }), + ); const input = screen.getByRole("textbox", { name: "delete environment check", }); - await act(async () => { - await userEvent.type(input, "connect"); - }); + await userEvent.type(input, "connect"); + + await userEvent.click(screen.getByRole("button", { name: "delete" })); - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "delete" })); - }); await act(async () => { await apiHelper.resolve(Maybe.some("error message")); }); @@ -191,14 +181,11 @@ test("GIVEN Environment Actions and delete modal WHEN form is valid and enter is render(component); - await act(async () => { - await userEvent.click( - await screen.findByRole("button", { name: "Delete environment" }), - ); - }); - await act(async () => { - await userEvent.keyboard("connect{enter}"); - }); + await userEvent.click( + await screen.findByRole("button", { name: "Delete environment" }), + ); + + await userEvent.keyboard("connect{enter}"); expect(apiHelper.pendingRequests).toHaveLength(1); expect(apiHelper.pendingRequests[0]).toEqual({ @@ -213,14 +200,11 @@ test("GIVEN Environment Actions and clear modal WHEN form is valid and enter is render(component); - await act(async () => { - await userEvent.click( - await screen.findByRole("button", { name: "Clear environment" }), - ); - }); - await act(async () => { - await userEvent.keyboard("connect{enter}"); - }); + await userEvent.click( + await screen.findByRole("button", { name: "Clear environment" }), + ); + + await userEvent.keyboard("connect{enter}"); expect(apiHelper.pendingRequests).toHaveLength(1); expect(apiHelper.pendingRequests[0]).toEqual({ @@ -235,23 +219,18 @@ test("GIVEN Environment Actions and clear modal WHEN correct env & clear button render(component); - await act(async () => { - await userEvent.click( - await screen.findByRole("button", { name: "Clear environment" }), - ); - }); + await userEvent.click( + await screen.findByRole("button", { name: "Clear environment" }), + ); const input = screen.getByRole("textbox", { name: "clear environment check", }); const clearButton = screen.getByRole("button", { name: "clear" }); - await act(async () => { - await userEvent.type(input, "connect"); - }); - await act(async () => { - await userEvent.click(clearButton); - }); + await userEvent.type(input, "connect"); + + await userEvent.click(clearButton); expect(apiHelper.pendingRequests).toHaveLength(1); const request = apiHelper.pendingRequests[0]; diff --git a/src/Slices/Settings/UI/Tabs/Environment/Components/Actions.tsx b/src/Slices/Settings/UI/Tabs/Environment/Components/Actions.tsx index affbe76ef..61b58ff88 100644 --- a/src/Slices/Settings/UI/Tabs/Environment/Components/Actions.tsx +++ b/src/Slices/Settings/UI/Tabs/Environment/Components/Actions.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from "react"; +import React, { useContext } from "react"; import { Button, DescriptionListDescription, @@ -9,31 +9,47 @@ import { TrashAltIcon } from "@patternfly/react-icons"; import { FlatEnvironment } from "@/Core"; import { ActionDisabledTooltip, TextWithCopy } from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; -import { useNavigateTo } from "@/UI/Routing"; +import { ModalContext } from "@/UI/Root/Components/ModalProvider"; import { words } from "@/UI/words"; -import { ConfirmationModal } from "./ConfirmationModal"; +import { ConfirmationForm } from "./ConfirmationForm"; -interface ActionsProps { +export type EnvActions = "delete" | "clear"; + +interface Props { environment: Pick; } -export const Actions: React.FC = ({ environment }) => { - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isClearModalOpen, setIsClearModalOpen] = useState(false); - const { commandResolver, environmentModifier } = - useContext(DependencyContext); - const navigateTo = useNavigateTo(); - const redirectToHome = () => navigateTo("Home", undefined); - const deleteTrigger = commandResolver.useGetTrigger<"DeleteEnvironment">({ - kind: "DeleteEnvironment", - id: environment.id, - }); - const clearTrigger = commandResolver.useGetTrigger<"ClearEnvironment">({ - kind: "ClearEnvironment", - id: environment.id, - }); +/** + * Actions component. + * @props {Props} props - The component props. + * @prop {FlatEnvironment} environment - An object that represents the environment. It is a subset of the `FlatEnvironment` type, including only the `id` and `name` properties. + * + * @returns {React.FC} - The rendered actions component. + */ +export const Actions: React.FC = ({ environment }) => { + const { triggerModal } = useContext(ModalContext); + const { environmentModifier } = useContext(DependencyContext); const isProtected = environmentModifier.useIsProtectedEnvironment(); + /** + * Opens a modal with a confirmation form. + * @param {string} type - The type of operation. It can be either "delete" or "clear". + * + * @returns {void} + */ + const openModal = (type: EnvActions): void => { + triggerModal({ + title: words("home.environment.delete.warning"), + description: ( +

+ {words(`home.environment.${type}.confirmation`)(environment.name)} +

+ ), + iconVariant: "danger", + content: , + }); + }; + return ( <> @@ -57,7 +73,7 @@ export const Actions: React.FC = ({ environment }) => { + + + + , - , - ]} - > -
- {errorMessage && ( - - - {errorMessage} - - - )} - - {words("home.environment.promtInput")(environment)} - - } - type="text" - fieldId="environmentName" - > - setCandidateEnv(val)} - autoFocus - /> - -
- - ); -}; - -const CustomLabel = styled.p` - font-weight: normal; -`; diff --git a/src/Slices/Settings/UI/Tabs/Environment/Components/ConfirmationModal/index.ts b/src/Slices/Settings/UI/Tabs/Environment/Components/ConfirmationModal/index.ts deleted file mode 100644 index d610d3122..000000000 --- a/src/Slices/Settings/UI/Tabs/Environment/Components/ConfirmationModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ConfirmationModal"; diff --git a/src/Slices/Settings/UI/Tabs/Environment/EnvironmentSettings.test.tsx b/src/Slices/Settings/UI/Tabs/Environment/EnvironmentSettings.test.tsx index a4d61b368..68316ce60 100644 --- a/src/Slices/Settings/UI/Tabs/Environment/EnvironmentSettings.test.tsx +++ b/src/Slices/Settings/UI/Tabs/Environment/EnvironmentSettings.test.tsx @@ -69,11 +69,9 @@ test("Given environment settings When clicking on the edit name button Then the await screen.findByRole("generic", { name: "Name-value" }), ).toBeVisible(); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Name-toggle-edit" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Name-toggle-edit" }), + ); expect( await screen.findByRole("textbox", { name: "Name-input" }), @@ -94,20 +92,15 @@ test("Given environment settings When submitting the edited name Then the backen render(component); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Name-toggle-edit" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Name-toggle-edit" }), + ); const textBox = await screen.findByRole("textbox", { name: "Name-input" }); - await act(async () => { - await userEvent.clear(textBox); - }); - await act(async () => { - await userEvent.type(textBox, `dev{enter}`); - }); + await userEvent.clear(textBox); + + await userEvent.type(textBox, `dev{enter}`); expect(apiHelper.pendingRequests).toHaveLength(1); @@ -152,25 +145,19 @@ test("Given environment settings When canceling a name edit Then the backend req render(component); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Name-toggle-edit" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Name-toggle-edit" }), + ); const textBox = await screen.findByRole("textbox", { name: "Name-input" }); - await act(async () => { - await userEvent.clear(textBox); - }); - await act(async () => { - await userEvent.type(textBox, "dev"); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Name-cancel-edit" }), - ); - }); + await userEvent.clear(textBox); + + await userEvent.type(textBox, "dev"); + + await userEvent.click( + screen.getByRole("button", { name: "Name-cancel-edit" }), + ); expect(apiHelper.pendingRequests).toHaveLength(0); // The field is shown with the original value @@ -199,20 +186,15 @@ test.each` render(component); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Name-toggle-edit" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Name-toggle-edit" }), + ); const textBox = await screen.findByRole("textbox", { name: "Name-input" }); - await act(async () => { - await userEvent.clear(textBox); - }); - await act(async () => { - await userEvent.type(textBox, `dev{enter}`); - }); + await userEvent.clear(textBox); + + await userEvent.type(textBox, `dev{enter}`); expect(apiHelper.pendingRequests).toHaveLength(1); @@ -237,9 +219,7 @@ test.each` ).not.toBeInTheDocument(); // Closing the alert - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: elementName })); - }); + await userEvent.click(screen.getByRole("button", { name: elementName })); expect( screen.queryByRole("generic", { name: "Name-error-message" }), @@ -265,11 +245,9 @@ test("Given environment settings When clicking on the edit repository settings b await screen.findByRole("generic", { name: "repo_url-value" }), ).toBeVisible(); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Repository Settings-toggle-edit" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Repository Settings-toggle-edit" }), + ); expect( await screen.findByRole("textbox", { name: "repo_branch-input" }), @@ -298,38 +276,29 @@ test("Given environment settings When submitting the edited repository settings const newRepository = "github.com/test-env"; const newBranch = "dev"; - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Repository Settings-toggle-edit" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Repository Settings-toggle-edit" }), + ); const branchTextBox = await screen.findByRole("textbox", { name: "repo_branch-input", }); - await act(async () => { - await userEvent.clear(branchTextBox); - }); - await act(async () => { - await userEvent.type(branchTextBox, newBranch); - }); + await userEvent.clear(branchTextBox); + + await userEvent.type(branchTextBox, newBranch); const urlTextBox = await screen.findByRole("textbox", { name: "repo_url-input", }); - await act(async () => { - await userEvent.clear(urlTextBox); - }); - await act(async () => { - await userEvent.type(urlTextBox, newRepository); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Repository Settings-submit-edit" }), - ); - }); + await userEvent.clear(urlTextBox); + + await userEvent.type(urlTextBox, newRepository); + + await userEvent.click( + screen.getByRole("button", { name: "Repository Settings-submit-edit" }), + ); expect(apiHelper.pendingRequests).toHaveLength(1); @@ -385,27 +354,21 @@ test("Given environment settings When canceling a repository edit Then the backe render(component); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Repository Settings-toggle-edit" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Repository Settings-toggle-edit" }), + ); const textBox = await screen.findByRole("textbox", { name: "repo_branch-input", }); - await act(async () => { - await userEvent.clear(textBox); - }); - await act(async () => { - await userEvent.type(textBox, "dev"); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Repository Settings-cancel-edit" }), - ); - }); + await userEvent.clear(textBox); + + await userEvent.type(textBox, "dev"); + + await userEvent.click( + screen.getByRole("button", { name: "Repository Settings-cancel-edit" }), + ); expect(apiHelper.pendingRequests).toHaveLength(0); @@ -441,22 +404,17 @@ test.each` render(component); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Repository Settings-toggle-edit" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Repository Settings-toggle-edit" }), + ); const textBox = await screen.findByRole("textbox", { name: "repo_branch-input", }); - await act(async () => { - await userEvent.clear(textBox); - }); - await act(async () => { - await userEvent.type(textBox, `dev{enter}`); - }); + await userEvent.clear(textBox); + + await userEvent.type(textBox, `dev{enter}`); expect(apiHelper.pendingRequests).toHaveLength(1); @@ -483,9 +441,7 @@ test.each` ).not.toBeInTheDocument(); // Closing the alert - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: elementName })); - }); + await userEvent.click(screen.getByRole("button", { name: elementName })); expect( screen.queryByRole("generic", { @@ -510,11 +466,9 @@ test("Given environment settings When clicking on the edit project button Then t await screen.findByRole("generic", { name: "Project Name-value" }), ).toBeVisible(); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Project Name-toggle-edit" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Project Name-toggle-edit" }), + ); expect( await screen.findByRole("combobox", { @@ -538,29 +492,21 @@ test("Given environment settings When submitting the edited project name Then th render(component); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Project Name-toggle-edit" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Project Name-toggle-edit" }), + ); const toggle = await screen.findByRole("combobox", { name: "Project Name-select-toggleFilterInput", }); - await act(async () => { - await userEvent.click(toggle); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("option", { name: "project_name_b" }), - ); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Project Name-submit-edit" }), - ); - }); + await userEvent.click(toggle); + + await userEvent.click(screen.getByRole("option", { name: "project_name_b" })); + + await userEvent.click( + screen.getByRole("button", { name: "Project Name-submit-edit" }), + ); expect(apiHelper.pendingRequests).toHaveLength(1); @@ -610,29 +556,21 @@ test("Given environment settings When canceling a project name edit Then the bac render(component); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Project Name-toggle-edit" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Project Name-toggle-edit" }), + ); const toggle = await screen.findByRole("combobox", { name: "Project Name-select-toggleFilterInput", }); - await act(async () => { - await userEvent.click(toggle); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("option", { name: "project_name_b" }), - ); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Project Name-cancel-edit" }), - ); - }); + await userEvent.click(toggle); + + await userEvent.click(screen.getByRole("option", { name: "project_name_b" })); + + await userEvent.click( + screen.getByRole("button", { name: "Project Name-cancel-edit" }), + ); expect(apiHelper.pendingRequests).toHaveLength(0); expect( @@ -660,29 +598,23 @@ test.each` render(component); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Project Name-toggle-edit" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Project Name-toggle-edit" }), + ); const toggle = await screen.findByRole("combobox", { name: "Project Name-select-toggleFilterInput", }); - await act(async () => { - await userEvent.click(toggle); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("option", { name: "project_name_b" }), - ); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Project Name-submit-edit" }), - ); - }); + await userEvent.click(toggle); + + await userEvent.click( + screen.getByRole("option", { name: "project_name_b" }), + ); + + await userEvent.click( + screen.getByRole("button", { name: "Project Name-submit-edit" }), + ); expect(apiHelper.pendingRequests).toHaveLength(1); @@ -709,9 +641,7 @@ test.each` ).not.toBeInTheDocument(); // Closing the alert - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: elementName })); - }); + await userEvent.click(screen.getByRole("button", { name: elementName })); expect( screen.queryByRole("generic", { name: "Project Name-error-message" }), @@ -733,11 +663,9 @@ test("Given environment settings When clicking on the edit description button Th await screen.findByRole("generic", { name: "Description-value" }), ).toBeVisible(); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Description-toggle-edit" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Description-toggle-edit" }), + ); expect( await screen.findByRole("textbox", { name: "Description-input" }), @@ -760,11 +688,9 @@ test("Given environment settings When clicking on the edit icon button Then the render(component); expect(await screen.findByRole("img", { name: "Icon-value" })).toBeVisible(); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { name: "Icon-toggle-edit" }), - ); - }); + await userEvent.click( + screen.getByRole("button", { name: "Icon-toggle-edit" }), + ); expect( await screen.findByRole("textbox", { name: "Icon-input" }), diff --git a/src/Slices/Settings/UI/Tabs/Environment/EnvironmentSettings.tsx b/src/Slices/Settings/UI/Tabs/Environment/EnvironmentSettings.tsx index 3a76d951e..5f04e5abf 100644 --- a/src/Slices/Settings/UI/Tabs/Environment/EnvironmentSettings.tsx +++ b/src/Slices/Settings/UI/Tabs/Environment/EnvironmentSettings.tsx @@ -1,6 +1,5 @@ import React, { useContext } from "react"; import { DescriptionList } from "@patternfly/react-core"; -import styled from "styled-components"; import { FlatEnvironment, Maybe, ProjectModel } from "@/Core"; import { EditableTextField, @@ -67,7 +66,7 @@ export const EnvironmentSettings: React.FC = ({ }); return ( - + = ({ onSubmit={onIconSubmit} /> - + ); }; - -const PaddedList = styled(DescriptionList)` - padding-top: 1em; - max-width: 600px; -`; diff --git a/src/Slices/Settings/UI/Tabs/Tabs.tsx b/src/Slices/Settings/UI/Tabs/Tabs.tsx index 50de804eb..07f6114af 100644 --- a/src/Slices/Settings/UI/Tabs/Tabs.tsx +++ b/src/Slices/Settings/UI/Tabs/Tabs.tsx @@ -1,5 +1,5 @@ import React, { useContext, useRef } from "react"; -import { Tooltip } from "@patternfly/react-core"; +import { TabContentBody, Tooltip } from "@patternfly/react-core"; import { CogIcon, InfoCircleIcon, KeyIcon } from "@patternfly/react-icons"; import { ErrorView, IconTabs, TabDescriptor } from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; @@ -59,7 +59,11 @@ const environmentTab = (): TabDescriptor => ({ id: TabKey.Environment, title: words("settings.tabs.environment"), icon: , - view: , + view: ( + + + + ), }); const configurationTab = (environmentId: string): TabDescriptor => ({ diff --git a/src/Slices/Settings/UI/Tabs/Token/Tab.test.tsx b/src/Slices/Settings/UI/Tabs/Token/Tab.test.tsx index 696c238d5..f2de54129 100644 --- a/src/Slices/Settings/UI/Tabs/Token/Tab.test.tsx +++ b/src/Slices/Settings/UI/Tabs/Token/Tab.test.tsx @@ -39,9 +39,7 @@ test("GIVEN TokenTab WHEN generate button is clicked THEN generate call is execu expect(generateButton).toBeEnabled(); expect(apiHelper.pendingRequests).toHaveLength(0); - await act(async () => { - await userEvent.click(generateButton); - }); + await userEvent.click(generateButton); expect(apiHelper.pendingRequests).toHaveLength(1); expect(apiHelper.pendingRequests[0]).toEqual({ @@ -57,16 +55,13 @@ test("GIVEN TokenTab WHEN api clientType is selected and generate button is clic render(component); - await act(async () => { - await userEvent.click(screen.getByRole("button", { name: "AgentOption" })); - }); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { - name: words("settings.tabs.token.generate"), - }), - ); - }); + await userEvent.click(screen.getByRole("button", { name: "AgentOption" })); + + await userEvent.click( + screen.getByRole("button", { + name: words("settings.tabs.token.generate"), + }), + ); expect(apiHelper.pendingRequests[0]).toEqual({ method: "POST", @@ -81,13 +76,11 @@ test("GIVEN TokenTab WHEN generate fails THEN the error is shown", async () => { render(component); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { - name: words("settings.tabs.token.generate"), - }), - ); - }); + await userEvent.click( + screen.getByRole("button", { + name: words("settings.tabs.token.generate"), + }), + ); await act(async () => { await apiHelper.resolve(Either.left("error message")); @@ -110,13 +103,12 @@ test("GIVEN TokenTab WHEN generate succeeds THEN the token is shown", async () = expect(copyButton).toBeDisabled(); expect(tokenOutput).toHaveValue(""); - await act(async () => { - await userEvent.click( - screen.getByRole("button", { - name: words("settings.tabs.token.generate"), - }), - ); - }); + await userEvent.click( + screen.getByRole("button", { + name: words("settings.tabs.token.generate"), + }), + ); + await act(async () => { await apiHelper.resolve(Either.right({ data: "tokenstring123" })); }); diff --git a/src/Slices/Status/UI/Components/DownloadButton.tsx b/src/Slices/Status/UI/Components/DownloadButton.tsx index d2028b949..901ce0d7f 100644 --- a/src/Slices/Status/UI/Components/DownloadButton.tsx +++ b/src/Slices/Status/UI/Components/DownloadButton.tsx @@ -1,6 +1,5 @@ import React from "react"; import { Button } from "@patternfly/react-core"; -import styled from "styled-components"; import { words } from "@/UI/words"; export type Phase = "Default" | "Downloading"; @@ -12,7 +11,7 @@ interface Props { export const DownloadButton: React.FC = ({ phase, onClick }) => { return ( - = ({ phase, onClick }) => { aria-label="DownloadArchiveButton" > {phaseLabelRecord[phase]} - + ); }; @@ -29,7 +28,3 @@ const phaseLabelRecord: Record = { Default: words("status.supportArchive.action.download"), Downloading: words("status.supportArchive.action.downloading"), }; - -const StyledButton = styled(Button)` - width: 32ch; -`; diff --git a/src/Slices/Status/UI/Page.test.tsx b/src/Slices/Status/UI/Page.test.tsx index d56e1d276..1fb83bf23 100644 --- a/src/Slices/Status/UI/Page.test.tsx +++ b/src/Slices/Status/UI/Page.test.tsx @@ -199,9 +199,7 @@ test("GIVEN StatusPage with support extension WHEN user click download THEN an a words("status.supportArchive.action.download"), ); - await act(async () => { - await userEvent.click(downloadButton); - }); + await userEvent.click(downloadButton); expect(downloadButton).toHaveTextContent( words("status.supportArchive.action.downloading"), @@ -245,9 +243,7 @@ test("GIVEN StatusPage with support extension WHEN user click download THEN butt words("status.supportArchive.action.download"), ); - await act(async () => { - await userEvent.click(downloadButton); - }); + await userEvent.click(downloadButton); expect(downloadButton).toHaveTextContent( words("status.supportArchive.action.downloading"), @@ -292,9 +288,8 @@ test("GIVEN StatusPage with support extension WHEN user click download and respo name: "DownloadArchiveButton", }); - await act(async () => { - await userEvent.click(downloadButton); - }); + await userEvent.click(downloadButton); + await act(async () => { await apiHelper.resolve(Either.left("error")); }); diff --git a/src/Slices/Status/UI/Page.tsx b/src/Slices/Status/UI/Page.tsx index 2761551fe..b6705e191 100644 --- a/src/Slices/Status/UI/Page.tsx +++ b/src/Slices/Status/UI/Page.tsx @@ -1,6 +1,5 @@ import React, { useContext } from "react"; import { Flex, FlexItem } from "@patternfly/react-core"; -import styled from "styled-components"; import { Description, PageContainer, RemoteDataView } from "@/UI/Components"; import { DependencyContext } from "@/UI/Dependency"; import { words } from "@/UI/words"; @@ -32,13 +31,9 @@ export const Page: React.FC = () => { retry={retry} label="ServerStatus" SuccessView={(status) => ( - + )} /> ); }; - -const PaddedStatusList = styled(StatusList)` - margin-top: 16px; -`; diff --git a/src/Slices/Status/UI/StatusItem.tsx b/src/Slices/Status/UI/StatusItem.tsx index b5f474e9c..2bd969777 100644 --- a/src/Slices/Status/UI/StatusItem.tsx +++ b/src/Slices/Status/UI/StatusItem.tsx @@ -6,56 +6,176 @@ import { DescriptionListGroup, DescriptionListDescription, Title, - Flex, + Label, + DataListItem, + DataListItemRow, + DataListItemCells, + DataListCell, + List, FlexItem, + Flex, } from "@patternfly/react-core"; -import styled from "styled-components"; +import { t_global_font_size_200 } from "@patternfly/react-tokens"; +import { uniqueId } from "lodash"; +import { DetailTuple } from "./StatusList"; interface Props { name: string; - details: [string, string][]; + details: DetailTuple[]; icon: React.ReactNode; category?: string; } +/** + * Renders a status item + * + * @props {Props} props - The properties for the status item component. + * @prop {string} name - The name of the status item. + * @prop {DetailTuple[]} details - The details of the status item, which can include nested objects. + * @prop {React.ReactNode} icon - The icon to display for the status item. + * @prop {string} [category] - The category of the status item. + * @returns {React.FC} The rendered status item component. + */ export const StatusItem: React.FC = ({ name, details, icon, category, }) => ( - - - - {name} - {category} - - {details.length > 0 && ( - - - {details.map(([key, value]) => ( - - {key} - {value} - - ))} - - - )} - - + + + + + + + {icon} {name} + + + {category && ( + + + + )} + + , + ]} + /> + + + + {details.length > 0 && ( + + {details.map(([key, value]) => { + if (typeof value === "object") { + return ( + + ); + } else { + return ( + + + + + {key} + + + {value} + + + + + ); + } + })} + + )} + , + ]} + /> + + ); -const Category = styled.span` - color: var(--pf-v5-global--palette--black-500); -`; - -const InlineTitle = styled(Title)` - display: inline-block; - padding-right: 8px; -`; +interface NestedListItemProps { + name: string; + properties: Record; +} -const CompactDescriptionList = styled(DescriptionList)` - --pf-v5-c-description-list--m-compact--RowGap: 0; - margin-bottom: 16px; -`; +/** + * Renders sub list for Status value that is a Record instead of string. + * + * @props {NestedListItemProps} props - The properties for the NestedListItem component. + * @prop {string} name - The name of the property. + * @prop {Record} properties - The sub properties to display in the NestedListItem components. + * @returns {React.FC} The rendered NestedListItem component. + */ +const NestedListItem: React.FC = ({ + name, + properties, +}) => { + return ( + + + + {name} + + + + {Object.entries(properties).map(([subKey, subValue]) => ( + + + + + {subKey} + + + {subValue} + + + + + ))} + + + ); +}; diff --git a/src/Slices/Status/UI/StatusList.test.tsx b/src/Slices/Status/UI/StatusList.test.tsx new file mode 100644 index 000000000..94e884ea3 --- /dev/null +++ b/src/Slices/Status/UI/StatusList.test.tsx @@ -0,0 +1,222 @@ +import React from "react"; +import { render, screen, within } from "@testing-library/react"; +import { ServerStatus } from "@/Core"; +import { dependencies } from "@/Test"; +import { DependencyProvider } from "@/UI"; +import { StatusList } from "./StatusList"; + +const status: ServerStatus = { + product: "Inmanta Service Orchestrator", + edition: "Standard", + version: "1.0.0", + license: "Inmanta EULA", + extensions: [ + { + name: "core", + version: "1.1.1", + package: "core", + }, + ], + slices: [ + { + name: "lsm.order", + status: {}, + }, + { + name: "core.transport", + status: { + inflight: 2, + running: true, + sockets: ["0.0.0.0:8888"], + }, + }, + { + name: "core.scheduler_manager", + status: { + resource_facts: 28, + sessions: 7, + database: "inmanta", + host: "localhost", + total: { + connected: true, + max_pool: 9, + open_connections: 3, + free_connections: 4, + pool_exhaustion_count: 209, + }, + prod: { + connected: true, + max_pool: 3, + open_connections: 1, + free_connections: 2, + pool_exhaustion_count: 209, + }, + }, + }, + ], + features: [], +}; + +describe("Given StatusList", () => { + it("WHEN receiving status object THEN should render correctly list", () => { + render( + + + , + ); + + expect(screen.getByRole("list", { name: "StatusList" })).toBeVisible(); + + const orchestratorItem = screen.getByRole("listitem", { + name: "StatusItem-Inmanta Service Orchestrator", + }); + + expect(orchestratorItem).toBeVisible(); + expect(screen.getByText("Inmanta Service Orchestrator")).toBeVisible(); + + expect(screen.getByText("edition")).toBeVisible(); + expect(screen.getByText("Standard")).toBeVisible(); + + expect(within(orchestratorItem).getByText("version")).toBeVisible(); + expect(screen.getByText("1.0.0")).toBeVisible(); + + expect(screen.getByText("license")).toBeVisible(); + expect(screen.getByText("Inmanta EULA")).toBeVisible(); + + const coreItem = screen.getByRole("listitem", { + name: "StatusItem-core", + }); + + expect(coreItem).toBeVisible(); + + expect( + within(coreItem).getByRole("heading", { name: "core" }), + ).toBeVisible(); + expect(within(coreItem).getByText("extension")).toBeVisible(); + + expect(within(coreItem).getByText("version")).toBeVisible(); + expect(within(coreItem).getByText("1.1.1")).toBeVisible(); + + expect(within(coreItem).getByText("package")).toBeVisible(); + expect(within(coreItem).getAllByText("core")[1]).toBeVisible(); //the first core component is heading + + const lsmOrderItem = screen.getByRole("listitem", { + name: "StatusItem-lsm.order", + }); + + expect(lsmOrderItem).toBeVisible(); + + expect(within(lsmOrderItem).getByText("lsm.order")).toBeVisible(); + expect(within(lsmOrderItem).getByText("component")).toBeVisible(); + + const coreTransportItem = screen.getByRole("listitem", { + name: "StatusItem-core.transport", + }); + + expect(coreTransportItem).toBeVisible(); + + within(coreTransportItem); + expect(within(coreTransportItem).getByText("core.transport")).toBeVisible(); + expect(within(coreTransportItem).getByText("component")).toBeVisible(); + + expect(within(coreTransportItem).getByText("inflight")).toBeVisible(); + expect(within(coreTransportItem).getByText("2")).toBeVisible(); + + expect(within(coreTransportItem).getByText("running")).toBeVisible(); + expect(within(coreTransportItem).getByText("true")).toBeVisible(); + + expect(within(coreTransportItem).getByText("sockets")).toBeVisible(); + expect(within(coreTransportItem).getByText("0.0.0.0:8888")).toBeVisible(); + + const coreSchedulerManagerItem = screen.getByRole("listitem", { + name: "StatusItem-core.scheduler_manager", + }); + + expect(coreSchedulerManagerItem).toBeVisible(); + + expect( + within(coreSchedulerManagerItem).getByText("core.scheduler_manager"), + ).toBeVisible(); + expect( + within(coreSchedulerManagerItem).getByText("component"), + ).toBeVisible(); + + expect( + within(coreSchedulerManagerItem).getByText("resource_facts"), + ).toBeVisible(); + expect(within(coreSchedulerManagerItem).getByText("28")).toBeVisible(); + + expect( + within(coreSchedulerManagerItem).getByText("sessions"), + ).toBeVisible(); + expect(within(coreSchedulerManagerItem).getByText("7")).toBeVisible(); + + expect( + within(coreSchedulerManagerItem).getByText("database"), + ).toBeVisible(); + expect(within(coreSchedulerManagerItem).getByText("inmanta")).toBeVisible(); + + expect(within(coreSchedulerManagerItem).getByText("host")).toBeVisible(); + expect( + within(coreSchedulerManagerItem).getByText("localhost"), + ).toBeVisible(); + + expect(within(coreSchedulerManagerItem).getByText("total")).toBeVisible(); + + const totalNestedListItem = screen.getByLabelText( + "StatusNestedListItem-total", + ); + + expect(totalNestedListItem).toBeVisible(); + expect(within(totalNestedListItem).getByText("total")).toBeVisible(); + + expect(within(totalNestedListItem).getByText("connected")).toBeVisible(); + expect(within(totalNestedListItem).getByText("true")).toBeVisible(); + + expect(within(totalNestedListItem).getByText("max_pool")).toBeVisible(); + expect(within(totalNestedListItem).getByText("9")).toBeVisible(); + + expect( + within(totalNestedListItem).getByText("open_connections"), + ).toBeVisible(); + expect(within(totalNestedListItem).getByText("3")).toBeVisible(); + + expect( + within(totalNestedListItem).getByText("free_connections"), + ).toBeVisible(); + expect(within(totalNestedListItem).getByText("4")).toBeVisible(); + + expect( + within(totalNestedListItem).getByText("pool_exhaustion_count"), + ).toBeVisible(); + expect(within(totalNestedListItem).getByText("209")).toBeVisible(); + + const prodNestedListItem = screen.getByLabelText( + "StatusNestedListItem-prod", + ); + + expect(prodNestedListItem).toBeVisible(); + expect(within(prodNestedListItem).getByText("prod")).toBeVisible(); + + expect(within(prodNestedListItem).getByText("connected")).toBeVisible(); + expect(within(prodNestedListItem).getByText("true")).toBeVisible(); + + expect(within(prodNestedListItem).getByText("max_pool")).toBeVisible(); + expect(within(prodNestedListItem).getByText("3")).toBeVisible(); + + expect( + within(prodNestedListItem).getByText("open_connections"), + ).toBeVisible(); + expect(within(prodNestedListItem).getByText("1")).toBeVisible(); + + expect( + within(prodNestedListItem).getByText("free_connections"), + ).toBeVisible(); + expect(within(prodNestedListItem).getByText("2")).toBeVisible(); + + expect( + within(prodNestedListItem).getByText("pool_exhaustion_count"), + ).toBeVisible(); + expect(within(prodNestedListItem).getByText("209")).toBeVisible(); + }); +}); diff --git a/src/Slices/Status/UI/StatusList.tsx b/src/Slices/Status/UI/StatusList.tsx index dfb6ff97f..f8fe17568 100644 --- a/src/Slices/Status/UI/StatusList.tsx +++ b/src/Slices/Status/UI/StatusList.tsx @@ -1,5 +1,5 @@ import React, { useContext } from "react"; -import { Icon, List } from "@patternfly/react-core"; +import { DataList, Icon } from "@patternfly/react-core"; import { ClusterIcon, DesktopIcon, @@ -18,6 +18,15 @@ interface Props { className?: string; } +/** + * Renders a list of status items, including product status, API status, web console status, extensions, and slices. + * + * @props {Props} props - The properties for the status list component. + * @prop {ServerStatus} status - The server status object + * @prop {string} apiUrl - The API URL to display in the status list. + * @prop {string} [className] - Optional additional class name for the list. + * @returns {React.FC} The rendered status list component. + */ export const StatusList: React.FC = ({ status, apiUrl, @@ -27,13 +36,11 @@ export const StatusList: React.FC = ({ const { featureManager } = useContext(DependencyContext); return ( - = ({ omit(status, ["product", "extensions", "slices", "features"]), )} icon={ - - + + } /> @@ -50,7 +61,7 @@ export const StatusList: React.FC = ({ name="API" details={[["url", apiUrl]]} icon={ - + } @@ -59,7 +70,7 @@ export const StatusList: React.FC = ({ name="Web Console" details={[["commit hash", featureManager.getCommitHash()]]} icon={ - + } @@ -70,10 +81,13 @@ export const StatusList: React.FC = ({ name={extension.name} details={toDetails(omit(extension, "name"))} icon={ - - + + } category="extension" @@ -85,16 +99,63 @@ export const StatusList: React.FC = ({ name={slice.name} details={toDetails(slice.status)} icon={ - + } category="component" /> ))} - + ); }; -const toDetails = (obj: Record): [string, string][] => - Object.entries(obj).map(([key, value]) => [key, `${value}`]); +type DetailKey = string; + +type DetailValue = Record | string; +export type DetailTuple = [DetailKey, DetailValue]; + +/** + * Converts a Record to an array of key-value pairs where values are either strings or nested records with string values. + * + * This function iterates over the entries of the provided record and converts each value to a string. If a value is a nested record, it recursively converts all nested values to strings. + * We know from the core team that we can safely assume that we don't need to handle nested records more than one level deep. + * + * @param {Record} obj - The record to convert. + * @returns {DetailTuple[]} An array of key-value pairs where values are either strings or nested records with string values. + */ +const toDetails = (obj: Record): DetailTuple[] => + Object.entries(obj).map(([key, value]) => { + return [ + key, + isRecord(value) ? stringifyRecordAttributes(value) : `${value}`, + ]; + }); + +/** + * Converts all attributes of a Record to strings. + * + * This function iterates over the entries of the provided record and converts each value to a string. + * + * @param {Record} obj - The record whose attributes are to be converted to strings. + * @returns {Record} A new record with all values converted to strings. + */ +const stringifyRecordAttributes = ( + obj: Record, +): Record => { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [key, String(value)]), + ); +}; + +/** + * Type guard to check if a value is a Record. + * + * This function checks if the provided value is an object, is not null, and is not an array. + * + * @param {unknown} value - The value to check. + * @returns {value is Record} True if the value is a Record, otherwise false. + */ +const isRecord = (value: unknown): value is Record => { + return typeof value === "object" && value !== null && !Array.isArray(value); +}; diff --git a/src/Slices/UserManagement/UI/Components/AddUserForm/UserCredentialsForm.tsx b/src/Slices/UserManagement/UI/Components/AddUserForm/UserCredentialsForm.tsx new file mode 100644 index 000000000..53cb2c2b1 --- /dev/null +++ b/src/Slices/UserManagement/UI/Components/AddUserForm/UserCredentialsForm.tsx @@ -0,0 +1,179 @@ +import React, { useContext, useEffect, useState } from "react"; +import { + FormHelperText, + HelperText, + HelperTextItem, + Form, + FormGroup, + TextInput, + InputGroup, + InputGroupItem, + Button, + ActionGroup, + ValidatedOptions, + Spinner, + Content, +} from "@patternfly/react-core"; +import { + ExclamationCircleIcon, + EyeIcon, + EyeSlashIcon, +} from "@patternfly/react-icons"; +import { useAddUser } from "@/Data/Managers/V2/POST/AddUser"; +import { words } from "@/UI"; +import { ModalContext } from "@/UI/Root/Components/ModalProvider"; + +interface UserCredentialsFormProps { + submitButtonText: string; + submitButtonLabel?: string; +} + +/** + * UserCredentialsForm component. + * @props {UserCredentialsFormProps} props - The component props. + * @prop {string} submitButtonText - The text to display on the submit button. + * @prop {string} submitButtonLabel - The aria-label for the submit button. + * + * @returns {React.FC} The rendered component. + */ +export const UserCredentialsForm: React.FC = ({ + submitButtonText, + submitButtonLabel = "login-button", +}) => { + const { mutate, isSuccess, isError, error, isPending } = useAddUser(); + const { closeModal } = useContext(ModalContext); + + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [isPasswordHidden, setIsPasswordHidden] = useState(true); + + /** + * Handle the change of the username input field. + * @param {React.FormEvent} _event - The event object. + * @param {string} value - The current value of the input field. + * + * @returns {void} + */ + const handleUsernameChange = ( + _event: React.FormEvent, + value: string, + ): void => { + setUsername(value); + }; + + /** + * Handle the change of the password input field. + * @param {React.FormEvent} _event The event object. + * @param {string} value The current value of the input field. + * + * @returns {void} + */ + const handlePasswordChange = ( + _event: React.FormEvent, + value: string, + ): void => { + setPassword(value); + }; + + /** + * Handles the submission of the form. + * + * This function is responsible for preventing the default form submission behavior and then calling the mutate function with the current username and password. + * @param {React.FormEvent | React.MouseEvent} event - The event that triggered the form submission. This can be either a form submission event or a button click event. + * + * @returns {void} + */ + const handleSubmit = ( + event: + | React.FormEvent + | React.MouseEvent, + ): void => { + event.preventDefault(); + mutate({ username, password }); + }; + + useEffect(() => { + if (isSuccess) { + closeModal(); + } + }, [isSuccess, closeModal]); + + return ( +
+ {isError && error && ( + + + } + aria-label="error-message" + > + {error.message} + + + + )} + + + + + + + + + + + + + + + + +
+ ); +}; diff --git a/src/UI/Components/UserCredentialsForm/index.tsx b/src/Slices/UserManagement/UI/Components/AddUserForm/index.tsx similarity index 100% rename from src/UI/Components/UserCredentialsForm/index.tsx rename to src/Slices/UserManagement/UI/Components/AddUserForm/index.tsx diff --git a/src/Slices/UserManagement/UI/Components/AddUserModal.tsx b/src/Slices/UserManagement/UI/Components/AddUserModal.tsx deleted file mode 100644 index 35739a19d..000000000 --- a/src/Slices/UserManagement/UI/Components/AddUserModal.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { useEffect } from "react"; -import { Modal } from "@patternfly/react-core"; -import { useAddUser } from "@/Data/Managers/V2/POST/AddUser"; -import { words } from "@/UI"; -import { UserCredentialsForm } from "@/UI/Components/UserCredentialsForm"; - -interface AddUserModalProps { - isOpen: boolean; - onClose: () => void; -} - -/** - * AddUserModal component. - */ -export const AddUserModal: React.FC = ({ - isOpen, - onClose, -}) => { - const { mutate, isSuccess, isPending, isError, error } = useAddUser(); - - useEffect(() => { - if (isSuccess) { - onClose(); - } - }, [isSuccess, onClose]); - - return ( - - mutate({ username, password })} - submitButtonText={words("userManagement.addUser")} - submitButtonLabel={"add_user-button"} - /> - - ); -}; diff --git a/src/Slices/UserManagement/UI/Components/DeleteUserModal.tsx b/src/Slices/UserManagement/UI/Components/DeleteUserModal.tsx deleted file mode 100644 index 948a069f9..000000000 --- a/src/Slices/UserManagement/UI/Components/DeleteUserModal.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from "react"; -import { Modal } from "@patternfly/react-core"; - -import { useRemoveUser } from "@/Data/Managers/V2/DELETE/RemoveUser"; -import { UserInfo } from "@/Data/Managers/V2/GETTERS/GetUsers"; -import { words } from "@/UI"; -import { ConfirmUserActionForm } from "@/UI/Components"; - -interface Props { - user: UserInfo; - isOpen: boolean; - onClose: () => void; -} - -/** - * DeleteUserModal component displays a modal for confirming deleting an user. - * - * @component - * @param {Props} props - The component props. - * @param {User} props.user - The user to be deleted. - * @param {boolean} props.isOpen - Indicates whether the modal is open or not. - * @param {() => void} props.onClose - Callback function to close the modal. - * @returns {JSX.Element} The DeleteUserModal component. - */ -export const DeleteUserModal: React.FC = ({ user, isOpen, onClose }) => { - const { mutate } = useRemoveUser(); - - return ( - -

{words("userManagement.deleteUserMessage")(user.username)}

- - mutate(user.username)} - onCancel={onClose} - /> -
- ); -}; diff --git a/src/Slices/UserManagement/UI/Components/UserInfoRow.tsx b/src/Slices/UserManagement/UI/Components/UserInfoRow.tsx index d0e01f7a2..2d5dcb1c2 100644 --- a/src/Slices/UserManagement/UI/Components/UserInfoRow.tsx +++ b/src/Slices/UserManagement/UI/Components/UserInfoRow.tsx @@ -1,41 +1,58 @@ -import React, { useState } from "react"; +import React, { useContext } from "react"; import { Button } from "@patternfly/react-core"; import { Td, Tr } from "@patternfly/react-table"; +import { useRemoveUser } from "@/Data/Managers/V2/DELETE/RemoveUser"; import { UserInfo } from "@/Data/Managers/V2/GETTERS/GetUsers"; import { words } from "@/UI"; -import { DeleteUserModal } from "./DeleteUserModal"; +import { ConfirmUserActionForm } from "@/UI/Components"; +import { ModalContext } from "@/UI/Root/Components/ModalProvider"; -interface UserInfoRowProps { +interface Props { user: UserInfo; } /** * A functional component that renders a row in the user information table. - * @param user The user information. + * @props {Props} props - The props of the component. + * @prop {UserInfo} user - The user information. + * + * @returns {React.FC} The rendered user info row with button to be able to delete the user. */ -export const UserInfoRow: React.FC = ({ user }) => { - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); +export const UserInfoRow: React.FC = ({ user }) => { + const { triggerModal, closeModal } = useContext(ModalContext); + const { mutate } = useRemoveUser(); /** - * Handles the delete button click event. + * Opens a modal with a confirmation form. + * + * @returns {void} */ - const onDelete = () => { - setIsDeleteModalOpen(true); + const openModal = (): void => { + triggerModal({ + title: words("userManagement.deleteUser.title"), + content: ( + <> +

{words("userManagement.deleteUserMessage")(user.username)}

+ { + mutate(user.username); + closeModal(); + }} + onCancel={closeModal} + /> + + ), + }); }; return ( {user.username} - - setIsDeleteModalOpen(false)} - /> ); }; diff --git a/src/Slices/UserManagement/UI/Components/index.ts b/src/Slices/UserManagement/UI/Components/index.ts index d095f286a..4ac363dcf 100644 --- a/src/Slices/UserManagement/UI/Components/index.ts +++ b/src/Slices/UserManagement/UI/Components/index.ts @@ -1,3 +1 @@ -export * from "./DeleteUserModal"; export * from "./UserInfoRow"; -export * from "./AddUserModal"; diff --git a/src/Slices/UserManagement/UI/Page.test.tsx b/src/Slices/UserManagement/UI/Page.test.tsx index 3a7033bb7..60faea107 100644 --- a/src/Slices/UserManagement/UI/Page.test.tsx +++ b/src/Slices/UserManagement/UI/Page.test.tsx @@ -2,7 +2,7 @@ import React, { act } from "react"; import { MemoryRouter } from "react-router-dom"; import { Page } from "@patternfly/react-core"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { fireEvent, render, screen } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; import { axe, toHaveNoViolations } from "jest-axe"; import { HttpResponse, http } from "msw"; @@ -10,6 +10,7 @@ import { setupServer } from "msw/node"; import { UserInfo } from "@/Data/Managers/V2/GETTERS/GetUsers"; import { dependencies } from "@/Test"; import { DependencyProvider, words } from "@/UI"; +import { ModalProvider } from "@/UI/Root/Components/ModalProvider"; import { UserManagementPage } from "./Page"; expect.extend(toHaveNoViolations); @@ -21,9 +22,11 @@ const setup = () => { - - - + + + + + @@ -198,6 +201,11 @@ describe("UserManagementPage", () => { auth_method: "database", }); }), + http.delete("/api/v2/user/test_user", async (): Promise => { + data.splice(0, 1); + + return HttpResponse.json({ status: 204 }); + }), ); server.listen(); @@ -216,21 +224,14 @@ describe("UserManagementPage", () => { expect(userRows).toHaveLength(2); - await act(async () => { - await userEvent.click(screen.getByText("Add User")); - }); + await userEvent.click(screen.getByText("Add User")); //mock error scenario - await act(async () => { - await userEvent.type(screen.getByLabelText("input-username"), "new_user"); - }); - await act(async () => { - await userEvent.type(screen.getByLabelText("input-password"), "123456"); - }); + await userEvent.type(screen.getByLabelText("input-username"), "new_user"); - await act(async () => { - await userEvent.click(screen.getByLabelText("add_user-button")); - }); + await userEvent.type(screen.getByLabelText("input-password"), "123456"); + + await userEvent.click(screen.getByLabelText("confirm-button")); const errorMessage = await screen.findByLabelText("error-message"); @@ -240,13 +241,9 @@ describe("UserManagementPage", () => { ); //mock success scenario - await act(async () => { - await userEvent.type(screen.getByLabelText("input-password"), "12345678"); - }); + await userEvent.type(screen.getByLabelText("input-password"), "12345678"); - await act(async () => { - fireEvent.click(screen.getByLabelText("add_user-button")); - }); + await userEvent.click(screen.getByLabelText("confirm-button")); const updatedRows = await screen.findAllByTestId("user-row"); @@ -269,7 +266,7 @@ describe("UserManagementPage", () => { { username: "test_user2", auth_method: "oidc" }, ]; const server = setupServer( - http.get("/api/v2/user", async () => { + http.get("/api/v2/user", async (): Promise => { return HttpResponse.json({ data, }); @@ -277,7 +274,7 @@ describe("UserManagementPage", () => { http.delete("/api/v2/user/test_user", async (): Promise => { data.splice(0, 1); - return HttpResponse.json(); + return HttpResponse.json({ status: 204 }); }), ); @@ -298,13 +295,9 @@ describe("UserManagementPage", () => { expect(userRows).toHaveLength(2); - await act(async () => { - fireEvent.click(screen.getAllByText("Delete")[0]); - }); + await userEvent.click(screen.getAllByText("Delete")[0]); - await act(async () => { - fireEvent.click(screen.getByText("Yes")); - }); + await userEvent.click(screen.getByText("Yes")); const updatedRows = await screen.findAllByTestId("user-row"); diff --git a/src/Slices/UserManagement/UI/Page.tsx b/src/Slices/UserManagement/UI/Page.tsx index a20596f28..a1213d594 100644 --- a/src/Slices/UserManagement/UI/Page.tsx +++ b/src/Slices/UserManagement/UI/Page.tsx @@ -1,7 +1,8 @@ -import React, { useState } from "react"; +import React, { useContext } from "react"; import { Button, Flex, FlexItem } from "@patternfly/react-core"; import { Table, Tbody, Th, Thead, Tr } from "@patternfly/react-table"; import { useGetUsers } from "@/Data/Managers/V2/GETTERS/GetUsers"; +import { UserCredentialsForm } from "@/Slices/UserManagement/UI/Components/AddUserForm"; import { words } from "@/UI"; import { EmptyView, @@ -9,15 +10,30 @@ import { LoadingView, PageContainer, } from "@/UI/Components"; -import { AddUserModal } from "./Components/AddUserModal"; +import { ModalContext } from "@/UI/Root/Components/ModalProvider"; import { UserInfoRow } from "./Components/UserInfoRow"; export const UserManagementPage: React.FC = () => { - const [isAddModalOpen, setIsAddModalOpen] = useState(false); + const { triggerModal } = useContext(ModalContext); const { data, isLoading, isError, error, refetch } = useGetUsers().useOneTime(); + /** + * Opens a modal with a form for user credentials. + */ + const openModal = () => { + triggerModal({ + title: words("userManagement.addUser"), + content: ( + + ), + }); + }; + if (isLoading) return ; if (isError) return ( @@ -32,13 +48,13 @@ export const UserManagementPage: React.FC = () => { return ( - setIsAddModalOpen(false)} - /> - diff --git a/src/Test/Data/Environment.ts b/src/Test/Data/Environment.ts index 91c584fed..f6c89759d 100644 --- a/src/Test/Data/Environment.ts +++ b/src/Test/Data/Environment.ts @@ -68,7 +68,7 @@ export const filterable: EnvironmentExpertOnly[] = [ { projectName: "prod", id: "789", - name: "test-env1", + name: "test-env2", project_id: "444", repo_branch: "master", repo_url: "gitlab.com/test", diff --git a/src/Test/Data/Field.ts b/src/Test/Data/Field.ts index fdfce7995..a14d65298 100644 --- a/src/Test/Data/Field.ts +++ b/src/Test/Data/Field.ts @@ -169,7 +169,7 @@ export const nestedDictList = (fields?: Field[]): DictListField => ({ isDisabled: false, min: 1, max: 4, - fields: [dictList(fields)] || [], + fields: [dictList(fields)], }); export const nestedEditable: Field[] = [ diff --git a/src/Test/Data/Row.ts b/src/Test/Data/Row.ts index 747b45ea8..e57b7ff1e 100644 --- a/src/Test/Data/Row.ts +++ b/src/Test/Data/Row.ts @@ -2,27 +2,16 @@ import { Row } from "@/Core"; export const a: Row = { id: { full: "service_instance_id_a", short: "id_a" }, - attributesSummary: { - candidate: true, - active: false, - rollback: true, - }, - attributes: { - candidate: null, - active: { - a: 123, - b: false, - }, - rollback: null, - }, createdAt: "2021-01-11T12:55:25.961567", updatedAt: "2021-01-11T12:55:25.961567", version: 2, - instanceSetStateTargets: [], environment: "env", service_entity: "entity", serviceIdentityValue: "instance1", deleted: false, + state: "creating", + deleteDisabled: false, + editDisabled: false, }; export const b: Row = { diff --git a/src/Test/Data/Service/EmbeddedEntity.ts b/src/Test/Data/Service/EmbeddedEntity.ts index 0a6d30aaf..a82a1e0e5 100644 --- a/src/Test/Data/Service/EmbeddedEntity.ts +++ b/src/Test/Data/Service/EmbeddedEntity.ts @@ -31,6 +31,7 @@ export const a: EmbeddedEntity = { }, ], embedded_entities: [], + inter_service_relations: [], name: "allocated", description: "Circuit allocated attributes", modifier: "r", @@ -171,6 +172,7 @@ export const a: EmbeddedEntity = { }, ], embedded_entities: [], + inter_service_relations: [], name: "allocated", description: "Allocated attributes for customer endpoint ", modifier: "r", @@ -178,6 +180,7 @@ export const a: EmbeddedEntity = { upper_limit: 1, }, ], + inter_service_relations: [], name: "customer_endpoint", description: "Attributes for customer endpoint which are provided through the NB API", @@ -377,6 +380,7 @@ export const a: EmbeddedEntity = { }, ], embedded_entities: [], + inter_service_relations: [], name: "allocated", description: "Allocated attributes for CSP endpoint", modifier: "r", @@ -384,6 +388,7 @@ export const a: EmbeddedEntity = { upper_limit: 1, }, ], + inter_service_relations: [], name: "csp_endpoint", description: "Attributes for CSP endpoint which are provided through the NB API", @@ -392,6 +397,7 @@ export const a: EmbeddedEntity = { upper_limit: 1, }, ], + inter_service_relations: [], name: "circuits", description: "Circuit attributes ", modifier: "rw+", @@ -446,6 +452,7 @@ export const nestedEditable: EmbeddedEntity[] = [ }, ], embedded_entities: [], + inter_service_relations: [], name: "embedded_single", description: "description", modifier: "rw", @@ -453,6 +460,7 @@ export const nestedEditable: EmbeddedEntity[] = [ upper_limit: 1, }, ], + inter_service_relations: [], name: "embedded", description: "description", modifier: "rw+", @@ -510,6 +518,7 @@ export const nestedEditable: EmbeddedEntity[] = [ ], }, ], + inter_service_relations: [], name: "another_embedded", modifier: "rw+", lower_limit: 0, @@ -528,6 +537,7 @@ export const nestedEditable: EmbeddedEntity[] = [ }, ], embedded_entities: [], + inter_service_relations: [], name: "not_editable", modifier: "rw", lower_limit: 1, @@ -546,6 +556,7 @@ export const nestedEditable: EmbeddedEntity[] = [ }, ], embedded_entities: [], + inter_service_relations: [], name: "editable_embedded_entity_relation_with_rw_attributes", modifier: "rw+", lower_limit: 1, @@ -584,6 +595,7 @@ export const multiNestedEditable: EmbeddedEntity[] = [ validation_parameters: null, }, ], + inter_service_relations: [], embedded_entities: [ { attributes: [ @@ -676,12 +688,14 @@ export const multiNestedEditable: EmbeddedEntity[] = [ inter_service_relations: [], }, ], + inter_service_relations: [], name: "another_embedded", modifier: "rw+", lower_limit: 0, description: "description", }, ], + inter_service_relations: [], name: "embedded_single", description: "description", modifier: "rw+", @@ -700,6 +714,7 @@ export const multiNestedEditable: EmbeddedEntity[] = [ export const embedded: EmbeddedEntity = { attributes: attributesList, embedded_entities: [], + inter_service_relations: [], name: "embedded", description: "desc", modifier: "rw", @@ -727,6 +742,7 @@ export const embedded_base: EmbeddedEntity = { lower_limit: 0, }, ], + inter_service_relations: [], name: "embedded_base", description: "desc", modifier: "rw", diff --git a/src/Test/Data/Service/index.ts b/src/Test/Data/Service/index.ts index bea1b3e59..80b819e1a 100644 --- a/src/Test/Data/Service/index.ts +++ b/src/Test/Data/Service/index.ts @@ -26,6 +26,7 @@ export const a: ServiceModel = { embedded_entities: EmbeddedEntity.list, owner: null, owned_entities: [], + inter_service_relations: [], }; export const b: ServiceModel = { diff --git a/src/Test/Inject/InventoryTablePresenter.ts b/src/Test/Inject/InventoryTablePresenter.ts deleted file mode 100644 index bd23da09c..000000000 --- a/src/Test/Inject/InventoryTablePresenter.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { - DummyActionPresenter, - DummyDatePresenter, - DummyStatePresenter, -} from "@/Test/Mock"; -import { ActionPresenter } from "@/UI/Presenters"; -import { AttributesPresenter } from "@S/ServiceInventory/UI/Presenters/AttributesPresenter"; -import { InventoryTablePresenter } from "@S/ServiceInventory/UI/Presenters/InventoryTablePresenter"; - -export const tablePresenter = (actionPresenter?: ActionPresenter) => - new InventoryTablePresenter( - new DummyDatePresenter(), - new AttributesPresenter(), - actionPresenter || new DummyActionPresenter(), - new DummyStatePresenter(), - ); - -export const tablePresenterWithIdentity = (actionPresenter?: ActionPresenter) => - new InventoryTablePresenter( - new DummyDatePresenter(), - new AttributesPresenter(), - actionPresenter || new DummyActionPresenter(), - new DummyStatePresenter(), - "order_id", - "Order ID", - ); diff --git a/src/Test/Inject/index.ts b/src/Test/Inject/index.ts index c44945fc8..426cefec1 100644 --- a/src/Test/Inject/index.ts +++ b/src/Test/Inject/index.ts @@ -1,3 +1,2 @@ -export * from "./InventoryTablePresenter"; export * from "./dependencies"; export * from "./AuthTestWrapper"; diff --git a/src/Test/Mock/DummyDatePresenter.ts b/src/Test/Mock/DummyDatePresenter.ts index 06106c9dd..737a889b5 100644 --- a/src/Test/Mock/DummyDatePresenter.ts +++ b/src/Test/Mock/DummyDatePresenter.ts @@ -27,6 +27,7 @@ export class DummyDatePresenter implements DatePresenter { return { full: "full", relative: "relative", + dateTimeMilliseconds: "dateTimeMilliseconds", }; } parseFull(): Date { diff --git a/src/Test/Mock/DummyStatePresenter.ts b/src/Test/Mock/DummyStatePresenter.ts deleted file mode 100644 index 1ad64f9f6..000000000 --- a/src/Test/Mock/DummyStatePresenter.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ReactElement } from "react"; -import { StatePresenter } from "@S/ServiceInventory/UI/Presenters"; - -export class DummyStatePresenter implements StatePresenter { - getForId(): ReactElement | null { - return null; - } -} diff --git a/src/Test/Mock/index.ts b/src/Test/Mock/index.ts index 4a019337e..dcc6f5e29 100644 --- a/src/Test/Mock/index.ts +++ b/src/Test/Mock/index.ts @@ -1,7 +1,6 @@ export * from "./DeferredApiHelper"; export * from "./DummyActionPresenter"; export * from "./DummyDatePresenter"; -export * from "./DummyStatePresenter"; export * from "./DynamicManagerResolver"; export * from "./InstantApiHelper"; export * from "./InstantFileFetcher"; diff --git a/src/UI/Components/BlockingModal/BlockingModal.tsx b/src/UI/Components/BlockingModal/BlockingModal.tsx index a8cfd1b5e..261bac038 100644 --- a/src/UI/Components/BlockingModal/BlockingModal.tsx +++ b/src/UI/Components/BlockingModal/BlockingModal.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; -import { Flex, FlexItem, Modal, Spinner, Text } from "@patternfly/react-core"; -import styled from "styled-components"; +import { Content, Flex, FlexItem, Spinner } from "@patternfly/react-core"; +import { Modal } from "@patternfly/react-core/deprecated"; import { words } from "@/UI/words"; export const BlockingModal = () => { @@ -27,7 +27,7 @@ export const BlockingModal = () => { }, []); return ( - { alignItems={{ default: "alignItemsCenter" }} > - {message} + {message} - + - + ); }; - -const StyledModal = styled(Modal)` - background-color: transparent; - box-shadow: none; -`; - -const StyledSpinner = styled(Spinner)` - --pf-v5-c-spinner--Color: var(--pf-v5-global--BackgroundColor--100); -`; - -const StyledText = styled(Text)` - color: var(--pf-v5-global--Color--light-100); - font-size: 1rem; - padding-bottom: 0.5rem; - text-transform: uppercase; -`; diff --git a/src/UI/Components/BooleanSwitch/BooleanSwitch.tsx b/src/UI/Components/BooleanSwitch/BooleanSwitch.tsx index aead62453..a0493f29c 100644 --- a/src/UI/Components/BooleanSwitch/BooleanSwitch.tsx +++ b/src/UI/Components/BooleanSwitch/BooleanSwitch.tsx @@ -20,7 +20,6 @@ export const BooleanSwitch: React.FC = ({ onChange(checked)} aria-label={isChecked ? `${name}-True` : `${name}-False`} diff --git a/src/UI/Components/CatalogActions/CatalogActions.test.tsx b/src/UI/Components/CatalogActions/CatalogActions.test.tsx index e805fbf67..ea1755d06 100644 --- a/src/UI/Components/CatalogActions/CatalogActions.test.tsx +++ b/src/UI/Components/CatalogActions/CatalogActions.test.tsx @@ -19,6 +19,7 @@ import { StaticScheduler, } from "@/Test"; import { DependencyProvider } from "@/UI/Dependency"; +import { ModalProvider } from "@/UI/Root/Components/ModalProvider"; import { words } from "@/UI/words"; import { CatalogActions } from "./CatalogActions"; expect.extend(toHaveNoViolations); @@ -61,7 +62,9 @@ function setup( queryResolver, }} > - + + + ); @@ -82,9 +85,7 @@ test("Given CatalogUpdateButton, when user clicks on button, it should display a expect(button).toBeVisible(); - await act(async () => { - await userEvent.click(button); - }); + await userEvent.click(button); expect( await screen.findByText(words("catalog.update.modal.title")), @@ -106,9 +107,7 @@ test("Given CatalogUpdateButton, when user cancels the modal, it should not fire name: words("catalog.button.update"), }); - await act(async () => { - await userEvent.click(button); - }); + await userEvent.click(button); const cancelButton = await screen.findByText(words("no")); @@ -120,9 +119,7 @@ test("Given CatalogUpdateButton, when user cancels the modal, it should not fire expect(results).toHaveNoViolations(); }); - await act(async () => { - await userEvent.click(cancelButton); - }); + await userEvent.click(cancelButton); expect(cancelButton).not.toBeVisible(); expect(apiHelper.pendingRequests).toHaveLength(0); @@ -144,17 +141,13 @@ test("Given CatalogUpdateButton, when user confirms update, it should fire the A name: words("catalog.button.update"), }); - await act(async () => { - await userEvent.click(button); - }); + await userEvent.click(button); const confirmButton = await screen.findByText(words("yes")); expect(confirmButton).toBeVisible(); - await act(async () => { - await userEvent.click(confirmButton); - }); + await userEvent.click(confirmButton); expect(confirmButton).not.toBeVisible(); expect(apiHelper.pendingRequests).toHaveLength(1); @@ -183,17 +176,13 @@ test("Given CatalogUpdateButton, when user confirms the update, it should fire t name: words("catalog.button.update"), }); - await act(async () => { - await userEvent.click(button); - }); + await userEvent.click(button); const confirmButton = await screen.findByText(words("yes")); expect(confirmButton).toBeVisible(); - await act(async () => { - await userEvent.click(confirmButton); - }); + await userEvent.click(confirmButton); expect(confirmButton).not.toBeVisible(); expect(apiHelper.pendingRequests).toHaveLength(1); diff --git a/src/UI/Components/CatalogActions/CatalogActions.tsx b/src/UI/Components/CatalogActions/CatalogActions.tsx index e82dbb30b..4df7fa507 100644 --- a/src/UI/Components/CatalogActions/CatalogActions.tsx +++ b/src/UI/Components/CatalogActions/CatalogActions.tsx @@ -2,14 +2,14 @@ import React, { useContext, useState } from "react"; import { AlertVariant, Button, - Modal, - ModalVariant, + Content, + Flex, Tooltip, } from "@patternfly/react-core"; import { FileCodeIcon } from "@patternfly/react-icons"; -import styled from "styled-components"; import { Either } from "@/Core"; import { DependencyContext } from "@/UI/Dependency"; +import { ModalContext } from "@/UI/Root/Components/ModalProvider"; import { words } from "@/UI/words"; import { ConfirmUserActionForm } from "../ConfirmUserActionForm"; import { ToastAlert } from "../ToastAlert"; @@ -26,6 +26,7 @@ import { ToastAlert } from "../ToastAlert"; * @returns CatalogActions */ export const CatalogActions: React.FC = () => { + const { triggerModal, closeModal } = useContext(ModalContext); const { commandResolver, urlManager, environmentHandler } = useContext(DependencyContext); @@ -33,17 +34,22 @@ export const CatalogActions: React.FC = () => { kind: "UpdateCatalog", }); - const [isOpen, setIsOpen] = useState(false); const [message, setMessage] = useState(""); const [toastTitle, setToastTitle] = useState(""); const [toastType, setToastType] = useState(AlertVariant.custom); - const handleModalToggle = () => { - setIsOpen(!isOpen); - }; - - const onSubmit = async () => { - handleModalToggle(); + /** + * Handles the submission of the form. + * + * This function closes the modal and triggers an asynchronous operation. + * If the operation is successful, it sets the toast title, message, and type to indicate success. + * If the operation fails, it sets the toast title, message, and type to indicate failure. + * The message in case of failure is the value of the result. + * + * @returns {Promise} A Promise that resolves when the operation is complete. + */ + const onSubmit = async (): Promise => { + closeModal(); const result = await trigger(); if (Either.isRight(result)) { @@ -57,6 +63,35 @@ export const CatalogActions: React.FC = () => { } }; + /** + * Opens a modal with a confirmation form. + * + * @returns {void} + */ + const openModal = (): void => { + triggerModal({ + title: words("catalog.update.modal.title"), + content: ( + <> + {words("catalog.update.confirmation.p1")} + + {words("catalog.update.confirmation.p2")} + + + + - {words("catalog.update.confirmation.p3")} + + + - {words("catalog.update.confirmation.p4")} + + + {words("catalog.update.confirmation.p5")} + + + ), + }); + }; + return ( <> { setMessage={setMessage} type={toastType} /> - + - + - - - - {words("catalog.update.confirmation.p1")} - -

- {words("catalog.update.confirmation.p2")} -

-
    -
  • - - {words("catalog.update.confirmation.p3")} -
  • -
  • - - {words("catalog.update.confirmation.p4")} -
  • -
- - {words("catalog.update.confirmation.p5")} - - -
+ ); }; - -const StyledWrapper = styled.div` - display: flex; - flex-direction: row; - justify-content: flex-end; - padding: var(--pf-v5-global--spacer--md); -} -`; -const StyledParagraph = styled.p` - padding-bottom: 10px; - padding-top: 10px; -`; diff --git a/src/UI/Components/ClipboardCopyButton/ClipboardCopyButton.test.tsx b/src/UI/Components/ClipboardCopyButton/ClipboardCopyButton.test.tsx index 8c35744e8..1dfd26397 100644 --- a/src/UI/Components/ClipboardCopyButton/ClipboardCopyButton.test.tsx +++ b/src/UI/Components/ClipboardCopyButton/ClipboardCopyButton.test.tsx @@ -1,4 +1,4 @@ -import React, { act } from "react"; +import React from "react"; import { render, screen } from "@testing-library/react"; import { userEvent } from "@testing-library/user-event"; @@ -13,9 +13,7 @@ test("Given a ClipboardCopyButton, when the button is hovered, then a tooltip sh ); const button = await screen.findByLabelText("Copy to clipboard"); - await act(async () => { - await userEvent.hover(button); - }); + await userEvent.hover(button); expect(await screen.findByRole("tooltip")).toBeInTheDocument(); }); diff --git a/src/UI/Components/ClipboardCopyButton/ClipboardCopyButton.tsx b/src/UI/Components/ClipboardCopyButton/ClipboardCopyButton.tsx index ca246067f..c89352e54 100644 --- a/src/UI/Components/ClipboardCopyButton/ClipboardCopyButton.tsx +++ b/src/UI/Components/ClipboardCopyButton/ClipboardCopyButton.tsx @@ -37,15 +37,15 @@ export const ClipboardCopyButton: React.FC = ({ return ( {tooltipText}} entryDelay={200}> + size="sm" + > ); }; diff --git a/src/UI/Components/CodeHighlighter/index.tsx b/src/UI/Components/CodeHighlighter/index.tsx index 294232505..a98d26f3a 100644 --- a/src/UI/Components/CodeHighlighter/index.tsx +++ b/src/UI/Components/CodeHighlighter/index.tsx @@ -17,7 +17,7 @@ import { CopyIcon, EllipsisVIcon, ExpandArrowsAltIcon, - InfoCircleIcon, + InfoAltIcon, ListOlIcon, LongArrowAltDownIcon, TextWidthIcon, @@ -74,11 +74,14 @@ export const CodeHighlighter: React.FC = ({ disabledContent={words("codehighlighter.lineWrapping.on")} key={`wraplonglines-${keyId}`} > - setWraplongLines(!wrapLongLines)}> - - - - + setWraplongLines(!wrapLongLines)} + icon={ + + + + } + /> , = ({ disabledContent={words("codehighlighter.lineNumbers.on")} key={`showlinenumbers-${keyId}`} > - setShowLineNumbers(!showLineNumbers)}> - - - - + setShowLineNumbers(!showLineNumbers)} + icon={ + + + + } + /> , ]; @@ -202,8 +208,8 @@ export const CodeHighlighter: React.FC = ({ <> {isEmpty(code) ? ( -