From 5607219d3b53f5d460cd40737e4af762843c313d Mon Sep 17 00:00:00 2001 From: yaacov Date: Sun, 9 Jul 2023 09:58:09 +0300 Subject: [PATCH] Providers details page Signed-off-by: yaacov --- .../plugin-extensions.ts | 2 +- .../plugin-metadata.ts | 2 +- .../actions/ProviderActionsDropdown.style.css | 4 + .../actions/ProviderActionsDropdown.tsx | 65 +++++ .../actions/ProviderActionsDropdownItems.tsx | 40 +++ .../src/modules/ProvidersNG/actions/index.ts | 4 + .../src/modules/ProvidersNG/dynamic-plugin.ts | 74 +++++ .../hooks/__tests__/useProviders.test.ts | 92 ++++++ .../src/modules/ProvidersNG/hooks/index.ts | 9 + .../hooks/useGetDeleteAndEditAccessReview.ts | 67 +++++ .../hooks/useK8sWatchProviderNames.ts | 50 ++++ .../hooks/useK8sWatchSecretData.ts | 45 +++ .../ProvidersNG/hooks/useProviderInventory.ts | 187 ++++++++++++ .../hooks/useProvidersInventoryList.ts | 139 +++++++++ .../modules/ProvidersNG/hooks/useToggle.ts | 27 ++ .../ProvidersNG/hooks/utils/constants.ts | 19 ++ .../modules/ProvidersNG/hooks/utils/index.ts | 3 + .../modals/DeleteModal/DeleteModal.tsx | 110 +++++++ .../ProvidersNG/modals/DeleteModal/index.ts | 3 + .../modals/EditModal/EditModal.style.css | 4 + .../modals/EditModal/EditModal.tsx | 185 ++++++++++++ .../ProvidersNG/modals/EditModal/index.ts | 4 + .../ProvidersNG/modals/EditModal/types.tsx | 105 +++++++ .../EditModal/utils/defaultOnConfirm.ts | 19 ++ .../EditProviderDefaultTransferNetwork.tsx | 31 ++ ...hiftEditProviderDefaultTransferNetwork.tsx | 132 +++++++++ .../index.ts | 4 + .../EditProviderURL/EditProviderURLModal.tsx | 38 +++ .../EditProviderURL/OpenshiftEditURLModal.tsx | 50 ++++ .../EditProviderURL/OpenstackEditURLModal.tsx | 48 ++++ .../EditProviderURL/OvirtEditURLModal.tsx | 48 ++++ .../EditProviderURL/VSphereEditURLModal.tsx | 48 ++++ .../modals/EditProviderURL/index.ts | 7 + .../EditProviderURL/utils/patchProviderURL.ts | 59 ++++ .../EditProviderVDDKImage.tsx | 29 ++ .../VSphereEditProviderVDDKImage.tsx | 43 +++ .../modals/EditProviderVDDKImage/index.ts | 4 + .../ProvidersNG/modals/ModalHOC/ModalHOC.tsx | 67 +++++ .../ProvidersNG/modals/ModalHOC/index.ts | 3 + .../components/AlertMessageForModals.tsx | 12 + .../modals/components/ItemIsOwnedAlert.tsx | 42 +++ .../ProvidersNG/modals/components/index.ts | 4 + .../src/modules/ProvidersNG/modals/index.ts | 9 + .../components/DetailsPage/DetailItem.tsx | 158 ++++++++++ .../DetailsPage/OwnerReferencesItem.tsx | 43 +++ .../DetailsPage/PageHeadings.style.css | 3 + .../components/DetailsPage/PageHeadings.tsx | 116 ++++++++ .../utils/components/DetailsPage/index.ts | 5 + .../components/Galerry/SelectableCard.tsx | 46 +++ .../Galerry/SelectableGallery.style.css | 7 + .../components/Galerry/SelectableGallery.tsx | 69 +++++ .../utils/components/TableCell/TableCell.tsx | 33 +++ .../components/TableCell/TableCells.style.css | 8 + .../components/TableCell/TableEmptyCell.tsx | 11 + .../components/TableCell/TableIconCell.tsx | 28 ++ .../components/TableCell/TableLabelCell.tsx | 35 +++ .../components/TableCell/TableLinkCell.tsx | 32 +++ .../utils/components/TableCell/index.ts | 7 + .../ProvidersNG/utils/components/index.ts | 4 + .../helpers/__tests__/validators.test.ts | 150 ++++++++++ .../utils/helpers/findInventoryByID.ts | 25 ++ .../utils/helpers/getCachedData.ts | 25 ++ .../utils/helpers/getInventoryApiUrl.ts | 12 + .../ProvidersNG/utils/helpers/getIsManaged.ts | 12 + .../ProvidersNG/utils/helpers/getIsTarget.ts | 24 ++ .../utils/helpers/getResourceUrl.ts | 31 ++ .../utils/helpers/getValueByJsonPath.ts | 32 +++ .../helpers/hasObjectChangedInGivenFields.ts | 28 ++ .../ProvidersNG/utils/helpers/index.ts | 14 + .../utils/helpers/isSecretDataChanged.ts | 35 +++ .../utils/helpers/missingKeysInSecretData.ts | 31 ++ .../utils/helpers/safeBase64Decode.ts | 9 + .../utils/helpers/setCachedData.ts | 15 + .../src/modules/ProvidersNG/utils/index.ts | 6 + .../ProvidersNG/utils/types/ProviderData.ts | 9 + .../utils/types/ProvidersPermissionStatus.ts | 17 ++ .../ProvidersNG/utils/types/Validation.ts | 1 + .../modules/ProvidersNG/utils/types/index.ts | 5 + .../ProvidersNG/utils/validators/common.ts | 78 +++++ .../ProvidersNG/utils/validators/index.ts | 6 + .../utils/validators/provider/index.ts | 7 + .../provider/openshiftProviderValidator.ts | 18 ++ .../provider/openstackProviderValidator.ts | 18 ++ .../provider/ovirtProviderValidator.ts | 18 ++ .../validators/provider/providerValidator.ts | 29 ++ .../provider/vsphereProviderValidator.ts | 23 ++ .../validators/providerAndSecretValidator.ts | 19 ++ .../utils/validators/secret-fields/index.ts | 6 + .../openshiftSecretFieldValidator.ts | 30 ++ .../openstackSecretFieldValidator.ts | 125 ++++++++ .../ovirtSecretFieldValidator.ts | 57 ++++ .../vsphereSecretFieldValidator.ts | 47 +++ .../utils/validators/secret/index.ts | 7 + .../secret/openshiftSecretValidator.ts | 26 ++ .../secret/openstackSecretValidator.ts | 86 ++++++ .../validators/secret/ovirtSecretValidator.ts | 26 ++ .../validators/secret/secretValidator.ts | 29 ++ .../secret/vsphereSecretValidator.ts | 26 ++ .../create/ProvidersCreatePage.style.css | 3 + .../views/create/ProvidersCreatePage.tsx | 271 ++++++++++++++++++ .../OpenshiftProviderCreateForm.tsx | 79 +++++ .../OpenstackProviderCreateForm.tsx | 80 ++++++ .../components/OvirtProviderCreateForm.tsx | 80 ++++++ .../create/components/ProviderCreateForm.tsx | 174 +++++++++++ .../components/VSphereProviderCreateForm.tsx | 117 ++++++++ .../views/create/components/index.ts | 8 + .../create/components/providerCardItems.ts | 20 ++ .../modules/ProvidersNG/views/create/index.ts | 6 + .../views/create/templates/index.ts | 4 + .../create/templates/providerTemplate.ts | 18 ++ .../views/create/templates/secretTemplate.ts | 12 + .../views/create/utils/createProvider.ts | 38 +++ .../views/create/utils/createSecret.ts | 49 ++++ .../ProvidersNG/views/create/utils/index.ts | 5 + .../views/create/utils/patchSecretOwner.ts | 43 +++ .../details/ProviderDetailsPage.style.css | 7 + .../views/details/ProviderDetailsPage.tsx | 155 ++++++++++ .../ConditionsSection/ConditionsSection.tsx | 68 +++++ .../components/ConditionsSection/index.ts | 3 + .../CredentialsSection/CredentialsSection.tsx | 66 +++++ .../CredentialsSection/MaskedData.tsx | 7 + .../OpenshiftCredentialsSection.tsx | 25 ++ .../OpenstackCredentialsSection.tsx | 23 ++ .../OvirtCredentialsSection.tsx | 23 ++ .../VSphereCredentialsSection.tsx | 23 ++ .../BaseCredentialsSection.style.css | 45 +++ .../components/BaseCredentialsSection.tsx | 212 ++++++++++++++ .../edit/OpenshiftCredentialsEdit.tsx | 101 +++++++ .../edit/OpenstackCredentialsEdit.tsx | 255 ++++++++++++++++ ...ionCredentialNameSecretFieldsFormGroup.tsx | 161 +++++++++++ .../ApplicationWithCredentialsIDFormGroup.tsx | 119 ++++++++ .../PasswordSecretFieldsFormGroup.tsx | 179 ++++++++++++ .../TokenWithUserIDSecretFieldsFormGroup.tsx | 135 +++++++++ ...TokenWithUsernameSecretFieldsFormGroup.tsx | 155 ++++++++++ .../index.ts | 7 + .../components/edit/OvirtCredentialsEdit.tsx | 170 +++++++++++ .../edit/VSphereCredentialsEdit.tsx | 156 ++++++++++ .../components/edit/index.ts | 8 + .../components/edit/patchSecretData.ts | 42 +++ .../CredentialsSection/components/index.ts | 5 + .../list/OpenshiftCredentialsList.tsx | 59 ++++ .../list/OpenstackCredentialsList.tsx | 236 +++++++++++++++ .../components/list/OvirtCredentialsList.tsx | 68 +++++ .../list/VSphereCredentialsList.tsx | 65 +++++ .../components/list/index.ts | 6 + .../components/CredentialsSection/index.ts | 9 + .../DetailsSection/DetailsSection.tsx | 35 +++ .../OpenshiftDetailsSection.tsx | 171 +++++++++++ .../OpenstackDetailsSection.tsx | 134 +++++++++ .../DetailsSection/OvirtDetailsSection.tsx | 134 +++++++++ .../DetailsSection/VSphereDetailsSection.tsx | 153 ++++++++++ .../components/DetailsSection/index.ts | 7 + .../InventorySection/InventorySection.tsx | 28 ++ .../OpenshiftInventorySection.tsx | 66 +++++ .../OpenstackInventorySection.tsx | 78 +++++ .../OvirtInventorySection.tsx | 78 +++++ .../VSphereInventorySection.tsx | 78 +++++ .../components/InventorySection/index.ts | 7 + .../views/details/components/index.ts | 6 + .../ProvidersNG/views/details/index.ts | 5 + .../tabs/Credentials/ProviderCredentials.tsx | 35 +++ .../views/details/tabs/Credentials/index.ts | 3 + .../details/tabs/Details/ProviderDetails.tsx | 50 ++++ .../views/details/tabs/Details/index.ts | 3 + .../details/tabs/Hosts/ProviderHosts.tsx | 65 +++++ .../views/details/tabs/Hosts/index.ts | 3 + .../tabs/Networks/ProviderNetworks.tsx | 94 ++++++ .../views/details/tabs/Networks/index.ts | 3 + .../ProviderVirtualMachines.tsx | 71 +++++ .../details/tabs/VirtualMachines/index.ts | 3 + .../views/details/tabs/YAML/ProviderYAML.tsx | 44 +++ .../views/details/tabs/YAML/index.ts | 3 + .../ProvidersNG/views/details/tabs/index.ts | 8 + .../src/modules/ProvidersNG/views/index.ts | 5 + .../ProvidersNG/views/list/ProviderRow.tsx | 97 +++++++ .../views/list/ProvidersListPage.tsx | 210 ++++++++++++++ .../views/list/components/CellProps.tsx | 9 + .../list/components/InventoryCellFactory.tsx | 31 ++ .../views/list/components/NamespaceCell.tsx | 22 ++ .../list/components/ProviderLinkCell.tsx | 32 +++ .../views/list/components/StatusCell.tsx | 110 +++++++ .../views/list/components/TypeCell.tsx | 27 ++ .../views/list/components/URLCell.tsx | 21 ++ .../views/list/components/index.ts | 9 + .../modules/ProvidersNG/views/list/index.ts | 5 + 185 files changed, 9245 insertions(+), 2 deletions(-) create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/actions/ProviderActionsDropdown.style.css create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/actions/ProviderActionsDropdown.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/actions/ProviderActionsDropdownItems.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/actions/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/dynamic-plugin.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/__tests__/useProviders.test.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useGetDeleteAndEditAccessReview.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useK8sWatchProviderNames.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useK8sWatchSecretData.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useProviderInventory.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useProvidersInventoryList.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useToggle.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/utils/constants.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/utils/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/DeleteModal/DeleteModal.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/DeleteModal/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/EditModal.style.css create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/EditModal.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/types.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/utils/defaultOnConfirm.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderDefaultTransferNetwork/EditProviderDefaultTransferNetwork.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderDefaultTransferNetwork/OpenshiftEditProviderDefaultTransferNetwork.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderDefaultTransferNetwork/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/EditProviderURLModal.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/OpenshiftEditURLModal.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/OpenstackEditURLModal.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/OvirtEditURLModal.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/VSphereEditURLModal.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/utils/patchProviderURL.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderVDDKImage/EditProviderVDDKImage.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderVDDKImage/VSphereEditProviderVDDKImage.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderVDDKImage/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/ModalHOC/ModalHOC.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/ModalHOC/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/components/AlertMessageForModals.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/components/ItemIsOwnedAlert.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/components/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/modals/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/DetailItem.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/OwnerReferencesItem.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/PageHeadings.style.css create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/PageHeadings.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/Galerry/SelectableCard.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/Galerry/SelectableGallery.style.css create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/Galerry/SelectableGallery.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableCells.style.css create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableEmptyCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableIconCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableLabelCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableLinkCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/__tests__/validators.test.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/findInventoryByID.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getCachedData.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getInventoryApiUrl.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getIsManaged.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getIsTarget.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getResourceUrl.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getValueByJsonPath.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/hasObjectChangedInGivenFields.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/isSecretDataChanged.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/missingKeysInSecretData.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/safeBase64Decode.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/setCachedData.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/ProviderData.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/ProvidersPermissionStatus.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/Validation.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/common.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/openshiftProviderValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/openstackProviderValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/ovirtProviderValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/providerValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/vsphereProviderValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/providerAndSecretValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/openshiftSecretFieldValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/openstackSecretFieldValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/ovirtSecretFieldValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/vsphereSecretFieldValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/openshiftSecretValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/openstackSecretValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/ovirtSecretValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/secretValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/vsphereSecretValidator.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/ProvidersCreatePage.style.css create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/ProvidersCreatePage.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/OpenshiftProviderCreateForm.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/OpenstackProviderCreateForm.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/OvirtProviderCreateForm.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/ProviderCreateForm.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/VSphereProviderCreateForm.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/providerCardItems.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/templates/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/templates/providerTemplate.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/templates/secretTemplate.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/createProvider.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/createSecret.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/patchSecretOwner.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/ProviderDetailsPage.style.css create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/ProviderDetailsPage.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/ConditionsSection/ConditionsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/ConditionsSection/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/CredentialsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/MaskedData.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/OpenshiftCredentialsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/OpenstackCredentialsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/OvirtCredentialsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/VSphereCredentialsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/BaseCredentialsSection.style.css create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/BaseCredentialsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenshiftCredentialsEdit.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEdit.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/ApplicationCredentialNameSecretFieldsFormGroup.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/ApplicationWithCredentialsIDFormGroup.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/PasswordSecretFieldsFormGroup.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/TokenWithUserIDSecretFieldsFormGroup.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/TokenWithUsernameSecretFieldsFormGroup.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OvirtCredentialsEdit.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/VSphereCredentialsEdit.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/patchSecretData.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/OpenshiftCredentialsList.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/OpenstackCredentialsList.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/OvirtCredentialsList.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/VSphereCredentialsList.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/DetailsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/OpenshiftDetailsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/OpenstackDetailsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/OvirtDetailsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/VSphereDetailsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/InventorySection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/OpenshiftInventorySection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/OpenstackInventorySection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/OvirtInventorySection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/VSphereInventorySection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Credentials/ProviderCredentials.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Credentials/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Details/ProviderDetails.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Details/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Hosts/ProviderHosts.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Hosts/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Networks/ProviderNetworks.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Networks/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/VirtualMachines/ProviderVirtualMachines.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/VirtualMachines/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/YAML/ProviderYAML.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/YAML/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/ProviderRow.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/ProvidersListPage.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/CellProps.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/InventoryCellFactory.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/NamespaceCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/ProviderLinkCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/StatusCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/TypeCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/URLCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/index.ts diff --git a/packages/forklift-console-plugin/plugin-extensions.ts b/packages/forklift-console-plugin/plugin-extensions.ts index df5b15f77..e0299e392 100644 --- a/packages/forklift-console-plugin/plugin-extensions.ts +++ b/packages/forklift-console-plugin/plugin-extensions.ts @@ -4,7 +4,7 @@ import type { NavSection } from '@openshift-console/dynamic-plugin-sdk'; import { extensions as mockConsoleExtensions } from './src/__mock-console-extension/dynamic-plugin'; import { extensions as networkMapExtensions } from './src/modules/NetworkMaps/dynamic-plugin'; import { extensions as planExtensions } from './src/modules/Plans/dynamic-plugin'; -import { extensions as providerExtensions } from './src/modules/Providers/dynamic-plugin'; +import { extensions as providerExtensions } from './src/modules/ProvidersNG/dynamic-plugin'; import { extensions as storageMapExtensions } from './src/modules/StorageMaps/dynamic-plugin'; const extensions: EncodedExtension[] = [ diff --git a/packages/forklift-console-plugin/plugin-metadata.ts b/packages/forklift-console-plugin/plugin-metadata.ts index 4087e93de..c2035e844 100644 --- a/packages/forklift-console-plugin/plugin-metadata.ts +++ b/packages/forklift-console-plugin/plugin-metadata.ts @@ -3,7 +3,7 @@ import type { ConsolePluginMetadata } from '@openshift-console/dynamic-plugin-sd import { exposedModules as mockExtensionModules } from './src/__mock-console-extension/dynamic-plugin'; import { exposedModules as networkMapModules } from './src/modules/NetworkMaps/dynamic-plugin'; import { exposedModules as planModules } from './src/modules/Plans/dynamic-plugin'; -import { exposedModules as providerModules } from './src/modules/Providers/dynamic-plugin'; +import { exposedModules as providerModules } from './src/modules/ProvidersNG/dynamic-plugin'; import { exposedModules as storageMapModules } from './src/modules/StorageMaps/dynamic-plugin'; import pkg from './package.json'; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/actions/ProviderActionsDropdown.style.css b/packages/forklift-console-plugin/src/modules/ProvidersNG/actions/ProviderActionsDropdown.style.css new file mode 100644 index 000000000..3cced821c --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/actions/ProviderActionsDropdown.style.css @@ -0,0 +1,4 @@ +.forklift-dropdown { + margin: 0; + padding: 0; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/actions/ProviderActionsDropdown.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/actions/ProviderActionsDropdown.tsx new file mode 100644 index 000000000..390dd6454 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/actions/ProviderActionsDropdown.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { Dropdown, DropdownPosition, DropdownToggle, KebabToggle } from '@patternfly/react-core'; + +import { useToggle } from '../hooks'; +import { ModalHOC } from '../modals'; +import { CellProps } from '../views'; + +import { ProviderActionsDropdownItems } from './ProviderActionsDropdownItems'; + +import './ProviderActionsDropdown.style.css'; + +/** + * ProviderActionsKebabDropdown_ is a helper component that displays a kebab dropdown menu. + * @param {CellProps} props - The properties passed to this component. + * @param {ProviderWithInventory} props.data - The data to be used in ProviderActionsDropdownItems. + * @returns {React.Element} The rendered dropdown menu component. + */ +const ProviderActionsKebabDropdown_: React.FC = ({ + data, + isKebab, +}) => { + const { t } = useForkliftTranslation(); + + // Hook for managing the open/close state of the dropdown + const [isDropdownOpen, toggle] = useToggle(); + + // Returning the Dropdown component from PatternFly library + return ( + + ) : ( + + {t('Actions')} + + ) + } + dropdownItems={ProviderActionsDropdownItems({ data })} + /> + ); +}; + +/** + * ProviderActionsDropdown is a component that provides a context for the dropdown menu. + * It uses a ModalProvider to manage modals that may be used in the dropdown menu. + * @param {CellProps} props - The properties passed to this component. + * @returns {React.Element} The rendered component with a ModalProvider context. + */ +export const ProviderActionsDropdown: React.FC = (props) => ( + + + +); + +export interface ProviderActionsDropdownProps extends CellProps { + isKebab?: boolean; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/actions/ProviderActionsDropdownItems.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/actions/ProviderActionsDropdownItems.tsx new file mode 100644 index 000000000..297a585e6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/actions/ProviderActionsDropdownItems.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ProviderModel, ProviderModelRef } from '@kubev2v/types'; +import { DropdownItem } from '@patternfly/react-core'; + +import { DeleteModal, useModal } from '../modals'; +import { getResourceUrl, ProviderData } from '../utils'; + +export const ProviderActionsDropdownItems = ({ data }: ProviderActionsDropdownItemsProps) => { + const { t } = useForkliftTranslation(); + const { showModal } = useModal(); + + const { provider } = data; + + const providerURL = getResourceUrl({ + reference: ProviderModelRef, + name: provider?.metadata?.name, + namespace: provider?.metadata?.namespace, + }); + + return [ + {t('Edit Provider')}} + />, + showModal()} + > + {t('Delete Provider')} + , + ]; +}; + +interface ProviderActionsDropdownItemsProps { + data: ProviderData; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/actions/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/actions/index.ts new file mode 100644 index 000000000..1644a9b0f --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/actions/index.ts @@ -0,0 +1,4 @@ +// @index('./*.tsx', f => `export * from '${f.path}';`) +export * from './ProviderActionsDropdown'; +export * from './ProviderActionsDropdownItems'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/dynamic-plugin.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/dynamic-plugin.ts new file mode 100644 index 000000000..c1a8f6376 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/dynamic-plugin.ts @@ -0,0 +1,74 @@ +import { ProviderModel, ProviderModelGroupVersionKind } from '@kubev2v/types'; +import { EncodedExtension } from '@openshift/dynamic-plugin-sdk'; +import { + CreateResource, + ModelMetadata, + ResourceDetailsPage, + ResourceListPage, + ResourceNSNavItem, +} from '@openshift-console/dynamic-plugin-sdk'; +import type { ConsolePluginMetadata } from '@openshift-console/dynamic-plugin-sdk-webpack/lib/schema/plugin-package'; + +export const exposedModules: ConsolePluginMetadata['exposedModules'] = { + ProvidersListPage: './modules/ProvidersNG/views/list/ProvidersListPage', + ProviderDetailsPage: './modules/ProvidersNG/views/details/ProviderDetailsPage', + ProvidersCreatePage: './modules/ProvidersNG/views/create/ProvidersCreatePage', +}; + +export const extensions: EncodedExtension[] = [ + { + type: 'console.navigation/resource-ns', + properties: { + id: 'providers-ng', + insertAfter: 'importSeparator', + perspective: 'admin', + section: 'migration', + // t('plugin__forklift-console-plugin~Providers for virtualization') + name: '%plugin__forklift-console-plugin~Providers for virtualization%', + model: ProviderModelGroupVersionKind, + dataAttributes: { + 'data-quickstart-id': 'qs-nav-providers', + 'data-testid': 'providers-nav-item', + }, + }, + } as EncodedExtension, + + { + type: 'console.page/resource/list', + properties: { + component: { + $codeRef: 'ProvidersListPage', + }, + model: ProviderModelGroupVersionKind, + }, + } as EncodedExtension, + + { + type: 'console.page/resource/details', + properties: { + component: { + $codeRef: 'ProviderDetailsPage', + }, + model: ProviderModelGroupVersionKind, + }, + } as EncodedExtension, + + { + type: 'console.model-metadata', + properties: { + model: ProviderModelGroupVersionKind, + ...ProviderModel, + }, + } as EncodedExtension, + + { + type: 'console.resource/create', + properties: { + component: { + $codeRef: 'ProvidersCreatePage', + }, + model: ProviderModelGroupVersionKind, + ...ProviderModel, + }, + } as EncodedExtension, +]; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/__tests__/useProviders.test.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/__tests__/useProviders.test.ts new file mode 100644 index 000000000..efcffd404 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/__tests__/useProviders.test.ts @@ -0,0 +1,92 @@ +import { V1beta1Provider } from '@kubev2v/types'; + +import { getIsManaged, getIsSource, getIsTarget } from '../../utils'; + +describe('Provider Utils', () => { + describe('getIsManaged', () => { + it('should return true if the provider has owner references', () => { + const provider: V1beta1Provider = { + metadata: { + ownerReferences: [ + { + apiVersion: '', + kind: '', + name: '', + uid: '', + }, + ], + }, + apiVersion: '', + kind: '', + }; + + expect(getIsManaged(provider)).toBe(true); + }); + + it('should return false if the provider has no owner references', () => { + const provider: V1beta1Provider = { + metadata: {}, + apiVersion: '', + kind: '', + }; + + expect(getIsManaged(provider)).toBe(false); + }); + }); + + describe('getIsTarget', () => { + it('should return true if the provider type is included in TARGET_PROVIDER_TYPES', () => { + const provider: V1beta1Provider = { + metadata: {}, + apiVersion: '', + kind: '', + spec: { + type: 'openshift', + }, + }; + + expect(getIsTarget(provider)).toBe(true); + }); + + it('should return false if the provider type is not included in TARGET_PROVIDER_TYPES', () => { + const provider: V1beta1Provider = { + metadata: {}, + apiVersion: '', + kind: '', + spec: { + type: 'nonTargetType', + }, + }; + + expect(getIsTarget(provider)).toBe(false); + }); + }); + + describe('getIsSource', () => { + it('should return true if the provider type is included in SOURCE_PROVIDER_TYPES', () => { + const provider: V1beta1Provider = { + metadata: {}, + apiVersion: '', + kind: '', + spec: { + type: 'vsphere', + }, + }; + + expect(getIsSource(provider)).toBe(true); + }); + + it('should return false if the provider type is not included in SOURCE_PROVIDER_TYPES', () => { + const provider: V1beta1Provider = { + metadata: {}, + apiVersion: '', + kind: '', + spec: { + type: 'nonSourceType', + }, + }; + + expect(getIsSource(provider)).toBe(false); + }); + }); +}); diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/index.ts new file mode 100644 index 000000000..93dad0390 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/index.ts @@ -0,0 +1,9 @@ +// @index(['./*', /__/g], f => `export * from '${f.path}';`) +export * from './useGetDeleteAndEditAccessReview'; +export * from './useK8sWatchProviderNames'; +export * from './useK8sWatchSecretData'; +export * from './useProviderInventory'; +export * from './useProvidersInventoryList'; +export * from './useToggle'; +export * from './utils'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useGetDeleteAndEditAccessReview.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useGetDeleteAndEditAccessReview.ts new file mode 100644 index 000000000..2010f3f3a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useGetDeleteAndEditAccessReview.ts @@ -0,0 +1,67 @@ +import { K8sModel, useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; + +import { ProvidersPermissionStatus } from '../utils'; + +/** + * Type for the parameters of the useGetDeleteAndEditAccessReview custom hook. + * + * @typedef {Object} K8sModelAccessReviewParams + * @property {K8sModel} model - The Kubernetes model to check permissions on. + * @property {string} [name] - The name of the specific instance of the model, if any. + * @property {string} [namespace] - The namespace in which to review access permissions. + */ +interface K8sModelAccessReviewParams { + model: K8sModel; + name?: string; + namespace?: string; +} + +/** + * A React hook that checks permissions for different actions on a Kubernetes model within a specified namespace. + * @param {K8sModelAccessReviewParams} param0 - An object that contains model, name and namespace details. + * @returns {Object} An object containing permissions and a loading state. + */ +export const useGetDeleteAndEditAccessReview: UseAccessReviewFn = ({ model, name, namespace }) => { + const [canCreate, loadingCreate] = useAccessReview({ + group: model.apiGroup, + resource: model.plural, + verb: 'create', + namespace, + }); + + const [canPatch, loadingPatch] = useAccessReview({ + group: model.apiGroup, + resource: model.plural, + verb: 'patch', + name, + namespace, + }); + + const [canDelete, loadingDelete] = useAccessReview({ + group: model.apiGroup, + resource: model.plural, + verb: 'delete', + name, + namespace, + }); + + const [canGet, loadingGet] = useAccessReview({ + group: model.apiGroup, + resource: model.plural, + verb: 'get', + name, + namespace, + }); + + return { + canCreate, + canPatch, + canDelete, + canGet, + loading: loadingCreate || loadingPatch || loadingDelete || loadingGet, + }; +}; + +type UseAccessReviewFn = (props: K8sModelAccessReviewParams) => ProvidersPermissionStatus; + +export default useGetDeleteAndEditAccessReview; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useK8sWatchProviderNames.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useK8sWatchProviderNames.ts new file mode 100644 index 000000000..6eb1836c6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useK8sWatchProviderNames.ts @@ -0,0 +1,50 @@ +import { useEffect, useState } from 'react'; + +import { ProviderModelGroupVersionKind, V1beta1Provider } from '@kubev2v/types'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; + +/** + * Type for the return value of the useK8sWatchProviderNames hook. + */ +type K8sProvidersWatchResult = [string[] | undefined, boolean, Error | null]; + +/** + * React hook to watch Provider resources and only trigger re-renders when the providers `metadata.name` changes. + * + * @param {string} namespace - namespace to watch. + * @returns {K8sProvidersWatchResult} - An array of names. + */ +export const useK8sWatchProviderNames = ({ namespace }): K8sProvidersWatchResult => { + const [names, setNames] = useState(undefined); + const [namesLoaded, setLoaded] = useState(false); + const [namesLoadError, setLoadError] = useState(null); + + const [providers, providersLoaded, providersLoadError] = useK8sWatchResource({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + isList: true, + namespace, + }); + + useEffect(() => { + if (providersLoaded && providersLoadError) { + handleLoadError(providersLoadError); + } else if (providersLoaded) { + handleLoadedProviders(providers); + } + }, [providers, providersLoaded, providersLoadError]); + + const handleLoadError = (error: Error | null) => { + setLoadError(error); + setLoaded(true); + }; + + const handleLoadedProviders = (providers: V1beta1Provider[] | null) => { + setLoaded(true); + + const names = (providers || []).map((p) => p.metadata.name); + setNames(names.filter((n) => n)); + }; + + return [names, namesLoaded, namesLoadError]; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useK8sWatchSecretData.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useK8sWatchSecretData.ts new file mode 100644 index 000000000..887902679 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useK8sWatchSecretData.ts @@ -0,0 +1,45 @@ +import { useEffect, useState } from 'react'; + +import { V1Secret } from '@kubev2v/types'; +import { useK8sWatchResource, WatchK8sResource } from '@openshift-console/dynamic-plugin-sdk'; + +/** + * Type for the return value of the useK8sWatchSecretData hook. + */ +type K8sSecretWatchResult = [Record | undefined, boolean, Error | null]; + +/** + * React hook to watch a specific Kubernetes Secret resource and only trigger re-renders when the Secret's `data` changes. + * + * @param {WatchK8sResource} resourceParams - Parameters specifying the Kubernetes Secret to watch. + * @returns {K8sSecretWatchResult} - An array containing the Secret's data (or `null` if not loaded), a boolean indicating if the data has been loaded, and any error that occurred while loading. + */ +export const useK8sWatchSecretData = (resourceParams: WatchK8sResource): K8sSecretWatchResult => { + const [secretData, setSecretData] = useState | undefined>(undefined); + const [secretLoaded, setLoaded] = useState(false); + const [secretLoadError, setLoadError] = useState(null); + + const [secret, loaded, error] = useK8sWatchResource(resourceParams); + + useEffect(() => { + if (loaded && error) { + handleLoadError(error); + } else if (loaded) { + handleLoadedSecret(secret); + } + }, [secret, loaded, error]); + + const handleLoadError = (error: Error | null) => { + setLoadError(error); + setLoaded(true); + }; + + const handleLoadedSecret = (secret: V1Secret | null) => { + setLoaded(true); + if (JSON.stringify(secret?.data) !== JSON.stringify(secretData)) { + setSecretData(secret?.data); + } + }; + + return [secretData, secretLoaded, secretLoadError]; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useProviderInventory.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useProviderInventory.ts new file mode 100644 index 000000000..74da41e52 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useProviderInventory.ts @@ -0,0 +1,187 @@ +import { useEffect, useRef, useState } from 'react'; + +import { V1beta1Provider } from '@kubev2v/types'; +import { consoleFetchJSON } from '@openshift-console/dynamic-plugin-sdk'; + +import { + getCachedData, + getInventoryApiUrl, + hasObjectChangedInGivenFields, + setCachedData, +} from '../utils/helpers'; + +import { DEFAULT_FIELDS_TO_COMPARE } from './utils'; + +/** + * @typedef {Object} useProviderInventoryParams + * + * @property {V1beta1Provider} provider - The provider from which the inventory is fetched. + * @property {string} [subPath] - The sub path to be used in the inventory fetch API URL. + * @property {string[]} [fieldsToCompare] - The fields to compare to check if the inventory has changed. + * @property {number} [interval] - The polling interval in milliseconds. + * @property {number} [cacheExpiryDuration] - Duration in milliseconds till the cache remains valid. + */ +interface useProviderInventoryParams { + provider: V1beta1Provider; + subPath?: string; + fieldsToCompare?: string[]; + interval?: number; + cacheExpiryDuration?: number; +} + +/** + * @typedef {Object} useProviderInventoryResult + * + * @property {T | null} inventory - The fetched inventory. + * @property {boolean} loading - Whether the inventory fetch is in progress. + * @property {Error | null} error - The error occurred during inventory fetch. + */ +interface useProviderInventoryResult { + inventory: T | null; + loading: boolean; + error: Error | null; +} + +/** + * A React hook to fetch and cache inventory data from a provider. + * It fetches new data on mount and then at the specified interval. + * If the new data is the same as the old data (compared using the specified fields), + * it does not update the state to prevent unnecessary re-renders. + * + * @param {Object} useProviderInventoryParams Configuration parameters for the hook + * @param {Object} useProviderInventoryParams.provider Provider object to get inventory data from + * @param {string} [useProviderInventoryParams.subPath=''] Sub-path to append to the provider API URL + * @param {Array} [useProviderInventoryParams.fieldsToCompare=DEFAULT_FIELDS_TO_COMPARE] Fields to use for comparing new data with old data + * @param {number} [useProviderInventoryParams.interval=10000] Interval (in milliseconds) to fetch new data at + * @param {number} [useProviderInventoryParams.cacheExpiryDuration=60000] Duration (in milliseconds) to keep data in cache, if zero, don't use cache + * + * @returns {Object} useProviderInventoryResult Contains the inventory data (or null if loading, not fetched yet, or error), + * the loading state, and the error state (or null if no errors) + * + * @template T Type of the inventory data + */ +export const useProviderInventory = ({ + provider, + subPath = '', + fieldsToCompare = DEFAULT_FIELDS_TO_COMPARE, + interval = 10000, + cacheExpiryDuration = 0, // default cache validity is 0 seconds (don't use cache) +}: useProviderInventoryParams): useProviderInventoryResult => { + const [inventory, setInventory] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const oldDataRef = useRef(null); + const oldErrorRef = useRef(null); + + const cacheKey = + provider?.metadata?.uid && + `forklift_inventory_${provider.spec.type}_${provider.metadata.uid}${ + subPath ? `_${subPath}` : '' + }`; + + // Fetch cached data + useEffect(() => { + if (cacheExpiryDuration > 0) { + const fetchCachedData = async () => { + if (!isValidProvider(provider)) { + const e = new Error('Invalid provider data'); + handleError(e); + + return; + } + + const cachedData = getCachedData(cacheKey, cacheExpiryDuration); + if (cachedData) { + updateInventoryIfChanged(cachedData, fieldsToCompare); + } + }; + + fetchCachedData(); + } + }, [provider, subPath, interval, cacheExpiryDuration]); + + // Fetch data from API + useEffect(() => { + const fetchData = async () => { + if (!isValidProvider(provider)) { + const e = new Error('Invalid provider data'); + handleError(e); + + return; + } + + try { + const newInventory = await consoleFetchJSON( + getInventoryApiUrl( + `providers/${provider.spec.type}/${provider.metadata.uid}${ + subPath ? `/${subPath}` : '' + }`, + ), + ); + + updateInventoryIfChanged(newInventory, fieldsToCompare); + setCachedData(cacheKey, newInventory); + } catch (e) { + handleError(e); + } + }; + + fetchData(); + const intervalId = setInterval(fetchData, interval); + + return () => clearInterval(intervalId); + }, [provider, subPath, interval, cacheExpiryDuration]); + + /** + * Handles any errors thrown when trying to fetch the inventory. + * If the error is new (compared to the last error), + * it sets the error state and stops the loading state. + * + * @param {Error} e The error object to handle + * @returns {void} + */ + function handleError(e: Error): void { + if (e?.toString() !== oldErrorRef.current?.error) { + setError(e); + setLoading(false); + + oldErrorRef.current = { error: e?.toString() }; + } + } + + /** + * Checks if provider object is valid. + * @param {V1beta1Provider} provider - The provider object to be validated. + * @returns {boolean} - True if the provider object is valid, false otherwise. + */ + function isValidProvider(provider: V1beta1Provider): boolean { + return Boolean( + provider && provider.spec && provider.metadata && provider.spec.type && provider.metadata.uid, + ); + } + + /** + * Checks if the inventory data has changed and updates the inventory state if it has. + * Also updates the loading state. + * @param {T} newInventory - The new inventory data. + * @param {string[]} fieldsToCompare - The fields to compare to check if the inventory data has changed. + */ + function updateInventoryIfChanged(newInventory: T, fieldsToCompare: string[]): void { + const needReRender = hasObjectChangedInGivenFields({ + oldObject: oldDataRef.current?.inventory, + newObject: newInventory, + fieldsToCompare: fieldsToCompare, + }); + + if (needReRender) { + setInventory(newInventory); + setLoading(false); + + oldDataRef.current = { inventory: newInventory }; + } + } + + return { inventory, loading, error }; +}; + +export default useProviderInventory; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useProvidersInventoryList.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useProvidersInventoryList.ts new file mode 100644 index 000000000..665fe83ec --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useProvidersInventoryList.ts @@ -0,0 +1,139 @@ +import { useEffect, useRef, useState } from 'react'; + +import { ProviderInventory, ProvidersInventoryList } from '@kubev2v/types'; +import { consoleFetchJSON } from '@openshift-console/dynamic-plugin-sdk'; + +import { getInventoryApiUrl, hasObjectChangedInGivenFields } from '../utils'; + +import { DEFAULT_FIELDS_TO_COMPARE, INVENTORY_TYPES } from './utils'; + +/** + * Configuration parameters for useProvidersInventoryList hook. + * @interface + * @property {number} interval - Polling interval in milliseconds. + */ +interface UseInventoryParams { + interval?: number; // Polling interval in milliseconds +} + +/** + * The result object from useProvidersInventoryList hook. + * @interface + * @property {ProvidersInventoryList | null} inventory - The fetched inventory data, or null if loading, not fetched yet, or error. + * @property {boolean} loading - Indicates whether the inventory data is currently being fetched. + * @property {Error | null} error - Any error that occurred when fetching the inventory data, or null if no errors. + */ +interface UseInventoryResult { + inventory: ProvidersInventoryList | null; + loading: boolean; + error: Error | null; +} + +/** + * A React hook to fetch and maintain an up-to-date list of providers' inventory data. + * It fetches data on mount and then at the specified interval. + * + * @param {UseInventoryParams} params - Configuration parameters for the hook. + * @param {number} [params.interval=10000] - Interval (in milliseconds) to fetch new data at. + * + * @returns {UseInventoryResult} result - Contains the inventory data, the loading state, and the error state. + */ +export const useProvidersInventoryList = ({ + interval = 10000, +}: UseInventoryParams): UseInventoryResult => { + const [inventory, setInventory] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const oldDataRef = useRef(null); + const oldErrorRef = useRef(null); + + useEffect(() => { + const fetchData = async () => { + try { + const newInventory: ProvidersInventoryList = await consoleFetchJSON( + getInventoryApiUrl(`providers?detail=1`), + ); + + updateInventoryIfChanged(newInventory, DEFAULT_FIELDS_TO_COMPARE); + } catch (e) { + if (e?.toString() !== oldErrorRef.current?.error) { + oldErrorRef.current = { error: e?.toString() }; + setError(e as Error); + setLoading(false); + } + } + }; + + fetchData(); + + // Polling interval set by the passed parameter + const intervalId = setInterval(fetchData, interval); + return () => clearInterval(intervalId); + }, [interval]); + + /** + * Checks if there have been changes to any inventory items, and if so, + * updates the inventory list, sets the loading status to false, + * and updates the reference to the old data. + * + * @param newInventoryList - The new inventory list. + * @param fieldsToCompare - The fields to compare in order to determine + * if an inventory item has changed. + * + * @returns {void} + */ + function updateInventoryIfChanged( + newInventoryList: ProvidersInventoryList, + fieldsToCompare: string[], + ): void { + // Calculate total lengths of old and new inventories. + const oldTotalLength = INVENTORY_TYPES.reduce( + (total, type) => total + (oldDataRef.current?.inventoryList?.[type]?.length || 0), + 0, + ); + const newTotalLength = INVENTORY_TYPES.reduce( + (total, type) => total + (newInventoryList[type]?.length || 0), + 0, + ); + + const hasInventorySizeChanged = oldTotalLength !== newTotalLength; + let needReRender = hasInventorySizeChanged; + + // Test if inventory items changed + if (!hasInventorySizeChanged) { + const oldFlatInventory = INVENTORY_TYPES.flatMap( + (type) => oldDataRef.current?.inventoryList?.[type] || [], + ); + const newFlatInventory = INVENTORY_TYPES.flatMap( + (type) => newInventoryList[type] || [], + ); + + // Create maps of old and new inventories, using 'uid' as the key. + const oldInventoryMap = new Map(oldFlatInventory.map((item) => [item.uid, item])); + const newInventoryMap = new Map(newFlatInventory.map((item) => [item.uid, item])); + + for (const [uid, oldItem] of oldInventoryMap) { + const newItem = newInventoryMap.get(uid); + + // If a matching item is not found in the new list, or the item has changed, we need to re-render. + if ( + !newItem || + hasObjectChangedInGivenFields({ oldObject: oldItem, newObject: newItem, fieldsToCompare }) + ) { + needReRender = true; + break; + } + } + } + + if (needReRender) { + setInventory(newInventoryList); + setLoading(false); + oldDataRef.current = { inventoryList: newInventoryList }; + } + } + + return { inventory, loading, error }; +}; + +export default useProvidersInventoryList; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useToggle.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useToggle.ts new file mode 100644 index 000000000..78eb1729c --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/useToggle.ts @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +/** + * `useToggle` is a hook that manages a single boolean state value. + * It initializes the state to the `initialValue` provided and returns + * the current state and a function to toggle it. + * + * @param {boolean} initialValue - The initial state. + * @returns {Array} An array where the first element is the current state + * and the second element is a function to toggle the state. + * + * @example + * const [isOpen, toggleIsOpen] = useToggle(false); + * // To toggle the isOpen state + * toggleIsOpen(); + */ +export const useToggle = (initialValue = false): [boolean, () => void] => { + const [value, setIsOpen] = useState(initialValue); + + const toggle = () => { + setIsOpen((v) => !v); + }; + + return [value, toggle]; +}; + +export default useToggle; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/utils/constants.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/utils/constants.ts new file mode 100644 index 000000000..4d57ee4b6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/utils/constants.ts @@ -0,0 +1,19 @@ +import { ProviderType } from '@kubev2v/types'; + +export const DEFAULT_FIELDS_TO_COMPARE = [ + 'vmCount', + 'networkCount', + 'storageClassCount', + 'regionCount', + 'projectCount', + 'imageCount', + 'volumeCount', + 'volumeTypeCount', + 'datacenterCount', + 'clusterCount', + 'hostCount', + 'storageDomainCount', + 'datastoreCount', +]; + +export const INVENTORY_TYPES: ProviderType[] = ['openshift', 'openstack', 'ovirt', 'vsphere']; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/utils/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/utils/index.ts new file mode 100644 index 000000000..ae755f3bd --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/hooks/utils/index.ts @@ -0,0 +1,3 @@ +// @index('./*.ts', f => `export * from '${f.path}';`) +export * from './constants'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/DeleteModal/DeleteModal.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/DeleteModal/DeleteModal.tsx new file mode 100644 index 000000000..dd26bef28 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/DeleteModal/DeleteModal.tsx @@ -0,0 +1,110 @@ +import React, { ReactNode, useCallback, useState } from 'react'; +import { Trans } from 'react-i18next'; +import { useHistory } from 'react-router-dom'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { + k8sDelete, + K8sGroupVersionKind, + K8sModel, + K8sResourceCommon, +} from '@openshift-console/dynamic-plugin-sdk'; +import { Button, Modal, ModalVariant } from '@patternfly/react-core'; + +import { getResourceUrl } from '../../utils'; +import { AlertMessageForModals, ItemIsOwnedAlert } from '../components'; +import { useModal } from '../ModalHOC'; + +/** + * Props for the DeleteModal component + * @typedef DeleteModalProps + * @property {string} title - The title to display in the modal + * @property {K8sResourceCommon} resource - The resource object to delete + * @property {K8sModel} model - The model used for deletion + * @property {string} [redirectTo] - Optional redirect URL after deletion + */ +interface DeleteModalProps { + resource: K8sResourceCommon; + model: K8sModel; + title?: string; + redirectTo?: string; +} + +/** + * A generic delete modal component + * @component + * @param {DeleteModalProps} props - Props for DeleteModal + * @returns {React.Element} The DeleteModal component + */ +export const DeleteModal: React.FC = ({ title, resource, model, redirectTo }) => { + const { t } = useForkliftTranslation(); + const { toggleModal } = useModal(); + const history = useHistory(); + const [alertMessage, setAlertMessage] = useState(null); + + const title_ = title || t('Delete {{model.label}}', { model }); + const { name, namespace } = resource?.metadata || {}; + const owner = resource?.metadata?.ownerReferences?.[0]; + const groupVersionKind: K8sGroupVersionKind = { + group: model.apiGroup, + version: model.apiVersion, + kind: model.kind, + }; + + const onDelete = useCallback(async () => { + const isOnResourcePage = () => { + const re = new RegExp(`/${name}(/|$)`); + return re.test(window.location.pathname); + }; + + try { + await k8sDelete({ model, resource }); + if (redirectTo) { + history.push(redirectTo); + } else if (isOnResourcePage()) { + history.push(getResourceUrl({ groupVersionKind, namespace })); + } + + toggleModal(); + } catch (err) { + setAlertMessage(); + } + }, [resource]); + + const actions = [ + , + , + ]; + + return ( + + {namespace ? ( + + Are you sure you want to delete{' '} + {{ resourceName: name }} in namespace{' '} + {{ namespace: namespace }}? + + ) : ( + + Are you sure you want to delete{' '} + {{ resourceName: name }}? + + )} + {typeof owner === 'object' && } + {alertMessage} + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/DeleteModal/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/DeleteModal/index.ts new file mode 100644 index 000000000..17ef7fdc7 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/DeleteModal/index.ts @@ -0,0 +1,3 @@ +// @index('./*.tsx', f => `export * from '${f.path}';`) +export * from './DeleteModal'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/EditModal.style.css b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/EditModal.style.css new file mode 100644 index 000000000..355fb3329 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/EditModal.style.css @@ -0,0 +1,4 @@ +.forklift-edit-modal-body { + margin: 0; + padding-bottom: var(--pf-global--spacer--md); +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/EditModal.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/EditModal.tsx new file mode 100644 index 000000000..c7e0ea548 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/EditModal.tsx @@ -0,0 +1,185 @@ +import React, { ReactNode, useCallback, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { + Button, + Form, + FormGroup, + Modal, + ModalVariant, + Popover, + TextInput, +} from '@patternfly/react-core'; +import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; + +import { getValueByJsonPath } from '../../utils'; +import { AlertMessageForModals, ItemIsOwnedAlert } from '../components'; +import { useModal } from '../ModalHOC'; + +import { defaultOnConfirm } from './utils/defaultOnConfirm'; +import { EditModalProps, ValidationResults } from './types'; + +import './EditModal.style.css'; + +/** + * `EditModal` is a React Functional Component that allows editing a Kubernetes resource property inside a modal. + * + * @component + * @param {object} props - The properties that define the behavior and display of the `EditModal`. + * @param {K8sResourceCommon} props.resource - The Kubernetes resource that will be modified. + * @param {K8sModel} props.model - The model for the Kubernetes resource. + * @param {string | string[]} props.jsonPath - The JSON path to the property in the resource that will be modified. + * @param {string} props.title - The title of the modal. + * @param {string} props.label - The label of the field being edited. + * @param {ReactNode} [props.body] - The body content of the modal. + * @param {ReactNode} [props.headerContent] - The help popup header content of the input field. + * @param {ReactNode} [props.bodyContent] - The help popup content in the body of the input field. + * @param {'small' | 'default' | 'medium' | 'large'} [props.variant] - The size of the modal. + * @param {OnConfirmHookType} [props.onConfirmHook] - A hook that gets called when the user confirms the edit. + * @param {ModalInputComponentType} [props.InputComponent] - The component used for the input field. + * @param {string} [props.helperText] - Helper text that will be displayed under the input field. + * @param {string} [props.redirectTo] - The path to redirect to after the modal is closed. + * @param {ValidationHookType} [props.validationHook] - A hook that is used to validate the new value. + * + * @returns {ReactElement} Returns a `Modal` React Element that renders the modal. + */ +export const EditModal: React.FC = ({ + title, + body, + label, + headerContent, + bodyContent, + resource, + jsonPath, + model, + InputComponent, + helperText, + variant, + redirectTo, + onConfirmHook = defaultOnConfirm, + validationHook, +}) => { + const { t } = useForkliftTranslation(); + const { toggleModal } = useModal(); + const history = useHistory(); + const [alertMessage, setAlertMessage] = useState(null); + const [value, setValue] = useState(getValueByJsonPath(resource, jsonPath) as string); + const [validation, setValidation] = useState<{ + helperText: string; + validated: ValidationResults; + }>({ helperText: '', validated: undefined }); + + const { namespace } = resource?.metadata || {}; + const owner = resource?.metadata?.ownerReferences?.[0]; + + /** + * Handles value change. + */ + const handleValueChange = (newValue: string) => { + setValue(newValue); + + if (validationHook) { + const validationResult = validationHook(newValue); + setValidation({ + helperText: validationResult.validationHelpText, + validated: validationResult.validated, + }); + } + }; + + /** + * Handles save action. + */ + const handleSave = useCallback(async () => { + try { + await onConfirmHook({ resource, jsonPath, model, newValue: value }); + + if (redirectTo) { + history.push(redirectTo); + } + + toggleModal(); + } catch (err) { + setAlertMessage( + , + ); + } + }, [resource, value]); + + /** + * LabelIcon is a (?) icon that triggers a Popover component when clicked. + */ + const LabelIcon = headerContent && bodyContent && ( + + + + ); + + /** + * InputComponent_ is a higher-order component that renders either the passed-in InputComponent, or a default TextInput, + */ + const InputComponent_ = InputComponent ? ( + handleValueChange(value)} /> + ) : ( + handleValueChange(value)} + validated={validation.validated} + /> + ); + + const actions = [ + , + , + ]; + + return ( + +
{body}
+ + + + {typeof owner === 'object' && } + {alertMessage} +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/index.ts new file mode 100644 index 000000000..e8fe77e78 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/index.ts @@ -0,0 +1,4 @@ +// @index('./*.tsx', f => `export * from '${f.path}';`) +export * from './EditModal'; +export * from './types'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/types.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/types.tsx new file mode 100644 index 000000000..87977b3fa --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/types.tsx @@ -0,0 +1,105 @@ +import React, { ReactNode } from 'react'; + +import { K8sModel, K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; + +import './EditModal.style.css'; + +export interface EditModalProps { + /** The Kubernetes resource being edited. This object contains all the information about the Kubernetes resource including its metadata, status, and spec. */ + resource: K8sResourceCommon; + + /** The model of the Kubernetes resource. This object contains the information about the Kubernetes kind, apiVersion, and other model specific details. */ + model: K8sModel; + + /** The JSON path of the value in the resource object that needs to be edited. This can either be a string or an array of strings. */ + jsonPath: string | string[]; + + /** The title of the modal that will be displayed at the top. */ + title: string; + + /** The label of the form input field. */ + label: string; + + /** Optional. The content to be displayed in the modal's body. */ + body?: ReactNode; + + /** Optional. The header of the field help popup. */ + headerContent?: ReactNode; + + /** Optional. The content of the field help popup. */ + bodyContent?: ReactNode; + + /** Optional. The size variant of the modal. Can be 'small', 'default', 'medium', or 'large'. */ + variant?: 'small' | 'default' | 'medium' | 'large'; + + /** Optional. The custom input component to be used in the form. If not provided, a default TextInput will be used. */ + InputComponent?: ModalInputComponentType; + + /** Optional. Helper text that provides additional hints to the user, printed in grayed text under the input field. */ + helperText?: string; + + /** Optional. The URL to which the user will be redirected after the confirmation action. */ + redirectTo?: string; + + /** Optional. The hook function to be called when the confirmation button is clicked. */ + onConfirmHook?: OnConfirmHookType; + + /** Optional. The validation hook function that checks the new input value and returns a helper text and validation status. */ + validationHook?: ValidationHookType; +} + +/** + * ValidationResults type defines the possible states of a validation result. + * 'success' means the validation was successful, + * 'error' means the validation failed, + * 'warning' indicates a potential issue but not a failure, + * undefined indicates no validation state is set. + */ +export type ValidationResults = 'success' | 'error' | 'warning' | undefined; + +/** + * ModalInputComponentType defines the functional component type for the input fields used in the modal. + * It accepts two props: + * 'value' which can be a string or a number, + * and 'onChange' a callback function which is triggered when the value of the input changes. + */ +export type ModalInputComponentType = React.FC<{ + value: string | number; + onChange: (value: string) => void; +}>; + +/** + * ValidationHookType defines the structure of a hook function that performs validation. + * It accepts a value, which can be a string or a number, and returns an object containing + * 'validationHelpText' which is a string giving details about the result of the validation, + * and 'validated' which indicates the status of the validation and is of type ValidationResults. + */ +export interface ValidationHookType { + (value: string | number): { + validationHelpText: string; + validated: ValidationResults; + }; +} + +/** + * OnConfirmHookType defines the structure of a hook function that is called when the confirmation action takes place. + * It accepts an object as an argument with four fields: + * 'resource' which is the Kubernetes resource being modified, + * 'newValue' which is the updated value for the resource, + * 'jsonPath' is the path in the JSON representation of the resource where the new value is applied, + * 'model' represents the model of the Kubernetes resource. + * The function returns a promise that resolves to the updated Kubernetes resource. + */ +export interface OnConfirmHookType { + ({ + resource, + newValue, + jsonPath, + model, + }: { + resource: K8sResourceCommon; + newValue: unknown; + jsonPath?: string | string[]; + model?: K8sModel; + }): Promise; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/utils/defaultOnConfirm.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/utils/defaultOnConfirm.ts new file mode 100644 index 000000000..7c9844f01 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditModal/utils/defaultOnConfirm.ts @@ -0,0 +1,19 @@ +import { getValueByJsonPath, jsonPathToPatch } from 'src/modules/ProvidersNG/utils'; + +import { k8sPatch } from '@openshift-console/dynamic-plugin-sdk'; + +export const defaultOnConfirm = async ({ resource, jsonPath, model, newValue: value }) => { + const op = getValueByJsonPath(resource, jsonPath) ? 'replace' : 'add'; + + await k8sPatch({ + model: model, + resource: resource, + data: [ + { + op, + path: jsonPathToPatch(jsonPath), + value: value, + }, + ], + }); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderDefaultTransferNetwork/EditProviderDefaultTransferNetwork.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderDefaultTransferNetwork/EditProviderDefaultTransferNetwork.tsx new file mode 100644 index 000000000..90c3c7d82 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderDefaultTransferNetwork/EditProviderDefaultTransferNetwork.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import { V1beta1Provider } from '@kubev2v/types'; +import { Modify } from '@kubev2v/types'; +import { K8sModel } from '@openshift-console/dynamic-plugin-sdk/lib/api/core-api'; + +import { EditModalProps } from '../EditModal'; + +import { OpenshiftEditProviderDefaultTransferNetwork } from './OpenshiftEditProviderDefaultTransferNetwork'; + +export type EditProviderDefaultTransferNetworkProps = Modify< + EditModalProps, + { + resource: V1beta1Provider; + title?: string; + label?: string; + model?: K8sModel; + jsonPath?: string | string[]; + } +>; + +export const EditProviderDefaultTransferNetwork: React.FC< + EditProviderDefaultTransferNetworkProps +> = (props) => { + switch (props.resource?.spec?.type) { + case 'openshift': + return ; + default: + return <>; + } +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderDefaultTransferNetwork/OpenshiftEditProviderDefaultTransferNetwork.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderDefaultTransferNetwork/OpenshiftEditProviderDefaultTransferNetwork.tsx new file mode 100644 index 000000000..504d1e3b5 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderDefaultTransferNetwork/OpenshiftEditProviderDefaultTransferNetwork.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { OpenShiftNetworkAttachmentDefinition, ProviderModel } from '@kubev2v/types'; +import { k8sPatch } from '@openshift-console/dynamic-plugin-sdk'; +import { Dropdown, DropdownItem, DropdownToggle } from '@patternfly/react-core'; + +import { useProviderInventory, useToggle } from '../../hooks'; +import { EditModal, ModalInputComponentType, OnConfirmHookType } from '../EditModal'; + +import { EditProviderDefaultTransferNetworkProps } from './EditProviderDefaultTransferNetwork'; + +/** + * Handles the confirmation action for editing a resource annotations. + * Adds or updates the 'forklift.konveyor.io/defaultTransferNetwork' annotation in the resource's metadata. + * + * @param {Object} options - Options for the confirmation action. + * @param {Object} options.resource - The resource to be modified. + * @param {Object} options.model - The model associated with the resource. + * @param {any} options.newValue - The new value for the 'forklift.konveyor.io/defaultTransferNetwork' annotation. + * @returns {Promise} - The modified resource. + */ +const onConfirm: OnConfirmHookType = async ({ resource, model, newValue: value }) => { + const currentAnnotations = resource?.metadata?.annotations; + const newAnnotations = { + ...currentAnnotations, + 'forklift.konveyor.io/defaultTransferNetwork': value || undefined, + }; + + const op = resource?.metadata?.annotations ? 'replace' : 'add'; + + const obj = await k8sPatch({ + model: model, + resource: resource, + data: [ + { + op, + path: '/metadata/annotations', + value: newAnnotations, + }, + ], + }); + + return obj; +}; + +export const OpenshiftNetworksInputFactory: ({ resource }) => ModalInputComponentType = ({ + resource: provider, +}) => { + // eslint-disable-next-line react/display-name, react/prop-types + return ({ value, onChange }) => { + const [isOpen, onToggle] = useToggle(false); + const { inventory: networks } = useProviderInventory({ + provider, + // eslint-disable-next-line @cspell/spellchecker + subPath: 'networkattachmentdefinitions?detail=4', + }); + + const name = getNetworkName(value); + + const dropdownItems = [ + onChange('')} + > + {'Pod network'} + , + ...(networks || []).map((n) => ( + onChange(`${n.namespace}/${n.name}`)} + > + {n.name} + + )), + ]; + + return ( + + {name} + + } + isOpen={isOpen} + dropdownItems={dropdownItems} + menuAppendTo="parent" + /> + ); + }; +}; + +export const OpenshiftEditProviderDefaultTransferNetwork: React.FC< + EditProviderDefaultTransferNetworkProps +> = (props) => { + const { t } = useForkliftTranslation(); + + return ( + + ); +}; + +/** + * Extracts the network name from a string. The input string can be of the form 'name' or 'namespace/name'. + * + * @param {string} value - The input string from which the network name is to be extracted. + * @returns {string} The network name extracted from the input string. + */ +function getNetworkName(value: string | number): string { + if (!value || typeof value !== 'string') { + return 'Pod network'; + } + + const parts = value.split('/'); + return parts[parts.length - 1]; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderDefaultTransferNetwork/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderDefaultTransferNetwork/index.ts new file mode 100644 index 000000000..26733d7b7 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderDefaultTransferNetwork/index.ts @@ -0,0 +1,4 @@ +// @index('./*.tsx', f => `export * from '${f.path}';`) +export * from './EditProviderDefaultTransferNetwork'; +export * from './OpenshiftEditProviderDefaultTransferNetwork'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/EditProviderURLModal.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/EditProviderURLModal.tsx new file mode 100644 index 000000000..2146c9b9b --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/EditProviderURLModal.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import { Modify } from '@kubev2v/types'; +import { V1beta1Provider } from '@kubev2v/types'; +import { K8sModel } from '@openshift-console/dynamic-plugin-sdk/lib/api/core-api'; + +import { EditModalProps } from '../EditModal'; + +import { OpenshiftEditURLModal } from './OpenshiftEditURLModal'; +import { OpenstackEditURLModal } from './OpenstackEditURLModal'; +import { OvirtEditURLModal } from './OvirtEditURLModal'; +import { VSphereEditURLModal } from './VSphereEditURLModal'; + +export type EditProviderURLModalProps = Modify< + EditModalProps, + { + resource: V1beta1Provider; + title?: string; + label?: string; + model?: K8sModel; + jsonPath?: string | string[]; + } +>; + +export const EditProviderURLModal: React.FC = (props) => { + switch (props.resource?.spec?.type) { + case 'ovirt': + return ; + case 'openshift': + return ; + case 'openstack': + return ; + case 'vsphere': + return ; + default: + return <>; + } +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/OpenshiftEditURLModal.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/OpenshiftEditURLModal.tsx new file mode 100644 index 000000000..42a691d09 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/OpenshiftEditURLModal.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ProviderModel } from '@kubev2v/types'; +import { ModalVariant } from '@patternfly/react-core'; + +import { validateURL } from '../../utils'; +import { EditModal, ValidationHookType } from '../EditModal'; + +import { patchProviderURL } from './utils/patchProviderURL'; +import { EditProviderURLModalProps } from './EditProviderURLModal'; + +export const OpenshiftEditURLModal: React.FC = (props) => { + const { t } = useForkliftTranslation(); + + const urlValidationHook: ValidationHookType = (value) => { + const isValidURL = validateURL(value.toString().trim()); + + return isValidURL + ? { + validationHelpText: undefined, + validated: 'success', + } + : { + validationHelpText: t( + 'URL must start with https:// or http:// and contain valid hostname and path', + ), + validated: 'error', + }; + }; + + return ( + :6443 for OpenShift.', + )} + helperText={t( + 'Please enter URL for the kubernetes API server, if empty URL default to this cluster.', + )} + validationHook={urlValidationHook} + onConfirmHook={patchProviderURL} + /> + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/OpenstackEditURLModal.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/OpenstackEditURLModal.tsx new file mode 100644 index 000000000..650b4503a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/OpenstackEditURLModal.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ProviderModel } from '@kubev2v/types'; +import { ModalVariant } from '@patternfly/react-core'; + +import { validateURL } from '../../utils'; +import { EditModal, ValidationHookType } from '../EditModal'; + +import { patchProviderURL } from './utils/patchProviderURL'; +import { EditProviderURLModalProps } from './EditProviderURLModal'; + +export const OpenstackEditURLModal: React.FC = (props) => { + const { t } = useForkliftTranslation(); + + const urlValidationHook: ValidationHookType = (value) => { + const isValidURL = validateURL(value.toString().trim()); + + return isValidURL + ? { + validationHelpText: undefined, + validated: 'success', + } + : { + validationHelpText: t( + 'URL must start with https:// or http:// and contain valid hostname and path', + ), + validated: 'error', + }; + }; + + return ( + /v3 for OpenStack.', + )} + helperText={t('Please enter URL for OpenStack services REST APIs.')} + onConfirmHook={patchProviderURL} + validationHook={urlValidationHook} + /> + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/OvirtEditURLModal.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/OvirtEditURLModal.tsx new file mode 100644 index 000000000..8f28d2eb7 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/OvirtEditURLModal.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ProviderModel } from '@kubev2v/types'; +import { ModalVariant } from '@patternfly/react-core'; + +import { validateURL } from '../../utils'; +import { EditModal, ValidationHookType } from '../EditModal'; + +import { patchProviderURL } from './utils/patchProviderURL'; +import { EditProviderURLModalProps } from './EditProviderURLModal'; + +export const OvirtEditURLModal: React.FC = (props) => { + const { t } = useForkliftTranslation(); + + const urlValidationHook: ValidationHookType = (value) => { + const isValidURL = validateURL(value.toString().trim()); + + return isValidURL + ? { + validationHelpText: undefined, + validated: 'success', + } + : { + validationHelpText: t( + 'URL must start with https:// or http:// and contain valid hostname and path', + ), + validated: 'error', + }; + }; + + return ( + /ovirt-engine/api/ for RHV.', + )} + helperText={t('Please enter the URL for oVirt engine server.')} + onConfirmHook={patchProviderURL} + validationHook={urlValidationHook} + /> + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/VSphereEditURLModal.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/VSphereEditURLModal.tsx new file mode 100644 index 000000000..637a0eae2 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/VSphereEditURLModal.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ProviderModel } from '@kubev2v/types'; +import { ModalVariant } from '@patternfly/react-core'; + +import { validateURL } from '../../utils'; +import { EditModal, ValidationHookType } from '../EditModal'; + +import { patchProviderURL } from './utils/patchProviderURL'; +import { EditProviderURLModalProps } from './EditProviderURLModal'; + +export const VSphereEditURLModal: React.FC = (props) => { + const { t } = useForkliftTranslation(); + + const urlValidationHook: ValidationHookType = (value) => { + const isValidURL = validateURL(value.toString().trim()); + + return isValidURL + ? { + validationHelpText: undefined, + validated: 'success', + } + : { + validationHelpText: t( + 'URL must start with https:// or http:// and contain valid hostname and path', + ), + validated: 'error', + }; + }; + + return ( + /sdk for vSphere.', + )} + helperText={t('Please enter URL for vSphere REST APIs server.')} + onConfirmHook={patchProviderURL} + validationHook={urlValidationHook} + /> + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/index.ts new file mode 100644 index 000000000..4db42c955 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/index.ts @@ -0,0 +1,7 @@ +// @index('./*.tsx', f => `export * from '${f.path}';`) +export * from './EditProviderURLModal'; +export * from './OpenshiftEditURLModal'; +export * from './OpenstackEditURLModal'; +export * from './OvirtEditURLModal'; +export * from './VSphereEditURLModal'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/utils/patchProviderURL.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/utils/patchProviderURL.ts new file mode 100644 index 000000000..a105883f3 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderURL/utils/patchProviderURL.ts @@ -0,0 +1,59 @@ +import { Base64 } from 'js-base64'; + +import { SecretModel, V1beta1Provider, V1Secret } from '@kubev2v/types'; +import { k8sPatch } from '@openshift-console/dynamic-plugin-sdk'; + +import { OnConfirmHookType } from '../../EditModal'; + +/** + * Handles the confirmation action for editing a resource annotations. + * Adds or updates the 'forklift.konveyor.io/defaultTransferNetwork' annotation in the resource's metadata. + * + * @param {Object} options - Options for the confirmation action. + * @param {Object} options.resource - The resource to be modified. + * @param {Object} options.model - The model associated with the resource. + * @param {any} options.newValue - The new value for the 'forklift.konveyor.io/defaultTransferNetwork' annotation. + * @returns {Promise} - The modified resource. + */ +export const patchProviderURL: OnConfirmHookType = async ({ resource, model, newValue: value }) => { + const provider: V1beta1Provider = resource as V1beta1Provider; + const providerOp = provider?.spec?.url ? 'replace' : 'add'; + + // Get providers secret stub + const secret: V1Secret = { + kind: 'Secret', + apiVersion: 'v1', + metadata: { + name: provider?.spec?.secret?.name, + namespace: provider?.spec?.secret?.namespace, + }, + }; + + // Patch provider secret + await k8sPatch({ + model: SecretModel, + resource: secret, + data: [ + { + op: providerOp, // assume secret and provider has the same url + path: '/data/url', + value: Base64.encode(value.toString().trim()), + }, + ], + }); + + // Patch provider URL + const obj = await k8sPatch({ + model: model, + resource: provider, + data: [ + { + op: providerOp, + path: '/spec/url', + value: value.toString().trim(), + }, + ], + }); + + return obj; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderVDDKImage/EditProviderVDDKImage.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderVDDKImage/EditProviderVDDKImage.tsx new file mode 100644 index 000000000..f7105f987 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderVDDKImage/EditProviderVDDKImage.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import { Modify } from '@kubev2v/types'; +import { V1beta1Provider } from '@kubev2v/types'; +import { K8sModel } from '@openshift-console/dynamic-plugin-sdk/lib/api/core-api'; + +import { EditModalProps } from '../EditModal'; + +import { VSPhereEditProviderVDDKImage } from './VSphereEditProviderVDDKImage'; + +export type EditProviderVDDKImageProps = Modify< + EditModalProps, + { + resource: V1beta1Provider; + title?: string; + label?: string; + model?: K8sModel; + jsonPath?: string | string[]; + } +>; + +export const EditProviderVDDKImage: React.FC = (props) => { + switch (props.resource?.spec?.type) { + case 'vsphere': + return ; + default: + return <>; + } +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderVDDKImage/VSphereEditProviderVDDKImage.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderVDDKImage/VSphereEditProviderVDDKImage.tsx new file mode 100644 index 000000000..f1bb56aaf --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderVDDKImage/VSphereEditProviderVDDKImage.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ProviderModel } from '@kubev2v/types'; + +import { validateContainerImage } from '../../utils'; +import { EditModal, ValidationHookType } from '../EditModal'; + +import { EditProviderVDDKImageProps } from './EditProviderVDDKImage'; + +export const VSPhereEditProviderVDDKImage: React.FC = (props) => { + const { t } = useForkliftTranslation(); + + const imageValidationHook: ValidationHookType = (value) => { + const isValidImage = validateContainerImage(value.toString().trim()); + + return isValidImage + ? { + validationHelpText: undefined, + validated: 'success', + } + : { + validationHelpText: t( + 'VDDK Init Image must be a valid container image, for example quay.io/kubev2v/example:latest', + ), + validated: 'error', + }; + }; + + return ( + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderVDDKImage/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderVDDKImage/index.ts new file mode 100644 index 000000000..d4ecbdbf8 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/EditProviderVDDKImage/index.ts @@ -0,0 +1,4 @@ +// @index('./*.tsx', f => `export * from '${f.path}';`) +export * from './EditProviderVDDKImage'; +export * from './VSphereEditProviderVDDKImage'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/ModalHOC/ModalHOC.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/ModalHOC/ModalHOC.tsx new file mode 100644 index 000000000..1ba11440d --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/ModalHOC/ModalHOC.tsx @@ -0,0 +1,67 @@ +import React, { createContext, ReactNode, useCallback, useContext, useState } from 'react'; + +import { useToggle } from '../../hooks'; + +/** + * A provider component that wraps its children with the modal context. + * + * @example + * // Usage: + * + * + * + * + * // In your child components, you can use the useModal hook to access showModal and toggleModal: + * const { showModal, toggleModal } = useModal(); + * showModal(); + * // To close the modal, call toggleModal(). + * + * @param {ModalHOCProps} props - The component props. + * @param {ReactNode} props.children - The children components to be wrapped. + * @returns {JSX.Element} The JSX element representing the ModalProvider. + */ +export const ModalHOC: React.FC = ({ children }) => { + const [modalComponent, setModalComponent] = useState(null); + const [isModalOpen, toggleModal] = useToggle(); + + const showModal = useCallback( + (modal) => { + setModalComponent(modal); + toggleModal(); + }, + [toggleModal], + ); + + return ( + + {children} + {isModalOpen && modalComponent} + + ); +}; + +/** + * A custom hook that provides access to the Forklift modal context. + * + * @returns {ModalContextType} The modal context object. + * @throws {Error} If used outside of the ModalProvider. + */ +export const useModal = (): ModalContextType => { + const context = useContext(ModalContext); + if (!context) { + throw new Error('useModal must be used within a ModalProvider'); + } + return context; +}; + +export interface ModalContextType { + showModal: (modal: ReactNode) => void; + toggleModal: () => void; +} + +export interface ModalHOCProps { + children: ReactNode; +} + +// Creating the context. +const ModalContext = createContext(undefined); diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/ModalHOC/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/ModalHOC/index.ts new file mode 100644 index 000000000..be2ba218e --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/ModalHOC/index.ts @@ -0,0 +1,3 @@ +// @index('./*.tsx', f => `export * from '${f.path}';`) +export * from './ModalHOC'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/components/AlertMessageForModals.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/components/AlertMessageForModals.tsx new file mode 100644 index 000000000..059a8a019 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/components/AlertMessageForModals.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +import { Alert } from '@patternfly/react-core'; + +export const AlertMessageForModals: React.FC<{ title: string; message: string }> = ({ + title, + message, +}) => ( + + {message} + +); diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/components/ItemIsOwnedAlert.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/components/ItemIsOwnedAlert.tsx new file mode 100644 index 000000000..0ba2bd2be --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/components/ItemIsOwnedAlert.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Trans } from 'react-i18next'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { + getGroupVersionKindForResource, + OwnerReference, + ResourceLink, +} from '@openshift-console/dynamic-plugin-sdk'; +import { Alert } from '@patternfly/react-core'; + +interface ItemIsOwnedAlertProps { + owner: OwnerReference; + namespace: string; +} + +export const ItemIsOwnedAlert: React.FC = ({ owner, namespace }) => { + const { t } = useForkliftTranslation(); + + return ( + + + This resource is managed by{' '} + {' '} + and any modifications may be overwritten. Edit the managing resource to preserve changes. + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/components/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/components/index.ts new file mode 100644 index 000000000..7fcd1fe4a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/components/index.ts @@ -0,0 +1,4 @@ +// @index('./*.tsx', f => `export * from '${f.path}';`) +export * from './AlertMessageForModals'; +export * from './ItemIsOwnedAlert'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/index.ts new file mode 100644 index 000000000..570d40d6d --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/modals/index.ts @@ -0,0 +1,9 @@ +// @index(['./*', /__/g], f => `export * from '${f.path}';`) +export * from './components'; +export * from './DeleteModal'; +export * from './EditModal'; +export * from './EditProviderDefaultTransferNetwork'; +export * from './EditProviderURL'; +export * from './EditProviderVDDKImage'; +export * from './ModalHOC'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/DetailItem.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/DetailItem.tsx new file mode 100644 index 000000000..dfcd0aaf6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/DetailItem.tsx @@ -0,0 +1,158 @@ +import React, { ReactNode } from 'react'; + +import { ExternalLink } from '@kubev2v/common'; +import { + Breadcrumb, + BreadcrumbItem, + Button, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + DescriptionListTermHelpText, + DescriptionListTermHelpTextButton, + Flex, + FlexItem, + Popover, +} from '@patternfly/react-core'; +import Pencil from '@patternfly/react-icons/dist/esm/icons/pencil-alt-icon'; + +/** + * Component for displaying a details item. + * It can optionally include a help text popover, breadcrumbs, and an edit button. + * + * @component + * @param {DetailsItemProps} props - The props of the details item. + */ +export const DetailsItem: React.FC = ({ + title, + content, + helpContent, + moreInfoLabel, + moreInfoLink, + crumbs, + onEdit, +}) => { + return ( + + {helpContent ? ( + + ) : ( + + )} + {onEdit ? ( + + ) : ( + + )} + + ); +}; + +/** + * Component for displaying title with help text in a popover. + * + * @component + */ +export const DescriptionTitleWithHelp: React.FC<{ + title: string; + helpContent: ReactNode; + moreInfoLabel?: string; + moreInfoLink?: string; + crumbs?: string[]; +}> = ({ title, helpContent, crumbs, moreInfoLabel = 'More info:', moreInfoLink }) => ( + + {title}} + bodyContent={ + + {helpContent} + + {moreInfoLink && ( + + {moreInfoLabel}{' '} + + {moreInfoLink} + + . + + )} + + {crumbs && crumbs.length > 0 && ( + + + {crumbs.map((c) => ( + {c} + ))} + + + )} + + } + > + {title} + + +); + +/** + * Component for displaying title. + * + * @component + */ +export const DescriptionTitle: React.FC<{ title: string }> = ({ title }) => ( + {title} +); + +/** + * Component for displaying an inline link button with editable content. + * + * @component + * @param {ReactNode} content - The content of the button. + * @param {Function} onEdit - Function to be called when the button is clicked. + */ +export const EditableContentButton: React.FC<{ content: ReactNode; onEdit: () => void }> = ({ + content, + onEdit, +}) => ( + +); + +/** + * Component for displaying a non-editable content. + * + * @component + * @param {ReactNode} content - The content of the description. + */ +export const NonEditableContent: React.FC<{ content: ReactNode }> = ({ content }) => ( + {content} +); + +/** + * Type for the props of the DetailsItem component. + * + * @typedef {Object} DetailsItemProps + * @property {string} title - The title of the details item. + * @property {ReactNode} content - The content of the details item. + * @property {ReactNode} [helpContent] - The content to display in the help popover. + * @property {string[]} [crumbs] - Breadcrumbs for the details item. + * @property {Function} [onEdit] - Function to be called when the edit button is clicked. + */ +export type DetailsItemProps = { + title: string; + content: ReactNode; + helpContent?: ReactNode; + moreInfoLabel?: string; + moreInfoLink?: string; + crumbs?: string[]; + onEdit?: () => void; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/OwnerReferencesItem.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/OwnerReferencesItem.tsx new file mode 100644 index 000000000..64a5f2bc9 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/OwnerReferencesItem.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { + K8sResourceCommon, + OwnerReference, + ResourceLink, +} from '@openshift-console/dynamic-plugin-sdk'; + +/** + * React Component to display a list of owner references for a given Kubernetes resource. + * + * @component + * @param {OwnerReferencesProps} props - Props for the OwnerReferences component. + * @param {K8sResourceCommon} props.resource - The resource whose owner references will be displayed. + * @returns {ReactElement} A list of owner references or a 'No owner' message if there are no owner references. + */ +export const OwnerReferencesItem: React.FC = ({ resource }) => { + const { t } = useForkliftTranslation(); + const owners = (resource?.metadata?.ownerReferences || []).map((o: OwnerReference) => ( + + )); + return owners.length ? <>{owners} : {t('No owner')}; +}; + +/** + * Type for the props of the OwnerReferences component. + * + * @typedef {Object} OwnerReferencesProps + * @property {K8sResourceCommon} resource - The resource whose owner references will be displayed. + */ +export type OwnerReferencesProps = { + resource: K8sResourceCommon; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/PageHeadings.style.css b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/PageHeadings.style.css new file mode 100644 index 000000000..b84b33907 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/PageHeadings.style.css @@ -0,0 +1,3 @@ +.forklift-page-headings { + margin-top: 1rem; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/PageHeadings.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/PageHeadings.tsx new file mode 100644 index 000000000..6181a00a7 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/PageHeadings.tsx @@ -0,0 +1,116 @@ +import React, { ReactNode } from 'react'; +import { Link } from 'react-router-dom'; +import { getResourceUrl } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ProviderModelGroupVersionKind } from '@kubev2v/types'; +import { + K8sGroupVersionKind, + K8sModel, + K8sResourceCommon, + ResourceIcon, + ResourceStatus, +} from '@openshift-console/dynamic-plugin-sdk'; +import Status from '@openshift-console/dynamic-plugin-sdk/lib/app/components/status/Status'; +import { Breadcrumb, BreadcrumbItem, Split, SplitItem } from '@patternfly/react-core'; + +import './PageHeadings.style.css'; + +export const PageHeadings: React.FC = ({ + model, + namespace, + obj: data, + children, + actions, +}) => { + const status = data?.['status']?.phase; + + return ( +
+ + +

+ + + {' '} + {data?.metadata?.name} + {status && ( + + + + )} + + +

+ + {actions} + +
+ {children} +
+ ); +}; + +export interface PageHeadingsProps { + model: K8sModel; + namespace?: string; + obj?: K8sResourceCommon; + title?: ReactNode; + actions?: ReactNode; +} + +const BreadCrumbs: React.FC = ({ model, namespace }) => { + const breadcrumbs = breadcrumbsForModel(model, namespace); + + return ( + + {breadcrumbs.map((crumb, i, { length }) => { + const isLast = i === length - 1; + + return ( + + {isLast ? ( + crumb.name + ) : ( + + {crumb.name} + + )} + + ); + })} + + ); +}; + +type BreadCrumbsProps = { + model: K8sModel; + namespace?: string; +}; + +const breadcrumbsForModel = (model: K8sModel, namespace: string) => { + const { t } = useForkliftTranslation(); + + const groupVersionKind: K8sGroupVersionKind = { + group: model.apiGroup, + version: model.apiVersion, + kind: model.kind, + }; + + return [ + { + name: `${model.labelPlural}`, + path: `${getResourceUrl({ groupVersionKind, namespace })}`, + }, + { + name: t('{{name}} Details', { name: model.label }), + }, + ]; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/index.ts new file mode 100644 index 000000000..570b431c3 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/DetailsPage/index.ts @@ -0,0 +1,5 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './DetailItem'; +export * from './OwnerReferencesItem'; +export * from './PageHeadings'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/Galerry/SelectableCard.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/Galerry/SelectableCard.tsx new file mode 100644 index 000000000..1b7162741 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/Galerry/SelectableCard.tsx @@ -0,0 +1,46 @@ +import React, { ReactNode } from 'react'; + +import { Card, CardBody, CardTitle } from '@patternfly/react-core'; + +interface SelectableCardProps { + /** The title of the card */ + title: ReactNode; + /** The content of the card */ + content: ReactNode; + /** Handler function to be called when the card is clicked */ + onChange: (isSelected: boolean) => void; + /** The selected state of the card */ + isSelected: boolean; +} + +/** + * SelectableCard component + * @param props The properties of the SelectableCard + */ +export const SelectableCard: React.FC = ({ + title, + content, + onChange, + isSelected, +}) => { + // Handler function to toggle selection and call onChange + const handleClick = () => { + // Flip the isSelected status and send the new status via the onChange handler + onChange(!isSelected); + }; + + return ( + + {title} + {content} + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/Galerry/SelectableGallery.style.css b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/Galerry/SelectableGallery.style.css new file mode 100644 index 000000000..ed6c96b0b --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/Galerry/SelectableGallery.style.css @@ -0,0 +1,7 @@ +.forklift-selectable-gallery { + padding-top: var(--pf-global--spacer--sm); +} + +.forklift-selectable-gallery-card { + height: 100%; +} \ No newline at end of file diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/Galerry/SelectableGallery.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/Galerry/SelectableGallery.tsx new file mode 100644 index 000000000..a21965817 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/Galerry/SelectableGallery.tsx @@ -0,0 +1,69 @@ +import React, { FC } from 'react'; + +import { Gallery, GalleryItem } from '@patternfly/react-core'; + +import { SelectableCard } from './SelectableCard'; + +import './SelectableGallery.style.css'; + +export interface SelectableGalleryItem { + /** The title of the item */ + title: string; + /** The content of the item */ + content: string; +} + +interface SelectableGalleryProps { + /** An object of items to be displayed in the gallery. Key is the item's id */ + items: Record; + /** Handler function to be called when a card is selected */ + onChange: (selectedCardId: string | null) => void; + /** A function to sort the items. Default is alphabetic sort on item titles. */ + sortFunction?: (a: [string, SelectableGalleryItem], b: [string, SelectableGalleryItem]) => number; + /** initial selected value */ + selectedID?: string; +} + +/** + * SelectableGallery component + * @param props The properties of the SelectableGallery + */ +export const SelectableGallery: FC = ({ + items, + onChange, + sortFunction = ([, a], [, b]) => a.title.localeCompare(b.title), + selectedID, +}) => { + // State to manage the selected card's id + const [selectedCardId, setSelectedCardId] = React.useState(selectedID); + + // Callback function for when a card is selected + const handleCardChange = (isSelected: boolean, id: string) => { + if (isSelected) { + setSelectedCardId(id); + onChange(id); + } else if (selectedCardId === id) { + // Unselect the card if it's currently selected + setSelectedCardId(null); + onChange(null); + } + }; + + // Convert the items object to an array and sort it + const sortedItems = Object.entries(items).sort(sortFunction); + + return ( + + {sortedItems.map(([id, item]) => ( + + handleCardChange(isSelected, id)} + /> + + ))} + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableCell.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableCell.tsx new file mode 100644 index 000000000..861c24a48 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableCell.tsx @@ -0,0 +1,33 @@ +import React, { Children, ReactNode } from 'react'; + +import { Flex, FlexItem } from '@patternfly/react-core'; + +import './TableCells.style.css'; + +/** + * A component that displays a table cell. + * + * @param {TableCellProps} props - The props for the component. + * @returns {ReactElement} The rendered TableCell component. + */ +export const TableCell: React.FC = ({ children }) => { + const arrayChildren = Children.toArray(children); + + return ( + + + {Children.map(arrayChildren, (child) => ( + {child} + ))} + + + ); +}; + +export interface TableCellProps { + children?: ReactNode; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableCells.style.css b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableCells.style.css new file mode 100644 index 000000000..9e5d4a469 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableCells.style.css @@ -0,0 +1,8 @@ +.forklift-table__flex-cell { + display: flex; + flex-wrap: wrap; +} + +.forklift-table__flex-cell-label { + margin-left: var(--pf-global--spacer--sm); +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableEmptyCell.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableEmptyCell.tsx new file mode 100644 index 000000000..908a77fe1 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableEmptyCell.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +import { Td } from '@patternfly/react-table'; + +/** + * A component that renders an empty cell with a dash symbol (-). + * @returns {JSX.Element} The JSX element representing the empty cell. + */ +export const TableEmptyCell: React.FC = () => { + return -; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableIconCell.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableIconCell.tsx new file mode 100644 index 000000000..3a9e3b30a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableIconCell.tsx @@ -0,0 +1,28 @@ +import React, { ReactNode } from 'react'; + +import { TableLabelCell, TableLabelCellProps } from './TableLabelCell'; + +/** + * A component that displays a table cell, with an optional icon. + * + * @param {TableIconCellProps} props - The props for the component. + * @returns {ReactElement} The rendered TableLinkCell component. + */ +export const TableIconCell: React.FC = ({ + children, + icon, + hasLabel = false, + label, + labelColor = 'grey', +}) => { + return ( + + {icon} + {children} + + ); +}; + +export interface TableIconCellProps extends TableLabelCellProps { + icon?: ReactNode; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableLabelCell.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableLabelCell.tsx new file mode 100644 index 000000000..5ea301d51 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableLabelCell.tsx @@ -0,0 +1,35 @@ +import React, { ReactNode } from 'react'; + +import { Label } from '@patternfly/react-core'; + +import { TableCell, TableCellProps } from './TableCell'; + +/** + * A component that displays a table cell, with an optional label. + * + * @param {TableLabelCellProps} props - The props for the component. + * @returns {ReactElement} The rendered TableLabelCell component. + */ +export const TableLabelCell: React.FC = ({ + children, + hasLabel = false, + label, + labelColor = 'grey', +}) => { + return ( + + {children} + {hasLabel && ( + + )} + + ); +}; + +export interface TableLabelCellProps extends TableCellProps { + hasLabel?: boolean; + label?: ReactNode; + labelColor?: 'blue' | 'cyan' | 'green' | 'orange' | 'purple' | 'red' | 'grey'; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableLinkCell.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableLinkCell.tsx new file mode 100644 index 000000000..00d0a181c --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/TableLinkCell.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import { K8sGroupVersionKind, ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; + +import { TableLabelCell, TableLabelCellProps } from './TableLabelCell'; + +/** + * A component that displays a resource link, with an optional label. + * + * @param {TableLinkCellProps} props - The props for the component. + * @returns {ReactElement} The rendered TableLinkCell component. + */ +export const TableLinkCell: React.FC = ({ + groupVersionKind, + name, + namespace, + hasLabel = false, + label, + labelColor = 'grey', +}) => { + return ( + + + + ); +}; + +export interface TableLinkCellProps extends TableLabelCellProps { + groupVersionKind: K8sGroupVersionKind; + name: string; + namespace: string; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/index.ts new file mode 100644 index 000000000..39a1e7400 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/TableCell/index.ts @@ -0,0 +1,7 @@ +// @index(['./*', /__/g, /style/g], f => `export * from '${f.path}';`) +export * from './TableCell'; +export * from './TableEmptyCell'; +export * from './TableIconCell'; +export * from './TableLabelCell'; +export * from './TableLinkCell'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/index.ts new file mode 100644 index 000000000..97e5ed955 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/components/index.ts @@ -0,0 +1,4 @@ +// @index(['./*', /__/g, /style/g], f => `export * from '${f.path}';`) +export * from './DetailsPage'; +export * from './TableCell'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/__tests__/validators.test.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/__tests__/validators.test.ts new file mode 100644 index 000000000..4cbf111b3 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/__tests__/validators.test.ts @@ -0,0 +1,150 @@ +/* eslint-disable @cspell/spellchecker */ +import { + validateContainerImage, + validateFingerprint, + validateK8sName, + validatePublicCert, + validateURL, +} from '../../validators/common'; + +describe('validator', () => { + // Tests for validateContainerImage + describe('validateContainerImage', () => { + it('should return true for valid container images', () => { + const images = [ + 'my-registry/my-repo/my-image:my-tag', + 'localhost:5000/my-repo/my-image:my-tag', + 'my-repo/my-image@sha256:389d6e4ec6277e14d3684195be4d0531ff666ff8a8ee9e6bb56837dec642283f', + 'my-registry/my-repo/my-image', + ]; + for (const image of images) { + expect(validateContainerImage(image)).toBe(true); + } + }); + + it('should return false for invalid container images', () => { + const images = [ + 'my-repo/my+image:my-tag', // invalid char + 'my-repo/my-image@sha256', // missing sha256 hash + ]; + for (const image of images) { + expect(validateContainerImage(image)).toBe(false); + } + }); + }); + + // Tests for validateURL + describe('validateURL', () => { + it('should return true for valid URLs', () => { + const urls = [ + 'https://example.com:8080/my/path?param=value', + 'http://192.168.1.1:8000', + 'https://www.example.co.uk', + ]; + for (const url of urls) { + expect(validateURL(url)).toBe(true); + } + }); + + it('should return false for invalid URLs', () => { + const urls = [ + 'http:/example.com', // missing slash + 'https://192.168.1.1.1', // invalid IP + 'http://example', // no TLD + ]; + for (const url of urls) { + expect(validateURL(url)).toBe(false); + } + }); + }); + + // Tests for validatePublicCert + describe('validatePublicCert', () => { + it('should return true for valid certificates', () => { + const certs = [ + ` +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAJC1HiIAZAiIMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRtZSBXaWRn +-----END CERTIFICATE----- + `, + ]; + for (const ca of certs) { + expect(validatePublicCert(ca.trim())).toBe(true); + } + }); + + it('should return false for invalid certificates', () => { + const certs = [ + ` +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAJC1HiIAZAiIMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRtZSBXaWRn + `, // missing end tag + ` +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAJC1HiIAZAiIMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN 0YXRlMSEwHwYDVQQKDBhJ= +-----END CERTIFICATE----- + `, // invalid Base64 content + '-----BEGIN CERTIFICATE-----', // missing content and end tag + ]; + for (const ca of certs) { + expect(validatePublicCert(ca.trim())).toBe(false); + } + }); + }); + + describe('validateFingerprint', () => { + it('validates correct fingerprints', () => { + const validFingerprint = '52:6C:4E:88:1D:78:AE:12:1C:F3:BB:6C:5B:F4:E2:82:86:A7:08:AF'; + expect(validateFingerprint(validFingerprint)).toBe(true); + }); + + it('invalidates fingerprints with wrong length', () => { + const invalidFingerprint = '52:6C:4E:88:1D:78:AE:12:1C:F3:BB:6C:5B:F4:E2:82:86:A7:08'; + expect(validateFingerprint(invalidFingerprint)).toBe(false); + }); + + it('invalidates fingerprints with wrong characters', () => { + const invalidFingerprint = 'G2:6C:4E:88:1D:78:AE:12:1C:F3:BB:6C:5B:F4:E2:82:86:A7:08:AF'; + expect(validateFingerprint(invalidFingerprint)).toBe(false); + }); + + it('invalidates fingerprints with missing colons', () => { + const invalidFingerprint = '526C4E881D78AE121CF3BB6C5BF4E28286A708AF'; + expect(validateFingerprint(invalidFingerprint)).toBe(false); + }); + + it('validates lowercase fingerprints', () => { + const validFingerprint = '52:6C:4E:88:1D:78:AE:12:1C:F3:BB:6C:5B:F4:E2:82:86:A7:08:AF'; + expect(validateFingerprint(validFingerprint)).toBe(true); + }); + }); + + describe('validateK8sName', () => { + it('validates correct k8s names', () => { + expect(validateK8sName('k8s-name')).toBe(true); + expect(validateK8sName('k8sname')).toBe(true); + expect(validateK8sName('k8')).toBe(true); + }); + + it('invalidates k8s names with invalid characters', () => { + expect(validateK8sName('k8s_name')).toBe(false); + expect(validateK8sName('k8s.name')).toBe(false); + }); + + it('invalidates k8s names that are too long', () => { + const longName = 'k'.repeat(254); + expect(validateK8sName(longName)).toBe(false); + }); + + it('invalidates k8s names that start with a hyphen', () => { + expect(validateK8sName('-k8sname')).toBe(false); + }); + + it('invalidates k8s names that end with a hyphen', () => { + expect(validateK8sName('k8sname-')).toBe(false); + }); + }); +}); diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/findInventoryByID.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/findInventoryByID.ts new file mode 100644 index 000000000..4c184b435 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/findInventoryByID.ts @@ -0,0 +1,25 @@ +import { ProviderInventory, ProvidersInventoryList } from '@kubev2v/types'; + +/** + * Finds an inventory by its unique identifier. + * + * @param {ProvidersInventoryList} inventory - The list of provider inventories by type. + * @param {string} uid - The unique identifier of the inventory to be found. + * @returns {ProviderInventory} - The inventory if found, undefined otherwise. + */ +export function findInventoryByID( + inventory: ProvidersInventoryList, + uid: string, +): ProviderInventory { + if (!inventory || !uid) { + return undefined; + } + + const providers = [ + ...inventory.openshift, + ...inventory.openstack, + ...inventory.ovirt, + ...inventory.vsphere, + ]; + return providers.find((provider) => provider.uid === uid); +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getCachedData.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getCachedData.ts new file mode 100644 index 000000000..5d06a9837 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getCachedData.ts @@ -0,0 +1,25 @@ +/** + * Fetches cached inventory data if it's still valid. + * @param {string} cacheKey - The key used to store inventory data in cache. + * @param {number} cacheExpiryDuration - The duration till cache is valid. + * @returns {T | null} - The cached inventory data if valid, null otherwise. + */ +export function getCachedData(cacheKey: string, cacheExpiryDuration: number): T | null { + if (cacheExpiryDuration < 1) { + return null; + } + + const cacheData = sessionStorage.getItem(cacheKey); + if (cacheData) { + const { data, timestamp } = JSON.parse(cacheData); + + // If cache is not expired, return data + if (Date.now() - timestamp < cacheExpiryDuration) { + return data; + } + } + + return null; +} + +export default getCachedData; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getInventoryApiUrl.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getInventoryApiUrl.ts new file mode 100644 index 000000000..f323e3523 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getInventoryApiUrl.ts @@ -0,0 +1,12 @@ +/** + * Provides API url for getting inventory. + * + * @param {string} relativePath - An optional relative path to append to the URL + * @returns {string} - The API URL for getting inventory + */ +export const getInventoryApiUrl = (relativePath = ''): string => { + const pluginPath = `/api/proxy/plugin/${process.env.PLUGIN_NAME}`; + const inventoryPath = '/forklift-inventory'; + + return `${pluginPath}${inventoryPath}/${relativePath}`; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getIsManaged.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getIsManaged.ts new file mode 100644 index 000000000..8ec51d1f0 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getIsManaged.ts @@ -0,0 +1,12 @@ +import { V1beta1Provider } from '@kubev2v/types'; + +/** + * Checks if the provider is managed or not. + * + * @param {V1beta1Provider} provider - The provider to be checked. + * @returns {boolean} - Returns true if the provider is managed, false otherwise. + */ +export function getIsManaged(provider: V1beta1Provider): boolean { + const ownerReferences = provider?.metadata?.ownerReferences || []; + return ownerReferences.length > 0; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getIsTarget.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getIsTarget.ts new file mode 100644 index 000000000..f7050a137 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getIsTarget.ts @@ -0,0 +1,24 @@ +import { ProviderType, V1beta1Provider } from '@kubev2v/types'; + +/** + * Checks if the provider is a target provider or not. + * + * @param {V1beta1Provider} provider - The provider to be checked. + * @returns {boolean} - Returns true if the provider is a target provider, false otherwise. + */ +export function getIsTarget(provider: V1beta1Provider): boolean { + return TARGET_PROVIDER_TYPES.includes(provider?.spec.type as ProviderType); +} + +/** + * Checks if the provider is a source provider or not. + * + * @param {V1beta1Provider} provider - The provider to be checked. + * @returns {boolean} - Returns true if the provider is a target provider, false otherwise. + */ +export function getIsSource(provider: V1beta1Provider): boolean { + return SOURCE_PROVIDER_TYPES.includes(provider?.spec.type as ProviderType); +} + +const SOURCE_PROVIDER_TYPES: ProviderType[] = ['vsphere', 'ovirt', 'openstack']; +const TARGET_PROVIDER_TYPES: ProviderType[] = ['openshift']; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getResourceUrl.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getResourceUrl.ts new file mode 100644 index 000000000..f35e73933 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getResourceUrl.ts @@ -0,0 +1,31 @@ +import { K8sGroupVersionKind } from '@openshift-console/dynamic-plugin-sdk'; + +/** + * Provides resource url. + * + * @param {GetResourceUrlProps} param0 - An object of GetResourceUrlProps + * @returns {string} - The resource URL + */ +export const getResourceUrl = ({ + reference, + groupVersionKind, + namespaced = true, + namespace, + name, +}: GetResourceUrlProps): string => { + const ns = namespace ? `ns/${namespace}` : 'all-namespaces'; + const resourcePath = namespaced ? ns : 'cluster'; + const reference_ = + reference || `${groupVersionKind.group}~${groupVersionKind.version}~${groupVersionKind.kind}`; + const name_ = name ? `/${encodeURIComponent(name)}` : ''; + + return `/k8s/${resourcePath}/${reference_}${name_}`; +}; + +interface GetResourceUrlProps { + reference?: string; + groupVersionKind?: K8sGroupVersionKind; + namespaced?: boolean; + namespace?: string; + name?: string; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getValueByJsonPath.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getValueByJsonPath.ts new file mode 100644 index 000000000..9a32377d4 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/getValueByJsonPath.ts @@ -0,0 +1,32 @@ +/** + * Retrieves the deep value of an object given a JSON path. + * + * @param obj - The object to retrieve the value from. + * @param path - The JSON path (dot notation) to the property. + * @returns The value at the given path, or undefined if the path doesn't exist. + */ +export function getValueByJsonPath(obj: T, path: string | string[]): unknown { + let pathParts = []; + + if (typeof path === 'string') { + pathParts = path.split('.'); + } else { + pathParts = path; + } + + return pathParts.reduce((o, key) => o?.[key], obj); +} + +export function jsonPathToPatch(path: string | string[]) { + let pathParts = []; + + if (typeof path === 'string') { + pathParts = path.split('.'); + } else { + pathParts = path; + } + + pathParts = pathParts.map((o) => o.replaceAll('/', '~1')); + + return `/${pathParts.join('/')}`; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/hasObjectChangedInGivenFields.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/hasObjectChangedInGivenFields.ts new file mode 100644 index 000000000..0e37cffb7 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/hasObjectChangedInGivenFields.ts @@ -0,0 +1,28 @@ +import { getValueByJsonPath } from './getValueByJsonPath'; + +type FieldsComparisonArgs = { + oldObject?: T; + newObject?: T; + fieldsToCompare: string[]; +}; + +/** + * Checks whether the specified fields have changed in two objects. + * + * @param params - An object containing the old object, new object, and fields to be compared. + * @returns A boolean indicating whether any of the specified fields have changed. + */ +export function hasObjectChangedInGivenFields(params: FieldsComparisonArgs): boolean { + if (!params?.oldObject && !params?.newObject) { + return false; + } + + if (!params?.oldObject || !params?.newObject) { + return true; + } + + return params.fieldsToCompare.some( + (field) => + getValueByJsonPath(params.oldObject, field) !== getValueByJsonPath(params.newObject, field), + ); +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/index.ts new file mode 100644 index 000000000..e4d03306e --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/index.ts @@ -0,0 +1,14 @@ +// @index(['./*.tsx', './*.ts', /__/g], f => `export * from '${f.path}';`) +export * from './findInventoryByID'; +export * from './getCachedData'; +export * from './getInventoryApiUrl'; +export * from './getIsManaged'; +export * from './getIsTarget'; +export * from './getResourceUrl'; +export * from './getValueByJsonPath'; +export * from './hasObjectChangedInGivenFields'; +export * from './isSecretDataChanged'; +export * from './missingKeysInSecretData'; +export * from './safeBase64Decode'; +export * from './setCachedData'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/isSecretDataChanged.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/isSecretDataChanged.ts new file mode 100644 index 000000000..4f18c726a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/isSecretDataChanged.ts @@ -0,0 +1,35 @@ +import { V1Secret } from '@kubev2v/types'; + +/** + * Compares the data records between two versions of a secret. + * + * @param {V1Secret} secret1 - The first version of the secret. + * @param {V1Secret} secret2 - The second version of the secret. + * @returns {boolean} Returns true if the data records have changed, otherwise returns false. + */ +export function isSecretDataChanged(secret1: V1Secret, secret2: V1Secret): boolean { + // Both secrets don't have data records + if (!secret1.data && !secret2.data) { + return false; + } + + // One of the secrets doesn't have data records + if (!secret1.data || !secret2.data) { + return true; + } + + // Both secrets have data records, but the number of records is different + if (Object.keys(secret1.data).length !== Object.keys(secret2.data).length) { + return true; + } + + // Compare each data record + for (const key in secret1.data) { + if (secret1.data[key] !== secret2.data[key]) { + return true; + } + } + + // No differences found + return false; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/missingKeysInSecretData.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/missingKeysInSecretData.ts new file mode 100644 index 000000000..143cfab9d --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/missingKeysInSecretData.ts @@ -0,0 +1,31 @@ +import { Base64 } from 'js-base64'; + +import { V1Secret } from '@kubev2v/types'; + +/** + * Checks if a list of keys exist in a secret's data, and verifies they are not null or empty strings. + * + * @param {V1Secret} secret - The secret to be checked. + * @param {string[]} keys - The list of keys to check. + * @returns {string[]} Returns a list of missing keys in secret data. + */ +export function missingKeysInSecretData(secret: V1Secret, keys: string[]): string[] { + // If secret or secret's data is not defined, return false + if (!secret || !secret.data) { + return keys; + } + + const missing: string[] = []; + + for (const key of keys) { + const secretValue = secret.data[key] && Base64.decode(secret.data[key]); + + // Check if the key exists and is not null or empty string + if (!secretValue || secretValue.trim() === '') { + missing.push(key); + } + } + + // All keys exist and are not null or empty string + return missing; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/safeBase64Decode.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/safeBase64Decode.ts new file mode 100644 index 000000000..11224454f --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/safeBase64Decode.ts @@ -0,0 +1,9 @@ +import { Base64 } from 'js-base64'; + +export function safeBase64Decode(value: string) { + try { + return Base64.decode(value); + } catch { + return ''; + } +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/setCachedData.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/setCachedData.ts new file mode 100644 index 000000000..768265267 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/helpers/setCachedData.ts @@ -0,0 +1,15 @@ +/** + * Saves the inventory data to cache. + * @param {string} cacheKey - The key used to store inventory data in cache. + * @param {T} data - The inventory data to be cached. + */ +export function setCachedData(cacheKey: string, data: T): void { + const cacheData = { + data, + timestamp: Date.now(), + }; + + sessionStorage.setItem(cacheKey, JSON.stringify(cacheData)); +} + +export default setCachedData; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/index.ts new file mode 100644 index 000000000..84510f407 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/index.ts @@ -0,0 +1,6 @@ +// @index(['./*', /__/g], f => `export * from '${f.path}';`) +export * from './components'; +export * from './helpers'; +export * from './types'; +export * from './validators'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/ProviderData.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/ProviderData.ts new file mode 100644 index 000000000..6470b5b35 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/ProviderData.ts @@ -0,0 +1,9 @@ +import { ProviderInventory, V1beta1Provider } from '@kubev2v/types'; + +import { ProvidersPermissionStatus } from './ProvidersPermissionStatus'; + +export interface ProviderData { + provider?: V1beta1Provider; + inventory?: ProviderInventory; + permissions?: ProvidersPermissionStatus; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/ProvidersPermissionStatus.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/ProvidersPermissionStatus.ts new file mode 100644 index 000000000..09b27865a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/ProvidersPermissionStatus.ts @@ -0,0 +1,17 @@ +/** + * Type for the return value of useAccessReviewProviders hook. + * + * @typedef {Object} ProvidersPermissionStatus + * @property {boolean} canCreate - Permission to create a resource. + * @property {boolean} canPatch - Permission to patch a resource. + * @property {boolean} canDelete - Permission to delete a resource. + * @property {boolean} canGet - Permission to get a resource. + * @property {boolean} loading - Flag indicating if any access review is pending. + */ +export type ProvidersPermissionStatus = { + canCreate: boolean; + canPatch: boolean; + canDelete: boolean; + canGet: boolean; + loading: boolean; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/Validation.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/Validation.ts new file mode 100644 index 000000000..7416895b7 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/Validation.ts @@ -0,0 +1 @@ +export type Validation = 'default' | 'success' | 'warning' | 'error'; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/index.ts new file mode 100644 index 000000000..db58b583c --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/types/index.ts @@ -0,0 +1,5 @@ +// @index('./*.ts', f => `export * from '${f.path}';`) +export * from './ProviderData'; +export * from './ProvidersPermissionStatus'; +export * from './Validation'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/common.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/common.ts new file mode 100644 index 000000000..b18885009 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/common.ts @@ -0,0 +1,78 @@ +// regex + +// validate container images +// example: quay.io/image:latest +const REGISTRY = '(?:[a-z0-9]+([.:_-][a-z0-9]+)*\\/)?'; +const IMAGE_NAME = '[a-z0-9]+([._-][a-z0-9]+)*(\\/[a-z0-9]+([._-][a-z0-9]+)*)*'; +const TAG = '[a-zA-Z0-9]+([._-][a-zA-Z0-9]+)*'; +const SHA256 = 'sha256:[A-Fa-f0-9]{64}'; + +const IMAGE_REGEX = new RegExp(`^${REGISTRY}?${IMAGE_NAME}((@${SHA256}|:${TAG}))?$`); + +// validate URL +// example: https://example.com/index +const PROTOCOL = '(https?:\\/\\/)'; +const IPV4 = '((?:[0-9]{1,3}\\.){3}[0-9]{1,3})'; +const HOSTNAME = '([a-zA-Z-_]+[a-zA-Z0-9-_]+\\.[a-zA-Z0-9-_\\.]+)'; +const PORT = '(:[0-9]+)?'; +const PATH = '(\\/[^ ]*)*'; +const QUERY_PARAMS = '(\\?[a-zA-Z0-9=&_]*)?'; + +const URL_REGEX = new RegExp( + `^${PROTOCOL}((${IPV4})|(${HOSTNAME}))((${PORT})(${PATH})?(${QUERY_PARAMS})?)?$`, +); + +// validate CA certification. +const CERTIFICATE_HEADER = '-----BEGIN CERTIFICATE-----'; +const CERTIFICATE_FOOTER = '-----END CERTIFICATE-----'; +const BASE64_LINE = '([A-Za-z0-9+\\/]{64}\\r?\\n)'; +const LAST_BASE64_LINE = '([A-Za-z0-9+\\/=]{1,64}\\r?\\n)?'; +const BASE64_CONTENT = `(${BASE64_LINE}*${LAST_BASE64_LINE})`; + +const EMPTY_LINES = '((\\#[^\\r\\n]*)?\\s*\\r?\\n)*'; + +const CERTIFICATE_REGEX = new RegExp( + `^(${EMPTY_LINES}${CERTIFICATE_HEADER}\\r?\\n${BASE64_CONTENT}${CERTIFICATE_FOOTER}${EMPTY_LINES})+$`, +); + +// validate CA certification fingerprint. +const FINGERPRINT_REGEX = /^([a-fA-F0-9]{2}:){19}[a-fA-F0-9]{2}$/; + +// validate sub domain names, used in K8s +const DNS_SUBDOMAINS_NAME_REGEXP = /^[a-z][a-z0-9-]{0,251}[a-z0-9]$/; + +// validate bearer tokens, used in K8s +const JWT_TOKEN_REGEX = /^([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_\-+/=]*)/gm; +const K8S_TOKEN_REGEX = /^[a-z0-9]{6}.[a-z0-9]{16}$/; + +// helper methods + +export function validateContainerImage(image: string) { + return IMAGE_REGEX.test(image); +} + +export function validateURL(url: string) { + return URL_REGEX.test(url); +} + +export function validatePublicCert(ca: string) { + return CERTIFICATE_REGEX.test(ca); +} + +export function validateFingerprint(fingerprint: string) { + return FINGERPRINT_REGEX.test(fingerprint); +} + +export function validateK8sName(k8sName: string) { + return DNS_SUBDOMAINS_NAME_REGEXP.test(k8sName); +} + +export function validateK8sToken(token: string) { + return JWT_TOKEN_REGEX.test(token) || K8S_TOKEN_REGEX.test(token); +} + +export function validateNoSpaces(value: string) { + // any string without spaces + // max length 128 chars + return /^[^\s]{1,128}$/.test(value); +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/index.ts new file mode 100644 index 000000000..4fcf50c41 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/index.ts @@ -0,0 +1,6 @@ +// @index(['./*', /__/g], f => `export * from '${f.path}';`) +export * from './common'; +export * from './provider'; +export * from './secret'; +export * from './secret-fields'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/index.ts new file mode 100644 index 000000000..7a8a85642 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/index.ts @@ -0,0 +1,7 @@ +// @index(['./*.tsx', './*.ts', /__/g], f => `export * from '${f.path}';`) +export * from './openshiftProviderValidator'; +export * from './openstackProviderValidator'; +export * from './ovirtProviderValidator'; +export * from './providerValidator'; +export * from './vsphereProviderValidator'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/openshiftProviderValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/openshiftProviderValidator.ts new file mode 100644 index 000000000..c00f5383c --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/openshiftProviderValidator.ts @@ -0,0 +1,18 @@ +import { V1beta1Provider } from '@kubev2v/types'; + +import { validateK8sName, validateURL } from '../common'; + +export function openshiftProviderValidator(provider: V1beta1Provider) { + const name = provider?.metadata?.name; + const url = provider?.spec?.url || ''; + + if (!validateK8sName(name)) { + return new Error('invalided kubernetes resource name'); + } + + if (url !== '' && !validateURL(url)) { + return new Error('invalided URL'); + } + + return null; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/openstackProviderValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/openstackProviderValidator.ts new file mode 100644 index 000000000..c3ff1e97f --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/openstackProviderValidator.ts @@ -0,0 +1,18 @@ +import { V1beta1Provider } from '@kubev2v/types'; + +import { validateK8sName, validateURL } from '../common'; + +export function openstackProviderValidator(provider: V1beta1Provider) { + const name = provider?.metadata?.name; + const url = provider?.spec?.url || ''; + + if (!validateK8sName(name)) { + return new Error('invalided kubernetes resource name'); + } + + if (!validateURL(url)) { + return new Error('invalided URL'); + } + + return null; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/ovirtProviderValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/ovirtProviderValidator.ts new file mode 100644 index 000000000..9fb96b4bc --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/ovirtProviderValidator.ts @@ -0,0 +1,18 @@ +import { V1beta1Provider } from '@kubev2v/types'; + +import { validateK8sName, validateURL } from '../common'; + +export function ovirtProviderValidator(provider: V1beta1Provider) { + const name = provider?.metadata?.name; + const url = provider?.spec?.url || ''; + + if (!validateK8sName(name)) { + return new Error('invalided kubernetes resource name'); + } + + if (!validateURL(url)) { + return new Error('invalided URL'); + } + + return null; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/providerValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/providerValidator.ts new file mode 100644 index 000000000..4f34f903b --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/providerValidator.ts @@ -0,0 +1,29 @@ +import { V1beta1Provider } from '@kubev2v/types'; + +import { openshiftProviderValidator } from './openshiftProviderValidator'; +import { openstackProviderValidator } from './openstackProviderValidator'; +import { ovirtProviderValidator } from './ovirtProviderValidator'; +import { vsphereProviderValidator } from './vsphereProviderValidator'; + +export function providerValidator(provider: V1beta1Provider) { + let validationError = null; + + switch (provider.spec.type) { + case 'openshift': + validationError = openshiftProviderValidator(provider); + break; + case 'openstack': + validationError = openstackProviderValidator(provider); + break; + case 'ovirt': + validationError = ovirtProviderValidator(provider); + break; + case 'vsphere': + validationError = vsphereProviderValidator(provider); + break; + default: + validationError = new Error('bad provider type'); + } + + return validationError; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/vsphereProviderValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/vsphereProviderValidator.ts new file mode 100644 index 000000000..0d914c0ff --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/provider/vsphereProviderValidator.ts @@ -0,0 +1,23 @@ +import { V1beta1Provider } from '@kubev2v/types'; + +import { validateContainerImage, validateK8sName, validateURL } from '../common'; + +export function vsphereProviderValidator(provider: V1beta1Provider) { + const name = provider?.metadata?.name; + const url = provider?.spec?.url || ''; + const vddkInitImage = provider?.spec?.settings?.['vddkInitImage']; + + if (!validateK8sName(name)) { + return new Error('invalided kubernetes resource name'); + } + + if (!validateURL(url)) { + return new Error('invalided URL'); + } + + if (vddkInitImage !== '' || !validateContainerImage(vddkInitImage)) { + return new Error('invalided VDDK Init Image'); + } + + return null; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/providerAndSecretValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/providerAndSecretValidator.ts new file mode 100644 index 000000000..4474e5440 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/providerAndSecretValidator.ts @@ -0,0 +1,19 @@ +import { V1beta1Provider, V1Secret } from '@kubev2v/types'; + +import { providerValidator } from './provider/providerValidator'; +import { secretValidator } from './secret/secretValidator'; + +export function providerAndSecretValidator(provider: V1beta1Provider, secret: V1Secret) { + const providerValidation = providerValidator(provider); + if (providerValidation) { + return providerValidation; + } + + const type = provider?.spec?.type || ''; + const secretValidation = secretValidator(type, secret); + if (secretValidation) { + return secretValidation; + } + + return null; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/index.ts new file mode 100644 index 000000000..dd4fb7808 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/index.ts @@ -0,0 +1,6 @@ +// @index(['./*.tsx', './*.ts', /__/g], f => `export * from '${f.path}';`) +export * from './openshiftSecretFieldValidator'; +export * from './openstackSecretFieldValidator'; +export * from './ovirtSecretFieldValidator'; +export * from './vsphereSecretFieldValidator'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/openshiftSecretFieldValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/openshiftSecretFieldValidator.ts new file mode 100644 index 000000000..0d107420e --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/openshiftSecretFieldValidator.ts @@ -0,0 +1,30 @@ +import { Validation } from '../../types'; +import { validateK8sToken } from '../common'; + +/** + * Validates form input fields based on their id. + * + * @param {string} id - The ID of the form field. + * @param {string} value - The value of the form field. + * + * @return {Validation} - The validation state of the form field. Can be one of the following: + * 'default' - The default state of the form field, used when the field is empty or a value hasn't been entered yet. + * 'success' - The field's value has passed validation. + * 'error' - The field's value has failed validation. + */ +export const openshiftSecretFieldValidator = (id: string, value: string) => { + const trimmedValue = value.trim(); + + let validationState: Validation = 'default'; + + switch (id) { + case 'token': + validationState = validateK8sToken(trimmedValue) ? 'success' : 'error'; + break; + default: + validationState = 'error'; + break; + } + + return validationState; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/openstackSecretFieldValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/openstackSecretFieldValidator.ts new file mode 100644 index 000000000..672e8a82e --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/openstackSecretFieldValidator.ts @@ -0,0 +1,125 @@ +import { Validation } from '../../types'; +import { validateNoSpaces, validatePublicCert } from '../common'; + +/** + * Validates form input fields based on their id. + * + * @param {string} id - The ID of the form field. + * @param {string} value - The value of the form field. + * + * @return {Validation} - The validation state of the form field. Can be one of the following: + * 'default' - The default state of the form field, used when the field is empty or a value hasn't been entered yet. + * 'success' - The field's value has passed validation. + * 'error' - The field's value has failed validation. + */ +export const openstackSecretFieldValidator = (id: string, value: string) => { + const trimmedValue = value.trim(); + + let validationState: Validation = 'default'; + + switch (id) { + case 'username': + validationState = validateUsername(trimmedValue) ? 'success' : 'error'; + break; + case 'password': + validationState = validatePassword(trimmedValue) ? 'success' : 'error'; + break; + case 'regionName': + validationState = validateRegionName(trimmedValue) ? 'success' : 'error'; + break; + case 'projectName': + validationState = validateProjectName(trimmedValue) ? 'success' : 'error'; + break; + case 'domainName': + validationState = validateDomainName(trimmedValue) ? 'success' : 'error'; + break; + case 'token': + validationState = validateToken(trimmedValue) ? 'success' : 'error'; + break; + case 'userID': + validationState = validateUserID(trimmedValue) ? 'success' : 'error'; + break; + case 'projectID': + validationState = validateProjectID(trimmedValue) ? 'success' : 'error'; + break; + case 'userDomainName': + validationState = validateUserDomainName(trimmedValue) ? 'success' : 'error'; + break; + case 'applicationCredentialID': + validationState = validateApplicationCredentialID(trimmedValue) ? 'success' : 'error'; + break; + case 'applicationCredentialSecret': + validationState = validateApplicationCredentialSecret(trimmedValue) ? 'success' : 'error'; + break; + case 'applicationCredentialName': + validationState = validateApplicationCredentialName(trimmedValue) ? 'success' : 'error'; + break; + case 'insecureSkipVerify': + validationState = validateInsecureSkipVerify(trimmedValue) ? 'success' : 'error'; + break; + case 'cacert': + validationState = validateCacert(trimmedValue) ? 'success' : 'error'; + break; + default: + validationState = 'error'; + break; + } + + return validationState; +}; + +const validateUsername = (value: string) => { + return validateNoSpaces(value); +}; + +const validatePassword = (value: string) => { + return validateNoSpaces(value); +}; + +const validateRegionName = (value: string) => { + return validateNoSpaces(value); +}; + +const validateProjectName = (value: string) => { + return validateNoSpaces(value); +}; + +const validateDomainName = (value: string) => { + return validateNoSpaces(value); +}; + +const validateToken = (value: string) => { + return validateNoSpaces(value); +}; + +const validateUserID = (value: string) => { + return validateNoSpaces(value); +}; + +const validateProjectID = (value: string) => { + return validateNoSpaces(value); +}; + +const validateUserDomainName = (value: string) => { + return validateNoSpaces(value); +}; + +const validateApplicationCredentialID = (value: string) => { + return validateNoSpaces(value); +}; + +const validateApplicationCredentialSecret = (value: string) => { + return validateNoSpaces(value); +}; + +const validateApplicationCredentialName = (value: string) => { + return validateNoSpaces(value); +}; + +const validateInsecureSkipVerify = (value: string) => { + return ['true', 'false', ''].includes(value); +}; + +const validateCacert = (value: string) => { + return value === '' || validatePublicCert(value); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/ovirtSecretFieldValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/ovirtSecretFieldValidator.ts new file mode 100644 index 000000000..67f457b3c --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/ovirtSecretFieldValidator.ts @@ -0,0 +1,57 @@ +import { Validation } from '../../types'; +import { validateNoSpaces, validatePublicCert } from '../common'; + +/** + * Validates form input fields based on their id. + * + * @param {string} id - The ID of the form field. + * @param {string} value - The value of the form field. + * + * @return {Validation} - The validation state of the form field. Can be one of the following: + * 'default' - The default state of the form field, used when the field is empty or a value hasn't been entered yet. + * 'success' - The field's value has passed validation. + * 'error' - The field's value has failed validation. + */ +export const ovirtSecretFieldValidator = (id: string, value: string) => { + const trimmedValue = value.trim(); + + let validationState: Validation = 'default'; + + switch (id) { + case 'user': + validationState = validateUser(trimmedValue) ? 'success' : 'error'; + break; + case 'password': + validationState = validatePassword(trimmedValue) ? 'success' : 'error'; + break; + case 'insecureSkipVerify': + validationState = 'default'; + break; + case 'cacert': + validationState = validateCacert(trimmedValue); + break; + default: + validationState = 'error'; + break; + } + + return validationState; +}; + +const validateUser = (value: string) => { + return validateNoSpaces(value); +}; + +const validatePassword = (value: string) => { + return validateNoSpaces(value); +}; + +const validateCacert = (value: string) => { + if (value === '') { + return 'default'; + } else if (validatePublicCert(value)) { + return 'success'; + } else { + return 'error'; + } +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/vsphereSecretFieldValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/vsphereSecretFieldValidator.ts new file mode 100644 index 000000000..d216e4ec2 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret-fields/vsphereSecretFieldValidator.ts @@ -0,0 +1,47 @@ +import { Validation } from '../../types'; +import { validateFingerprint, validateNoSpaces } from '../common'; + +/** + * Validates form input fields based on their id. + * + * @param {string} id - The ID of the form field. + * @param {string} value - The value of the form field. + * + * @return {Validation} - The validation state of the form field. Can be one of the following: + * 'default' - The default state of the form field, used when the field is empty or a value hasn't been entered yet. + * 'success' - The field's value has passed validation. + * 'error' - The field's value has failed validation. + */ +export const vsphereSecretFieldValidator = (id: string, value: string) => { + const trimmedValue = value.trim(); + + let validationState: Validation = 'default'; + + switch (id) { + case 'user': + validationState = validateUser(trimmedValue) ? 'success' : 'error'; + break; + case 'password': + validationState = validatePassword(trimmedValue) ? 'success' : 'error'; + break; + case 'insecureSkipVerify': + validationState = 'default'; + break; + case 'thumbprint': + validationState = validateFingerprint(trimmedValue) ? 'success' : 'error'; + break; + default: + validationState = 'error'; + break; + } + + return validationState; +}; + +const validateUser = (value: string) => { + return validateNoSpaces(value); +}; + +const validatePassword = (value: string) => { + return validateNoSpaces(value); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/index.ts new file mode 100644 index 000000000..cb6277b4e --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/index.ts @@ -0,0 +1,7 @@ +// @index(['./*.tsx', './*.ts', /__/g], f => `export * from '${f.path}';`) +export * from './openshiftSecretValidator'; +export * from './openstackSecretValidator'; +export * from './ovirtSecretValidator'; +export * from './secretValidator'; +export * from './vsphereSecretValidator'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/openshiftSecretValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/openshiftSecretValidator.ts new file mode 100644 index 000000000..da5497626 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/openshiftSecretValidator.ts @@ -0,0 +1,26 @@ +import { Base64 } from 'js-base64'; + +import { V1Secret } from '@kubev2v/types'; + +import { missingKeysInSecretData } from '../../helpers'; +import { openshiftSecretFieldValidator } from '../secret-fields'; + +export function openshiftSecretValidator(secret: V1Secret) { + const requiredFields = ['token']; + const validateFields = ['token']; + + const missingRequiredFields = missingKeysInSecretData(secret, requiredFields); + if (missingRequiredFields.length > 0) { + return new Error(`missing required fields [${missingRequiredFields.join(', ')}]`); + } + + for (const id of validateFields) { + const value = Base64.decode(secret?.data?.[id] || ''); + + if (openshiftSecretFieldValidator(id, value) === 'error') { + return new Error(`invalid ${id}`); + } + } + + return null; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/openstackSecretValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/openstackSecretValidator.ts new file mode 100644 index 000000000..d32a7c0d8 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/openstackSecretValidator.ts @@ -0,0 +1,86 @@ +import { V1Secret } from '@kubev2v/types'; + +import { missingKeysInSecretData, safeBase64Decode } from '../../helpers'; +import { openstackSecretFieldValidator } from '../secret-fields'; + +export function openstackSecretValidator(secret: V1Secret) { + const authType = safeBase64Decode(secret?.data?.['authType']) || 'password'; + + let requiredFields = []; + let validateFields = []; + + // guess authenticationType based on authType and username + switch (authType) { + case 'password': + requiredFields = ['username', 'password', 'regionName', 'projectName', 'domainName']; + validateFields = [ + 'username', + 'password', + 'regionName', + 'projectName', + 'domainName', + 'cacert', + 'insecureSkipVerify', + ]; + break; + case 'token': + if (secret?.data?.['username']) { + requiredFields = ['token', 'username', 'projectName', 'userDomainName']; + validateFields = [ + 'token', + 'username', + 'projectName', + 'userDomainName', + 'cacert', + 'insecureSkipVerify', + ]; + } else { + requiredFields = ['token', 'userID', 'projectID']; + validateFields = ['token', 'userID', 'projectID', 'cacert', 'insecureSkipVerify']; + } + break; + case 'applicationcredential': + if (secret?.data?.['username']) { + requiredFields = [ + 'applicationCredentialName', + 'applicationCredentialSecret', + 'username', + 'domainName', + ]; + validateFields = [ + 'applicationCredentialName', + 'applicationCredentialSecret', + 'username', + 'domainName', + 'cacert', + 'insecureSkipVerify', + ]; + } else { + requiredFields = ['applicationCredentialID', 'applicationCredentialSecret']; + validateFields = [ + 'applicationCredentialID', + 'applicationCredentialSecret', + 'cacert', + 'insecureSkipVerify', + ]; + } + break; + default: + return new Error(`invalid authType`); + } + + const missingRequiredFields = missingKeysInSecretData(secret, requiredFields); + if (missingRequiredFields.length > 0) { + return new Error(`missing required fields [${missingRequiredFields.join(', ')}]`); + } + + for (const id of validateFields) { + const value = safeBase64Decode(secret?.data?.[id] || ''); + + if (openstackSecretFieldValidator(id, value) === 'error') { + return new Error(`invalid ${id}`); + } + } + + return null; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/ovirtSecretValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/ovirtSecretValidator.ts new file mode 100644 index 000000000..682cb77d5 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/ovirtSecretValidator.ts @@ -0,0 +1,26 @@ +import { Base64 } from 'js-base64'; + +import { V1Secret } from '@kubev2v/types'; + +import { missingKeysInSecretData } from '../../helpers'; +import { ovirtSecretFieldValidator } from '../secret-fields'; + +export function ovirtSecretValidator(secret: V1Secret) { + const requiredFields = ['user', 'password']; + const validateFields = ['user', 'password', 'cacert']; + + const missingRequiredFields = missingKeysInSecretData(secret, requiredFields); + if (missingRequiredFields.length > 0) { + return new Error(`missing required fields [${missingRequiredFields.join(', ')}]`); + } + + for (const id of validateFields) { + const value = Base64.decode(secret?.data?.[id] || ''); + + if (ovirtSecretFieldValidator(id, value) === 'error') { + return new Error(`invalid ${id}`); + } + } + + return null; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/secretValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/secretValidator.ts new file mode 100644 index 000000000..1c7eaef0e --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/secretValidator.ts @@ -0,0 +1,29 @@ +import { V1Secret } from '@kubev2v/types'; + +import { openshiftSecretValidator } from './openshiftSecretValidator'; +import { openstackSecretValidator } from './openstackSecretValidator'; +import { ovirtSecretValidator } from './ovirtSecretValidator'; +import { vsphereSecretValidator } from './vsphereSecretValidator'; + +export function secretValidator(type: string, secret: V1Secret) { + let validationError = null; + + switch (type) { + case 'openshift': + validationError = openshiftSecretValidator(secret); + break; + case 'openstack': + validationError = openstackSecretValidator(secret); + break; + case 'ovirt': + validationError = ovirtSecretValidator(secret); + break; + case 'vsphere': + validationError = vsphereSecretValidator(secret); + break; + default: + validationError = new Error('bad provider type'); + } + + return validationError; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/vsphereSecretValidator.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/vsphereSecretValidator.ts new file mode 100644 index 000000000..a824d0285 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/utils/validators/secret/vsphereSecretValidator.ts @@ -0,0 +1,26 @@ +import { Base64 } from 'js-base64'; + +import { V1Secret } from '@kubev2v/types'; + +import { missingKeysInSecretData } from '../../helpers'; +import { vsphereSecretFieldValidator } from '../secret-fields'; + +export function vsphereSecretValidator(secret: V1Secret) { + const requiredFields = ['user', 'password', 'thumbprint']; + const validateFields = ['user', 'password', 'thumbprint']; + + const missingRequiredFields = missingKeysInSecretData(secret, requiredFields); + if (missingRequiredFields.length > 0) { + return new Error(`missing required fields [${missingRequiredFields.join(', ')}]`); + } + + for (const id of validateFields) { + const value = Base64.decode(secret?.data?.[id] || ''); + + if (vsphereSecretFieldValidator(id, value) === 'error') { + return new Error(`invalid ${id}`); + } + } + + return null; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/ProvidersCreatePage.style.css b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/ProvidersCreatePage.style.css new file mode 100644 index 000000000..9366ffee6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/ProvidersCreatePage.style.css @@ -0,0 +1,3 @@ +.forklift-create-provider-edit-section { + padding-top: var(--pf-global--spacer--md); +} \ No newline at end of file diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/ProvidersCreatePage.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/ProvidersCreatePage.tsx new file mode 100644 index 000000000..b59390447 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/ProvidersCreatePage.tsx @@ -0,0 +1,271 @@ +import React, { useReducer } from 'react'; +import { useHistory } from 'react-router'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ProviderModelRef, V1beta1Provider, V1Secret } from '@kubev2v/types'; +import { + Alert, + Button, + Divider, + Flex, + FlexItem, + HelperText, + HelperTextItem, + PageSection, + Title, +} from '@patternfly/react-core'; + +import { useK8sWatchProviderNames } from '../../hooks'; +import { getResourceUrl, Validation } from '../../utils'; +import { providerAndSecretValidator } from '../../utils/validators/providerAndSecretValidator'; + +import { ProvidersCreateForm } from './components'; +import { providerTemplate, secretTemplate } from './templates'; +import { createProvider, createSecret, patchSecretOwner } from './utils'; + +import './ProvidersCreatePage.style.css'; + +interface ProvidersCreatePageState { + newSecret: V1Secret; + newProvider: V1beta1Provider; + validationError: Error | null; + apiError: Error | null; + validation: { + name: Validation; + }; +} + +export const ProvidersCreatePage: React.FC<{ + namespace: string; +}> = ({ namespace }) => { + const { t } = useForkliftTranslation(); + const history = useHistory(); + + const [providerNames] = useK8sWatchProviderNames({ namespace }); + + const initialState: ProvidersCreatePageState = { + newSecret: { + ...secretTemplate, + metadata: { ...secretTemplate.metadata, namespace: namespace || 'default' }, + }, + newProvider: { + ...providerTemplate, + metadata: { ...providerTemplate.metadata, namespace: namespace || 'default' }, + }, + validationError: new Error('Missing provider name'), + apiError: null, + validation: { + name: 'default', + }, + }; + + function reducer( + state: ProvidersCreatePageState, + action: { type: string; payload?: string | V1Secret | V1beta1Provider }, + ): ProvidersCreatePageState { + switch (action.type) { + case 'SET_NEW_SECRET': { + const value = action.payload as V1beta1Provider; + let validationError = providerAndSecretValidator(state.newProvider, value); + + if (providerNames.includes(state.newProvider?.metadata?.name)) { + validationError = new Error('new provider name is not unique'); + } + + if (!state.newProvider?.metadata?.name) { + validationError = new Error('Missing provider name'); + } + + return { + ...state, + validationError: validationError, + newSecret: value, + apiError: null, + }; + } + case 'SET_NEW_PROVIDER': { + const value = action.payload as V1beta1Provider; + let validationError = providerAndSecretValidator(value, state.newSecret); + + if (providerNames.includes(value?.metadata?.name)) { + validationError = new Error('new provider name is not unique'); + } + + if (!value?.metadata?.name) { + validationError = new Error('Missing provider name'); + } + + return { + ...state, + validationError: validationError, + newProvider: value, + apiError: null, + }; + } + case 'SET_API_ERROR': { + const value = action.payload as Error | null; + return { ...state, apiError: value }; + } + default: + return state; + } + } + + const [state, dispatch] = useReducer(reducer, initialState); + + if (!state.newSecret) { + return {t('No credentials found.')}; + } + + // Handle user edits + function onNewSecretChange(newValue: V1Secret) { + // update staged secret with new value + dispatch({ type: 'SET_NEW_SECRET', payload: newValue }); + } + + // Handle user edits + function onNewProviderChange(newValue: V1beta1Provider) { + // update staged provider with new value + dispatch({ type: 'SET_NEW_PROVIDER', payload: newValue }); + } + + // Handle user clicking "save" + async function onUpdate() { + let secret: V1Secret; + let provider: V1beta1Provider; + + // try to generate a secret with data + // + // add generateName using provider name as prefix + // add createdForProviderType label + // add url + try { + secret = await createSecret(state.newProvider, state.newSecret); + } catch (err) { + dispatch({ + type: 'SET_API_ERROR', + payload: err, + }); + + return; + } + + // try to create a provider with secret + // add spec.secret + try { + provider = await createProvider(state.newProvider, secret); + } catch (err) { + dispatch({ + type: 'SET_API_ERROR', + payload: err, + }); + + return; + } + + // set secret ownership using provider uid + try { + await patchSecretOwner(provider, secret); + } catch (err) { + dispatch({ + type: 'SET_API_ERROR', + payload: err, + }); + + return; + } + + // go to providers derails page + const providerURL = getResourceUrl({ + reference: ProviderModelRef, + namespace: namespace, + name: provider.metadata.name, + }); + + history.push(providerURL); + } + + const providersListURL = getResourceUrl({ + reference: ProviderModelRef, + namespace: namespace, + }); + + return ( +
+ + {t('Create Provider')} + + + + {t( + 'Create by using the form or manually entering YAML or JSON definitions, Provider CR stores attributes that enable MTV to connect to and interact with the source and target providers.', + )} + + + + + + + + + + + + + + {state.validationError ? ( + + {state.validationError.toString()} + + ) : ( + {t('Create new provider')} + )} + + + + + {state.apiError && ( + + {state.apiError.message || state.apiError.toString()} + + )} + + {!namespace && ( + + {t( + 'This provider will be created in the default namespace, if you wish to choose another namespace please cancel, and choose a namespace from the top bar.', + )} + + )} + + + +
+ ); +}; + +export default ProvidersCreatePage; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/OpenshiftProviderCreateForm.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/OpenshiftProviderCreateForm.tsx new file mode 100644 index 000000000..9a21898a2 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/OpenshiftProviderCreateForm.tsx @@ -0,0 +1,79 @@ +import React, { useCallback, useReducer } from 'react'; +import { validateURL, Validation } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { V1beta1Provider } from '@kubev2v/types'; +import { Form, FormGroup, TextInput } from '@patternfly/react-core'; + +export interface OpenshiftProviderCreateFormProps { + provider: V1beta1Provider; + onChange: (newValue: V1beta1Provider) => void; +} + +export const OpenshiftProviderFormCreate: React.FC = ({ + provider, + onChange, +}) => { + const { t } = useForkliftTranslation(); + + const url = provider?.spec?.url || ''; + + const initialState = { + validation: { + url: 'default' as Validation, + }, + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'SET_FIELD_VALIDATED': + return { + ...state, + validation: { + ...state.validation, + [action.payload.field]: action.payload.validationState, + }, + }; + default: + return state; + } + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + const handleChange = useCallback( + (id, value) => { + const trimmedValue = value.trim(); + + if (id === 'url') { + const validationState = + trimmedValue === '' || validateURL(trimmedValue) ? 'success' : 'error'; + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + onChange({ ...provider, spec: { ...provider.spec, url: trimmedValue } }); + } + }, + [provider], + ); + + return ( +
+ + handleChange('url', value)} + /> + +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/OpenstackProviderCreateForm.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/OpenstackProviderCreateForm.tsx new file mode 100644 index 000000000..81ca779aa --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/OpenstackProviderCreateForm.tsx @@ -0,0 +1,80 @@ +import React, { useCallback, useReducer } from 'react'; +import { validateURL, Validation } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { V1beta1Provider } from '@kubev2v/types'; +import { Form, FormGroup, TextInput } from '@patternfly/react-core'; + +export interface OpenstackProviderCreateFormProps { + provider: V1beta1Provider; + onChange: (newValue: V1beta1Provider) => void; +} + +export const OpenstackProviderCreateForm: React.FC = ({ + provider, + onChange, +}) => { + const { t } = useForkliftTranslation(); + + const url = provider?.spec?.url || ''; + + const initialState = { + validation: { + url: 'default' as Validation, + }, + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'SET_FIELD_VALIDATED': + return { + ...state, + validation: { + ...state.validation, + [action.payload.field]: action.payload.validationState, + }, + }; + default: + return state; + } + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + const handleChange = useCallback( + (id, value) => { + const trimmedValue = value.trim(); + + if (id === 'url') { + const validationState = validateURL(trimmedValue) ? 'success' : 'error'; + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + onChange({ ...provider, spec: { ...provider.spec, url: trimmedValue } }); + } + }, + [provider], + ); + + return ( +
+ + handleChange('url', value)} + /> + +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/OvirtProviderCreateForm.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/OvirtProviderCreateForm.tsx new file mode 100644 index 000000000..1791a169d --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/OvirtProviderCreateForm.tsx @@ -0,0 +1,80 @@ +import React, { useCallback, useReducer } from 'react'; +import { validateURL, Validation } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { V1beta1Provider } from '@kubev2v/types'; +import { Form, FormGroup, TextInput } from '@patternfly/react-core'; + +export interface OvirtProviderCreateFormProps { + provider: V1beta1Provider; + onChange: (newValue: V1beta1Provider) => void; +} + +export const OvirtProviderCreateForm: React.FC = ({ + provider, + onChange, +}) => { + const { t } = useForkliftTranslation(); + + const url = provider?.spec?.url || ''; + + const initialState = { + validation: { + url: 'default' as Validation, + }, + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'SET_FIELD_VALIDATED': + return { + ...state, + validation: { + ...state.validation, + [action.payload.field]: action.payload.validationState, + }, + }; + default: + return state; + } + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + const handleChange = useCallback( + (id, value) => { + const trimmedValue = value.trim(); + + if (id === 'url') { + const validationState = validateURL(trimmedValue) ? 'success' : 'error'; + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + onChange({ ...provider, spec: { ...provider.spec, url: trimmedValue } }); + } + }, + [provider], + ); + + return ( +
+ + handleChange('url', value)} + /> + +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/ProviderCreateForm.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/ProviderCreateForm.tsx new file mode 100644 index 000000000..5cf5db27e --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/ProviderCreateForm.tsx @@ -0,0 +1,174 @@ +import React, { useReducer } from 'react'; +import { Base64 } from 'js-base64'; +import { validateK8sName, Validation } from 'src/modules/ProvidersNG/utils'; +import { SelectableCard } from 'src/modules/ProvidersNG/utils/components/Galerry/SelectableCard'; +import { SelectableGallery } from 'src/modules/ProvidersNG/utils/components/Galerry/SelectableGallery'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ProviderType, V1beta1Provider, V1Secret } from '@kubev2v/types'; +import { Flex, FlexItem, Form, FormGroup, TextInput } from '@patternfly/react-core'; + +import { + OpenshiftCredentialsEdit, + OpenstackCredentialsEdit, + OvirtCredentialsEdit, + VSphereCredentialsEdit, +} from '../../details'; + +import { OpenshiftProviderFormCreate } from './OpenshiftProviderCreateForm'; +import { OpenstackProviderCreateForm } from './OpenstackProviderCreateForm'; +import { OvirtProviderCreateForm } from './OvirtProviderCreateForm'; +import { providerCardItems } from './providerCardItems'; +import { VSphereProviderCreateForm } from './VSphereProviderCreateForm'; + +export interface ProvidersCreateFormProps { + newProvider: V1beta1Provider; + newSecret: V1Secret; + onNewProviderChange: (V1beta1Provider) => void; + onNewSecretChange: (V1Secret) => void; + providerNames: string[]; +} + +export const ProvidersCreateForm: React.FC = ({ + newProvider, + newSecret, + onNewProviderChange, + onNewSecretChange, + providerNames = [], +}) => { + const { t } = useForkliftTranslation(); + + const initialState = { + validation: { + name: 'default', + }, + }; + + const [state, dispatch] = useReducer((state, action) => { + switch (action.type) { + case 'SET_VALIDATION': + return { ...state, validation: action.payload }; + default: + return state; + } + }, initialState); + + const handleNameChange = (name: string) => { + const trimmedValue = name.trim(); + const validation: Validation = + !providerNames.includes(trimmedValue) && validateK8sName(trimmedValue) ? 'success' : 'error'; + + dispatch({ + type: 'SET_VALIDATION', + payload: { name: validation }, + }); + + onNewProviderChange({ + ...newProvider, + metadata: { ...newProvider?.metadata, name: trimmedValue }, + }); + }; + + const handleTypeChange = (type: ProviderType) => { + // default auth type for openstack (if not defined) + if (type === 'openstack' && !newSecret?.data?.authType) { + onNewSecretChange({ + ...newSecret, + data: { ...newSecret.data, authType: Base64.encode('applicationcredential') }, + }); + } + + onNewProviderChange({ ...newProvider, spec: { ...newProvider?.spec, type: type } }); + }; + + const EditProvider = () => { + switch (newProvider?.spec?.type) { + case 'openstack': + return ( + <> + + + + ); + case 'openshift': + return ( + <> + + + + ); + case 'ovirt': + return ( + <> + + + + ); + case 'vsphere': + return ( + <> + + + + ); + default: + return <>; + } + }; + + return ( + <> +
+
+ + handleNameChange(value)} // Call the custom handler method + /> + +
+ +
+ + {newProvider?.spec?.type ? ( + + + handleTypeChange(null)} + isSelected={true} + /> + + + ) : ( + + )} + +
+
+ +
{EditProvider()}
+ + ); +}; + +export default ProvidersCreateForm; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/VSphereProviderCreateForm.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/VSphereProviderCreateForm.tsx new file mode 100644 index 000000000..1a246a3d4 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/VSphereProviderCreateForm.tsx @@ -0,0 +1,117 @@ +import React, { useCallback, useReducer } from 'react'; +import { validateContainerImage, validateURL, Validation } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { V1beta1Provider } from '@kubev2v/types'; +import { Form, FormGroup, TextInput } from '@patternfly/react-core'; + +export interface VSphereProviderCreateFormProps { + provider: V1beta1Provider; + onChange: (newValue: V1beta1Provider) => void; +} + +export const VSphereProviderCreateForm: React.FC = ({ + provider, + onChange, +}) => { + const { t } = useForkliftTranslation(); + + const url = provider?.spec?.url || ''; + const vddkInitImage = provider?.spec?.settings?.['vddkInitImage'] || ''; + + const initialState = { + validation: { + url: 'default' as Validation, + vddkInitImage: 'default' as Validation, + }, + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'SET_FIELD_VALIDATED': + return { + ...state, + validation: { + ...state.validation, + [action.payload.field]: action.payload.validationState, + }, + }; + default: + return state; + } + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + const handleChange = useCallback( + (id, value) => { + const trimmedValue = value.trim(); + + if (id == 'vddkInitImage') { + const validationState = validateContainerImage(trimmedValue) ? 'success' : 'error'; + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + onChange({ + ...provider, + spec: { + type: provider.spec.type, + url: provider.spec.url, + ...provider?.spec, + settings: { + ...(provider?.spec?.settings as object), + vddkInitImage: value.trim(), + }, + }, + }); + } + + if (id === 'url') { + const validationState = validateURL(trimmedValue) ? 'success' : 'error'; + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + onChange({ ...provider, spec: { ...provider.spec, url: trimmedValue } }); + } + }, + [provider], + ); + + return ( +
+ + handleChange('url', value)} + /> + + + + handleChange('vddkInitImage', value)} + /> + +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/index.ts new file mode 100644 index 000000000..a421326e6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/index.ts @@ -0,0 +1,8 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './OpenshiftProviderCreateForm'; +export * from './OpenstackProviderCreateForm'; +export * from './OvirtProviderCreateForm'; +export * from './providerCardItems'; +export * from './ProviderCreateForm'; +export * from './VSphereProviderCreateForm'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/providerCardItems.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/providerCardItems.ts new file mode 100644 index 000000000..69d249e95 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/components/providerCardItems.ts @@ -0,0 +1,20 @@ +import { SelectableGalleryItem } from 'src/modules/ProvidersNG/utils/components/Galerry/SelectableGallery'; + +export const providerCardItems: Record = { + openshift: { + title: 'OpenShift Virtualization', + content: 'OpenShift Virtualization run and manage virtual machine in Openshift.', + }, + openstack: { + title: 'OpenStack', + content: 'OpenStack is a cloud computing platform that controls large pools of resources.', + }, + ovirt: { + title: 'Red Hat Virtualization', + content: 'Red Hat Virtualization (RHV) is a virtualization platform from Red Hat.', + }, + vsphere: { + title: 'vSphere', + content: "vSphere is VMware's cloud computing virtualization platform.", + }, +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/index.ts new file mode 100644 index 000000000..11b1670bc --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/index.ts @@ -0,0 +1,6 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './components'; +export * from './ProvidersCreatePage'; +export * from './templates'; +export * from './utils'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/templates/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/templates/index.ts new file mode 100644 index 000000000..9408052f7 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/templates/index.ts @@ -0,0 +1,4 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './providerTemplate'; +export * from './secretTemplate'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/templates/providerTemplate.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/templates/providerTemplate.ts new file mode 100644 index 000000000..1810ac939 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/templates/providerTemplate.ts @@ -0,0 +1,18 @@ +import { V1beta1Provider } from '@kubev2v/types'; + +export const providerTemplate: V1beta1Provider = { + apiVersion: 'forklift.konveyor.io/v1beta1', + kind: 'Provider', + metadata: { + name: undefined, + namespace: undefined, + }, + spec: { + secret: { + name: undefined, + namespace: undefined, + }, + type: undefined, + url: undefined, + }, +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/templates/secretTemplate.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/templates/secretTemplate.ts new file mode 100644 index 000000000..df3173227 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/templates/secretTemplate.ts @@ -0,0 +1,12 @@ +import { V1Secret } from '@kubev2v/types'; + +export const secretTemplate: V1Secret = { + kind: 'Secret', + apiVersion: 'v1', + metadata: { + name: undefined, + namespace: undefined, + }, + data: undefined, + type: 'Opaque', +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/createProvider.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/createProvider.ts new file mode 100644 index 000000000..8658a2ff0 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/createProvider.ts @@ -0,0 +1,38 @@ +import { ProviderModel, V1beta1Provider, V1Secret } from '@kubev2v/types'; +import { k8sCreate } from '@openshift-console/dynamic-plugin-sdk'; + +/** + * Creates a new provider with the specified secret information. + * + * @param {V1beta1Provider} provider - The provider object to be cloned and modified. + * @param {V1Secret} secret - The secret object used to update the provider's secret information. + * @returns {Promise} A Promise that resolves to the created provider object. + * + * @async + * @throws Will throw an error if the k8sCreate operation fails. + * + * @example + * + * const provider = { metadata: { name: 'my-provider' }, spec: {}}; + * const secret = { metadata: { name: 'my-secret', namespace: 'my-namespace' }}; + * + * createProvider(provider, secret) + * .then(newProvider => console.log(newProvider)) + * .catch(err => console.error(err)); + */ +export async function createProvider(provider: V1beta1Provider, secret: V1Secret) { + const newProvider: V1beta1Provider = { + ...provider, + spec: { + ...provider?.spec, + secret: { name: secret?.metadata?.name, namespace: secret?.metadata?.namespace }, + }, + }; + + const obj = await k8sCreate({ + model: ProviderModel, + data: newProvider, + }); + + return obj; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/createSecret.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/createSecret.ts new file mode 100644 index 000000000..e0d97235a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/createSecret.ts @@ -0,0 +1,49 @@ +import { Base64 } from 'js-base64'; + +import { SecretModel, V1beta1Provider, V1Secret } from '@kubev2v/types'; +import { k8sCreate } from '@openshift-console/dynamic-plugin-sdk'; + +/** + * Creates a new Kubernetes secret using the provided provider and secret data. + * + * @param {V1beta1Provider} provider - The provider object which includes metadata and spec information. + * @param {V1Secret} secret - The base secret object to be cloned and modified. + * @returns {Promise} A Promise that resolves to the created Kubernetes secret object. + * + * @async + * @throws Will throw an error if the k8sCreate operation fails. + * + * @example + * + * const provider = { metadata: { name: 'my-provider', namespace: 'my-namespace' }, spec: { type: 'my-type', url: 'http://example.com' }}; + * const secret = { metadata: { namespace: 'my-namespace' }, data: {}}; + * + * createSecret(provider, secret) + * .then(newSecret => console.log(newSecret)) + * .catch(err => console.error(err)); + */ +export async function createSecret(provider: V1beta1Provider, secret: V1Secret) { + const url = provider?.spec?.url; + const encodedURL = url ? Base64.encode(url) : undefined; + const generateName = `${provider.metadata.name}-`; + + const newSecret: V1Secret = { + ...secret, + metadata: { + ...secret?.metadata, + generateName: generateName, + labels: { + ...secret?.metadata?.labels, + createdForProviderType: provider?.spec?.type, + }, + }, + data: { ...secret?.data, url: encodedURL }, + }; + + const obj = await k8sCreate({ + model: SecretModel, + data: newSecret, + }); + + return obj; +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/index.ts new file mode 100644 index 000000000..fd3702eda --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/index.ts @@ -0,0 +1,5 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './createProvider'; +export * from './createSecret'; +export * from './patchSecretOwner'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/patchSecretOwner.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/patchSecretOwner.ts new file mode 100644 index 000000000..d1b66637f --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/create/utils/patchSecretOwner.ts @@ -0,0 +1,43 @@ +import { SecretModel, V1beta1Provider, V1Secret } from '@kubev2v/types'; +import { k8sPatch } from '@openshift-console/dynamic-plugin-sdk'; + +/** + * Updates the owner reference of the specified secret to point to the provided provider. + * + * @param {V1beta1Provider} provider - The provider object to be set as the owner of the secret. + * @param {V1Secret} secret - The secret object to be updated with the provider's owner reference. + * + * @async + * @throws Will throw an error if the k8sPatch operation fails. + * + * @example + * + * const provider = { metadata: { name: 'my-provider', uid: 'uid-123' }}; + * const secret = { metadata: {}, data: {}}; + * + * patchSecretOwner(provider, secret) + * .then(() => console.log('Secret owner patched successfully')) + * .catch(err => console.error(err)); + */ +export async function patchSecretOwner(provider: V1beta1Provider, secret: V1Secret) { + const op = secret?.metadata?.ownerReferences ? 'replace' : 'add'; + + await k8sPatch({ + model: SecretModel, + resource: secret, + data: [ + { + op, + path: '/metadata/ownerReferences', + value: [ + { + apiVersion: 'forklift.konveyor.io/v1beta1', + kind: 'Provider', + name: provider.metadata.name, + uid: provider.metadata.uid, + }, + ], + }, + ], + }); +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/ProviderDetailsPage.style.css b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/ProviderDetailsPage.style.css new file mode 100644 index 000000000..ccda3b670 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/ProviderDetailsPage.style.css @@ -0,0 +1,7 @@ +.forklift-page-headings-alerts { + padding-left: 0; +} + +.forklift-page-section { + border-top: 1px solid var(--pf-global--BorderColor--100); +} diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/ProviderDetailsPage.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/ProviderDetailsPage.tsx new file mode 100644 index 000000000..342210136 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/ProviderDetailsPage.tsx @@ -0,0 +1,155 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { + ProviderInventory, + ProviderModel, + ProviderModelGroupVersionKind, + V1beta1Provider, +} from '@kubev2v/types'; +import { + HorizontalNav, + K8sModel, + useK8sWatchResource, +} from '@openshift-console/dynamic-plugin-sdk'; +import { PageSection } from '@patternfly/react-core'; + +import { ProviderActionsDropdown } from '../../actions'; +import { useGetDeleteAndEditAccessReview, useProviderInventory } from '../../hooks'; +import { PageHeadings } from '../../utils'; + +import { + ProviderCredentials, + ProviderDetails, + ProviderHosts, + ProviderNetworks, + ProviderVirtualMachines, + ProviderYAMLPage, +} from './tabs'; + +import './ProviderDetailsPage.style.css'; + +export const ProviderDetailsPage: React.FC = ({ name, namespace }) => { + const { t } = useForkliftTranslation(); + + const [provider, providerLoaded, providerLoadError] = useK8sWatchResource({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + name, + namespace, + }); + + const { inventory } = useProviderInventory({ + provider, + }); + + const permissions = useGetDeleteAndEditAccessReview({ + model: ProviderModel, + namespace, + }); + + const data = { provider, inventory, permissions }; + const alerts = []; + const loaded = providerLoaded; + const loadError = providerLoadError; + const type = provider?.spec?.type; + + const providerHasSecret = provider?.spec?.secret?.name; + const providerHasHosts = ['vsphere'].includes(type); + const ProviderHasVirtualMachines = ['openshift', 'openstack', 'ovirt', 'vsphere'].includes(type); + const providerHasNetworks = ['openshift'].includes(type); + + const pages = [ + { + href: '', + name: t('Details'), + component: () => { + return ; + }, + }, + { + href: 'yaml', + name: t('YAML'), + component: () => { + return ( + + ); + }, + }, + providerHasSecret && { + href: 'credentials', + name: t('Credentials'), + component: () => { + return ( + + ); + }, + }, + + ProviderHasVirtualMachines && { + href: 'vms', + name: t('Virtual Machines'), + component: () => { + return ( + + ); + }, + }, + + providerHasHosts && { + href: 'hosts', + name: t('Hosts'), + component: () => { + return ; + }, + }, + + providerHasNetworks && { + href: 'networks', + name: t('Networks'), + component: () => { + return ( + + ); + }, + }, + ]; + + return ( + <> + } + > + {alerts && alerts.length > 0 && ( + {alerts} + )} + + p)} /> + + ); +}; +ProviderDetailsPage.displayName = 'ProviderDetails'; + +type ProviderDetailsPageProps = { + kind: string; + kindObj: K8sModel; + match: { path: string; url: string; isExact: boolean; params: unknown }; + name: string; + namespace?: string; +}; + +export default ProviderDetailsPage; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/ConditionsSection/ConditionsSection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/ConditionsSection/ConditionsSection.tsx new file mode 100644 index 000000000..c9e8788be --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/ConditionsSection/ConditionsSection.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { K8sResourceCondition } from '@kubev2v/types'; +import { Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; + +/** + * React Component to display a table of conditions. + * + * @component + * @param {ConditionsProps} props - Props for the Conditions component. + * @param {K8sResourceCondition[]} props.conditions - Array of conditions to be displayed. + * @returns {ReactElement} A table displaying the provided conditions. + */ +export const ConditionsSection: React.FC = ({ conditions }) => { + const { t } = useForkliftTranslation(); + + const getStatusLabel = (status: string) => { + switch (status) { + case 'True': + return t('True'); + case 'False': + return t('False'); + default: + return status; + } + }; + + return ( + <> + + + + {t('Type')} + {t('Status')} + {t('Updated')} + {t('Reason')} + {t('Message')} + + + + {conditions.map((condition) => ( + + {condition.type} + {getStatusLabel(condition.status)} + + + + {condition.reason} + {condition?.message || '-'} + + ))} + + + + ); +}; + +/** + * Type for the props of the Conditions component. + * + * @typedef {Object} ConditionsProps + * @property {K8sResourceCondition[]} conditions - The conditions to be displayed. + */ +export type ConditionsProps = { + conditions: K8sResourceCondition[]; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/ConditionsSection/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/ConditionsSection/index.ts new file mode 100644 index 000000000..75876a3f6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/ConditionsSection/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './ConditionsSection'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/CredentialsSection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/CredentialsSection.tsx new file mode 100644 index 000000000..dcb920434 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/CredentialsSection.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { ProviderData } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { V1Secret } from '@kubev2v/types'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; + +import { OpenshiftCredentialsSection } from './OpenshiftCredentialsSection'; +import { OpenstackCredentialsSection } from './OpenstackCredentialsSection'; +import { OvirtCredentialsSection } from './OvirtCredentialsSection'; +import { VSphereCredentialsSection } from './VSphereCredentialsSection'; + +export const CredentialsSection: React.FC = (props) => { + const { t } = useForkliftTranslation(); + const { data, loaded: providerLoaded, loadError: providerError } = props; + const { provider } = data; + + const [secret, loaded, loadError] = useK8sWatchResource({ + groupVersionKind: { version: 'v1', kind: 'Secret' }, + namespaced: true, + namespace: provider?.spec?.secret?.namespace, + name: provider?.spec?.secret?.name, + }); + + // Checking if all necessary data is available + const isDataLoaded = secret && loaded && !loadError && providerLoaded && !providerError; + const isProviderDataAvailable = provider?.spec?.secret?.name && provider?.spec?.secret?.namespace; + const isSecretDataAvailable = secret?.metadata?.name && secret?.metadata?.namespace; + + // Checking if provider data matches secret data + const doesProviderDataMatchSecret = + secret?.metadata?.name === provider?.spec?.secret?.name && + secret?.metadata?.namespace === provider?.spec?.secret?.namespace; + + if ( + !isDataLoaded || + !isProviderDataAvailable || + !isSecretDataAvailable || + !doesProviderDataMatchSecret + ) { + return ( +
+ {t('No secret found.')} +
+ ); + } + + switch (provider?.spec?.type) { + case 'ovirt': + return ; + case 'openshift': + return ; + case 'openstack': + return ; + case 'vsphere': + return ; + default: + return <>; + } +}; + +export type CredentialsProps = { + data: ProviderData; + loaded: boolean; + loadError: unknown; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/MaskedData.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/MaskedData.tsx new file mode 100644 index 000000000..9057d9589 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/MaskedData.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +import { TextInput } from '@patternfly/react-core'; + +export const MaskedData: React.FC = () => { + return ; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/OpenshiftCredentialsSection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/OpenshiftCredentialsSection.tsx new file mode 100644 index 000000000..b133d0476 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/OpenshiftCredentialsSection.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { openshiftSecretValidator } from 'src/modules/ProvidersNG/utils'; + +import { + BaseCredentialsSection, + BaseCredentialsSectionProps, +} from './components/BaseCredentialsSection'; +import { OpenshiftCredentialsEdit } from './components/edit/OpenshiftCredentialsEdit'; +import { OpenshiftCredentialsList } from './components/list/OpenshiftCredentialsList'; + +export type OpenshiftCredentialsSectionProps = Omit< + BaseCredentialsSectionProps, + 'ListComponent' | 'EditComponent' | 'validator' +>; + +export const OpenshiftCredentialsSection: React.FC = (props) => { + return ( + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/OpenstackCredentialsSection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/OpenstackCredentialsSection.tsx new file mode 100644 index 000000000..c61eac65d --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/OpenstackCredentialsSection.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { openstackSecretValidator } from 'src/modules/ProvidersNG/utils'; + +import { + BaseCredentialsSection, + BaseCredentialsSectionProps, +} from './components/BaseCredentialsSection'; +import { OpenstackCredentialsEdit } from './components/edit/OpenstackCredentialsEdit'; +import { OpenstackCredentialsList } from './components/list/OpenstackCredentialsList'; + +export type OpenstackCredentialsSectionProps = Omit< + BaseCredentialsSectionProps, + 'ListComponent' | 'EditComponent' | 'validator' +>; + +export const OpenstackCredentialsSection: React.FC = (props) => ( + +); diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/OvirtCredentialsSection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/OvirtCredentialsSection.tsx new file mode 100644 index 000000000..54670dfa9 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/OvirtCredentialsSection.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { ovirtSecretValidator } from 'src/modules/ProvidersNG/utils'; + +import { + BaseCredentialsSection, + BaseCredentialsSectionProps, +} from './components/BaseCredentialsSection'; +import { OvirtCredentialsEdit } from './components/edit/OvirtCredentialsEdit'; +import { OvirtCredentialsList } from './components/list/OvirtCredentialsList'; + +export type OvirtCredentialsSectionProps = Omit< + BaseCredentialsSectionProps, + 'ListComponent' | 'EditComponent' | 'validator' +>; + +export const OvirtCredentialsSection: React.FC = (props) => ( + +); diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/VSphereCredentialsSection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/VSphereCredentialsSection.tsx new file mode 100644 index 000000000..aa842918d --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/VSphereCredentialsSection.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { vsphereSecretValidator } from 'src/modules/ProvidersNG/utils'; + +import { + BaseCredentialsSection, + BaseCredentialsSectionProps, +} from './components/BaseCredentialsSection'; +import { VSphereCredentialsEdit } from './components/edit/VSphereCredentialsEdit'; +import { VSphereCredentialsList } from './components/list/VSphereCredentialsList'; + +export type VSphereCredentialsSectionProps = Omit< + BaseCredentialsSectionProps, + 'ListComponent' | 'EditComponent' | 'validator' +>; + +export const VSphereCredentialsSection: React.FC = (props) => ( + +); diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/BaseCredentialsSection.style.css b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/BaseCredentialsSection.style.css new file mode 100644 index 000000000..b94dfceb3 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/BaseCredentialsSection.style.css @@ -0,0 +1,45 @@ +.forklift-page-secret-title-div { + padding-top: var(--pf-global--spacer--sm); + padding-bottom: var(--pf-global--spacer--xm); +} + +.forklift-page-secret-content-div{ + padding-top: var(--pf-global--spacer--sm); + padding-bottom: var(--pf-global--spacer--xm); +} + +.forklift-page-secret-title { + margin-top: var(--pf-global--spacer--sm); + margin-bottom: 0; +} + +.forklift-page-secret-subtitle { + margin-top: var(--pf-global--spacer--xs); + margin-bottom: var(--pf-global--spacer--md); + color: var(--pf-global--secondary-color--100); +} + +.forklift-page-details-edit-pencil{ + color: var(--pf-c-button--m-plain--Color); +} + +.forklift-section-secret-content-div{ + padding-top: var(--pf-global--spacer--md); +} + +.forklift-section-secret-edit{ + padding-top: var(--pf-global--spacer--md); +} + +.forklift-form-section-heading { + border-top: 1px solid var(--pf-global--BorderColor--100); +} + +.forklift-create-subtitle { + padding-bottom: var(--pf-global--spacer--md); +} + +.forklift-create-subtitle-errors { + padding-top: var(--pf-global--spacer--xs); + padding-bottom: var(--pf-global--spacer--sm); +} \ No newline at end of file diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/BaseCredentialsSection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/BaseCredentialsSection.tsx new file mode 100644 index 000000000..3936a5f72 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/BaseCredentialsSection.tsx @@ -0,0 +1,212 @@ +import React, { ReactNode, useReducer } from 'react'; +import { AlertMessageForModals } from 'src/modules/ProvidersNG/modals'; +import { isSecretDataChanged } from 'src/modules/ProvidersNG/utils'; +import { ProviderData } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { V1Secret } from '@kubev2v/types'; +import { + Button, + Divider, + Flex, + FlexItem, + HelperText, + HelperTextItem, +} from '@patternfly/react-core'; +import EyeIcon from '@patternfly/react-icons/dist/esm/icons/eye-icon'; +import EyeSlashIcon from '@patternfly/react-icons/dist/esm/icons/eye-slash-icon'; +import Pencil from '@patternfly/react-icons/dist/esm/icons/pencil-alt-icon'; + +import { patchSecretData } from './edit'; + +import './BaseCredentialsSection.style.css'; + +export interface ListComponentProps { + secret: V1Secret; + reveal: boolean; +} + +export interface EditComponentProps { + secret: V1Secret; + onChange: (newValue: V1Secret) => void; +} + +/** + * Represents the state of the secret edit form. + * + * @typedef {Object} BaseCredentialsSecretState + * @property {boolean} reveal - Determines whether the secret's values are visible. + * @property {boolean} edit - Determines whether the secret is currently being edited. + * @property {V1Secret} newSecret - The new version of the secret being edited. + * @property {boolean} dataChanged - Determines whether the secret's data has changed. + * @property {boolean} dataIsValid - Determines whether the new secret's data is valid. + * @property {ReactNode} alertMessage - The message to display when a validation error occurs. + */ +export interface BaseCredentialsSecretState { + reveal: boolean; + edit: boolean; + newSecret: V1Secret; + dataChanged: boolean; + dataError: Error | null; + alertMessage: ReactNode; +} + +export type BaseCredentialsSectionProps = { + data: ProviderData; + loaded: boolean; + loadError: unknown; + secret: V1Secret; + validator: (secret: V1Secret) => Error | null; + ListComponent: React.FC; + EditComponent: React.FC; +}; + +export const BaseCredentialsSection: React.FC = ({ + secret, + validator, + ListComponent, + EditComponent, +}) => { + const { t } = useForkliftTranslation(); + const initialState: BaseCredentialsSecretState = { + reveal: false, + edit: false, + newSecret: secret, + dataChanged: false, + dataError: null, + alertMessage: null, + }; + + function reducer( + state: BaseCredentialsSecretState, + action: { type: string; payload?: V1Secret }, + ): BaseCredentialsSecretState { + switch (action.type) { + case 'TOGGLE_REVEAL': + return { ...state, reveal: !state.reveal }; + case 'TOGGLE_EDIT': + return { ...state, edit: !state.edit }; + case 'SET_NEW_SECRET': { + const dataChanged = isSecretDataChanged(secret, action.payload); + const validationError = validator(action.payload); + + return { + ...state, + dataChanged, + dataError: validationError, + newSecret: action.payload, + alertMessage: null, + }; + } + case 'SET_ALERT_MESSAGE': + return { ...state, alertMessage: action.payload }; + default: + return state; + } + } + const [state, dispatch] = useReducer(reducer, initialState); + + if (!secret || !secret?.data) { + return {t('No credentials found.')}; + } + + // toggle between view and edit mode + function toggleEdit() { + dispatch({ type: 'TOGGLE_EDIT' }); + } + + // toggle secrets visible and hidden in view mode + function toggleReveal() { + dispatch({ type: 'TOGGLE_REVEAL' }); + } + + // Handle user edits + function onNewSecretChange(newValue: V1Secret) { + // update staged secret with new value + dispatch({ type: 'SET_NEW_SECRET', payload: newValue }); + } + + // Handle user clicking "cancel" + function onCancel() { + // clear changes and return to view mode + dispatch({ type: 'SET_NEW_SECRET', payload: secret }); + toggleEdit(); + } + + // Handle user clicking "save" + async function onUpdate() { + try { + // Patch provider secret, set clean to `true` + // to remove old values from the secret + await patchSecretData(state.newSecret, true); + + toggleEdit(); + } catch (err) { + dispatch({ + type: 'SET_ALERT_MESSAGE', + payload: ( + + ), + }); + } + } + + return state.edit ? ( + <> + + + + + + + + + + + {state.dataError ? ( + {state.dataError.toString()} + ) : ( + + {t( + 'Click the update credentials button to save your changes, button is disabled until a change is detected.', + )} + + )} + + + + + {state.alertMessage} + + + ) : ( + <> + + + + + + + + + + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenshiftCredentialsEdit.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenshiftCredentialsEdit.tsx new file mode 100644 index 000000000..d85ecdeee --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenshiftCredentialsEdit.tsx @@ -0,0 +1,101 @@ +import React, { useCallback, useReducer } from 'react'; +import { Base64 } from 'js-base64'; +import { + openshiftSecretFieldValidator, + safeBase64Decode, + Validation, +} from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { Button, Form, FormGroup, TextInput } from '@patternfly/react-core'; +import EyeIcon from '@patternfly/react-icons/dist/esm/icons/eye-icon'; +import EyeSlashIcon from '@patternfly/react-icons/dist/esm/icons/eye-slash-icon'; + +import { EditComponentProps } from '../BaseCredentialsSection'; + +export const OpenshiftCredentialsEdit: React.FC = ({ secret, onChange }) => { + const { t } = useForkliftTranslation(); + + const token = safeBase64Decode(secret?.data?.token || ''); + + const initialState = { + passwordHidden: true, + validation: { + token: 'default' as Validation, + }, + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'SET_FIELD_VALIDATED': + return { + ...state, + validation: { + ...state.validation, + [action.payload.field]: action.payload.validationState, + }, + }; + case 'TOGGLE_PASSWORD_HIDDEN': + return { ...state, passwordHidden: !state.passwordHidden }; + default: + return state; + } + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + // Define handleChange and validation functions + const handleChange = useCallback( + (id, value) => { + const validationState = openshiftSecretFieldValidator(id, value); + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + // don't trim fields that allow spaces + const encodedValue = ['cacert'].includes(id) + ? Base64.encode(value) + : Base64.encode(value.trim()); + + onChange({ ...secret, data: { ...secret.data, [id]: encodedValue } }); + }, + [secret], + ); + + // Handle password hide/reveal click + function togglePasswordHidden() { + dispatch({ type: 'TOGGLE_PASSWORD_HIDDEN' }); + } + + return ( +
+ + handleChange('token', value)} + value={token} + validated={state.validation.token} + /> + + +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEdit.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEdit.tsx new file mode 100644 index 000000000..2cd66c2dc --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEdit.tsx @@ -0,0 +1,255 @@ +import React, { useCallback, useReducer } from 'react'; +import { Base64 } from 'js-base64'; +import { + openstackSecretFieldValidator, + safeBase64Decode, + Validation, +} from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { Checkbox, Divider, FileUpload, Form, FormGroup, Radio } from '@patternfly/react-core'; + +import { EditComponentProps } from '../BaseCredentialsSection'; + +import { + ApplicationCredentialNameSecretFieldsFormGroup, + ApplicationWithCredentialsIDFormGroup, + PasswordSecretFieldsFormGroup, + TokenWithUserIDSecretFieldsFormGroup, + TokenWithUsernameSecretFieldsFormGroup, +} from './OpenstackCredentialsEditFormGroups'; + +export const OpenstackCredentialsEdit: React.FC = ({ secret, onChange }) => { + const { t } = useForkliftTranslation(); + + const authType = safeBase64Decode(secret?.data?.authType || ''); + const username = safeBase64Decode(secret?.data?.username || ''); + const insecureSkipVerify = safeBase64Decode(secret?.data?.insecureSkipVerify || '') === 'true'; + const cacert = safeBase64Decode(secret?.data?.cacert || ''); + + let authenticationType: + | 'passwordSecretFields' + | 'tokenWithUserIDSecretFields' + | 'tokenWithUsernameSecretFields' + | 'applicationCredentialIdSecretFields' + | 'applicationCredentialNameSecretFields'; + + // guess initial authenticationType based on authType and username + switch (authType) { + case 'password': + authenticationType = 'passwordSecretFields'; + break; + case 'token': + if (username) { + authenticationType = 'tokenWithUsernameSecretFields'; + } else { + authenticationType = 'tokenWithUserIDSecretFields'; + } + break; + case 'applicationcredential': + if (username) { + authenticationType = 'applicationCredentialNameSecretFields'; + } else { + authenticationType = 'applicationCredentialIdSecretFields'; + } + break; + default: + authenticationType = 'passwordSecretFields'; + break; + } + + const initialState = { + authenticationType: authenticationType, + validation: { + cacert: 'default' as Validation, + }, + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'SET_FIELD_VALIDATED': + return { + ...state, + validation: { + ...state.validation, + [action.payload.field]: action.payload.validationState, + }, + }; + case 'SET_AUTHENTICATION_TYPE': + return { + ...state, + authenticationType: action.payload, + }; + default: + return state; + } + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + // Define handleChange and validation functions + const handleChange = useCallback( + (id, value) => { + const validationState = openstackSecretFieldValidator(id, value); + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + // don't trim fields that allow spaces + const encodedValue = ['cacert'].includes(id) + ? Base64.encode(value) + : Base64.encode(value.trim()); + + onChange({ ...secret, data: { ...secret.data, [id]: encodedValue } }); + }, + [secret], + ); + + const handleAuthTypeChange = useCallback( + (type: string) => { + dispatch({ type: 'SET_AUTHENTICATION_TYPE', payload: type }); + + switch (type) { + case 'passwordSecretFields': + onChange({ + ...secret, + data: { ...secret.data, ['authType']: Base64.encode('password') }, + }); + break; + case 'tokenWithUserIDSecretFields': + case 'tokenWithUsernameSecretFields': + // on change also clean userID and username + onChange({ + ...secret, + data: { + ...secret.data, + ['authType']: Base64.encode('token'), + userID: '', + username: '', + }, + }); + break; + case 'applicationCredentialIdSecretFields': + case 'applicationCredentialNameSecretFields': + // on change also clean userID and username + onChange({ + ...secret, + data: { + ...secret.data, + ['authType']: Base64.encode('applicationcredential'), + applicationCredentialID: '', + username: '', + }, + }); + break; + } + }, + [secret], + ); + + return ( +
+ + handleAuthTypeChange('passwordSecretFields')} + /> + handleAuthTypeChange('tokenWithUserIDSecretFields')} + /> + handleAuthTypeChange('tokenWithUsernameSecretFields')} + /> + handleAuthTypeChange('applicationCredentialIdSecretFields')} + /> + handleAuthTypeChange('applicationCredentialNameSecretFields')} + /> + + + + + {state.authenticationType === 'passwordSecretFields' && ( + + )} + {state.authenticationType === 'tokenWithUserIDSecretFields' && ( + + )} + {state.authenticationType === 'tokenWithUsernameSecretFields' && ( + + )} + {state.authenticationType === 'applicationCredentialIdSecretFields' && ( + + )} + {state.authenticationType === 'applicationCredentialNameSecretFields' && ( + + )} + + + + + handleChange('insecureSkipVerify', value ? 'true' : 'false')} + /> + + + handleChange('cacert', value)} + onTextChange={(value) => handleChange('cacert', value)} + onClearClick={() => handleChange('cacert', '')} + browseButtonText="Upload" + isDisabled={insecureSkipVerify} + /> + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/ApplicationCredentialNameSecretFieldsFormGroup.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/ApplicationCredentialNameSecretFieldsFormGroup.tsx new file mode 100644 index 000000000..e1eca0fd7 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/ApplicationCredentialNameSecretFieldsFormGroup.tsx @@ -0,0 +1,161 @@ +import React, { useCallback, useReducer } from 'react'; +import { Base64 } from 'js-base64'; +import { + openstackSecretFieldValidator, + safeBase64Decode, + Validation, +} from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { Button, FormGroup, TextInput } from '@patternfly/react-core'; +import EyeIcon from '@patternfly/react-icons/dist/esm/icons/eye-icon'; +import EyeSlashIcon from '@patternfly/react-icons/dist/esm/icons/eye-slash-icon'; + +import { EditComponentProps } from '../../BaseCredentialsSection'; + +export const ApplicationCredentialNameSecretFieldsFormGroup: React.FC = ({ + secret, + onChange, +}) => { + const { t } = useForkliftTranslation(); + + const applicationCredentialName = safeBase64Decode(secret?.data?.applicationCredentialName || ''); + const applicationCredentialSecret = safeBase64Decode( + secret?.data?.applicationCredentialSecret || '', + ); + const username = safeBase64Decode(secret?.data?.username || ''); + const domainName = safeBase64Decode(secret?.data?.domainName || ''); + + const initialState = { + passwordHidden: true, + validation: { + applicationCredentialName: 'default' as Validation, + applicationCredentialSecret: 'default' as Validation, + username: 'default' as Validation, + domainName: 'default' as Validation, + }, + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'SET_FIELD_VALIDATED': + return { + ...state, + validation: { + ...state.validation, + [action.payload.field]: action.payload.validationState, + }, + }; + case 'TOGGLE_PASSWORD_HIDDEN': + return { ...state, passwordHidden: !state.passwordHidden }; + default: + return state; + } + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + // Define handleChange and validation functions + const handleChange = useCallback( + (id, value) => { + const validationState = openstackSecretFieldValidator(id, value); + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + const encodedValue = Base64.encode(value.trim()); + onChange({ ...secret, data: { ...secret.data, [id]: encodedValue } }); + }, + [secret, onChange], + ); + + const togglePasswordHidden = () => { + dispatch({ type: 'TOGGLE_PASSWORD_HIDDEN' }); + }; + + return ( + <> + + handleChange('applicationCredentialName', value)} + validated={state.validation.applicationCredentialName} + /> + + + + handleChange('applicationCredentialSecret', value)} + validated={state.validation.applicationCredentialSecret} + /> + + + + + handleChange('username', value)} + validated={state.validation.username} + /> + + + + handleChange('domainName', value)} + validated={state.validation.domainName} + /> + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/ApplicationWithCredentialsIDFormGroup.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/ApplicationWithCredentialsIDFormGroup.tsx new file mode 100644 index 000000000..2350c235c --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/ApplicationWithCredentialsIDFormGroup.tsx @@ -0,0 +1,119 @@ +import React, { useCallback, useReducer } from 'react'; +import { Base64 } from 'js-base64'; +import { + openstackSecretFieldValidator, + safeBase64Decode, + Validation, +} from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { Button, FormGroup, TextInput } from '@patternfly/react-core'; +import EyeIcon from '@patternfly/react-icons/dist/esm/icons/eye-icon'; +import EyeSlashIcon from '@patternfly/react-icons/dist/esm/icons/eye-slash-icon'; + +import { EditComponentProps } from '../../BaseCredentialsSection'; + +export const ApplicationWithCredentialsIDFormGroup: React.FC = ({ + secret, + onChange, +}) => { + const { t } = useForkliftTranslation(); + + const applicationCredentialID = safeBase64Decode(secret?.data?.applicationCredentialID || ''); + const applicationCredentialSecret = safeBase64Decode( + secret?.data?.applicationCredentialSecret || '', + ); + + const initialState = { + passwordHidden: true, + validation: { + applicationCredentialID: 'default' as Validation, + applicationCredentialSecret: 'default' as Validation, + }, + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'SET_FIELD_VALIDATED': + return { + ...state, + validation: { + ...state.validation, + [action.payload.field]: action.payload.validationState, + }, + }; + case 'TOGGLE_PASSWORD_HIDDEN': + return { ...state, passwordHidden: !state.passwordHidden }; + default: + return state; + } + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + // Define handleChange and validation functions + const handleChange = useCallback( + (id, value) => { + const validationState = openstackSecretFieldValidator(id, value); + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + const encodedValue = Base64.encode(value.trim()); + onChange({ ...secret, data: { ...secret.data, [id]: encodedValue } }); + }, + [secret, onChange], + ); + + const togglePasswordHidden = () => { + dispatch({ type: 'TOGGLE_PASSWORD_HIDDEN' }); + }; + + return ( + <> + + handleChange('applicationCredentialID', value)} + validated={state.validation.applicationCredentialID} + /> + + + + handleChange('applicationCredentialSecret', value)} + validated={state.validation.applicationCredentialSecret} + /> + + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/PasswordSecretFieldsFormGroup.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/PasswordSecretFieldsFormGroup.tsx new file mode 100644 index 000000000..0514947ff --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/PasswordSecretFieldsFormGroup.tsx @@ -0,0 +1,179 @@ +import React, { useCallback, useReducer } from 'react'; +import { Base64 } from 'js-base64'; +import { + openstackSecretFieldValidator, + safeBase64Decode, + Validation, +} from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { Button, FormGroup, TextInput } from '@patternfly/react-core'; +import EyeIcon from '@patternfly/react-icons/dist/esm/icons/eye-icon'; +import EyeSlashIcon from '@patternfly/react-icons/dist/esm/icons/eye-slash-icon'; + +import { EditComponentProps } from '../../BaseCredentialsSection'; + +export const PasswordSecretFieldsFormGroup: React.FC = ({ + secret, + onChange, +}) => { + const { t } = useForkliftTranslation(); + + const username = safeBase64Decode(secret?.data?.username || ''); + const password = safeBase64Decode(secret?.data?.password || ''); + const regionName = safeBase64Decode(secret?.data?.regionName || ''); + const projectName = safeBase64Decode(secret?.data?.projectName || ''); + const domainName = safeBase64Decode(secret?.data?.domainName || ''); + + const initialState = { + passwordHidden: true, + validation: { + username: 'default' as Validation, + password: 'default' as Validation, + regionName: 'default' as Validation, + projectName: 'default' as Validation, + domainName: 'default' as Validation, + }, + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'SET_FIELD_VALIDATED': + return { + ...state, + validation: { + ...state.validation, + [action.payload.field]: action.payload.validationState, + }, + }; + case 'TOGGLE_PASSWORD_HIDDEN': + return { ...state, passwordHidden: !state.passwordHidden }; + default: + return state; + } + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + // Define handleChange and validation functions + const handleChange = useCallback( + (id, value) => { + const validationState = openstackSecretFieldValidator(id, value); + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + const encodedValue = Base64.encode(value.trim()); + onChange({ ...secret, data: { ...secret.data, [id]: encodedValue } }); + }, + [secret, onChange], + ); + + const togglePasswordHidden = () => { + dispatch({ type: 'TOGGLE_PASSWORD_HIDDEN' }); + }; + + return ( + <> + + handleChange('username', value)} + validated={state.validation.username} + /> + + + + handleChange('password', value)} + validated={state.validation.password} + /> + + + + + handleChange('regionName', value)} + validated={state.validation.regionName} + /> + + + + handleChange('projectName', value)} + validated={state.validation.projectName} + /> + + + + handleChange('domainName', value)} + validated={state.validation.domainName} + /> + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/TokenWithUserIDSecretFieldsFormGroup.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/TokenWithUserIDSecretFieldsFormGroup.tsx new file mode 100644 index 000000000..02db6ec63 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/TokenWithUserIDSecretFieldsFormGroup.tsx @@ -0,0 +1,135 @@ +import React, { useCallback, useReducer } from 'react'; +import { Base64 } from 'js-base64'; +import { + openstackSecretFieldValidator, + safeBase64Decode, + Validation, +} from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { Button, FormGroup, TextInput } from '@patternfly/react-core'; +import EyeIcon from '@patternfly/react-icons/dist/esm/icons/eye-icon'; +import EyeSlashIcon from '@patternfly/react-icons/dist/esm/icons/eye-slash-icon'; + +import { EditComponentProps } from '../../BaseCredentialsSection'; + +export const TokenWithUserIDSecretFieldsFormGroup: React.FC = ({ + secret, + onChange, +}) => { + const { t } = useForkliftTranslation(); + + const token = safeBase64Decode(secret?.data?.token || ''); + const userID = safeBase64Decode(secret?.data?.userID || ''); + const projectID = safeBase64Decode(secret?.data?.projectID || ''); + + const initialState = { + passwordHidden: true, + validation: { + token: 'default' as Validation, + userID: 'default' as Validation, + projectID: 'default' as Validation, + }, + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'SET_FIELD_VALIDATED': + return { + ...state, + validation: { + ...state.validation, + [action.payload.field]: action.payload.validationState, + }, + }; + case 'TOGGLE_PASSWORD_HIDDEN': + return { ...state, passwordHidden: !state.passwordHidden }; + default: + return state; + } + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + const handleChange = useCallback( + (id, value) => { + const validationState = openstackSecretFieldValidator(id, value); + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + const encodedValue = Base64.encode(value.trim()); + onChange({ ...secret, data: { ...secret.data, [id]: encodedValue } }); + }, + [secret, onChange], + ); + + const togglePasswordHidden = () => { + dispatch({ type: 'TOGGLE_PASSWORD_HIDDEN' }); + }; + + return ( + <> + + handleChange('token', value)} + validated={state.validation.token} + /> + + + + handleChange('userID', value)} + validated={state.validation.userID} + /> + + + handleChange('projectID', value)} + validated={state.validation.projectID} + /> + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/TokenWithUsernameSecretFieldsFormGroup.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/TokenWithUsernameSecretFieldsFormGroup.tsx new file mode 100644 index 000000000..6619aef70 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/TokenWithUsernameSecretFieldsFormGroup.tsx @@ -0,0 +1,155 @@ +import React, { useCallback, useReducer } from 'react'; +import { Base64 } from 'js-base64'; +import { + openstackSecretFieldValidator, + safeBase64Decode, + Validation, +} from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { Button, FormGroup, TextInput } from '@patternfly/react-core'; +import EyeIcon from '@patternfly/react-icons/dist/esm/icons/eye-icon'; +import EyeSlashIcon from '@patternfly/react-icons/dist/esm/icons/eye-slash-icon'; + +import { EditComponentProps } from '../../BaseCredentialsSection'; + +export const TokenWithUsernameSecretFieldsFormGroup: React.FC = ({ + secret, + onChange, +}) => { + const { t } = useForkliftTranslation(); + + const token = safeBase64Decode(secret?.data?.token || ''); + const username = safeBase64Decode(secret?.data?.username || ''); + const projectName = safeBase64Decode(secret?.data?.projectName || ''); + const userDomainName = safeBase64Decode(secret?.data?.userDomainName || ''); + + const initialState = { + passwordHidden: true, + validation: { + token: 'default' as Validation, + username: 'default' as Validation, + projectName: 'default' as Validation, + userDomainName: 'default' as Validation, + }, + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'SET_FIELD_VALIDATED': + return { + ...state, + validation: { + ...state.validation, + [action.payload.field]: action.payload.validationState, + }, + }; + case 'TOGGLE_PASSWORD_HIDDEN': + return { ...state, passwordHidden: !state.passwordHidden }; + default: + return state; + } + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + const handleChange = useCallback( + (id, value) => { + const validationState = openstackSecretFieldValidator(id, value); + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + const encodedValue = Base64.encode(value.trim()); + onChange({ ...secret, data: { ...secret.data, [id]: encodedValue } }); + }, + [secret, onChange], + ); + + const togglePasswordHidden = () => { + dispatch({ type: 'TOGGLE_PASSWORD_HIDDEN' }); + }; + + return ( + <> + + handleChange('token', value)} + validated={state.validation.token} + /> + + + + handleChange('username', value)} + validated={state.validation.username} + /> + + + handleChange('projectName', value)} + validated={state.validation.projectName} + /> + + + handleChange('userDomainName', value)} + validated={state.validation.userDomainName} + /> + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/index.ts new file mode 100644 index 000000000..72c94c1a3 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OpenstackCredentialsEditFormGroups/index.ts @@ -0,0 +1,7 @@ +// @index('./*', f => `export * from '${f.path}';`) +export * from './ApplicationCredentialNameSecretFieldsFormGroup'; +export * from './ApplicationWithCredentialsIDFormGroup'; +export * from './PasswordSecretFieldsFormGroup'; +export * from './TokenWithUserIDSecretFieldsFormGroup'; +export * from './TokenWithUsernameSecretFieldsFormGroup'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OvirtCredentialsEdit.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OvirtCredentialsEdit.tsx new file mode 100644 index 000000000..f74cb52ad --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/OvirtCredentialsEdit.tsx @@ -0,0 +1,170 @@ +import React, { useCallback, useReducer } from 'react'; +import { Base64 } from 'js-base64'; +import { + ovirtSecretFieldValidator, + safeBase64Decode, + Validation, +} from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { + Button, + Checkbox, + Divider, + FileUpload, + Form, + FormGroup, + TextInput, +} from '@patternfly/react-core'; +import EyeIcon from '@patternfly/react-icons/dist/esm/icons/eye-icon'; +import EyeSlashIcon from '@patternfly/react-icons/dist/esm/icons/eye-slash-icon'; + +import { EditComponentProps } from '../BaseCredentialsSection'; + +export const OvirtCredentialsEdit: React.FC = ({ secret, onChange }) => { + const { t } = useForkliftTranslation(); + + const user = safeBase64Decode(secret?.data?.user || ''); + const password = safeBase64Decode(secret?.data?.password || ''); + const insecureSkipVerify = safeBase64Decode(secret?.data?.insecureSkipVerify || '') === 'true'; + const cacert = safeBase64Decode(secret?.data?.cacert || ''); + + const initialState = { + passwordHidden: true, + validation: { + user: 'default' as Validation, + password: 'default' as Validation, + cacert: 'default' as Validation, + }, + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'SET_FIELD_VALIDATED': + return { + ...state, + validation: { + ...state.validation, + [action.payload.field]: action.payload.validationState, + }, + }; + case 'TOGGLE_PASSWORD_HIDDEN': + return { ...state, passwordHidden: !state.passwordHidden }; + default: + return state; + } + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + // Define handleChange and validation functions + const handleChange = useCallback( + (id, value) => { + const validationState = ovirtSecretFieldValidator(id, value); + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + // don't trim fields that allow spaces + const encodedValue = ['cacert'].includes(id) + ? Base64.encode(value) + : Base64.encode(value.trim()); + + onChange({ ...secret, data: { ...secret.data, [id]: encodedValue } }); + }, + [secret], + ); + + const togglePasswordHidden = () => { + dispatch({ type: 'TOGGLE_PASSWORD_HIDDEN' }); + }; + + return ( +
+ + handleChange('user', value)} + /> + + + handleChange('password', value)} + /> + + + + + + + handleChange('insecureSkipVerify', value ? 'true' : 'false')} + /> + + + handleChange('cacert', value)} + onTextChange={(value) => handleChange('cacert', value)} + onClearClick={() => handleChange('cacert', '')} + browseButtonText="Upload" + isDisabled={insecureSkipVerify} + /> + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/VSphereCredentialsEdit.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/VSphereCredentialsEdit.tsx new file mode 100644 index 000000000..3ba6121a9 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/VSphereCredentialsEdit.tsx @@ -0,0 +1,156 @@ +import React, { useCallback, useReducer } from 'react'; +import { Base64 } from 'js-base64'; +import { + safeBase64Decode, + Validation, + vsphereSecretFieldValidator, +} from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { Button, Checkbox, Divider, Form, FormGroup, TextInput } from '@patternfly/react-core'; +import EyeIcon from '@patternfly/react-icons/dist/esm/icons/eye-icon'; +import EyeSlashIcon from '@patternfly/react-icons/dist/esm/icons/eye-slash-icon'; + +import { EditComponentProps } from '../BaseCredentialsSection'; + +export const VSphereCredentialsEdit: React.FC = ({ secret, onChange }) => { + const { t } = useForkliftTranslation(); + + const user = safeBase64Decode(secret?.data?.user || ''); + const password = safeBase64Decode(secret?.data?.password || ''); + const thumbprint = safeBase64Decode(secret?.data?.thumbprint || ''); + const insecureSkipVerify = safeBase64Decode(secret?.data?.insecureSkipVerify || '') === 'true'; + + const initialState = { + passwordHidden: true, + validation: { + user: 'default' as Validation, + password: 'default' as Validation, + thumbprint: 'default' as Validation, + }, + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'SET_FIELD_VALIDATED': + return { + ...state, + validation: { + ...state.validation, + [action.payload.field]: action.payload.validationState, + }, + }; + case 'TOGGLE_PASSWORD_HIDDEN': + return { ...state, passwordHidden: !state.passwordHidden }; + default: + return state; + } + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + const handleChange = useCallback( + (id, value) => { + const validationState = vsphereSecretFieldValidator(id, value); + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + // don't trim fields that allow spaces + const encodedValue = ['cacert'].includes(id) + ? Base64.encode(value) + : Base64.encode(value.trim()); + + onChange({ ...secret, data: { ...secret.data, [id]: encodedValue } }); + }, + [secret, onChange], + ); + + const togglePasswordHidden = () => { + dispatch({ type: 'TOGGLE_PASSWORD_HIDDEN' }); + }; + + return ( +
+ + handleChange('user', value)} + value={user} + validated={state.validation.user} + /> + + + handleChange('password', value)} + value={password} + validated={state.validation.password} + /> + + + + + + + handleChange('thumbprint', value)} + value={thumbprint} + validated={state.validation.thumbprint} + /> + + + + handleChange('insecureSkipVerify', value ? 'true' : 'false')} + /> + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/index.ts new file mode 100644 index 000000000..c75ffa21b --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/index.ts @@ -0,0 +1,8 @@ +// @index('./*', f => `export * from '${f.path}';`) +export * from './OpenshiftCredentialsEdit'; +export * from './OpenstackCredentialsEdit'; +export * from './OpenstackCredentialsEditFormGroups'; +export * from './OvirtCredentialsEdit'; +export * from './patchSecretData'; +export * from './VSphereCredentialsEdit'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/patchSecretData.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/patchSecretData.ts new file mode 100644 index 000000000..e39cc33d3 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/edit/patchSecretData.ts @@ -0,0 +1,42 @@ +import { SecretModel, V1Secret } from '@kubev2v/types'; +import { k8sPatch } from '@openshift-console/dynamic-plugin-sdk'; + +/** + * Updates the data of a Kubernetes secret resource. + * + * @param {V1Secret} secret - The secret object containing the updated data. + * @param {boolean} clean - Clean old values from the secret before patching. + * @returns {Promise} A promise that resolves when the patch operation is complete. + */ +export async function patchSecretData(secret: V1Secret, clean?: boolean) { + const op = secret?.data ? 'replace' : 'add'; + + await k8sPatch({ + model: SecretModel, + resource: secret, + data: [ + { + op, + path: '/data', + value: clean ? { ...EmptyOpenstackCredentials, ...secret.data } : secret.data, + }, + ], + }); +} + +// when patching a secret with new data, first remove all other fields +const EmptyOpenstackCredentials = { + authType: undefined, + username: undefined, + password: undefined, + regionName: undefined, + projectName: undefined, + domainName: undefined, + token: undefined, + userID: undefined, + projectID: undefined, + userDomainName: undefined, + applicationCredentialID: undefined, + applicationCredentialSecret: undefined, + applicationCredentialName: undefined, +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/index.ts new file mode 100644 index 000000000..face51176 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/index.ts @@ -0,0 +1,5 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './BaseCredentialsSection'; +export * from './edit'; +export * from './list'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/OpenshiftCredentialsList.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/OpenshiftCredentialsList.tsx new file mode 100644 index 000000000..469049ea4 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/OpenshiftCredentialsList.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Base64 } from 'js-base64'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ClipboardCopy, ClipboardCopyVariant, Text, TextVariants } from '@patternfly/react-core'; + +import { MaskedData } from '../../MaskedData'; +import { ListComponentProps } from '../BaseCredentialsSection'; + +export const OpenshiftCredentialsList: React.FC = ({ secret, reveal }) => { + const { t } = useForkliftTranslation(); + + const items = []; + + const fields = { + token: { + label: t('Service account token'), + description: t( + 'User or service account bearer token for service accounts or user authentication.', + ), + }, + }; + + for (const key in fields) { + const field = fields[key]; + const base64Value = secret.data?.[key]; + const value = base64Value ? Base64.decode(secret.data[key]) : undefined; + + items.push( + <> +
+ + {field.label} + + + {field.description} + +
+
+ {reveal ? ( + 128 ? ClipboardCopyVariant.expansion : undefined} + > + {value} + + ) : ( + + )} +
+ , + ); + } + + return <>{items}; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/OpenstackCredentialsList.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/OpenstackCredentialsList.tsx new file mode 100644 index 000000000..9ef8f2fc3 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/OpenstackCredentialsList.tsx @@ -0,0 +1,236 @@ +import React from 'react'; +import { Base64 } from 'js-base64'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ClipboardCopy, ClipboardCopyVariant, Text, TextVariants } from '@patternfly/react-core'; + +import { MaskedData } from '../../MaskedData'; +import { ListComponentProps } from '../BaseCredentialsSection'; + +export const OpenstackCredentialsList: React.FC = ({ secret, reveal }) => { + const { t } = useForkliftTranslation(); + + const items = []; + + const fields = { + passwordSecretFields: { + authType: { + label: t('Authentication type'), + description: t('Type of authentication to use when connecting to Openstack REST API.'), + }, + username: { label: t('Username'), description: t('Openstack REST API user name.') }, + password: { + label: t('Password'), + description: t('Openstack REST API password credentials.'), + }, + regionName: { + label: t('Region'), + description: t('Openstack region for password credentials.'), + }, + projectName: { + label: t('Project'), + description: t('Openstack project for password credentials.'), + }, + domainName: { + label: t('Domain'), + description: t('Openstack domain for password credentials.'), + }, + insecureSkipVerify: { + label: t('Skip certificate validation'), + description: t("If true, the provider's REST API TLS certificate won't be validated."), + }, + cacert: { + label: t('CA certificate'), + description: t( + 'Custom certification used to verify the Openstack REST API server, when empty use system certificate.', + ), + }, + }, + + tokenWithUserIDSecretFields: { + authType: { + label: t('Authentication type'), + description: t('Type of authentication to use when connecting to Openstack REST API.'), + }, + token: { + label: t('Token'), + description: t('Openstack REST API token credentials.'), + }, + userID: { + label: t('User ID'), + description: t('Openstack REST API user ID.'), + }, + projectID: { + label: t('Project ID'), + description: t('Openstack project ID for token credentials.'), + }, + insecureSkipVerify: { + label: t('Skip certificate validation'), + description: t("If true, the provider's REST API TLS certificate won't be validated."), + }, + cacert: { + label: t('CA certificate'), + description: t( + 'Custom certification used to verify the Openstack REST API server, when empty use system certificate.', + ), + }, + }, + + tokenWithUsernameSecretFields: { + authType: { + label: t('Authentication type'), + description: t('Type of authentication to use when connecting to Openstack REST API.'), + }, + token: { + label: t('Token'), + description: t('Openstack REST API token credentials.'), + }, + username: { + label: t('Username'), + description: t('Openstack REST API user name.'), + }, + projectName: { + label: t('Project'), + description: t('Openstack project for token credentials.'), + }, + userDomainName: { + label: t('User Domain Name'), + description: t('Openstack user domain name for token credentials.'), + }, + insecureSkipVerify: { + label: t('Skip certificate validation'), + description: t("If true, the provider's REST API TLS certificate won't be validated."), + }, + cacert: { + label: t('CA certificate'), + description: t( + 'Custom certification used to verify the Openstack REST API server, when empty use system certificate.', + ), + }, + }, + + applicationCredentialIdSecretFields: { + authType: { + label: t('Authentication type'), + description: t('Type of authentication to use when connecting to Openstack REST API.'), + }, + applicationCredentialID: { + label: t('Application Credential ID'), + description: t('Openstack REST API Application Credential ID.'), + }, + applicationCredentialSecret: { + label: t('Application Credential Secret'), + description: t('Openstack REST API Application Credential Secret.'), + }, + insecureSkipVerify: { + label: t('Skip certificate validation'), + description: t("If true, the provider's REST API TLS certificate won't be validated."), + }, + cacert: { + label: t('CA certificate'), + description: t( + 'Custom certification used to verify the Openstack REST API server, when empty use system certificate.', + ), + }, + }, + + applicationCredentialNameSecretFields: { + authType: { + label: t('Authentication type'), + description: t('Type of authentication to use when connecting to Openstack REST API.'), + }, + applicationCredentialName: { + label: t('Application Credential Name'), + description: t('Openstack REST API Application Credential Name.'), + }, + applicationCredentialSecret: { + label: t('Application Credential Secret'), + description: t('Openstack REST API Application Credential Secret.'), + }, + username: { + label: t('Username'), + description: t('Openstack REST API user name.'), + }, + domainName: { + label: t('Domain'), + description: t('Openstack domain for application credential credentials.'), + }, + insecureSkipVerify: { + label: t('Skip certificate validation'), + description: t("If true, the provider's REST API TLS certificate won't be validated."), + }, + cacert: { + label: t('CA certificate'), + description: t( + 'Custom certification used to verify the Openstack REST API server, when empty use system certificate.', + ), + }, + }, + }; + + let openstackSecretFields = {}; + + const decodedAuthType = secret?.data?.authType ? Base64.decode(secret.data.authType) : ''; + + switch (decodedAuthType) { + case '': + case 'password': + openstackSecretFields = fields.passwordSecretFields; + break; + + case 'token': + if (!secret?.data?.userID) { + openstackSecretFields = fields.tokenWithUserIDSecretFields; + } else if (!secret?.data?.username) { + openstackSecretFields = fields.tokenWithUsernameSecretFields; + } + break; + + case 'applicationcredential': + if (!secret?.data?.applicationCredentialID) { + openstackSecretFields = fields.applicationCredentialIdSecretFields; + } else if (!secret?.data?.applicationCredentialName) { + openstackSecretFields = fields.applicationCredentialNameSecretFields; + } + break; + + default: + // Handle case when none of the conditions are met + } + + for (const key in openstackSecretFields) { + const field = openstackSecretFields[key]; + const base64Value = secret.data?.[key]; + const value = base64Value ? Base64.decode(secret.data[key]) : undefined; + + items.push( + <> +
+ + {field.label} + + + {field.description} + +
+
+ {reveal ? ( + 128 ? ClipboardCopyVariant.expansion : undefined} + > + {value} + + ) : ( + + )} +
+ , + ); + } + + return <>{items}; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/OvirtCredentialsList.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/OvirtCredentialsList.tsx new file mode 100644 index 000000000..99aa6e057 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/OvirtCredentialsList.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { Base64 } from 'js-base64'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ClipboardCopy, ClipboardCopyVariant, Text, TextVariants } from '@patternfly/react-core'; + +import { MaskedData } from '../../MaskedData'; +import { ListComponentProps } from '../BaseCredentialsSection'; + +export const OvirtCredentialsList: React.FC = ({ secret, reveal }) => { + const { t } = useForkliftTranslation(); + + const items = []; + + const fields = { + user: { label: t('Username'), description: t('RH Virtualization engine REST API user name.') }, + password: { + label: t('Password'), + description: t('RH Virtualization engine REST API password credentials.'), + }, + insecureSkipVerify: { + label: t('Skip certificate validation'), + description: t("If true, the provider's REST API TLS certificate won't be validated."), + }, + cacert: { + label: t('CA certificate'), + description: t( + 'The CA certificate is the /etc/pki/ovirt-engine/apache-ca.pem file on the Manager machine.', + ), + }, + }; + + for (const key in fields) { + const field = fields[key]; + const base64Value = secret.data?.[key]; + const value = base64Value ? Base64.decode(secret.data[key]) : undefined; + + items.push( + <> +
+ + {field.label} + + + {field.description} + +
+
+ {reveal ? ( + 128 ? ClipboardCopyVariant.expansion : undefined} + > + {value} + + ) : ( + + )} +
+ , + ); + } + + return <>{items}; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/VSphereCredentialsList.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/VSphereCredentialsList.tsx new file mode 100644 index 000000000..b36ba870f --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/VSphereCredentialsList.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { Base64 } from 'js-base64'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ClipboardCopy, ClipboardCopyVariant, Text, TextVariants } from '@patternfly/react-core'; + +import { MaskedData } from '../../MaskedData'; +import { ListComponentProps } from '../BaseCredentialsSection'; + +export const VSphereCredentialsList: React.FC = ({ secret, reveal }) => { + const { t } = useForkliftTranslation(); + + const items = []; + + const fields = { + user: { label: t('Username'), description: t('vSphere REST API user name.') }, + password: { label: t('Password'), description: t('vSphere REST API password credentials.') }, + thumbprint: { + label: t('SSHA-1 fingerprint'), + description: t( + "The provider currently requires the SHA-1 fingerprint of the vCenter Server's TLS certificate in all circumstances. vSphere calls this the server's thumbprint.", + ), + }, + insecureSkipVerify: { + label: t('Skip certificate validation'), + description: t("If true, the provider's TLS certificate won't be validated."), + }, + }; + + for (const key in fields) { + const field = fields[key]; + const base64Value = secret.data?.[key]; + const value = base64Value ? Base64.decode(secret.data[key]) : undefined; + + items.push( + <> +
+ + {field.label} + + + {field.description} + +
+
+ {reveal ? ( + 128 ? ClipboardCopyVariant.expansion : undefined} + > + {value} + + ) : ( + + )} +
+ , + ); + } + + return <>{items}; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/index.ts new file mode 100644 index 000000000..c73006769 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/components/list/index.ts @@ -0,0 +1,6 @@ +// @index('./*.tsx', f => `export * from '${f.path}';`) +export * from './OpenshiftCredentialsList'; +export * from './OpenstackCredentialsList'; +export * from './OvirtCredentialsList'; +export * from './VSphereCredentialsList'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/index.ts new file mode 100644 index 000000000..8387f033f --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/CredentialsSection/index.ts @@ -0,0 +1,9 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './components'; +export * from './CredentialsSection'; +export * from './MaskedData'; +export * from './OpenshiftCredentialsSection'; +export * from './OpenstackCredentialsSection'; +export * from './OvirtCredentialsSection'; +export * from './VSphereCredentialsSection'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/DetailsSection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/DetailsSection.tsx new file mode 100644 index 000000000..c66186033 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/DetailsSection.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { ModalHOC } from 'src/modules/ProvidersNG/modals'; +import { ProviderData } from 'src/modules/ProvidersNG/utils'; + +import { OpenshiftDetailsSection } from './OpenshiftDetailsSection'; +import { OpenstackDetailsSection } from './OpenstackDetailsSection'; +import { OvirtDetailsSection } from './OvirtDetailsSection'; +import { VSphereDetailsSection } from './VSphereDetailsSection'; + +const DetailsSection_: React.FC = (props) => { + const { provider } = props.data; + + switch (provider?.spec?.type) { + case 'ovirt': + return ; + case 'openshift': + return ; + case 'openstack': + return ; + case 'vsphere': + return ; + default: + return <>; + } +}; + +export const DetailsSection: React.FC = (props) => ( + + + +); + +export type DetailsSectionProps = { + data: ProviderData; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/OpenshiftDetailsSection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/OpenshiftDetailsSection.tsx new file mode 100644 index 000000000..911f4e4a9 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/OpenshiftDetailsSection.tsx @@ -0,0 +1,171 @@ +import React from 'react'; +import { + EditProviderDefaultTransferNetwork, + EditProviderURLModal, + useModal, +} from 'src/modules/ProvidersNG/modals'; +import { HELP_LINK_HREF } from 'src/utils/constants'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ResourceLink, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { DescriptionList, Label, Text } from '@patternfly/react-core'; + +import { DetailsItem, OwnerReferencesItem } from '../../../../utils'; + +import { DetailsSectionProps } from './DetailsSection'; + +export const OpenshiftDetailsSection: React.FC = ({ data }) => { + const { t } = useForkliftTranslation(); + const { showModal } = useModal(); + + const { provider } = data; + + return ( + + + {provider?.spec?.type}{' '} + {!provider?.spec?.url && ( + + )} + + } + moreInfoLink={HELP_LINK_HREF} + helpContent={ + {t(`Allowed values are openshift, ovirt, vsphere, and openstack.`)} + } + crumbs={['Provider', 'spec', 'type']} + /> + + + + + {t( + 'Name is primarily intended for creation idempotence and configuration definition. Cannot be updated.', + )} + + } + crumbs={['Provider', 'metadata', 'name']} + /> + + + } + moreInfoLink={ + 'https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces' + } + helpContent={t( + `Namespace defines the space within which each name must be unique. + An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. + Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.`, + )} + crumbs={['Provider', 'metadata', 'namespace']} + /> + + {t('Empty')}} + moreInfoLink={ + 'https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces' + } + helpContent={{t(`The provider URL. Empty may be used for the host provider.`)}} + crumbs={['Provider', 'spec', 'url']} + onEdit={ + provider?.spec?.url && (() => showModal()) + } + /> + + + ) : ( + {t('No secret')} + ) + } + helpContent={t( + `References a secret containing credentials and other confidential information. Empty may be used for the host provider.`, + )} + crumbs={['Provider', 'spec', 'secret']} + /> + + } + helpContent={ + + {t( + `CreationTimestamp is a timestamp representing the server time when this object was created. + It is not guaranteed to be set in happens-before order across separate operations. + Clients may not set this value. It is represented in RFC3339 form and is in UTC.`, + )} + + } + crumbs={['Provider', 'metadata', 'creationTimestamp']} + /> + + {t('Empty')} + ) + } + helpContent={ + + {t( + `The default network attachment definition that should be used for disk transfer. + If not available in the target namespace or empty, Pod network will be used`, + )} + + } + crumbs={[ + 'Provider', + 'metadata', + 'annotations', + 'forklift.konveyor.io/defaultTransferNetwork', + ]} + onEdit={() => showModal()} + /> + + } + helpContent={ + + {t( + `List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. + If this object is managed by a controller, then an entry in this list will point to this controller, + with the controller field set to true. There cannot be more than one managing controller.`, + )} + + } + crumbs={['Provider', 'metadata', 'ownerReferences']} + /> + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/OpenstackDetailsSection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/OpenstackDetailsSection.tsx new file mode 100644 index 000000000..bda41f907 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/OpenstackDetailsSection.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { EditProviderURLModal, useModal } from 'src/modules/ProvidersNG/modals'; +import { HELP_LINK_HREF } from 'src/utils/constants'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ResourceLink, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { DescriptionList, Text } from '@patternfly/react-core'; + +import { DetailsItem, OwnerReferencesItem } from '../../../../utils'; + +import { DetailsSectionProps } from './DetailsSection'; + +export const OpenstackDetailsSection: React.FC = ({ data }) => { + const { t } = useForkliftTranslation(); + const { showModal } = useModal(); + + const { provider } = data; + + return ( + + {t(`Allowed values are openshift, ovirt, vsphere, and openstack.`)} + } + crumbs={['Provider', 'spec', 'type']} + /> + + + + + {t( + 'Name is primarily intended for creation idempotence and configuration definition. Cannot be updated.', + )} + + } + crumbs={['Provider', 'metadata', 'name']} + /> + + + } + moreInfoLink={ + 'https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces' + } + helpContent={t( + `Namespace defines the space within which each name must be unique. + An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. + Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.`, + )} + crumbs={['Provider', 'metadata', 'namespace']} + /> + + {t('Empty')}} + moreInfoLink={ + 'https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces' + } + helpContent={{t(`The provider URL. Empty may be used for the host provider.`)}} + crumbs={['Provider', 'spec', 'url']} + onEdit={() => showModal()} + /> + + + ) : ( + {t('No secret')} + ) + } + helpContent={t( + `References a secret containing credentials and other confidential information. Empty may be used for the host provider.`, + )} + crumbs={['Provider', 'spec', 'secret']} + /> + + } + helpContent={ + + {t( + `CreationTimestamp is a timestamp representing the server time when this object was created. + It is not guaranteed to be set in happens-before order across separate operations. + Clients may not set this value. It is represented in RFC3339 form and is in UTC.`, + )} + + } + crumbs={['Provider', 'metadata', 'creationTimestamp']} + /> + + + + } + helpContent={ + + {t( + `List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. + If this object is managed by a controller, then an entry in this list will point to this controller, + with the controller field set to true. There cannot be more than one managing controller.`, + )} + + } + crumbs={['Provider', 'metadata', 'ownerReferences']} + /> + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/OvirtDetailsSection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/OvirtDetailsSection.tsx new file mode 100644 index 000000000..2b32be983 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/OvirtDetailsSection.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { EditProviderURLModal, useModal } from 'src/modules/ProvidersNG/modals'; +import { HELP_LINK_HREF } from 'src/utils/constants'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ResourceLink, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { DescriptionList, Text } from '@patternfly/react-core'; + +import { DetailsItem, OwnerReferencesItem } from '../../../../utils'; + +import { DetailsSectionProps } from './DetailsSection'; + +export const OvirtDetailsSection: React.FC = ({ data }) => { + const { t } = useForkliftTranslation(); + const { showModal } = useModal(); + + const { provider } = data; + + return ( + + {t(`Allowed values are openshift, ovirt, vsphere, and openstack.`)} + } + crumbs={['Provider', 'spec', 'type']} + /> + + + + + {t( + 'Name is primarily intended for creation idempotence and configuration definition. Cannot be updated.', + )} + + } + crumbs={['Provider', 'metadata', 'name']} + /> + + + } + moreInfoLink={ + 'https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces' + } + helpContent={t( + `Namespace defines the space within which each name must be unique. + An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. + Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.`, + )} + crumbs={['Provider', 'metadata', 'namespace']} + /> + + {t('Empty')}} + moreInfoLink={ + 'https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces' + } + helpContent={{t(`The provider URL. Empty may be used for the host provider.`)}} + crumbs={['Provider', 'spec', 'url']} + onEdit={() => showModal()} + /> + + + ) : ( + {t('No secret')} + ) + } + helpContent={t( + `References a secret containing credentials and other confidential information. Empty may be used for the host provider.`, + )} + crumbs={['Provider', 'spec', 'secret']} + /> + + } + helpContent={ + + {t( + `CreationTimestamp is a timestamp representing the server time when this object was created. + It is not guaranteed to be set in happens-before order across separate operations. + Clients may not set this value. It is represented in RFC3339 form and is in UTC.`, + )} + + } + crumbs={['Provider', 'metadata', 'creationTimestamp']} + /> + + + + } + helpContent={ + + {t( + `List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. + If this object is managed by a controller, then an entry in this list will point to this controller, + with the controller field set to true. There cannot be more than one managing controller.`, + )} + + } + crumbs={['Provider', 'metadata', 'ownerReferences']} + /> + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/VSphereDetailsSection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/VSphereDetailsSection.tsx new file mode 100644 index 000000000..b78a7e84a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/VSphereDetailsSection.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { + EditProviderURLModal, + EditProviderVDDKImage, + useModal, +} from 'src/modules/ProvidersNG/modals'; +import { HELP_LINK_HREF } from 'src/utils/constants'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ResourceLink, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { DescriptionList, Text } from '@patternfly/react-core'; + +import { DetailsItem, OwnerReferencesItem } from '../../../../utils'; + +import { DetailsSectionProps } from './DetailsSection'; + +export const VSphereDetailsSection: React.FC = ({ data }) => { + const { t } = useForkliftTranslation(); + const { showModal } = useModal(); + + const { provider, inventory } = data; + + return ( + + {t(`Allowed values are openshift, ovirt, vsphere, and openstack.`)} + } + crumbs={['Provider', 'spec', 'type']} + /> + + {t('Empty')}} + helpContent={{t(`VMware only: vSphere product name.`)}} + crumbs={['Inventory', 'providers', `${provider.spec.type}`, '[UID]']} + /> + + + {t( + 'Name is primarily intended for creation idempotence and configuration definition. Cannot be updated.', + )} + + } + crumbs={['Provider', 'metadata', 'name']} + /> + + + } + moreInfoLink={ + 'https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces' + } + helpContent={t( + `Namespace defines the space within which each name must be unique. + An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. + Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.`, + )} + crumbs={['Provider', 'metadata', 'namespace']} + /> + + {t('Empty')}} + moreInfoLink={ + 'https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces' + } + helpContent={{t(`The provider URL. Empty may be used for the host provider.`)}} + crumbs={['Provider', 'spec', 'url']} + onEdit={() => showModal()} + /> + + + ) : ( + {t('No secret')} + ) + } + helpContent={t( + `References a secret containing credentials and other confidential information. Empty may be used for the host provider.`, + )} + crumbs={['Provider', 'spec', 'secret']} + /> + + } + helpContent={ + + {t( + `CreationTimestamp is a timestamp representing the server time when this object was created. + It is not guaranteed to be set in happens-before order across separate operations. + Clients may not set this value. It is represented in RFC3339 form and is in UTC.`, + )} + + } + crumbs={['Provider', 'metadata', 'creationTimestamp']} + /> + + {t('Empty')} + ) + } + helpContent={{t(`VMware only: Specify the VDDK image that you created.`)}} + crumbs={['Provider', 'spec', 'settings', 'vddkInitImage']} + onEdit={() => showModal()} + /> + + } + helpContent={ + + {t( + `List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. + If this object is managed by a controller, then an entry in this list will point to this controller, + with the controller field set to true. There cannot be more than one managing controller.`, + )} + + } + crumbs={['Provider', 'metadata', 'ownerReferences']} + /> + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/index.ts new file mode 100644 index 000000000..ec4ca3979 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/DetailsSection/index.ts @@ -0,0 +1,7 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './DetailsSection'; +export * from './OpenshiftDetailsSection'; +export * from './OpenstackDetailsSection'; +export * from './OvirtDetailsSection'; +export * from './VSphereDetailsSection'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/InventorySection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/InventorySection.tsx new file mode 100644 index 000000000..6cfd0e299 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/InventorySection.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { ProviderData } from 'src/modules/ProvidersNG/utils'; + +import { OpenshiftInventorySection } from './OpenshiftInventorySection'; +import { OpenstackInventorySection } from './OpenstackInventorySection'; +import { OvirtInventorySection } from './OvirtInventorySection'; +import { VSphereInventorySection } from './VSphereInventorySection'; + +export const InventorySection: React.FC = (props) => { + const { provider } = props.data; + + switch (provider?.spec?.type) { + case 'ovirt': + return ; + case 'openshift': + return ; + case 'openstack': + return ; + case 'vsphere': + return ; + default: + return <>; + } +}; + +export type InventoryProps = { + data: ProviderData; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/OpenshiftInventorySection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/OpenshiftInventorySection.tsx new file mode 100644 index 000000000..f573c963b --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/OpenshiftInventorySection.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { DescriptionList } from '@patternfly/react-core'; + +import { DetailsItem } from '../../../../utils'; + +import { InventoryProps } from './InventorySection'; + +export const OpenshiftInventorySection: React.FC = ({ data }) => { + const { t } = useForkliftTranslation(); + const { provider, inventory } = data; + + if (!provider || !inventory) { + return {t('No credentials found.')}; + } + + const inventoryItems = { + vmCount: { + title: t('Virtual machines'), + helpContent: t('Number of virtual machines in cluster'), + }, + networkCount: { + title: t('Network interfaces'), + helpContent: t('Number of network interfaces in provider cluster'), + }, + storageClassCount: { + title: t('Storage classes'), + helpContent: t('Number of storage classes in provider cluster'), + }, + }; + + const items = []; + + for (const key in inventoryItems) { + const item = inventoryItems?.[key]; + + if (item) { + const value = inventory[key] || '-'; + items.push( + , + ); + } + } + + return ( + + {items} + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/OpenstackInventorySection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/OpenstackInventorySection.tsx new file mode 100644 index 000000000..a863c05e9 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/OpenstackInventorySection.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { DescriptionList } from '@patternfly/react-core'; + +import { DetailsItem } from '../../../../utils'; + +import { InventoryProps } from './InventorySection'; + +export const OpenstackInventorySection: React.FC = ({ data }) => { + const { t } = useForkliftTranslation(); + const { provider, inventory } = data; + + if (!provider || !inventory) { + return {t('No credentials found.')}; + } + + const inventoryItems = { + vmCount: { + title: t('Virtual machines'), + helpContent: t('Number of virtual machines in cluster'), + }, + networkCount: { + title: t('Network interfaces'), + helpContent: t('Number of network interfaces in provider cluster'), + }, + regionCount: { + title: t('Regions'), + helpContent: t('Number of regions in Openstack cluster'), + }, + projectCount: { + title: t('Projects'), + helpContent: t('Number of projects in Openstack cluster'), + }, + volumeCount: { + title: t('Volumes'), + helpContent: t('Number of storage volumes in cluster'), + }, + volumeTypeCount: { + title: t('Volume Types'), + helpContent: t('Number of storage types in cluster'), + }, + }; + + const items = []; + + for (const key in inventoryItems) { + const item = inventoryItems?.[key]; + + if (item) { + const value = inventory[key] || '-'; + items.push( + , + ); + } + } + + return ( + + {items} + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/OvirtInventorySection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/OvirtInventorySection.tsx new file mode 100644 index 000000000..f83599dcb --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/OvirtInventorySection.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { DescriptionList } from '@patternfly/react-core'; + +import { DetailsItem } from '../../../../utils'; + +import { InventoryProps } from './InventorySection'; + +export const OvirtInventorySection: React.FC = ({ data }) => { + const { t } = useForkliftTranslation(); + const { provider, inventory } = data; + + if (!provider || !inventory) { + return {t('No credentials found.')}; + } + + const inventoryItems = { + vmCount: { + title: t('Virtual machines'), + helpContent: t('Number of virtual machines in cluster'), + }, + networkCount: { + title: t('Network interfaces'), + helpContent: t('Number of network interfaces in provider cluster'), + }, + datacenterCount: { + title: t('Data centers'), + helpContent: t('Number of data centers in provider'), + }, + storageDomainCount: { + title: t('Storage domains'), + helpContent: t('Number of storage domains in provider'), + }, + clusterCount: { + title: t('Clusters'), + helpContent: t('Number of cluster in provider'), + }, + hostCount: { + title: t('Hosts'), + helpContent: t('Number of hosts in provider clusters'), + }, + }; + + const items = []; + + for (const key in inventoryItems) { + const item = inventoryItems?.[key]; + + if (item) { + const value = inventory[key] || '-'; + items.push( + , + ); + } + } + + return ( + + {items} + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/VSphereInventorySection.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/VSphereInventorySection.tsx new file mode 100644 index 000000000..3d52c74df --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/VSphereInventorySection.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { DescriptionList } from '@patternfly/react-core'; + +import { DetailsItem } from '../../../../utils'; + +import { InventoryProps } from './InventorySection'; + +export const VSphereInventorySection: React.FC = ({ data }) => { + const { t } = useForkliftTranslation(); + const { provider, inventory } = data; + + if (!provider || !inventory) { + return {t('No credentials found.')}; + } + + const inventoryItems = { + vmCount: { + title: t('Virtual machines'), + helpContent: t('Number of virtual machines in cluster'), + }, + networkCount: { + title: t('Network interfaces'), + helpContent: t('Number of network interfaces in provider cluster'), + }, + datacenterCount: { + title: t('Data centers'), + helpContent: t('Number of data centers in provider'), + }, + datastoreCount: { + title: t('Data stores'), + helpContent: t('Number of data stores in provider'), + }, + clusterCount: { + title: t('Clusters'), + helpContent: t('Number of cluster in provider'), + }, + hostCount: { + title: t('Hosts'), + helpContent: t('Number of hosts in provider clusters'), + }, + }; + + const items = []; + + for (const key in inventoryItems) { + const item = inventoryItems?.[key]; + + if (item) { + const value = inventory[key] || '-'; + items.push( + , + ); + } + } + + return ( + + {items} + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/index.ts new file mode 100644 index 000000000..42833fa93 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/InventorySection/index.ts @@ -0,0 +1,7 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './InventorySection'; +export * from './OpenshiftInventorySection'; +export * from './OpenstackInventorySection'; +export * from './OvirtInventorySection'; +export * from './VSphereInventorySection'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/index.ts new file mode 100644 index 000000000..534645655 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/components/index.ts @@ -0,0 +1,6 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './ConditionsSection'; +export * from './CredentialsSection'; +export * from './DetailsSection'; +export * from './InventorySection'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/index.ts new file mode 100644 index 000000000..20ce21249 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/index.ts @@ -0,0 +1,5 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './components'; +export * from './ProviderDetailsPage'; +export * from './tabs'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Credentials/ProviderCredentials.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Credentials/ProviderCredentials.tsx new file mode 100644 index 000000000..7c4456045 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Credentials/ProviderCredentials.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { ProviderData } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { PageSection, Title } from '@patternfly/react-core'; + +import { CredentialsSection } from '../../components'; + +interface ProviderCredentialsProps extends RouteComponentProps { + obj: ProviderData; + ns?: string; + name?: string; + loaded?: boolean; + loadError?: unknown; +} + +export const ProviderCredentials: React.FC = ({ + obj, + loaded, + loadError, +}) => { + const { t } = useForkliftTranslation(); + + return ( +
+ + + {t('Credentials')} + + + +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Credentials/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Credentials/index.ts new file mode 100644 index 000000000..781dc3223 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Credentials/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './ProviderCredentials'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Details/ProviderDetails.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Details/ProviderDetails.tsx new file mode 100644 index 000000000..0ccb5bcb7 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Details/ProviderDetails.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { ProviderData } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { PageSection, Title } from '@patternfly/react-core'; + +import { ConditionsSection, DetailsSection, InventorySection } from '../../components'; + +interface ProviderDetailsProps extends RouteComponentProps { + obj: ProviderData; + ns?: string; + name?: string; + loaded?: boolean; + loadError?: unknown; +} + +export const ProviderDetails: React.FC = ({ obj }) => { + const { t } = useForkliftTranslation(); + const { provider } = obj; + + if (!provider?.metadata?.name) { + return <>; + } + + return ( +
+ + + {t('Provider details')} + + + + + + + {t('Provider inventory')} + + + + + + + {t('Conditions')} + + + +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Details/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Details/index.ts new file mode 100644 index 000000000..66f1baed5 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Details/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './ProviderDetails'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Hosts/ProviderHosts.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Hosts/ProviderHosts.tsx new file mode 100644 index 000000000..280c0378b --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Hosts/ProviderHosts.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { useProviderInventory } from 'src/modules/ProvidersNG/hooks'; +import { ProviderData } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ProviderHost } from '@kubev2v/types'; +import { PageSection, Title } from '@patternfly/react-core'; +import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; + +interface ProviderHostsProps extends RouteComponentProps { + obj: ProviderData; + ns?: string; + name?: string; + loaded?: boolean; + loadError?: unknown; +} + +export const ProviderHosts: React.FC = ({ obj }) => { + const { t } = useForkliftTranslation(); + const { provider } = obj; + + const { inventory: hosts } = useProviderInventory({ + provider, + subPath: 'hosts?detail=4', + }); + + if (!hosts || hosts.length === 0) { + return ( + + {t('No hosts found.')} + + ); + } + + return ( +
+ + + {t('Hosts')} + + + + + + + {t('Name')} + {t('ID')} + + + + {hosts && + hosts.length > 0 && + hosts.map((host) => ( + + {host.name} + {host.id || '-'} + + ))} + + + +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Hosts/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Hosts/index.ts new file mode 100644 index 000000000..3305f9fb6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Hosts/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './ProviderHosts'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Networks/ProviderNetworks.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Networks/ProviderNetworks.tsx new file mode 100644 index 000000000..77a32a711 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Networks/ProviderNetworks.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { useProviderInventory } from 'src/modules/ProvidersNG/hooks'; +import { ProviderData } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { CnoConfig, OpenShiftNetworkAttachmentDefinition } from '@kubev2v/types'; +import { Label, PageSection, Title } from '@patternfly/react-core'; +import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; + +interface ProviderNetworksProps extends RouteComponentProps { + obj: ProviderData; + ns?: string; + name?: string; + loaded?: boolean; + loadError?: unknown; +} + +export const ProviderNetworks: React.FC = ({ obj }) => { + const { t } = useForkliftTranslation(); + const { provider } = obj; + + const { inventory: networks } = useProviderInventory({ + provider, + // eslint-disable-next-line @cspell/spellchecker + subPath: 'networkattachmentdefinitions?detail=4', + }); + + if (!networks || networks.length === 0) { + return ( + + {t('No networks found.')} + + ); + } + + const defaultNetwork = + provider?.metadata?.annotations?.['forklift.konveyor.io/defaultTransferNetwork']; + const networkData = networks.map((net) => ({ + name: net.name, + namespace: net.namespace, + isDefault: `${net.namespace}/${net.name}` === defaultNetwork, + config: JSON.parse(net?.object?.spec?.config || '{}') as CnoConfig, + })); + + return ( +
+ + + {t('NetworkAttachmentDefinitions')} + + + + + + + {t('Name')} + {t('Namespace')} + {t('Type')} + + + + + + {'Pod network'}{' '} + {!defaultNetwork && ( + + )} + + {'-'} + {'pod-network'} + + {networkData.map((data) => ( + + + {data.name}{' '} + {data.isDefault && ( + + )} + + {data?.namespace || '-'} + {data?.config?.type || '-'} + + ))} + + + +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Networks/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Networks/index.ts new file mode 100644 index 000000000..485802c5d --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/Networks/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './ProviderNetworks'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/VirtualMachines/ProviderVirtualMachines.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/VirtualMachines/ProviderVirtualMachines.tsx new file mode 100644 index 000000000..c8fe176a0 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/VirtualMachines/ProviderVirtualMachines.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { useProviderInventory } from 'src/modules/ProvidersNG/hooks'; +import { ProviderData } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ProviderVirtualMachine } from '@kubev2v/types'; +import { List, ListItem, PageSection, Title } from '@patternfly/react-core'; +import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; + +interface ProviderVirtualMachinesProps extends RouteComponentProps { + obj: ProviderData; + ns?: string; + name?: string; + loaded?: boolean; + loadError?: unknown; +} + +export const ProviderVirtualMachines: React.FC = ({ obj }) => { + const { t } = useForkliftTranslation(); + const { provider } = obj; + + const { inventory: vms } = useProviderInventory({ + provider, + subPath: 'vms?detail=4', + }); + + if (!vms || vms.length === 0) { + return ( + + {t('No virtual machines found.')} + + ); + } + + return ( +
+ + + {t('Virtual Machined')} + + + + + + + {t('Name')} + {t('Concerns')} + + + + {vms && + vms.length > 0 && + vms.map((vm) => ( + + {vm.name} + + + {vm?.concerns?.map((c) => ( + {c.label} + ))} + + + + ))} + + + +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/VirtualMachines/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/VirtualMachines/index.ts new file mode 100644 index 000000000..e2ef18d6a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/VirtualMachines/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './ProviderVirtualMachines'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/YAML/ProviderYAML.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/YAML/ProviderYAML.tsx new file mode 100644 index 000000000..c82ba641a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/YAML/ProviderYAML.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { ProviderData } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ResourceYAMLEditor } from '@openshift-console/dynamic-plugin-sdk'; +import { Bullseye } from '@patternfly/react-core'; + +interface ProviderYAMLPageProps extends RouteComponentProps { + obj: ProviderData; + ns?: string; + name?: string; + loaded?: boolean; + loadError?: unknown; +} + +export const ProviderYAMLPage: React.FC = ({ obj, loaded, loadError }) => { + const { t } = useForkliftTranslation(); + const { provider } = obj; + + return ( + + + + } + > + {provider && loaded && !loadError && ( + + )} + + ); +}; + +const Loading: React.FC = () => ( +
+
+
+
+
+); + +export default ProviderYAMLPage; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/YAML/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/YAML/index.ts new file mode 100644 index 000000000..e5c12df2e --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/YAML/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './ProviderYAML'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/index.ts new file mode 100644 index 000000000..572c94d4d --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/details/tabs/index.ts @@ -0,0 +1,8 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './Credentials'; +export * from './Details'; +export * from './Hosts'; +export * from './Networks'; +export * from './VirtualMachines'; +export * from './YAML'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/index.ts new file mode 100644 index 000000000..4eb11bef6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/index.ts @@ -0,0 +1,5 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './create'; +export * from './details'; +export * from './list'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/ProviderRow.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/ProviderRow.tsx new file mode 100644 index 000000000..13ac48076 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/ProviderRow.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { ProviderData } from 'src/modules/ProvidersNG/utils'; + +import { RowProps } from '@kubev2v/common'; +import { ResourceField } from '@kubev2v/common'; +import { + DatabaseIcon, + NetworkIcon, + OutlinedHddIcon, + VirtualMachineIcon, +} from '@patternfly/react-icons'; +import { Td, Tr } from '@patternfly/react-table'; + +import { ProviderActionsDropdown } from '../../actions'; +import { TableEmptyCell } from '../../utils'; + +import { + CellProps, + InventoryCellFactory, + NamespaceCell, + ProviderLinkCell, + StatusCell, + TypeCell, + URLCell, +} from './components'; + +/** + * Function component to render a table row (Tr) for a provider with inventory. + * Each cell (Td) in the row is rendered by the `renderTd` function. + * + * @param {RowProps} props - The properties passed to the component. + * @param {ResourceField[]} props.resourceFields - The fields to be displayed in the table row. + * @param {ProviderData} props.resourceData - The data for the provider, including its inventory. + * + * @returns {ReactNode - A React table row (Tr) component. + */ +export const ProviderRow: React.FC> = ({ resourceFields, resourceData }) => { + return ( + + {resourceFields.map(({ resourceFieldId }) => + renderTd({ resourceData, resourceFieldId, resourceFields }), + )} + + ); +}; + +/** + * Function to render a table cell (Td). + * If the cell is an inventory cell (NETWORK_COUNT, STORAGE_COUNT, VM_COUNT, or HOST_COUNT) + * and there's no inventory data, it won't render the cell. + * + * @param {RenderTdProps} props - An object holding all the parameters. + * @param {ProviderData} props.resourceData - The data for the resource. + * @param {string} props.resourceFieldId - The field ID for the resource. + * @param {ResourceField[]} props.resourceFields - Array of resource fields + * + * @returns {ReactNode | undefined} - A React table cell (Td) component or undefined. + */ +const renderTd = ({ resourceData, resourceFieldId, resourceFields }: RenderTdProps) => { + const fieldId = resourceFieldId; + const hasInventory = resourceData?.inventory !== undefined; + const inventoryCells = ['networkCount', 'storageCount', 'vmCount', 'hostCount']; + + // If the current cell is an inventory cell and there's no inventory data, + // don't render the cell + if (inventoryCells.includes(fieldId) && !hasInventory) { + return ; + } + + const CellRenderer = cellRenderers?.[fieldId] ?? (() => <>); + return ( + + + + ); +}; + +const cellRenderers: Record> = { + ['name']: ProviderLinkCell, + ['phase']: StatusCell, + ['url']: URLCell, + ['type']: TypeCell, + ['namespace']: NamespaceCell, + ['networkCount']: InventoryCellFactory({ icon: }), + ['storageCount']: InventoryCellFactory({ icon: }), + ['vmCount']: InventoryCellFactory({ icon: }), + ['hostCount']: InventoryCellFactory({ icon: }), + ['actions']: (props) => ProviderActionsDropdown({ isKebab: true, ...props }), +}; + +interface RenderTdProps { + resourceData: ProviderData; + resourceFieldId: string; + resourceFields: ResourceField[]; +} + +export default ProviderRow; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/ProvidersListPage.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/ProvidersListPage.tsx new file mode 100644 index 000000000..b81a1716a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/ProvidersListPage.tsx @@ -0,0 +1,210 @@ +import React, { useState } from 'react'; +import StandardPage from 'src/components/page/StandardPage'; +import { ProviderData } from 'src/modules/ProvidersNG/utils'; +import { PROVIDER_STATUS, PROVIDERS } from 'src/utils/enums'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { EnumToTuple, loadUserSettings } from '@kubev2v/common'; +import { ResourceFieldFactory } from '@kubev2v/common'; +import { ProviderType, SOURCE_PROVIDER_TYPES } from '@kubev2v/legacy/common/constants'; +import { + ProviderModel, + ProviderModelGroupVersionKind, + ProviderModelRef, + V1beta1Provider, +} from '@kubev2v/types'; +import { ListPageCreate, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { Alert, Text, TextContent, TextVariants } from '@patternfly/react-core'; + +import { useGetDeleteAndEditAccessReview, useProvidersInventoryList } from '../../hooks'; +import { findInventoryByID } from '../../utils'; + +import ProviderRow from './ProviderRow'; + +export const fieldsMetadataFactory: ResourceFieldFactory = (t) => [ + { + resourceFieldId: 'name', + jsonPath: '$.provider.metadata.name', + label: t('Name'), + isVisible: true, + isIdentity: true, // Name is sufficient ID when Namespace is pre-selected + filter: { + type: 'freetext', + placeholderLabel: t('Filter by name'), + }, + sortable: true, + }, + { + resourceFieldId: 'namespace', + jsonPath: '$.provider.metadata.namespace', + label: t('Namespace'), + isVisible: true, + isIdentity: true, + filter: { + type: 'freetext', + placeholderLabel: t('Filter by namespace'), + }, + sortable: true, + }, + { + resourceFieldId: 'phase', + jsonPath: '$.provider.status.phase', + label: t('Status'), + isVisible: true, + filter: { + type: 'enum', + primary: true, + placeholderLabel: t('Status'), + values: EnumToTuple(PROVIDER_STATUS), + }, + sortable: true, + }, + { + resourceFieldId: 'url', + jsonPath: '$.provider.spec.url', + label: t('Endpoint'), + isVisible: true, + filter: { + type: 'freetext', + placeholderLabel: t('Filter by endpoint'), + }, + sortable: true, + }, + { + resourceFieldId: 'type', + jsonPath: '$.provider.spec.type', + label: t('Type'), + isVisible: true, + filter: { + type: 'groupedEnum', + primary: true, + placeholderLabel: t('Type'), + values: EnumToTuple(PROVIDERS).map(({ id, ...rest }) => ({ + id, + groupId: SOURCE_PROVIDER_TYPES.includes(id as ProviderType) ? 'source' : 'target', + ...rest, + })), + groups: [ + { groupId: 'target', label: t('Target') }, + { groupId: 'source', label: t('Source') }, + ], + }, + sortable: true, + }, + { + resourceFieldId: 'vmCount', + jsonPath: '$.inventory.vmCount', + label: t('VMs'), + isVisible: true, + sortable: true, + }, + { + resourceFieldId: 'networkCount', + jsonPath: '$.inventory.networkCount', + label: t('Networks'), + isVisible: true, + sortable: true, + }, + { + resourceFieldId: 'clusterCount', + jsonPath: '$.inventory.clusterCount', + label: t('Clusters'), + isVisible: false, + sortable: true, + }, + { + resourceFieldId: 'hostCount', + jsonPath: '$.inventory.hostCount', + label: t('Hosts'), + isVisible: true, + sortable: true, + }, + { + resourceFieldId: 'storageCount', + jsonPath: '$.inventory.storageCount', + label: t('Storage'), + isVisible: false, + sortable: true, + }, + { + resourceFieldId: 'actions', + label: '', + isAction: true, + isVisible: true, + sortable: false, + }, +]; + +const ProvidersListPage: React.FC<{ + namespace: string; +}> = ({ namespace }) => { + const { t } = useForkliftTranslation(); + const [userSettings] = useState(() => loadUserSettings({ pageId: 'Providers' })); + + const [providers, providersLoaded, providersLoadError] = useK8sWatchResource({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + isList: true, + namespace, + }); + + const { + inventory, + loading: inventoryLoading, + error: inventoryError, + } = useProvidersInventoryList({}); + + const permissions = useGetDeleteAndEditAccessReview({ + model: ProviderModel, + namespace, + }); + + const data: ProviderData[] = providers.map((provider) => ({ + provider, + inventory: findInventoryByID(inventory, provider.metadata?.uid), + permissions, + })); + + const accessReview = { + groupVersionKind: ProviderModelRef, + namespace: namespace || 'default', + }; + + const AddButton = ( + + {t('Create Provider')} + + ); + + const inventoryNotReachable = ( + + + + {t( + 'Inventory server is not reachable. To troubleshoot, check the Forklift controller pod logs.', + )} + + + + ); + + return ( + + data-testid="providers-list" + addButton={AddButton} + dataSource={[data || [], providersLoaded, providersLoadError]} + RowMapper={ProviderRow} + fieldsMetadata={fieldsMetadataFactory(t)} + namespace={namespace} + title={t('Providers')} + userSettings={userSettings} + alerts={!inventoryLoading && inventoryError ? [inventoryNotReachable] : undefined} + /> + ); +}; + +export default ProvidersListPage; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/CellProps.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/CellProps.tsx new file mode 100644 index 000000000..87f9d050b --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/CellProps.tsx @@ -0,0 +1,9 @@ +import { ProviderData } from 'src/modules/ProvidersNG/utils'; + +import { ResourceField } from '@kubev2v/common'; + +export type CellProps = { + data: ProviderData; + fieldId: string; + fields: ResourceField[]; +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/InventoryCellFactory.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/InventoryCellFactory.tsx new file mode 100644 index 000000000..9d671cb6f --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/InventoryCellFactory.tsx @@ -0,0 +1,31 @@ +import React, { ReactNode } from 'react'; +import { TableEmptyCell, TableIconCell } from 'src/modules/ProvidersNG/utils'; + +import { getResourceFieldValue } from '@kubev2v/common'; + +import { CellProps } from './CellProps'; + +/** + * Factory function for creating InventoryCell components. + * @param {Object} param0 - The icon for the component. + * @returns {Function} - A function that returns a TableIconCell component. + */ +export const InventoryCellFactory: CellFactory = ({ icon }) => { + /** + * Inner function that returns a TableIconCell component. + * @param {CellProps} param1 - The props for the component. + * @returns {JSX.Element} - The rendered component. + */ + // eslint-disable-next-line react/display-name + return ({ data, fieldId, fields }: CellProps) => { + const { provider, inventory } = data; + const value = getResourceFieldValue({ ...provider, inventory }, fieldId, fields); + + if (value === undefined) { + return ; + } + return {value}; + }; +}; + +type CellFactory = (props: { icon: ReactNode }) => React.FC; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/NamespaceCell.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/NamespaceCell.tsx new file mode 100644 index 000000000..e38129fb8 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/NamespaceCell.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { TableLinkCell } from 'src/modules/ProvidersNG/utils'; + +import { CellProps } from './CellProps'; + +/** + * NamespaceCell component, used for displaying a link cell with information about the namespace. + * @param {CellProps} props - The props for the component. + * @returns {JSX.Element} - The rendered component. + */ +export const NamespaceCell: React.FC = ({ data }) => { + const { provider } = data; + const { namespace } = provider?.metadata || {}; + + return ( + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/ProviderLinkCell.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/ProviderLinkCell.tsx new file mode 100644 index 000000000..bcbf80e3f --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/ProviderLinkCell.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { TableLinkCell } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ProviderModelGroupVersionKind } from '@kubev2v/types'; + +import { CellProps } from './CellProps'; + +/** + * ProviderLinkCell component, used for displaying a link cell with information about the provider. + * @param {CellProps} props - The props for the component. + * @returns {JSX.Element} - The rendered component. + */ +export const ProviderLinkCell: React.FC = ({ data }) => { + const { t } = useForkliftTranslation(); + + const { provider } = data; + const { name, namespace } = provider?.metadata || {}; + const localCluster = + provider?.spec?.type === 'openshift' && (!provider?.spec?.url || provider?.spec?.url === ''); + + return ( + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/StatusCell.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/StatusCell.tsx new file mode 100644 index 000000000..4c40820c8 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/StatusCell.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { TFunction } from 'react-i18next'; +import Linkify from 'react-linkify'; +import { Link } from 'react-router-dom'; +import { getResourceUrl, TableIconCell } from 'src/modules/ProvidersNG/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { getResourceFieldValue } from '@kubev2v/common'; +import { ProviderModelRef } from '@kubev2v/types'; +import { + GreenCheckCircleIcon, + RedExclamationCircleIcon, +} from '@openshift-console/dynamic-plugin-sdk'; +import { Button, Popover, Spinner, Text, TextContent, TextVariants } from '@patternfly/react-core'; + +import { CellProps } from './CellProps'; + +/** + * StatusCell component, used for displaying the status of a resource. + * @param {CellProps} props - The props for the component. + * @returns {JSX.Element} - The rendered component. + */ +export const StatusCell: React.FC = ({ data, fields, fieldId }) => { + const { t } = useForkliftTranslation(); + + const phase = getResourceFieldValue(data, 'phase', fields); + const phaseLabel = phaseLabels[phase] ? t(phaseLabels[phase]) : t('Undefined'); + + switch (phase) { + case 'ConnectionFailed': + case 'ValidationFailed': + return ErrorStatusCell({ + t, + data, + fields, + fieldId, + }); + default: + return {phaseLabel}; + } +}; + +/** + * A component that displays an error status cell with popover content. + * @param {Object} props - The component props. + * @param {Object} props.data - The data object for the cell. + * @param {Object} props.fields - The fields object for the cell. + * @returns {JSX.Element} The JSX element representing the error status cell. + */ +export const ErrorStatusCell: React.FC = ({ t, data, fields }) => { + const { provider } = data; + const phase = getResourceFieldValue(data, 'phase', fields); + const phaseLabel = phaseLabels[phase] ? t(phaseLabels[phase]) : t('Undefined'); + const providerURL = getResourceUrl({ + reference: ProviderModelRef, + name: provider?.metadata?.name, + namespace: provider?.metadata?.namespace, + }); + + // Find the error message from the status conditions + const bodyContent = provider?.status?.conditions.find( + (condition) => condition?.category === 'Critical', + )?.message; + + // Set the footer content + const footerContent = ( + + {t(`The provider is not ready.`)} + + {t( + `To troubleshoot, view the provider status available in the provider details page + and check the Forklift controller pod logs.`, + )} + + + {t('View provider details')} + + + ); + + return ( + {bodyContent}} + footerContent={footerContent} + > + + + ); +}; + +const statusIcons = { + ValidationFailed: , + ConnectionFailed: , + Ready: , + Staging: , +}; + +const phaseLabels = { + // t('Ready') + Ready: 'Ready', + // t('Connection Failed') + ConnectionFailed: 'Connection Failed', + // t('Validation Failed') + ValidationFailed: 'Validation Failed', + // t('Staging') + Staging: 'Staging', +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/TypeCell.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/TypeCell.tsx new file mode 100644 index 000000000..d1b88b916 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/TypeCell.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { getIsSource, TableLabelCell } from 'src/modules/ProvidersNG/utils'; +import { PROVIDERS } from 'src/utils/enums'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { getResourceFieldValue } from '@kubev2v/common'; + +import { CellProps } from './CellProps'; + +/** + * TypeCell component, used for displaying the type of a resource. + * @param {CellProps} props - The props for the component. + * @returns {JSX.Element} - The rendered component. + */ +export const TypeCell: React.FC = ({ data, fields }) => { + const { t } = useForkliftTranslation(); + + const { provider } = data; + const type = getResourceFieldValue(data, 'type', fields); + const isSource = getIsSource(provider); + + return ( + + {PROVIDERS?.[type] || ''} + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/URLCell.tsx b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/URLCell.tsx new file mode 100644 index 000000000..06ae26f26 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/URLCell.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { TableCell } from 'src/modules/ProvidersNG/utils'; + +import { getResourceFieldValue } from '@kubev2v/common'; +import { Truncate } from '@patternfly/react-core'; + +import { CellProps } from './CellProps'; + +/** + * URLCell component, used for displaying a TableCell with a URL string. + * @param {CellProps} props - The props for the component. + * @returns {JSX.Element} - The rendered component. + */ +export const URLCell: React.FC = ({ data, fieldId, fields }) => { + const url = (getResourceFieldValue(data, fieldId, fields) ?? '').toString(); + return ( + + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/index.ts new file mode 100644 index 000000000..e009778c6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/components/index.ts @@ -0,0 +1,9 @@ +// @index('./*.tsx', f => `export * from '${f.path}';`) +export * from './CellProps'; +export * from './InventoryCellFactory'; +export * from './NamespaceCell'; +export * from './ProviderLinkCell'; +export * from './StatusCell'; +export * from './TypeCell'; +export * from './URLCell'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/index.ts b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/index.ts new file mode 100644 index 000000000..1b15d7d81 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/ProvidersNG/views/list/index.ts @@ -0,0 +1,5 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './components'; +export * from './ProviderRow'; +export * from './ProvidersListPage'; +// @endindex