diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json index 7bcf99435..717bce59b 100644 --- a/locales/en/plugin__odf-console.json +++ b/locales/en/plugin__odf-console.json @@ -85,6 +85,11 @@ "There are no namespaces to display. Select a cluster first to view namespaces.": "There are no namespaces to display. Select a cluster first to view namespaces.", "There are no namespaces to display.": "There are no namespaces to display.", "This list does not include namespaces where applications are enrolled separately under disaster recovery protection.": "This list does not include namespaces where applications are enrolled separately under disaster recovery protection.", + "Name requirements": "Name requirements", + "Example": "Example", + "Enter a unique name": "Enter a unique name", + "Name input": "Name input", + "A unique identifier for ACM discovered applications from selected namespaces.": "A unique identifier for ACM discovered applications from selected namespaces.", "Select a DRCluster to choose your namespace.": "Select a DRCluster to choose your namespace.", "No DR cluster found": "No DR cluster found", "Namespace selection": "Namespace selection", @@ -422,8 +427,6 @@ "Create BlockPool": "Create BlockPool", "Data loss may occur, only recommended for small clusters or when backups are available or data loss is acceptable": "Data loss may occur, only recommended for small clusters or when backups are available or data loss is acceptable", "{{replica}} Replication": "{{replica}} Replication", - "Name requirements": "Name requirements", - "Example": "Example", "pool-name-help": "pool-name-help", "my-block-pool": "my-block-pool", "Data protection policy": "Data protection policy", @@ -980,8 +983,8 @@ "Access Key Field": "Access Key Field", "Secret Key Field": "Secret Key Field", "ObjectBucketClaim Name": "ObjectBucketClaim Name", - "If not provided a generic name will be generated.": "If not provided a generic name will be generated.", "my-object-bucket": "my-object-bucket", + "If not provided a generic name will be generated.": "If not provided a generic name will be generated.", "StorageClass": "StorageClass", "Defines the object-store service and the bucket provisioner.": "Defines the object-store service and the bucket provisioner.", "BucketClass": "BucketClass", diff --git a/packages/mco/components/discovered-application-wizard/enroll-discovered-application.spec.tsx b/packages/mco/components/discovered-application-wizard/enroll-discovered-application.spec.tsx index 0b1fb3d4b..f56e509fa 100644 --- a/packages/mco/components/discovered-application-wizard/enroll-discovered-application.spec.tsx +++ b/packages/mco/components/discovered-application-wizard/enroll-discovered-application.spec.tsx @@ -265,18 +265,59 @@ jest.mock( }) ); -// Mocking as "fireEvent" is throwing warning for FieldLevelHelp -jest.mock('@odf/shared/generic', () => ({ - ...jest.requireActual('@odf/shared/generic'), - FieldLevelHelp: () => <>, +// Mocking as "Popover" is throwing warning for FieldLevelHelp & TextInputWithFieldRequirements +jest.mock('@patternfly/react-core', () => ({ + ...jest.requireActual('@patternfly/react-core'), + Popover: () => <>, })); +const moveToStep = async (step: number) => { + if (step > 1) { + // Select cluster + fireEvent.click(screen.getByText('Select cluster')); + fireEvent.click(screen.getByText('east-1')); + + // Select namespaces + fireEvent.click(screen.getByLabelText('Select row 0')); + fireEvent.click(screen.getByLabelText('Select row 1')); + + // Name input + fireEvent.change(screen.getByLabelText('Name input'), { + target: { value: 'my-name' }, + }); + await waitFor(() => { + expect(screen.getByDisplayValue('my-name')).toBeInTheDocument(); + }); + + // Next wizard step + fireEvent.click(screen.getByText('Next')); + } + + if (step > 2) { + // Select recipe + fireEvent.click(screen.getByText('Select a recipe')); + fireEvent.click(screen.getByText('mock-recipe-1')); + + // Next wizard step + fireEvent.click(screen.getByText('Next')); + } + + if (step > 3) { + // Select policy + fireEvent.click(screen.getByText('Select a policy')); + fireEvent.click(screen.getByText('mock-policy-1')); + + // Next wizard step + fireEvent.click(screen.getByText('Next')); + } +}; + describe('Test namespace step', () => { beforeEach(() => { - testCase += 1; render(); }); test('Namespace selection form test', async () => { + testCase = 1; // Step1 title expect(screen.getByText('Namespace selection')).toBeInTheDocument(); // Step1 title description @@ -311,6 +352,13 @@ describe('Test namespace step', () => { 'This list does not include namespaces where applications are enrolled separately under disaster recovery protection.' ) ).toBeInTheDocument(); + // Name input + expect(screen.getByText('Name')).toBeInTheDocument(); + expect( + screen.getByText( + 'A unique identifier for ACM discovered applications from selected namespaces.' + ) + ).toBeInTheDocument(); // Footer expect(screen.getByText('Next')).toBeInTheDocument(); @@ -331,6 +379,7 @@ describe('Test namespace step', () => { }); test('No namespace found test', async () => { + testCase = 2; // Cluster selection fireEvent.click(screen.getByText('Select cluster')); fireEvent.click(screen.getByText('east-1')); @@ -347,6 +396,7 @@ describe('Test namespace step', () => { }); test('Namespace selection test', async () => { + testCase = 3; // Cluster east-1 selection fireEvent.click(screen.getByText('Select cluster')); fireEvent.click(screen.getByText('east-1')); @@ -384,28 +434,27 @@ describe('Test namespace step', () => { expect(() => screen.getByText('openshift')).toThrow( 'Unable to find an element' ); + // Name input + fireEvent.change(screen.getByLabelText('Name input'), { + target: { value: 'my-name' }, + }); + await waitFor(() => { + expect(screen.getByDisplayValue('my-name')).toBeInTheDocument(); + }); }); }); describe('Test configure step', () => { beforeEach(() => { - testCase += 1; render(); - // Select cluster - fireEvent.click(screen.getByText('Select cluster')); - fireEvent.click(screen.getByText('east-1')); - - // Select namespaces - fireEvent.click(screen.getByLabelText('Select row 0')); - fireEvent.click(screen.getByLabelText('Select row 1')); - - // Next wizard step - fireEvent.click(screen.getByText('Next')); }); + test('Configure form test', async () => { - // Step1 title + testCase = 4; + await moveToStep(2); + // Step2 title expect(screen.getByText('Configure definition')).toBeInTheDocument(); - // Step1 title description + // Step2 title description expect( screen.getByText( 'Choose your configuration preference to protect resources (application volumes/PVCs, or Kubernetes objects).' @@ -462,32 +511,16 @@ describe('Test configure step', () => { describe('Test replication step', () => { beforeEach(() => { - testCase += 1; render(); - // Select cluster - fireEvent.click(screen.getByText('Select cluster')); - fireEvent.click(screen.getByText('east-1')); - - // Select namespaces - fireEvent.click(screen.getByLabelText('Select row 0')); - fireEvent.click(screen.getByLabelText('Select row 1')); - - // Next wizard step - fireEvent.click(screen.getByText('Next')); - - // Select recipe - fireEvent.click(screen.getByText('Select a recipe')); - fireEvent.click(screen.getByText('mock-recipe-1')); - - // Next wizard step - fireEvent.click(screen.getByText('Next')); }); test('Replication form test', async () => { - // Step1 title + testCase = 5; + await moveToStep(3); + // Step3 title expect( screen.getByText('Volume and Kubernetes object replication') ).toBeInTheDocument(); - // Step1 title description + // Step3 title description expect( screen.getByText( 'Define where to sync or replicate your application volumes and Kubernetes object using a disaster recovery policy.' @@ -544,38 +577,11 @@ describe('Test replication step', () => { }); describe('Test review step', () => { beforeEach(() => { - testCase += 1; render(); - // Select cluster - fireEvent.click(screen.getByText('Select cluster')); - fireEvent.click(screen.getByText('east-1')); - - // Select namespaces - fireEvent.click(screen.getByLabelText('Select row 0')); - fireEvent.click(screen.getByLabelText('Select row 1')); - - // Next wizard step - fireEvent.click(screen.getByText('Next')); - - if (testCase === 6) { - // Select recipe - fireEvent.click(screen.getByText('Select a recipe')); - fireEvent.click(screen.getByText('mock-recipe-1')); - - // Next wizard step - fireEvent.click(screen.getByText('Next')); - } else { - // ToDo: Resource label selection reivew - } - - // Select policy - fireEvent.click(screen.getByText('Select a policy')); - fireEvent.click(screen.getByText('mock-policy-1')); - - // Next wizard step - fireEvent.click(screen.getByText('Next')); }); test('Review form test', async () => { + testCase = 6; + await moveToStep(4); // Namespace selection test expect(screen.getAllByText('Namespace').length === 2).toBeTruthy(); expect(screen.getByText('Cluster:')).toBeInTheDocument(); diff --git a/packages/mco/components/discovered-application-wizard/footer.tsx b/packages/mco/components/discovered-application-wizard/footer.tsx index c51ee4c84..664824418 100644 --- a/packages/mco/components/discovered-application-wizard/footer.tsx +++ b/packages/mco/components/discovered-application-wizard/footer.tsx @@ -25,7 +25,9 @@ import { } from './utils/reducer'; const validateNamespaceStep = (state: EnrollDiscoveredApplicationState) => - !!state.namespace.clusterName && !!state.namespace.namespaces.length; + !!state.namespace.clusterName && + !!state.namespace.namespaces.length && + !!state.namespace.name; const validateConfigurationStep = (state: EnrollDiscoveredApplicationState) => { const { recipe, resourceLabels, protectionMethod } = state.configuration; diff --git a/packages/mco/components/discovered-application-wizard/utils/reducer.ts b/packages/mco/components/discovered-application-wizard/utils/reducer.ts index 903bea1ee..bceb2f867 100644 --- a/packages/mco/components/discovered-application-wizard/utils/reducer.ts +++ b/packages/mco/components/discovered-application-wizard/utils/reducer.ts @@ -21,6 +21,7 @@ export enum EnrollDiscoveredApplicationStateType { SET_PVC_LABEL_EXPRESSIONS = 'CONFIGURATION/RESOURCE_LABEL/SET_PVC_LABEL_EXPRESSIONS', SET_POLICY = 'REPLICATION/SET_POLICY', SET_K8S_RESOURCE_REPLICATION_INTERVAL = 'REPLICATION/SET_K8S_RESOURCE_REPLICATION_INTERVAL', + SET_NAME = 'NAMESPACE/SET_NAME', } export type EnrollDiscoveredApplicationState = { @@ -29,6 +30,8 @@ export type EnrollDiscoveredApplicationState = { namespaces: K8sResourceCommon[]; // Cluster name of the discovered application clusterName: string; + // DRPC name to protect the discovered application + name: string; }; configuration: { // recipe CRD (or) normal K8s CR label based protection @@ -60,6 +63,7 @@ export const initialState: EnrollDiscoveredApplicationState = { namespace: { clusterName: '', namespaces: [], + name: '', }, configuration: { protectionMethod: ProtectionMethodType.RECIPE, @@ -111,6 +115,10 @@ export type EnrollDiscoveredApplicationAction = | { type: EnrollDiscoveredApplicationStateType.SET_K8S_RESOURCE_REPLICATION_INTERVAL; payload: string; + } + | { + type: EnrollDiscoveredApplicationStateType.SET_NAME; + payload: string; }; export const reducer: EnrollReducer = (state, action) => { @@ -127,6 +135,7 @@ export const reducer: EnrollReducer = (state, action) => { namespace: { ...newState.namespace, clusterName: action.payload, + name: state.namespace.name, }, }; } @@ -207,6 +216,15 @@ export const reducer: EnrollReducer = (state, action) => { }, }; } + case EnrollDiscoveredApplicationStateType.SET_NAME: { + return { + ...state, + namespace: { + ...state.namespace, + name: action.payload, + }, + }; + } default: throw new TypeError(`${action} is not a valid reducer action`); } diff --git a/packages/mco/components/discovered-application-wizard/wizard-steps/namespace-step/namesapce-table.tsx b/packages/mco/components/discovered-application-wizard/wizard-steps/namespace-step/namesapce-table.tsx index 191242f6c..ee6ded0a0 100644 --- a/packages/mco/components/discovered-application-wizard/wizard-steps/namespace-step/namesapce-table.tsx +++ b/packages/mco/components/discovered-application-wizard/wizard-steps/namespace-step/namesapce-table.tsx @@ -1,8 +1,5 @@ import * as React from 'react'; -import { - getDRPlacementControlResourceObj, - useACMSafeFetch, -} from '@odf/mco/hooks'; +import { useACMSafeFetch } from '@odf/mco/hooks'; import { DRPlacementControlKind, DRPolicyKind } from '@odf/mco/types'; import { queryNamespacesUsingCluster, @@ -21,7 +18,6 @@ import { isSystemNamespace, sortRows } from '@odf/shared/utils'; import { K8sResourceCommon, ListPageFilter, - useK8sWatchResource, useListPageFilter, } from '@openshift-console/dynamic-plugin-sdk'; import { TFunction } from 'i18next'; @@ -79,13 +75,16 @@ const TableRow: React.FC> = ({ }; export const NamespaceSelectionTable: React.FC = - ({ namespaces, clusterName, policies, isValidationEnabled, dispatch }) => { + ({ + namespaces, + clusterName, + policies, + drPlacements, + isValidationEnabled, + dispatch, + }) => { const { t } = useCustomTranslation(); - const [drPlacements, drpcLoaded, drpcLoadError] = useK8sWatchResource< - DRPlacementControlKind[] - >(getDRPlacementControlResourceObj()); - const protectedNamespaces = React.useMemo(() => { const eligiblePolicies = findAllEligiblePolicies(clusterName, policies); return getProtectedNamespaces(drPlacements, eligiblePolicies); @@ -100,11 +99,8 @@ export const NamespaceSelectionTable: React.FC = const [searchResult, searchError, searchLoaded] = useACMSafeFetch(searchQuery); - const loaded = searchLoaded && drpcLoaded; - const loadError = searchError || drpcLoadError; - const userNamespaces: K8sResourceCommon[] = React.useMemo(() => { - if (loaded && !loadError) { + if (searchLoaded && !searchError) { // Converting from search result type to K8sResourceCommon for shared components compatibility. const allNamespaces = convertSearchResultToK8sResourceCommon( searchResult?.data.searchResult?.[0]?.items || [] @@ -119,7 +115,7 @@ export const NamespaceSelectionTable: React.FC = }); } return []; - }, [searchResult, protectedNamespaces, loaded, loadError]); + }, [searchResult, protectedNamespaces, searchLoaded, searchError]); const [data, filteredData, onFilterChange] = useListPageFilter(userNamespaces); @@ -147,7 +143,7 @@ export const NamespaceSelectionTable: React.FC = {namespaceValidated && ( @@ -190,6 +186,7 @@ type NamespaceSelectionTableProps = { namespaces: K8sResourceCommon[]; clusterName: string; policies: DRPolicyKind[]; + drPlacements: DRPlacementControlKind[]; isValidationEnabled: boolean; dispatch: React.Dispatch; }; diff --git a/packages/mco/components/discovered-application-wizard/wizard-steps/namespace-step/namespace-step.tsx b/packages/mco/components/discovered-application-wizard/wizard-steps/namespace-step/namespace-step.tsx index 923881864..25f5b68ce 100644 --- a/packages/mco/components/discovered-application-wizard/wizard-steps/namespace-step/namespace-step.tsx +++ b/packages/mco/components/discovered-application-wizard/wizard-steps/namespace-step/namespace-step.tsx @@ -1,12 +1,26 @@ import * as React from 'react'; +import { RAMEN_PROTECTED_APPS_NAMESPACE } from '@odf/mco/constants'; +import { getDRPlacementControlResourceObj } from '@odf/mco/hooks'; import { DRPolicyModel } from '@odf/mco/models'; -import { DRPolicyKind } from '@odf/mco/types'; +import { DRPlacementControlKind, DRPolicyKind } from '@odf/mco/types'; +import { + fieldRequirementsTranslations, + formSettings, +} from '@odf/shared/constants'; import { SingleSelectDropdown } from '@odf/shared/dropdown'; import { StatusBox } from '@odf/shared/generic/status-box'; import { useK8sList } from '@odf/shared/hooks'; +import { TextInputWithFieldRequirements } from '@odf/shared/input-with-requirements'; +import { getName, getNamespace } from '@odf/shared/selectors'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; import { getValidatedProp } from '@odf/shared/utils'; +import validationRegEx from '@odf/shared/utils/validation'; +import { useYupValidationResolver } from '@odf/shared/yup-validation-resolver'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; import { SelectOption } from '@patternfly/react-core/deprecated'; +import { TFunction } from 'i18next'; +import { useForm } from 'react-hook-form'; +import * as Yup from 'yup'; import { Alert, AlertVariant, @@ -44,6 +58,103 @@ const clusterOptions = (clusters: string[]): JSX.Element[] => /> )); +const getInputValidationSchema = (t: TFunction, existingNames: string[]) => { + const fieldRequirements = [ + fieldRequirementsTranslations.maxChars(t, 63), + fieldRequirementsTranslations.startAndEndName(t), + fieldRequirementsTranslations.alphaNumericPeriodAdnHyphen(t), + fieldRequirementsTranslations.cannotBeUsedBefore(t), + ]; + + const schema = Yup.object({ + 'name-input': Yup.string() + .required() + .max(63, fieldRequirements[0]) + .matches( + validationRegEx.startAndEndsWithAlphanumerics, + fieldRequirements[1] + ) + .matches( + validationRegEx.alphaNumericsPeriodsHyphensNonConsecutive, + fieldRequirements[2] + ) + .test( + 'unique-name', + fieldRequirements[3], + (value: string) => !existingNames.includes(value) + ), + }); + + return { schema, fieldRequirements }; +}; + +export const NameInput: React.FC = ({ + name, + drPlacements, + dispatch, +}) => { + const { t } = useCustomTranslation(); + + const { schema, fieldRequirements } = React.useMemo(() => { + const existingNames = + drPlacements + ?.filter( + (drpc) => getNamespace(drpc) === RAMEN_PROTECTED_APPS_NAMESPACE + ) + .map(getName) || []; + return getInputValidationSchema(t, existingNames); + }, [drPlacements, t]); + + const resolver = useYupValidationResolver(schema); + const { + control, + watch, + formState: { isValid }, + } = useForm({ + ...formSettings, + resolver, + }); + + const newName: string = watch('name-input'); + + React.useEffect( + () => + dispatch({ + type: EnrollDiscoveredApplicationStateType.SET_NAME, + payload: isValid ? newName : '', + }), + [newName, isValid, dispatch] + ); + + return ( + + ); +}; + export const NamespaceSelection: React.FC = ({ state, isValidationEnabled, @@ -51,10 +162,17 @@ export const NamespaceSelection: React.FC = ({ }) => { const { t } = useCustomTranslation(); - const [drPolicies, loaded, loadError] = + const [drPolicies, policyloaded, policyLoadError] = useK8sList(DRPolicyModel); - const { clusterName, namespaces } = state.namespace; + const [drPlacements, drpcLoaded, drpcLoadError] = useK8sWatchResource< + DRPlacementControlKind[] + >(getDRPlacementControlResourceObj()); + + const loaded = policyloaded && drpcLoaded; + const loadError = policyLoadError || drpcLoadError; + + const { clusterName, namespaces, name } = state.namespace; const clusterNameHelperText = t( 'Select a DRCluster to choose your namespace.' ); @@ -135,10 +253,16 @@ export const NamespaceSelection: React.FC = ({ namespaces={namespaces} clusterName={clusterName} policies={drPolicies} + drPlacements={drPlacements} isValidationEnabled={isValidationEnabled} dispatch={dispatch} /> + ) : ( @@ -153,3 +277,9 @@ type NamespaceSelectionProps = { isValidationEnabled: boolean; dispatch: React.Dispatch; }; + +type NameInputProps = { + name: string; + drPlacements: DRPlacementControlKind[]; + dispatch: React.Dispatch; +}; diff --git a/packages/ocs/block-pool/body.tsx b/packages/ocs/block-pool/body.tsx index 2cb873145..eb3139b6a 100644 --- a/packages/ocs/block-pool/body.tsx +++ b/packages/ocs/block-pool/body.tsx @@ -13,6 +13,7 @@ import { CephClusterKind, } from '@odf/shared/types'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import validationRegEx from '@odf/shared/utils/validation'; import { useYupValidationResolver } from '@odf/shared/yup-validation-resolver'; import { Dropdown, @@ -30,7 +31,6 @@ import { EmptyStateHeader, } from '@patternfly/react-core'; import { CaretDownIcon } from '@patternfly/react-icons'; -import validationRegEx from '../../odf/utils/validation'; import { OCS_DEVICE_REPLICA, POOL_PROGRESS, POOL_STATE } from '../constants'; import { StorageClusterModel, CephBlockPoolModel } from '../models'; import { diff --git a/packages/odf/components/bucket-class/wizard-pages/general-page.tsx b/packages/odf/components/bucket-class/wizard-pages/general-page.tsx index 459ddcf67..0a9dbeb5a 100644 --- a/packages/odf/components/bucket-class/wizard-pages/general-page.tsx +++ b/packages/odf/components/bucket-class/wizard-pages/general-page.tsx @@ -8,6 +8,7 @@ import { TextInputWithFieldRequirements } from '@odf/shared/input-with-requireme import { getName } from '@odf/shared/selectors'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; import { isValidIP } from '@odf/shared/utils'; +import validationRegEx from '@odf/shared/utils/validation'; import { useYupValidationResolver } from '@odf/shared/yup-validation-resolver'; import { TFunction } from 'i18next'; import { useForm } from 'react-hook-form'; @@ -22,7 +23,6 @@ import { } from '@patternfly/react-core'; import { NooBaaBucketClassModel } from '../../../models'; import { BucketClassKind, BucketClassType } from '../../../types'; -import validationRegEx from '../../../utils/validation'; import { ExternalLink } from '../../mcg-endpoints/gcp-endpoint-type'; import { Action, State } from '../state'; import '../create-bc.scss'; diff --git a/packages/odf/components/create-bs/create-bs.tsx b/packages/odf/components/create-bs/create-bs.tsx index 7d393d2db..f289a2fe9 100644 --- a/packages/odf/components/create-bs/create-bs.tsx +++ b/packages/odf/components/create-bs/create-bs.tsx @@ -14,6 +14,7 @@ import { SecretModel } from '@odf/shared/models'; import { getName } from '@odf/shared/selectors'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; import { referenceForModel } from '@odf/shared/utils'; +import validationRegEx from '@odf/shared/utils/validation'; import { useYupValidationResolver } from '@odf/shared/yup-validation-resolver'; import { getAPIVersionForModel, @@ -41,7 +42,6 @@ import { getProviders, secretPayloadCreator, } from '../../utils'; -import validationRegEx from '../../utils/validation'; import { GCPEndpointType } from '../mcg-endpoints/gcp-endpoint-type'; import { PVCType } from '../mcg-endpoints/pvc-endpoint-type'; import { S3EndPointType } from '../mcg-endpoints/s3-endpoint-type'; diff --git a/packages/odf/components/create-storage-system/create-storage-system-steps/create-local-volume-set-step/body.tsx b/packages/odf/components/create-storage-system/create-storage-system-steps/create-local-volume-set-step/body.tsx index 0e2f55642..03745553d 100644 --- a/packages/odf/components/create-storage-system/create-storage-system-steps/create-local-volume-set-step/body.tsx +++ b/packages/odf/components/create-storage-system/create-storage-system-steps/create-local-volume-set-step/body.tsx @@ -20,6 +20,7 @@ import { getName } from '@odf/shared/selectors'; import { NodeKind } from '@odf/shared/types'; import { StorageSystemKind } from '@odf/shared/types'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import validationRegEx from '@odf/shared/utils/validation'; import { useYupValidationResolver } from '@odf/shared/yup-validation-resolver'; import { SelectOption } from '@patternfly/react-core/deprecated'; import { TFunction } from 'i18next'; @@ -40,7 +41,6 @@ import { HelperTextItem, } from '@patternfly/react-core'; import { getValidatedDeviceTypes } from '../../../../utils'; -import validationRegEx from '../../../../utils/validation'; import { LocalVolumeSet, WizardDispatch, WizardState } from '../../reducer'; import { SelectNodesTable } from '../../select-nodes-table/select-nodes-table'; import '../../../../style.scss'; diff --git a/packages/odf/components/create-storage-system/create-storage-system-steps/create-storage-class-step/create-storage-class-step.tsx b/packages/odf/components/create-storage-system/create-storage-system-steps/create-storage-class-step/create-storage-class-step.tsx index 40bf6b83d..6e2075c4c 100644 --- a/packages/odf/components/create-storage-system/create-storage-system-steps/create-storage-class-step/create-storage-class-step.tsx +++ b/packages/odf/components/create-storage-system/create-storage-system-steps/create-storage-class-step/create-storage-class-step.tsx @@ -24,11 +24,11 @@ import { getName } from '@odf/shared/selectors'; import { SecretKind, StorageClassResourceKind } from '@odf/shared/types'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; import { isValidIP } from '@odf/shared/utils'; +import validationRegEx from '@odf/shared/utils/validation'; import { useYupValidationResolver } from '@odf/shared/yup-validation-resolver'; import { useForm } from 'react-hook-form'; import * as Yup from 'yup'; import { Form, TextContent, TextVariants, Text } from '@patternfly/react-core'; -import validationRegEx from '../../../../utils/validation'; import { WizardDispatch, WizardState } from '../../reducer'; import './create-storage-class-step.scss'; diff --git a/packages/odf/components/mcg/CreateObjectBucketClaim.tsx b/packages/odf/components/mcg/CreateObjectBucketClaim.tsx index 7526331ae..9c3b89e4f 100644 --- a/packages/odf/components/mcg/CreateObjectBucketClaim.tsx +++ b/packages/odf/components/mcg/CreateObjectBucketClaim.tsx @@ -259,7 +259,6 @@ export const CreateOBCForm: React.FC = (props) => { label: t('ObjectBucketClaim Name'), fieldId: 'obc-name', className: 'control-label', - helperText: t('If not provided a generic name will be generated.'), }} textInputProps={{ id: 'obc-name', @@ -270,6 +269,7 @@ export const CreateOBCForm: React.FC = (props) => { 'aria-describedby': 'obc-name-help', 'data-test': 'obc-name', }} + helperText={t('If not provided a generic name will be generated.')} /> ; diff --git a/packages/odf/components/namespace-store/namespace-store-form.tsx b/packages/odf/components/namespace-store/namespace-store-form.tsx index 96fbeb5b3..95b542857 100644 --- a/packages/odf/components/namespace-store/namespace-store-form.tsx +++ b/packages/odf/components/namespace-store/namespace-store-form.tsx @@ -12,6 +12,7 @@ import { PersistentVolumeClaimModel, SecretModel } from '@odf/shared/models'; import { getName } from '@odf/shared/selectors'; import { PersistentVolumeClaimKind, SecretKind } from '@odf/shared/types'; import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import validationRegEx from '@odf/shared/utils/validation'; import { useYupValidationResolver } from '@odf/shared/yup-validation-resolver'; import { getAPIVersionForModel, @@ -43,7 +44,6 @@ import { getProviders, secretPayloadCreator, } from '../../utils'; -import validationRegEx from '../../utils/validation'; import { S3EndPointType } from '../mcg-endpoints/s3-endpoint-type'; import { initialState, diff --git a/packages/shared/src/input-with-requirements/TextInputWithFieldRequirements.tsx b/packages/shared/src/input-with-requirements/TextInputWithFieldRequirements.tsx index 7f9de4813..ea26f7ed2 100644 --- a/packages/shared/src/input-with-requirements/TextInputWithFieldRequirements.tsx +++ b/packages/shared/src/input-with-requirements/TextInputWithFieldRequirements.tsx @@ -28,7 +28,9 @@ export type TextInputWithFieldRequirementsProps = { fieldRequirements: string[]; control: Control; defaultValue?: any; - formGroupProps: FormGroupProps & { helperText?: string }; + formGroupProps: FormGroupProps; + // In PF5 FormGroupProps don't have helperText + helperText?: string; textInputProps: TextInputProps & { 'data-test': string; disabled?: boolean; @@ -71,6 +73,7 @@ const TextInputWithFieldRequirements: React.FC { const { field: { name, value, onChange, onBlur }, @@ -176,9 +179,7 @@ const TextInputWithFieldRequirements: React.FC - - {formGroupProps?.helperText} - + {helperText} diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index 8c37b9fce..22e667e27 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -13,3 +13,4 @@ export * from './link'; export * from './valid-k8s-resources'; export * from './table'; export * from './namespace'; +export * from './validation'; diff --git a/packages/odf/utils/validation.ts b/packages/shared/src/utils/validation.ts similarity index 100% rename from packages/odf/utils/validation.ts rename to packages/shared/src/utils/validation.ts