diff --git a/libs/locales/lib/en/translation.json b/libs/locales/lib/en/translation.json index b9ce10423b..5427d87d94 100644 --- a/libs/locales/lib/en/translation.json +++ b/libs/locales/lib/en/translation.json @@ -45,6 +45,7 @@ "ai:A comma separated list of IP or domain names of the NTP pools or servers. Additional NTP sources are added to all hosts to ensure all hosts clocks are synchronized with a valid NTP server. It may take a few minutes for the new NTP sources to sync.": "A comma-separated list of IP addresses or domain names of the NTP pools or servers. Additional NTP sources are added to all hosts to ensure that all clocks of the hosts are synchronized with a valid NTP server. It might take a few minutes for the new NTP sources to synchronize.", "ai:A Red Hat account's pull secret can be found in ": "A Red Hat account pull secret can be found in ", "ai:A role will be chosen automatically based on detected hardware and network latency.": "A role will be chosen automatically based on detected hardware and network latency.", + "ai:A value is required": "A value is required", "ai:Actions": "Actions", "ai:Active NIC": "Active NIC", "ai:Add": "Add", @@ -85,6 +86,7 @@ "ai:An error occured": "An error occured.", "ai:An error occured while approving agents": "An error occured while approving agents.", "ai:An error occured while starting installation.": "An error occured while starting installation.", + "ai:An IP address to where any IP packet should be forwarded in case there is no other routing rule configured for a destination IP.": "An IP address to where any IP packet should be forwarded in case there is no other routing rule configured for a destination IP.", "ai:And verify that this is the output:": "And verify the following output:", "ai:API connectivity failure": "API connectivity failure", "ai:API domain name resolution": "API domain name resolution", @@ -201,6 +203,7 @@ "ai:Configure host inventory settings": "Configure host inventory settings", "ai:Configure load balancer on Amazon Web Services for me.": "Configure load balancer on Amazon Web Services for me.", "ai:Configure the SSH key and proxy settings after the modal appears (optional).": "Configure the SSH key and proxy settings after the modal appears (optional).", + "ai:Configure via": "Configure via", "ai:Configure your own NTP sources to sychronize the time between the hosts that will be added to this infrastructure environment.": "Configure your own NTP sources to sychronize the time between the hosts that will be added to this infrastructure environment.", "ai:Configure your own NTP sources to synchronize the time between the hosts that will be added to this infrastructure environment.": "Configure your own NTP sources to synchronize the time between the hosts that will be added to this infrastructure environment.", "ai:Configuring the host inventory settings will enable the Central Infrastructure Management.": "Configuring the host inventory settings will enable the Central Infrastructure Management.", @@ -235,6 +238,7 @@ "ai:Created at": "Created at", "ai:Currently, adding additional machines to your cluster is not supported.": "Currently, adding additional machines to your cluster is not supported.", "ai:Database storage": "Database storage", + "ai:Default gateway": "Default gateway", "ai:Default route to host": "Default route to host", "ai:Define the quantity of worker nodes and nodepools to create for your cluster. Additional worker nodes and nodepools can be added after the cluster is created.": "Define the quantity of worker nodes and nodepools to create for your cluster. Additional worker nodes and nodepools can be added after the cluster is created.", "ai:Deleted hosts": "Deleted hosts", @@ -283,6 +287,7 @@ "ai:Draft": "Draft", "ai:Drag a file here or browse to upload": "Drag a file here or browse to upload", "ai:Drive type": "Drive type", + "ai:Dual-stack": "Dual-stack", "ai:Edit BMC": "Edit BMC", "ai:Edit BMH": "Edit BMH", "ai:Edit BMH dialog": "Edit BMH dialog", @@ -348,6 +353,7 @@ "ai:Finalizing": "Finalizing", "ai:Find by hostname": "Find by hostname", "ai:For example: host-{{n}}": "For example: host-{{n}}", + "ai:Form view": "Form view", "ai:Format?": "Format?", "ai:Forwarding it could put your credentials and personal data at risk.": "Forwarding it might put your credentials and personal data at risk.", "ai:Full image file": "Full image file", @@ -420,6 +426,7 @@ "ai:If not, please start your VMs with the following configuration:": "If not, start your VMs with the following configuration:", "ai:If the cluster hosts are in a network with a re-encrypting (MITM) proxy or the cluster needs to trust certificates for other purposes (e.g. container image registries).": "If the cluster hosts are in a network with a re-encrypting (MITM) proxy or the cluster needs to trust certificates for other purposes (e.g. container image registries).", "ai:If the configuration is taking longer than 5 minutes, you might need to troubleshoot.": "If the configuration is taking longer than 5 minutes, you might need to troubleshoot.", + "ai:If the hosts are in a sub network, enter the VLAN ID.": "If the hosts are in a sub network, enter the VLAN ID.", "ai:If there are many clusters, use higher values for the storage fields.": "If there are many clusters, use higher values for the storage fields.", "ai:If you prefer using the CLI, follow the instructions in": "If you prefer using the CLI, follow the instructions in", "ai:If you used DHCP networking, verify that your DHCP server is enabled": "If you used DHCP networking, verify that your DHCP server is enabled", @@ -462,7 +469,9 @@ "ai:Insufficient": "Insufficient", "ai:IP address block from which Pod IPs are allocated This block must not overlap with existing physical networks. These IP addresses are used for the Pod network, and if you need to access the Pods from an external network, configure load balancers and routers to manage the traffic.": "IP address block from which Pod IPs are allocated. This block must not overlap with existing physical networks. These IP addresses are used for the Pod network, and if you need to access the Pods from an external network, configure load balancers and routers to manage the traffic.", "ai:IP allocation from the DHCP server timed out.": "IP allocation from the DHCP server timed out.", + "ai:IPv4": "IPv4", "ai:IPv4 address": "IPv4 address", + "ai:IPv6": "IPv6", "ai:IPv6 address": "IPv6 address", "ai:iPXE script file is ready to be downloaded": "iPXE script file is ready to be downloaded", "ai:iPXE script file URL": "iPXE script file URL", @@ -548,7 +557,10 @@ "ai:MTU (maximum transmission unit) failure": "MTU (maximum transmission unit) failure", "ai:MTU requirements": "MTU requirements", "ai:Multicluster engine requirements": "Multicluster engine requirements", + "ai:Must be a number": "Must be a number", "ai:Must be at least 1": "Must be at least 1", + "ai:Must be less than or equal to {{value}}": "Must be less than or equal to {{value}}", + "ai:Must be more than or equal to 1": "Must be more than or equal to 1", "ai:Must be unique": "Must be unique", "ai:Must have a storage class": "Must have a storage class", "ai:Must start and end with an alphanumeric character": "Must start and end with an alphanumeric character", @@ -563,6 +575,7 @@ "ai:Network type": "Network type", "ai:Network type selection is not supported for SNO clusters or when IPv6 is detected.": "Network type selection is not supported for SNO clusters or when IPv6 is detected.", "ai:Networking": "Networking", + "ai:Networking stack type": "Networking stack type", "ai:Networks same address families": "Networks same address families", "ai:Never share your downloaded ISO with anyone else.": "Never share your downloaded ISO with anyone else.", "ai:New hostname": "New hostname", @@ -663,6 +676,9 @@ "ai:Please select one host for the cluster.": "Select one host for the cluster.", "ai:Please wait till all checks are finished.": "Wait until all of the checks are finished.", "ai:Port of the NodePort service. If set to 0, the port is dynamically assigned when the service is created.": "Port of the NodePort service. If set to 0, the port is dynamically assigned when the service is created.", + "ai:Prefix length is required": "Prefix length is required", + "ai:Prefix length must be less than or equal to {{value}}": "Prefix length must be less than or equal to {{value}}", + "ai:Prefix length must be more than or equal to 1": "Prefix length must be more than or equal to 1", "ai:Preparing for installation": "Preparing for installation", "ai:Preparing step failed": "Preparing step failed", "ai:Preparing step successful": "Preparing step successful", @@ -805,6 +821,7 @@ "ai:The hostname cannot be changed.": "The hostname cannot be changed.", "ai:The hosts you selected are using different proxy settings. Configure a proxy that will be applied for these hosts. Configure at least one of the proxy settings below.": "The hosts you selected are using different proxy settings. Configure a proxy that will be applied for these hosts. Configure at least one of the following proxy settings.", "ai:The HTTP proxy URL that agents should use to access the discovery service.": "The HTTP proxy URL that agents should use to access the discovery service.", + "ai:The IP address must not match the network or broadcast address": "The IP address must not match the network or broadcast address", "ai:The IP address pool to use for service IP addresses. You can enter only one IP address pool. If you need to access the services from an external network, configure load balancers and routers to manage the traffic.": "The IP address pool to use for service IP addresses. You can enter only one IP address pool. If you need to access the services from an external network, configure load balancers and routers to manage the traffic.", "ai:The MAC address of the host's network connected NIC that will be used to provision the host.": "The MAC address of the host's network connected NIC that will be used to provision the host.", "ai:The output displays the following:": "The output displays the following:", @@ -889,6 +906,7 @@ "ai:Use lowercase alphanumeric characters or hyphen (-)": "Use lowercase alphanumeric characters or hyphen (-)", "ai:Use lowercase alphanumeric characters, dot (.) or hyphen (-)": "Use lowercase alphanumeric characters, dot (.) or hyphen (-)", "ai:Use the same host discovery SSH key": "Use the same host discovery SSH key", + "ai:Use VLAN": "Use VLAN", "ai:Use when you have an iPXE server that has already been set up": "Use when you have an iPXE server that has already been set up", "ai:useAlerts must be used within AlertsContextProvider": "useAlerts must be used within AlertsContextProvider", "ai:Used to describe hosts' physical location. Helps for quicker host selection during cluster creation.": "Used to describe hosts' physical location. Helps for quicker host selection during cluster creation.", @@ -901,6 +919,8 @@ "ai:Valid network type": "Valid network type", "ai:Validating...": "Validating...", "ai:Validations are running. If they take more than 2 minutes, please attend to the alert below.": "Validations are running. If they take more than 2 minutes, resolve the alert that is displayed.", + "ai:Value \"{{value}}\" is not valid MAC address.": "Value \"{{value}}\" is not valid MAC address.", + "ai:Value {{value}} is not a valid {{protocolVersionLabel}} address": "Value {{value}} is not a valid {{protocolVersionLabel}} address", "ai:Vendor": "Vendor", "ai:Vendor ID": "Vendor ID", "ai:Verify that you can access your host machine using SSH, or a console such as BMC or virtual machine console. In the CLI, enter the following command:": "Verify that you can access your host machine using SSH, or a console such as BMC or virtual machine console. In the CLI, enter the following command:", @@ -911,6 +931,7 @@ "ai:View host events": "View host events", "ai:VIP IP allocation from DHCP server has been timed out": "VIP IP allocation from DHCP server has timed out", "ai:Virtual machine": "Virtual machine", + "ai:VLAN ID": "VLAN ID", "ai:Vsphere disk uuid enabled": "Vsphere disk uuid enabled", "ai:Waiting for host...": "Waiting for host...", "ai:Waiting for host..._plural": "Waiting for hosts...", @@ -929,6 +950,7 @@ "ai:Workers: At least {{worker_cpu_cores}} CPU cores, {{worker_ram}} RAM, {{worker_disksize}} GB disk size for each worker ": "Workers: At least {{worker_cpu_cores}} CPU cores, {{worker_ram}} RAM, {{worker_disksize}} GB disk size for each worker ", "ai:World Wide Name (WWN) is a unique disk identifier.": "World Wide Name (WWN) is a unique disk identifier.", "ai:x86_64": "x86_64", + "ai:Yaml view": "Yaml view", "ai:You are approving multiple hosts. All hosts listed below will be approved to join the infrastructure environment if you continue. Make sure that you expect and recognize the hosts before approving.": "You are approving multiple hosts. All hosts listed will be approved to join the infrastructure environment, if you continue. Make sure that you expect and recognize the hosts before approving.", "ai:You can either wait or investigate. A common issue can be misconfigured storage. Once you fix the issue, you can delete AgentServiceConfig to try again.": "You can either wait or investigate. A common issue can be misconfigured storage. After you fix the issue, delete AgentServiceConfig and try again.", "ai:You can upload additional trusted certificates in PEM-encoded X.509 format if the cluster hosts are in a network with a re-encrypting (MITM) proxy or the cluster needs to trust certificates for other purposes (for example, container image registries).": "You can upload additional trusted certificates in PEM-encoded X.509 format if the cluster hosts are in a network with a re-encrypting (MITM) proxy or the cluster needs to trust certificates for other purposes (for example, container image registries).", diff --git a/libs/ui-lib/lib/cim/components/Agent/BMCForm.tsx b/libs/ui-lib/lib/cim/components/Agent/BMCForm.tsx deleted file mode 100644 index 07af1bb618..0000000000 --- a/libs/ui-lib/lib/cim/components/Agent/BMCForm.tsx +++ /dev/null @@ -1,362 +0,0 @@ -import * as React from 'react'; -import * as yaml from 'js-yaml'; -import { - Alert, - AlertActionCloseButton, - AlertVariant, - Button, - ButtonVariant, - Form, - FormGroup, - Grid, - GridItem, - ModalBoxBody, - ModalBoxFooter, - Text, - TextInputTypes, - TextVariants, -} from '@patternfly/react-core'; -import { - Formik, - FormikProps, - FormikConfig, - FieldArray, - useField, - useFormikContext, - FormikErrors, -} from 'formik'; -import * as Yup from 'yup'; -import { TFunction } from 'i18next'; - -import { - InputField, - macAddressValidationSchema, - CodeField, - getFieldId, - richNameValidationSchema, - getRichTextValidation, - RichInputField, - BMCValidationMessages, - bmcAddressValidationSchema, -} from '../../../common'; -import { Language } from '@patternfly/react-code-editor'; -import { MinusCircleIcon } from '@patternfly/react-icons/dist/js/icons/minus-circle-icon'; -import { PlusCircleIcon } from '@patternfly/react-icons/dist/js/icons/plus-circle-icon'; -import { - InfraEnvK8sResource, - SecretK8sResource, - NMStateK8sResource, - BareMetalHostK8sResource, -} from '../../types'; -import { AddBmcValues, BMCFormProps } from './types'; -import { - AGENT_BMH_NAME_LABEL_KEY, - BMH_HOSTNAME_ANNOTATION, - INFRAENV_AGENTINSTALL_LABEL_KEY, -} from '../common'; -import { getErrorMessage } from '../../../common/utils'; -import { useTranslation } from '../../../common/hooks/use-translation-wrapper'; - -type MacMappingFieldProps = { macAddress: string; name: string }[]; - -const getFieldError = (errors: FormikErrors, fieldName: string): string => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access - return errors[fieldName]?.[0] || ''; -}; - -const MacMapping = () => { - const [field] = useField({ - name: 'macMapping', - }); - const { errors } = useFormikContext(); - const fieldId = getFieldId('macMapping', 'input'); - const { t } = useTranslation(); - - return ( - - ( - - - MAC address - - - NIC - - {field.value.map((mac, index) => { - const macField = `macMapping[${index}].macAddress`; - const nameField = `macMapping[${index}].name`; - return ( - - - - - - - - {index !== 0 && ( - - remove(index)} /> - - )} - - ); - })} - - - - - )} - /> - - ); -}; - -const getNMState = (values: AddBmcValues, infraEnv: InfraEnvK8sResource): NMStateK8sResource => { - const config = yaml.load(values.nmState); - const nmState = { - apiVersion: 'agent-install.openshift.io/v1beta1', - kind: 'NMStateConfig', - metadata: { - generateName: `${infraEnv.metadata?.name || ''}-`, - namespace: infraEnv.metadata?.namespace, - labels: { - [AGENT_BMH_NAME_LABEL_KEY]: values.name, - [INFRAENV_AGENTINSTALL_LABEL_KEY]: infraEnv?.metadata?.name || '', - }, - }, - spec: { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - config, - interfaces: values.macMapping.filter((m) => m.macAddress.length && m.name.length), - }, - }; - return nmState; -}; - -const getValidationSchema = (usedHostnames: string[], origHostname: string, t: TFunction) => { - return Yup.object({ - name: Yup.string().required(t('ai:Required field')), - hostname: richNameValidationSchema(t, usedHostnames, origHostname), - bmcAddress: bmcAddressValidationSchema(t), - username: Yup.string().required(t('ai:Required field')), - password: Yup.string().required(t('ai:Required field')), - bootMACAddress: macAddressValidationSchema, - nmState: Yup.string(), - macMapping: Yup.array().of( - Yup.object().shape( - { - macAddress: macAddressValidationSchema.when('name', { - is: (name: string) => !!name, - then: () => macAddressValidationSchema.required(t('ai:MAC has to be specified')), - }), - name: Yup.string().when('macAddress', { - is: (name: string) => !!name, - then: () => Yup.string().required(t('ai:Name has to be specified')), - }), - }, - [['name', 'macAddress']], - ), - ), - }); -}; - -const emptyValues: AddBmcValues = { - name: '', - hostname: '', - bmcAddress: '', - username: '', - password: '', - bootMACAddress: '', - disableCertificateVerification: true, // TODO(mlibra) - online: true, - nmState: `interfaces: - - name: - type: ethernet - state: up - ipv4: - address: - - ip: - prefix-length: 24 - enabled: true -dns-resolver: - config: - server: - - -routes: - config: - - destination: 0.0.0.0/0 - next-hop-address: - next-hop-interface: - `, - macMapping: [{ macAddress: '', name: '' }], -}; - -const getInitValues = ( - bmh?: BareMetalHostK8sResource, - nmState?: NMStateK8sResource, - secret?: SecretK8sResource, - isEdit?: boolean, - addNMState?: boolean, -): AddBmcValues => { - let values = emptyValues; - - if (isEdit) { - values = { - name: bmh?.metadata?.name || '', - hostname: bmh?.metadata?.annotations?.[BMH_HOSTNAME_ANNOTATION] || '', - bmcAddress: bmh?.spec?.bmc?.address || '', - username: secret?.data?.username ? atob(secret.data.username) : '', - password: secret?.data?.password ? atob(secret.data.password) : '', - bootMACAddress: bmh?.spec?.bootMACAddress || '', - disableCertificateVerification: !!bmh?.spec?.bmc?.disableCertificateVerification, - online: !!bmh?.spec?.online, - nmState: nmState ? yaml.dump(nmState?.spec?.config) : emptyValues.nmState, - macMapping: nmState?.spec?.interfaces || [{ macAddress: '', name: '' }], - }; - } - - if (!addNMState) { - values.nmState = ''; - } - return values; -}; - -const BMCForm: React.FC = ({ - onCreateBMH, - onClose, - hasDHCP, - infraEnv, - bmh, - nmState, - secret, - isEdit, - usedHostnames, -}) => { - const [error, setError] = React.useState(); - - const handleSubmit: FormikConfig['onSubmit'] = async (values) => { - try { - setError(undefined); - const nmState = values.nmState ? getNMState(values, infraEnv) : undefined; - await onCreateBMH(values, nmState); - onClose(); - } catch (e) { - setError(getErrorMessage(e)); - } - }; - - const { t } = useTranslation(); - const { initValues, validationSchema } = React.useMemo(() => { - const addNmState = - infraEnv.metadata?.labels && infraEnv.metadata?.labels['networkType'] === 'static'; - - const initValues = getInitValues(bmh, nmState, secret, isEdit, addNmState); - const validationSchema = getValidationSchema(usedHostnames, initValues.hostname, t); - return { initValues, validationSchema }; - }, [infraEnv.metadata?.labels, usedHostnames, bmh, nmState, secret, isEdit, t]); - - return ( - - {({ isSubmitting, isValid, submitForm }: FormikProps) => ( - <> - -
- - - - - - - {!hasDHCP && ( - <> - - - - )} - - {error && ( - setError(undefined)} />} - > - {error} - - )} -
- - {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */} - - - - - )} -
- ); -}; - -export default BMCForm; diff --git a/libs/ui-lib/lib/cim/components/Agent/BMCForm/BMCForm.tsx b/libs/ui-lib/lib/cim/components/Agent/BMCForm/BMCForm.tsx new file mode 100644 index 0000000000..5ee045d658 --- /dev/null +++ b/libs/ui-lib/lib/cim/components/Agent/BMCForm/BMCForm.tsx @@ -0,0 +1,149 @@ +import * as React from 'react'; +import { + Alert, + AlertActionCloseButton, + AlertVariant, + Button, + ButtonVariant, + Form, + ModalBoxBody, + ModalBoxFooter, + TextInputTypes, +} from '@patternfly/react-core'; +import { Formik, FormikProps, FormikConfig } from 'formik'; + +import { + InputField, + getRichTextValidation, + RichInputField, + BMCValidationMessages, +} from '../../../../common'; +import { AddBmcValues, BMCFormProps } from '../types'; +import { getErrorMessage } from '../../../../common/utils'; +import { useTranslation } from '../../../../common/hooks/use-translation-wrapper'; +import { getInitValues, getNMState, getValidationSchema } from './validationSchemas'; +import ProvisioningConfigErrorAlert from '../../modals/ProvisioningConfigErrorAlert'; +import { NMStateConfig } from './NMstateConfig'; + +const BMCForm: React.FC = ({ + onCreateBMH, + onClose, + hasDHCP, + infraEnv, + bmh, + nmState, + secret, + isEdit, + usedHostnames, + provisioningConfigError, +}) => { + const { t } = useTranslation(); + const [error, setError] = React.useState(); + + const addNmState = + infraEnv.metadata?.labels && infraEnv.metadata?.labels['networkType'] === 'static'; + + const { initValues, validationSchema } = React.useMemo(() => { + const initValues = getInitValues(bmh, nmState, secret, isEdit, addNmState); + const validationSchema = getValidationSchema(usedHostnames, initValues.hostname, t); + return { initValues, validationSchema }; + }, [bmh, nmState, secret, isEdit, addNmState, usedHostnames, t]); + + const handleSubmit: FormikConfig['onSubmit'] = async (values) => { + try { + setError(undefined); + const nmState = addNmState ? getNMState(values, infraEnv) : undefined; + + await onCreateBMH(values, nmState); + onClose(); + } catch (e) { + setError(getErrorMessage(e)); + } + }; + + return ( + + {({ isSubmitting, isValid, submitForm }: FormikProps) => ( + <> + + {provisioningConfigError && ( + + )} + +
+ + + + + + + {!hasDHCP && } + + {error && ( + setError(undefined)} />} + > + {error} + + )} +
+ + {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */} + + + + + )} +
+ ); +}; + +export default BMCForm; diff --git a/libs/ui-lib/lib/cim/components/Agent/BMCForm/MacMapping.tsx b/libs/ui-lib/lib/cim/components/Agent/BMCForm/MacMapping.tsx new file mode 100644 index 0000000000..be0d4d91eb --- /dev/null +++ b/libs/ui-lib/lib/cim/components/Agent/BMCForm/MacMapping.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { FieldArray, FormikErrors, useField, useFormikContext } from 'formik'; +import { getFieldId, InputField, useTranslation } from '../../../../common'; +import { Button, FormGroup, Grid, GridItem, Text, TextVariants } from '@patternfly/react-core'; +import PlusCircleIcon from '@patternfly/react-icons/dist/js/icons/plus-circle-icon'; +import MinusCircleIcon from '@patternfly/react-icons/dist/js/icons/minus-circle-icon'; +import { AddBmcValues } from '../types'; + +type MacMappingFieldProps = { macAddress: string; name: string }[]; + +const getFieldError = (errors: FormikErrors, fieldName: string): string => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + return errors[fieldName]?.[0] || ''; +}; + +export const MacMapping = () => { + const [field] = useField({ + name: 'macMapping', + }); + const { errors } = useFormikContext(); + const fieldId = getFieldId('macMapping', 'input'); + const { t } = useTranslation(); + + return ( + + ( + + + MAC address + + + NIC + + {field.value.map((mac, index) => { + const macField = `macMapping[${index}].macAddress`; + const nameField = `macMapping[${index}].name`; + return ( + + + + + + + + {index !== 0 && ( + + remove(index)} /> + + )} + + ); + })} + + + + + )} + /> + + ); +}; diff --git a/libs/ui-lib/lib/cim/components/Agent/BMCForm/NMstateConfig.tsx b/libs/ui-lib/lib/cim/components/Agent/BMCForm/NMstateConfig.tsx new file mode 100644 index 0000000000..eae07a8298 --- /dev/null +++ b/libs/ui-lib/lib/cim/components/Agent/BMCForm/NMstateConfig.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { useFormikContext } from 'formik'; +import { Language } from '@patternfly/react-code-editor'; +import { FormGroup } from '@patternfly/react-core'; + +import { + CheckboxField, + CodeField, + InputField, + IpConfigFields, + MAX_VLAN_ID, + MIN_VLAN_ID, + PopoverIcon, + ProtocolVersion, + RadioField, + StaticIpView, + useTranslation, +} from '../../../../common'; +import { AddBmcValues } from '../types'; +import { MacMapping } from './MacMapping'; + +export const NMStateConfig = () => { + const { t } = useTranslation(); + const { values, setFieldValue } = useFormikContext(); + + return ( + <> + + + + + + {values.staticIPView === StaticIpView.FORM ? ( + <> + + + + + + + {t('ai:Use VLAN')}{' '} + + + } + onChange={() => setFieldValue('vlanId', '')} + /> + + {values.useVlan === true && ( +
+ +
+ )} + + + + {values.protocolType === 'ipv4' ? ( + + ) : ( + <> + + + + )} + + ) : ( + + )} + + + ); +}; diff --git a/libs/ui-lib/lib/cim/components/Agent/BMCForm/validationSchemas.tsx b/libs/ui-lib/lib/cim/components/Agent/BMCForm/validationSchemas.tsx new file mode 100644 index 0000000000..2ed2951bba --- /dev/null +++ b/libs/ui-lib/lib/cim/components/Agent/BMCForm/validationSchemas.tsx @@ -0,0 +1,290 @@ +import { TFunction } from 'i18next'; +import * as Yup from 'yup'; +import * as yaml from 'js-yaml'; + +import { AddBmcValues } from '../types'; +import { + BareMetalHostK8sResource, + InfraEnvK8sResource, + NMStateK8sResource, + SecretK8sResource, +} from '../../../types'; +import { + bmcAddressValidationSchema, + Cidr, + getDNSValidationSchema, + StaticProtocolType, + ipConfigsValidationSchemas, + macAddressValidationSchema, + richNameValidationSchema, + StaticIpView, + VlanIdValidationSchema, +} from '../../../../common'; +import { + AGENT_BMH_NAME_LABEL_KEY, + BMH_HOSTNAME_ANNOTATION, + INFRAENV_AGENTINSTALL_LABEL_KEY, +} from '../../common'; + +export const emptyValues: AddBmcValues = { + name: '', + hostname: '', + bmcAddress: '', + username: '', + password: '', + bootMACAddress: '', + disableCertificateVerification: true, // TODO(mlibra) + online: true, + staticIPView: StaticIpView.FORM, + nmState: `interfaces: + - name: + type: ethernet + state: up + ipv4: + address: + - ip: + prefix-length: 24 + enabled: true +dns-resolver: + config: + server: + - +routes: + config: + - destination: 0.0.0.0/0 + next-hop-address: + next-hop-interface: + `, + + protocolType: StaticProtocolType.ipv4, + useVlan: false, + vlanId: '', + dns: '', + ipConfigs: { + ipv4: { machineNetwork: { ip: '', prefixLength: '' }, gateway: '' }, + ipv6: { machineNetwork: { ip: '', prefixLength: '' }, gateway: '' }, + }, + + macMapping: [{ macAddress: '', name: '' }], +}; + +const getIpConfigs = (nmState?: NMStateK8sResource) => { + if (!nmState || !nmState.spec?.interfaces || nmState.spec?.interfaces.length < 1) { + return emptyValues.ipConfigs; + } else { + return { + ipv4: { + machineNetwork: { + ip: nmState?.spec?.config.interfaces?.[0].ipv4?.address?.[0].ip || '', + prefixLength: nmState?.spec?.config.interfaces?.[0].ipv4?.address?.[0]['prefix-length'], + } as Cidr, + gateway: nmState?.spec?.config.routes?.config?.[0]['next-hop-address'] || '', + }, + ipv6: { + machineNetwork: { + ip: nmState.spec.config.interfaces?.[0].ipv6?.address?.[0].ip || '', + prefixLength: + nmState.spec.config.interfaces?.[0].ipv6?.address?.[0]['prefix-length'] || '', + } as Cidr, + gateway: nmState?.spec?.config.routes?.config?.[1]?.['next-hop-address'] || '', + }, + }; + } +}; + +export const getInitValues = ( + bmh?: BareMetalHostK8sResource, + nmState?: NMStateK8sResource, + secret?: SecretK8sResource, + isEdit?: boolean, + addNMState?: boolean, +): AddBmcValues => { + let values = emptyValues; + const staticIpView = + nmState?.metadata?.labels?.['configured-via'] === StaticIpView.YAML + ? StaticIpView.YAML + : StaticIpView.FORM; + + if (isEdit) { + values = { + ...values, + name: bmh?.metadata?.name || '', + hostname: bmh?.metadata?.annotations?.[BMH_HOSTNAME_ANNOTATION] || '', + bmcAddress: bmh?.spec?.bmc?.address || '', + username: secret?.data?.username ? atob(secret.data.username) : '', + password: secret?.data?.password ? atob(secret.data.password) : '', + bootMACAddress: bmh?.spec?.bootMACAddress || '', + disableCertificateVerification: !!bmh?.spec?.bmc?.disableCertificateVerification, + online: !!bmh?.spec?.online, + + staticIPView: staticIpView, + macMapping: nmState?.spec?.interfaces || [{ macAddress: '', name: '' }], + }; + + if (addNMState && staticIpView === StaticIpView.FORM) { + const staticIpFormValues = { + protocolType: nmState?.spec?.config.interfaces?.[0].ipv6 + ? StaticProtocolType.dualStack + : StaticProtocolType.ipv4, + useVlan: !!nmState?.spec?.config.interfaces?.[0].vlan, + vlanId: nmState?.spec?.config.interfaces?.[0].vlan?.id + ? Number(nmState?.spec?.config.interfaces?.[0].vlan?.id) + : emptyValues.vlanId, + dns: nmState?.spec?.config['dns-resolver']?.config.server?.[0] || emptyValues.dns, + ipConfigs: getIpConfigs(nmState), + }; + + values = { ...values, ...staticIpFormValues }; + } else if (addNMState && staticIpView === StaticIpView.YAML) { + const staticIpYamlValues = { + nmState: nmState ? yaml.dump(nmState?.spec?.config) : emptyValues.nmState, + }; + + values = { ...values, ...staticIpYamlValues }; + } + } + + if (!addNMState) { + values.nmState = ''; + } + + return values; +}; + +export const getValidationSchema = ( + usedHostnames: string[], + origHostname: string, + t: TFunction, +) => { + return Yup.lazy((values: AddBmcValues) => + Yup.object({ + name: Yup.string().required(t('ai:Required field')), + hostname: richNameValidationSchema(t, usedHostnames, origHostname), + bmcAddress: bmcAddressValidationSchema(t), + username: Yup.string().required(t('ai:Required field')), + password: Yup.string().required(t('ai:Required field')), + bootMACAddress: macAddressValidationSchema(t), + nmState: Yup.mixed().when('staticIPView', { + is: (staticIpView: StaticIpView) => staticIpView === StaticIpView.YAML, + then: () => Yup.string().required(t('ai:Required field')), + }), + + useVlan: Yup.boolean(), + vlanId: Yup.mixed().when('useVlan', { + is: (useVlan: boolean) => useVlan, + then: () => VlanIdValidationSchema(values.vlanId, t), + }), + dns: Yup.mixed().when('staticIPView', { + is: (staticIpView: StaticIpView) => staticIpView === StaticIpView.FORM, + then: () => getDNSValidationSchema(values.protocolType, t), + }), + ipConfigs: Yup.mixed().when('staticIPView', { + is: (staticIpView: StaticIpView) => staticIpView === StaticIpView.FORM, + then: () => ipConfigsValidationSchemas(values.ipConfigs, values.protocolType, t), + }), + + macMapping: Yup.array().of( + Yup.object().shape( + { + macAddress: macAddressValidationSchema(t).when('name', { + is: (name: string) => !!name, + then: () => macAddressValidationSchema(t).required(t('ai:MAC has to be specified')), + }), + name: Yup.string().when('macAddress', { + is: (name: string) => !!name, + then: () => Yup.string().required(t('ai:Name has to be specified')), + }), + }, + [['name', 'macAddress']], + ), + ), + }), + ); +}; + +export const getNMState = ( + values: AddBmcValues, + infraEnv: InfraEnvK8sResource, +): NMStateK8sResource => { + let config; + + if (values.staticIPView === StaticIpView.YAML) { + config = yaml.load(values.nmState); + } else { + config = { + interfaces: [ + { + name: values.macMapping[0].name, + type: values.useVlan ? 'vlan' : 'ethernet', + state: 'up', + vlan: values.useVlan + ? { + 'base-iface': 'eth0', + id: values.vlanId, + } + : undefined, + ipv4: { + address: [ + { + ip: values.ipConfigs.ipv4.machineNetwork.ip, + 'prefix-length': values.ipConfigs.ipv4.machineNetwork.prefixLength, + }, + ], + enabled: true, + dhcp: false, + }, + ipv6: + values.protocolType === 'dualStack' + ? { + address: [ + { + ip: values.ipConfigs.ipv6.machineNetwork.ip, + 'prefix-length': values.ipConfigs.ipv6.machineNetwork.prefixLength, + }, + ], + enabled: true, + dhcp: false, + } + : undefined, + }, + ], + 'dns-resolver': { config: { server: [values.dns] } }, + routes: { + config: [ + { + destination: '0.0.0.0/0', + 'next-hop-address': values.ipConfigs.ipv4.gateway, + 'next-hop-interface': + values.useVlan && values.vlanId ? `eth0.${values.vlanId}` : 'eth0', + }, + values.protocolType === 'dualStack' && { + destination: '::/0', + 'next-hop-address': values.ipConfigs.ipv6.gateway, + 'next-hop-interface': + values.useVlan && values.vlanId ? `eth0.${values.vlanId}` : 'eth0', + }, + ].filter(Boolean), + }, + }; + } + + const nmState = { + apiVersion: 'agent-install.openshift.io/v1beta1', + kind: 'NMStateConfig', + metadata: { + generateName: `${infraEnv.metadata?.name || ''}-`, + namespace: infraEnv.metadata?.namespace, + labels: { + [AGENT_BMH_NAME_LABEL_KEY]: values.name, + [INFRAENV_AGENTINSTALL_LABEL_KEY]: infraEnv?.metadata?.name || '', + 'configured-via': values.staticIPView, + }, + }, + spec: { + config, + interfaces: values.macMapping.filter((m) => m.macAddress.length && m.name.length), + }, + }; + + return nmState as NMStateK8sResource; +}; diff --git a/libs/ui-lib/lib/cim/components/Agent/index.ts b/libs/ui-lib/lib/cim/components/Agent/index.ts index e41274b24a..16cc017488 100644 --- a/libs/ui-lib/lib/cim/components/Agent/index.ts +++ b/libs/ui-lib/lib/cim/components/Agent/index.ts @@ -1,3 +1,3 @@ export { default as AgentTable } from './AgentTable'; -export { default as BMCForm } from './BMCForm'; +export { default as BMCForm } from './BMCForm/BMCForm'; export * from './types'; diff --git a/libs/ui-lib/lib/cim/components/Agent/types.ts b/libs/ui-lib/lib/cim/components/Agent/types.ts index d46bbbcd93..87726e7ad7 100644 --- a/libs/ui-lib/lib/cim/components/Agent/types.ts +++ b/libs/ui-lib/lib/cim/components/Agent/types.ts @@ -1,3 +1,4 @@ +import { IpConfigs, StaticIpView, StaticProtocolType } from '../../../common'; import { InfraEnvK8sResource, SecretK8sResource } from '../../types'; import { BareMetalHostK8sResource } from '../../types/k8s/bare-metal-host'; import { NMStateK8sResource } from '../../types/k8s/nm-state'; @@ -11,7 +12,14 @@ export type AddBmcValues = { bootMACAddress: string; disableCertificateVerification: boolean; online: boolean; + staticIPView: StaticIpView; nmState: string; + + protocolType: StaticProtocolType; + useVlan: boolean; + vlanId: number | ''; + dns: string; + ipConfigs: IpConfigs; macMapping: { macAddress: string; name: string }[]; }; @@ -26,6 +34,7 @@ export type BMCFormProps = { nmState?: NMStateK8sResource; secret?: SecretK8sResource; isEdit?: boolean; + provisioningConfigError?: unknown; }; export type AddYamlValues = { diff --git a/libs/ui-lib/lib/cim/components/modals/AddBmcHostModal.tsx b/libs/ui-lib/lib/cim/components/modals/AddBmcHostModal.tsx index b99413e704..0af0679d6c 100644 --- a/libs/ui-lib/lib/cim/components/modals/AddBmcHostModal.tsx +++ b/libs/ui-lib/lib/cim/components/modals/AddBmcHostModal.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; import { Modal, ModalVariant } from '@patternfly/react-core'; -import BMCForm from '../Agent/BMCForm'; +import BMCForm from '../Agent/BMCForm/BMCForm'; import { AddBmcHostModalProps } from './types'; import { useTranslation } from '../../../common/hooks/use-translation-wrapper'; import { EnvironmentErrors } from '../InfraEnv/EnvironmentErrors'; -import ProvisioningConfigErrorAlert from './ProvisioningConfigErrorAlert'; const AddBmcHostModal: React.FC = ({ isOpen, @@ -28,13 +27,13 @@ const AddBmcHostModal: React.FC = ({ id="add-host-modal" > - diff --git a/libs/ui-lib/lib/cim/components/modals/EditBMHModal.tsx b/libs/ui-lib/lib/cim/components/modals/EditBMHModal.tsx index 0a06cdb3e4..524268d498 100644 --- a/libs/ui-lib/lib/cim/components/modals/EditBMHModal.tsx +++ b/libs/ui-lib/lib/cim/components/modals/EditBMHModal.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Modal, ModalVariant } from '@patternfly/react-core'; -import BMCForm from '../Agent/BMCForm'; +import BMCForm from '../Agent/BMCForm/BMCForm'; import { SecretK8sResource } from '../../types'; import { LoadingState } from '../../../common'; import { EditBMHModalProps } from './types'; @@ -55,7 +55,7 @@ const EditBMHModal: React.FC = ({ title={t('ai:Edit BMH')} isOpen={isOpen} onClose={onClose} - variant={ModalVariant.small} + variant={ModalVariant.medium} hasNoBodyWrapper id="edit-bmh-modal" > diff --git a/libs/ui-lib/lib/cim/components/modals/ProvisioningConfigErrorAlert.tsx b/libs/ui-lib/lib/cim/components/modals/ProvisioningConfigErrorAlert.tsx index ee1a3c0f6b..76cfc5b0bd 100644 --- a/libs/ui-lib/lib/cim/components/modals/ProvisioningConfigErrorAlert.tsx +++ b/libs/ui-lib/lib/cim/components/modals/ProvisioningConfigErrorAlert.tsx @@ -9,7 +9,12 @@ const ProvisioningConfigErrorAlert = ({ error }: { error: unknown }) => { return null; } return ( - + {getErrorMessage(error)} ); diff --git a/libs/ui-lib/lib/cim/types/k8s/nm-state.ts b/libs/ui-lib/lib/cim/types/k8s/nm-state.ts index 8f8764c409..74e4a41bf6 100644 --- a/libs/ui-lib/lib/cim/types/k8s/nm-state.ts +++ b/libs/ui-lib/lib/cim/types/k8s/nm-state.ts @@ -3,7 +3,35 @@ import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; export type NMStateK8sResource = K8sResourceCommon & { spec?: { // eslint-disable-next-line @typescript-eslint/no-explicit-any - config: any; + config: { + interfaces?: { + name?: string; + type?: string; + state?: string; + vlan?: { + 'base-iface'?: string; + id?: number; + }; + ipv4?: { + address?: { ip?: string; 'prefix-length'?: number }[]; + }; + ipv6?: { + address?: { ip?: string; 'prefix-length'?: number }[]; + }; + }[]; + 'dns-resolver'?: { + config: { + server?: string[]; + }; + }; + routes?: { + config?: { + destination?: string; + 'next-hop-address'?: string; + 'next-hop-interface'?: string; + }[]; + }; + }; interfaces: { macAddress: string; name: string; diff --git a/libs/ui-lib/lib/common/components/index.ts b/libs/ui-lib/lib/common/components/index.ts index c6694bb7e0..dd2491ff56 100644 --- a/libs/ui-lib/lib/common/components/index.ts +++ b/libs/ui-lib/lib/common/components/index.ts @@ -9,3 +9,4 @@ export * from './hosts'; export * from './fetching'; export * from './AddHosts'; export * from './featureSupportLevels'; +export * from './staticIP'; diff --git a/libs/ui-lib/lib/common/components/staticIP/IPConfigFields.tsx b/libs/ui-lib/lib/common/components/staticIP/IPConfigFields.tsx new file mode 100644 index 0000000000..dc1d48d285 --- /dev/null +++ b/libs/ui-lib/lib/common/components/staticIP/IPConfigFields.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { FormGroup, Grid } from '@patternfly/react-core'; +import { TFunction } from 'react-i18next'; + +import { getFieldId, InputField, PopoverIcon } from '../ui'; +import { MachineNetworkField } from './MachineNetworkField'; +import { ProtocolVersion } from './types'; +import { useTranslation } from '../../hooks'; + +type IpConfigFieldsProps = { + fieldName: string; + protocolVersion: ProtocolVersion; + isDisabled?: boolean; +}; + +export const getProtocolVersionLabel = (protocolVersion: ProtocolVersion, t: TFunction) => + protocolVersion === ProtocolVersion.ipv4 ? t('ai:IPv4') : t('ai:IPv6'); + +export const IpConfigFields = ({ + protocolVersion, + fieldName, + isDisabled = false, +}: IpConfigFieldsProps) => { + const { t } = useTranslation(); + + return ( + + + + + } + name={`${fieldName}.gateway`} + data-testid={`${protocolVersion}-gateway`} + /> + + + ); +}; diff --git a/libs/ui-lib/lib/common/components/staticIP/MachineNetworkField.tsx b/libs/ui-lib/lib/common/components/staticIP/MachineNetworkField.tsx new file mode 100644 index 0000000000..12ab4b00a9 --- /dev/null +++ b/libs/ui-lib/lib/common/components/staticIP/MachineNetworkField.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { useField } from 'formik'; +import { + FormGroup, + Flex, + FlexItem, + TextInputTypes, + FormHelperText, + HelperText, + HelperTextItem, +} from '@patternfly/react-core'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/js/icons/exclamation-circle-icon'; + +import { getMachineNetworkCidr } from './machineNetwork'; +import { MAX_PREFIX_LENGTH, MIN_PREFIX_LENGTH } from './validationSchemas'; +import { getHumanizedSubnetRange } from '../clusterConfiguration'; +import { getFieldId, InputField, PopoverIcon } from '../ui'; +import { ProtocolVersion, Cidr } from './types'; + +import useFieldErrorMsg from '../../hooks/useFieldErrorMsg'; +import { getAddressObject } from './protocolVersion'; + +export const MachineNetworkField = ({ + fieldName, + protocolVersion, + isDisabled = false, +}: { + fieldName: string; + protocolVersion: ProtocolVersion; + isDisabled?: boolean; +}) => { + const [{ value }] = useField(fieldName); + const ipFieldName = `${fieldName}.ip`; + const prefixLengthFieldName = `${fieldName}.prefixLength`; + const ipErrorMessage = useFieldErrorMsg({ name: ipFieldName }); + const prefixLengthErrorMessage = useFieldErrorMsg({ name: prefixLengthFieldName }); + const errorMessage = ipErrorMessage || prefixLengthErrorMessage; + const machineNetworkHelptext = React.useMemo(() => { + if (errorMessage) { + return ''; + } + const cidr = getMachineNetworkCidr(value); + return getHumanizedSubnetRange(getAddressObject(cidr, protocolVersion)); + }, [value, protocolVersion, errorMessage]); + const fieldId = getFieldId(`${fieldName}`, 'input'); + return ( + + } + label="Machine network" + fieldId={fieldId} + isRequired + className="machine-network" + > + + + + + {'/'} + + + + + {(errorMessage || machineNetworkHelptext) && ( + + + : null} + variant={errorMessage ? 'error' : 'default'} + id={errorMessage ? `${fieldId}-helper-error` : `${fieldId}-helper`} + > + {errorMessage ? errorMessage : machineNetworkHelptext} + + + + )} + + ); +}; diff --git a/libs/ui-lib/lib/common/components/staticIP/index.tsx b/libs/ui-lib/lib/common/components/staticIP/index.tsx new file mode 100644 index 0000000000..cb8ebfe932 --- /dev/null +++ b/libs/ui-lib/lib/common/components/staticIP/index.tsx @@ -0,0 +1,6 @@ +export * from './MachineNetworkField'; +export * from './validationSchemas'; +export * from './types'; +export * from './IPConfigFields'; +export * from './machineNetwork'; +export * from './protocolVersion'; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/machineNetwork.ts b/libs/ui-lib/lib/common/components/staticIP/machineNetwork.ts similarity index 78% rename from libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/machineNetwork.ts rename to libs/ui-lib/lib/common/components/staticIP/machineNetwork.ts index faa35f3ce2..2a0998d6eb 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/machineNetwork.ts +++ b/libs/ui-lib/lib/common/components/staticIP/machineNetwork.ts @@ -1,4 +1,4 @@ -import { Cidr } from './dataTypes'; +import { Cidr } from './types'; export const getMachineNetworkCidr = (machineNetwork: Cidr) => { return `${machineNetwork.ip}/${machineNetwork.prefixLength}`; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/protocolVersion.ts b/libs/ui-lib/lib/common/components/staticIP/protocolVersion.ts similarity index 84% rename from libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/protocolVersion.ts rename to libs/ui-lib/lib/common/components/staticIP/protocolVersion.ts index caed24d7ed..c3c78cde02 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/protocolVersion.ts +++ b/libs/ui-lib/lib/common/components/staticIP/protocolVersion.ts @@ -1,5 +1,5 @@ import { Address4, Address6 } from 'ip-address'; -import { ProtocolVersion, StaticProtocolType } from './dataTypes'; +import { ProtocolVersion, StaticProtocolType } from './types'; // eslint-disable-next-line @typescript-eslint/no-unused-vars export const showIpv4 = (protocolType: StaticProtocolType) => { @@ -27,9 +27,6 @@ export const getShownProtocolVersions = (protocolType: StaticProtocolType): Prot : [ProtocolVersion.ipv4]; }; -export const getProtocolVersionLabel = (protocolVersion: ProtocolVersion) => - protocolVersion === ProtocolVersion.ipv4 ? 'IPv4' : 'IPv6'; - export const getAddressObject = ( ip: string, protocolVersion: ProtocolVersion, diff --git a/libs/ui-lib/lib/common/components/staticIP/types.tsx b/libs/ui-lib/lib/common/components/staticIP/types.tsx new file mode 100644 index 0000000000..b7d988589b --- /dev/null +++ b/libs/ui-lib/lib/common/components/staticIP/types.tsx @@ -0,0 +1,26 @@ +export enum StaticIpView { + YAML = 'yaml', + FORM = 'form', +} + +export enum ProtocolVersion { + ipv4 = 'ipv4', + ipv6 = 'ipv6', +} + +export enum StaticProtocolType { + ipv4 = 'ipv4', + dualStack = 'dualStack', +} + +export type Cidr = { + ip: string; + prefixLength: number | ''; +}; + +export type IpConfig = { + machineNetwork: Cidr; + gateway: string; +}; + +export type IpConfigs = { [protocolVersion in ProtocolVersion]: IpConfig }; diff --git a/libs/ui-lib/lib/common/components/staticIP/validationSchemas.tsx b/libs/ui-lib/lib/common/components/staticIP/validationSchemas.tsx new file mode 100644 index 0000000000..36068cf2a9 --- /dev/null +++ b/libs/ui-lib/lib/common/components/staticIP/validationSchemas.tsx @@ -0,0 +1,344 @@ +import * as Yup from 'yup'; +import { Cidr, IpConfig, IpConfigs, ProtocolVersion, StaticProtocolType } from './types'; +import { getDuplicates } from '../ui'; +import { Address4, Address6 } from 'ip-address'; +import { getAddressObject, showProtocolVersion } from './protocolVersion'; +import { isInSubnet } from 'is-in-subnet'; +import { getMachineNetworkCidr } from './machineNetwork'; +import { TFunction } from 'i18next'; + +const getValidationMesages = (t: TFunction) => ({ + REQUIRED_MESSAGE: t('ai:A value is required'), + MUST_BE_A_NUMBER: t('ai:Must be a number'), +}); + +export const MIN_PREFIX_LENGTH = 1; +export const MAX_PREFIX_LENGTH = { + ipv4: 32, + ipv6: 128, +}; + +export const MIN_VLAN_ID = 1; +export const MAX_VLAN_ID = 4094; + +const ONLY_DIGITS_REGEX = /^\d+$/; +const RESERVED_IPS = ['127.0.0.0', '127.0.0.1', '0.0.0.0', '255.255.255.255']; + +const validateNumber = (vlanId: number | '') => { + //We need to validate that value is a number(without letters) and is not an exponential number (ex: 1e2) + return new RegExp(ONLY_DIGITS_REGEX).test((vlanId || '').toString()); +}; + +const transformNumber = (originalValue: number) => { + return isNaN(originalValue) ? null : originalValue; +}; + +export const VlanIdValidationSchema = (vlanId: number | '', t: TFunction) => { + const messages = getValidationMesages(t); + return Yup.number() + .required(messages.REQUIRED_MESSAGE) + .min(1, t('ai:Must be more than or equal to 1')) + .max(MAX_VLAN_ID, t('ai:Must be less than or equal to {{value}}', { value: MAX_VLAN_ID })) + .test('not-number', messages.MUST_BE_A_NUMBER, () => validateNumber(vlanId)) + .nullable() + .transform(transformNumber) as Yup.NumberSchema; +}; + +export const isReservedIpv6Address = (ipv6Address: Address6) => { + return ipv6Address.isLoopback() || ipv6Address.isMulticast(); +}; + +export const isReservedAddress = (addressStr: string, protocolVersion?: ProtocolVersion) => { + try { + if ( + (protocolVersion === undefined && isValidIPv4Address(addressStr)) || + protocolVersion === ProtocolVersion.ipv4 + ) { + return RESERVED_IPS.includes(addressStr); + } else { + return isReservedIpv6Address(new Address6(addressStr)); + } + } catch (e) { + return false; + } +}; + +export const areNotReservedAdresses = ( + value: unknown, + protocolVersion?: ProtocolVersion, +): boolean => { + if (!value) { + return true; + } + // The field may admit multiple values as a comma-separated string + const addresses = (value as string).split(','); + return addresses.every((address) => !isReservedAddress(address, protocolVersion)); +}; + +export const isValidIPv4Address = (addressStr: string) => { + try { + // ip-address package treats cidr addresses as valid so need to verify it isn't a cidr + // Can't use Address4.isValid() + const address = new Address4(addressStr); + return !address.parsedSubnet; + } catch (e) { + return false; + } +}; + +export const isValidIPv6Address = (addressStr: string) => { + try { + // ip-address package treats cidr addresses as valid so need to verify it isn't a cidr + // Can't use Address6.isValid() + const address = new Address6(addressStr); + return !address.parsedSubnet; + } catch (e) { + return false; + } +}; + +export const isValidAddress = (addressStr: string, protocolVersion?: ProtocolVersion) => { + if (protocolVersion === undefined) { + return isValidIPv4Address(addressStr) || isValidIPv6Address(addressStr); + } + return protocolVersion === ProtocolVersion.ipv4 + ? isValidIPv4Address(addressStr) + : isValidIPv6Address(addressStr); +}; + +export const getMultipleIpAddressValidationSchema = (protocolVersion?: ProtocolVersion) => { + const validationId = protocolVersion === undefined ? 'is-ipv4-or-ipv6-csv' : protocolVersion; + const protocolVersionLabel = + protocolVersion === undefined + ? 'IPv4 or IPv6' + : protocolVersion === ProtocolVersion.ipv4 + ? 'IPv4' + : 'IPv6'; + return Yup.string().test( + validationId, + ({ value }) => { + const addresses = (value as string).split(','); + const invalidAddresses = addresses.filter( + (address) => !isValidAddress(address, protocolVersion), + ); + const displayValue = invalidAddresses.join(', '); + if (invalidAddresses.length === 1) { + return `Value ${displayValue} is not a valid ${protocolVersionLabel} address`; + } else if (invalidAddresses.length > 1) { + return `The values ${displayValue} are not valid ${protocolVersionLabel} addresses`; + } + // If all addresses are valid, then there must be duplicated addresses + const duplicates = getDuplicates(addresses); + return `The following IP addresses are duplicated: ${duplicates.join(',')}`; + }, + (value?: string) => { + if (!value) { + return true; + } + const addresses: string[] = value.split(','); + const duplicates = getDuplicates(addresses); + if (duplicates.length !== 0) { + return false; + } + + return addresses.every((address) => isValidAddress(address, protocolVersion)); + }, + ); +}; + +export const isNotReservedHostDNSAddress = (protocolVersion?: ProtocolVersion) => { + return Yup.string().test( + 'is-not-reserved-dns-address', + ({ value }) => { + const addresses = (value as string).split(','); + if (addresses.length === 1) { + return `The provided IP address is not a valid DNS address.`; + } + + const reservedAddresses = addresses.filter((address) => { + return isReservedAddress(address, protocolVersion); + }); + return `The provided IP addresses ${reservedAddresses.join( + ', ', + )} are not valid DNS addresses.`; + }, + (value) => areNotReservedAdresses(value, protocolVersion), + ); +}; + +export const getDNSValidationSchema = (protocolType: StaticProtocolType, t: TFunction) => { + const messages = getValidationMesages(t); + if (protocolType === 'dualStack') { + return getMultipleIpAddressValidationSchema() + .required(messages.REQUIRED_MESSAGE) + .concat(isNotReservedHostDNSAddress()); + } + return getMultipleIpAddressValidationSchema(ProtocolVersion.ipv4) + .required(messages.REQUIRED_MESSAGE) + .concat(isNotReservedHostDNSAddress(ProtocolVersion.ipv4)); +}; + +export const getIpAddressValidationSchema = (protocolVersion: ProtocolVersion, t: TFunction) => { + const protocolVersionLabel = protocolVersion === ProtocolVersion.ipv4 ? 'IPv4' : 'IPv6'; + return Yup.string().test( + protocolVersion, + (value: string) => + t('ai:Value {{value}} is not a valid {{protocolVersionLabel}} address', { + value, + protocolVersionLabel, + }), + (value?: string) => { + if (!value) { + return true; + } + return isValidAddress(value, protocolVersion); + }, + ); +}; + +export const getIpAddressInSubnetValidationSchema = ( + protocolVersion: ProtocolVersion, + subnet: string, + t: TFunction, +) => { + return Yup.string().test( + 'is-in-subnet', + `IP Address is outside of the machine network ${subnet}`, + (value, testContext: Yup.TestContext) => { + if (!value) { + return true; + } + const ipValidationSchema = getIpAddressValidationSchema(protocolVersion, t); + try { + ipValidationSchema.validateSync(value); + } catch (err) { + const error = err as { message: string }; + return testContext.createError({ message: error.message }); + } + try { + const inSubnet = isInSubnet(value, subnet); + return inSubnet; + } catch (err) { + //if isInSubnet fails it means the machine network cidr isn't valid and this validation is irrelevant + return true; + } + }, + ); +}; + +export const getInMachineNetworkValidationSchema = ( + protocolVersion: ProtocolVersion, + machineNetwork: Cidr, + t: TFunction, +) => { + return getIpAddressInSubnetValidationSchema( + protocolVersion, + getMachineNetworkCidr(machineNetwork), + t, + ); +}; + +export const getIpIsNotNetworkOrBroadcastAddressSchema = ( + protocolVersion: ProtocolVersion, + subnet: string, + t: TFunction, +) => { + return Yup.string().test( + 'is-not-network-or-broadcast', + t('ai:The IP address must not match the network or broadcast address'), + (value) => { + // Allow both addresses for IPv4 /31 subnets (RFC 3021) + if (protocolVersion === ProtocolVersion.ipv4 && subnet.endsWith('/31')) { + return true; + } + const subnetAddr = getAddressObject(subnet, protocolVersion); + if (!subnetAddr) { + return true; + } else { + const subnetStart = subnetAddr?.startAddress().correctForm(); + const subnetEnd = subnetAddr?.endAddress().correctForm(); + return !(value === subnetStart || value === subnetEnd); + } + }, + ); +}; + +export const getIsNotNetworkOrBroadcastAddressSchema = ( + protocolVersion: ProtocolVersion, + machineNetwork: Cidr, + t: TFunction, +) => { + return getIpIsNotNetworkOrBroadcastAddressSchema( + protocolVersion, + getMachineNetworkCidr(machineNetwork), + t, + ); +}; + +const getMachineNetworkValidationSchema = (protocolVersion: ProtocolVersion, t: TFunction) => + Yup.object().shape({ + ip: getIPValidationSchema(protocolVersion, t), + prefixLength: Yup.number() + .required(t('ai:Prefix length is required')) + .min(1, t('ai:Prefix length must be more than or equal to 1')) + .max( + MAX_PREFIX_LENGTH[protocolVersion], + t('ai:Prefix length must be less than or equal to {{value}}', { + value: MAX_PREFIX_LENGTH[protocolVersion], + }), + ) + .nullable() + .transform(transformNumber) as Yup.NumberSchema, //add casting to not get typescript error caused by nullable + }); + +export const isNotReservedHostIPAddress = (protocolVersion?: ProtocolVersion) => { + return Yup.string().test( + 'is-not-reserved-ip-address', + ({ value }) => { + const addresses = (value as string).split(','); + if (addresses.length === 1) { + return `The provided IP address is not a correct address for an interface.`; + } + + const reservedAddresses = addresses.filter((address) => { + return isReservedAddress(address, protocolVersion); + }); + return `The provided IP addresses ${reservedAddresses.join( + ', ', + )} are not correct addresses for an interface.`; + }, + (value) => areNotReservedAdresses(value, protocolVersion), + ); +}; + +const getIPValidationSchema = (protocolVersion: ProtocolVersion, t: TFunction) => { + const messages = getValidationMesages(t); + return getIpAddressValidationSchema(protocolVersion, t) + .required(messages.REQUIRED_MESSAGE) + .concat(isNotReservedHostIPAddress(protocolVersion)); +}; + +const getAddressDataValidationSchema = ( + protocolVersion: ProtocolVersion, + ipConfig: IpConfig, + t: TFunction, +) => { + return Yup.object({ + machineNetwork: getMachineNetworkValidationSchema(protocolVersion, t), + gateway: getIPValidationSchema(protocolVersion, t) + .concat(getInMachineNetworkValidationSchema(protocolVersion, ipConfig.machineNetwork, t)) + .concat(getIsNotNetworkOrBroadcastAddressSchema(protocolVersion, ipConfig.machineNetwork, t)), + }); +}; + +export const ipConfigsValidationSchemas = ( + ipConfigs: IpConfigs, + protocolType: StaticProtocolType, + t: TFunction, +) => + Yup.object({ + ipv4: getAddressDataValidationSchema(ProtocolVersion.ipv4, ipConfigs.ipv4, t), + ipv6: showProtocolVersion(protocolType, ProtocolVersion.ipv6) + ? getAddressDataValidationSchema(ProtocolVersion.ipv6, ipConfigs.ipv6, t) + : Yup.object(), + }); diff --git a/libs/ui-lib/lib/common/components/ui/formik/_validationSchemas.test.ts b/libs/ui-lib/lib/common/components/ui/formik/_validationSchemas.test.ts index dbfc51da45..dfd24d16f4 100644 --- a/libs/ui-lib/lib/common/components/ui/formik/_validationSchemas.test.ts +++ b/libs/ui-lib/lib/common/components/ui/formik/_validationSchemas.test.ts @@ -181,7 +181,7 @@ describe('validationSchemas', () => { await Promise.all( valid.map((value) => - macAddressValidationSchema + macAddressValidationSchema(t) .validate(value) .catch((msg: string) => expect(value).toBe(`was rejected but is valid: ${msg}`)), ), @@ -190,10 +190,12 @@ describe('validationSchemas', () => { let counter = 0; await Promise.all( invalid.map((value) => - macAddressValidationSchema.validate(value).then( - () => expect(value).toBe('should be rejected since it is invalid'), - () => counter++, - ), + macAddressValidationSchema(t) + .validate(value) + .then( + () => expect(value).toBe('should be rejected since it is invalid'), + () => counter++, + ), ), ); diff --git a/libs/ui-lib/lib/common/components/ui/formik/validationSchemas.ts b/libs/ui-lib/lib/common/components/ui/formik/validationSchemas.ts index 54614c5b8a..8a47ea55ff 100644 --- a/libs/ui-lib/lib/common/components/ui/formik/validationSchemas.ts +++ b/libs/ui-lib/lib/common/components/ui/formik/validationSchemas.ts @@ -180,10 +180,11 @@ export const ipNoSuffixValidationSchema = Yup.string().test( }, ); -export const macAddressValidationSchema = Yup.string().matches(MAC_REGEX, { - message: 'Value "${value}" is not valid MAC address.', // eslint-disable-line no-template-curly-in-string - excludeEmptyString: true, -}); +export const macAddressValidationSchema = (t: TFunction) => + Yup.string().matches(MAC_REGEX, { + message: (value) => t('ai:Value "{{value}}" is not valid MAC address.', { value }), + excludeEmptyString: true, + }); export const vipRangeValidationSchema = ( hostSubnets: HostSubnets, diff --git a/libs/ui-lib/lib/ocm/components/AddHosts/day2Wizard/Day2StaticIP.tsx b/libs/ui-lib/lib/ocm/components/AddHosts/day2Wizard/Day2StaticIP.tsx index 81d1da38fd..0404c62e45 100644 --- a/libs/ui-lib/lib/ocm/components/AddHosts/day2Wizard/Day2StaticIP.tsx +++ b/libs/ui-lib/lib/ocm/components/AddHosts/day2Wizard/Day2StaticIP.tsx @@ -9,7 +9,7 @@ import { Button, ButtonVariant, } from '@patternfly/react-core'; -import { ClusterWizardStep, ErrorState, LoadingState } from '../../../../common'; +import { ClusterWizardStep, ErrorState, LoadingState, StaticIpView } from '../../../../common'; import { HostsNetworkConfigurationType, InfraEnvsService } from '../../../services'; import { FormViewHosts } from '../../clusterConfiguration/staticIp/components/FormViewHosts/FormViewHosts'; import { FormViewNetworkWide } from '../../clusterConfiguration/staticIp/components/FormViewNetworkWide/FormViewNetworkWide'; @@ -19,7 +19,7 @@ import { } from '../../clusterConfiguration/staticIp/components/propTypes'; import StaticIpViewRadioGroup from '../../clusterConfiguration/staticIp/components/StaticIpViewRadioGroup'; import { YamlView } from '../../clusterConfiguration/staticIp/components/YamlView/YamlView'; -import { StaticIpInfo, StaticIpView } from '../../clusterConfiguration/staticIp/data/dataTypes'; +import { StaticIpInfo } from '../../clusterConfiguration/staticIp/data/dataTypes'; import { getStaticIpInfo } from '../../clusterConfiguration/staticIp/data/fromInfraEnv'; import { getDummyStaticIpInfo } from '../../clusterConfiguration/staticIp/data/dummyData'; import { useModalDialogsContext } from '../../hosts/ModalDialogsContext'; diff --git a/libs/ui-lib/lib/ocm/components/AddHosts/day2Wizard/Day2WizardContext.tsx b/libs/ui-lib/lib/ocm/components/AddHosts/day2Wizard/Day2WizardContext.tsx index 03803380b2..c4d92544c4 100644 --- a/libs/ui-lib/lib/ocm/components/AddHosts/day2Wizard/Day2WizardContext.tsx +++ b/libs/ui-lib/lib/ocm/components/AddHosts/day2Wizard/Day2WizardContext.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { HostsNetworkConfigurationType } from '../../../services'; -import { StaticIpView } from '../../clusterConfiguration/staticIp/data/dataTypes'; +import { CpuArchitecture, StaticIpView } from '../../../../common'; import { Day2WizardStepsType } from './constants'; -import { CpuArchitecture } from '../../../../common'; export type Day2WizardContextType = { currentStepId: Day2WizardStepsType; diff --git a/libs/ui-lib/lib/ocm/components/AddHosts/day2Wizard/Day2WizardContextProvider.tsx b/libs/ui-lib/lib/ocm/components/AddHosts/day2Wizard/Day2WizardContextProvider.tsx index c1d9c5292c..852f32a8e5 100644 --- a/libs/ui-lib/lib/ocm/components/AddHosts/day2Wizard/Day2WizardContextProvider.tsx +++ b/libs/ui-lib/lib/ocm/components/AddHosts/day2Wizard/Day2WizardContextProvider.tsx @@ -4,10 +4,10 @@ import { AssistedInstallerOCMPermissionTypesListType, CpuArchitecture, getDefaultCpuArchitecture, + StaticIpView, } from '../../../../common'; import { Day2WizardStepsType, defaultWizardSteps, staticIpFormViewSubSteps } from './constants'; import { HostsNetworkConfigurationType } from '../../../services'; -import { StaticIpView } from '../../clusterConfiguration/staticIp/data/dataTypes'; import { Cluster, InfraEnv } from '@openshift-assisted/types/assisted-installer-service'; const getWizardStepIds = (staticIpView?: string): Day2WizardStepsType[] => { diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/commonValidationSchemas.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/commonValidationSchemas.tsx index ad5cb633cb..aa24e4a5b7 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/commonValidationSchemas.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/commonValidationSchemas.tsx @@ -1,11 +1,4 @@ -import { Address4, Address6 } from 'ip-address'; -import { isInSubnet } from 'is-in-subnet'; import * as Yup from 'yup'; -import { getAddressObject } from './data/protocolVersion'; -import { ProtocolVersion } from './data/dataTypes'; -import { getDuplicates } from '../../../../common'; - -const RESERVED_IPS = ['127.0.0.0', '127.0.0.1', '0.0.0.0', '255.255.255.255']; export type UniqueStringArrayExtractor = ( values: FormValues, @@ -41,209 +34,3 @@ export const getUniqueValidationSchema = ( }, ); }; - -const isValidIPv4Address = (addressStr: string) => { - try { - // ip-address package treats cidr addresses as valid so need to verify it isn't a cidr - // Can't use Address4.isValid() - const address = new Address4(addressStr); - return !address.parsedSubnet; - } catch (e) { - return false; - } -}; - -const isValidIPv6Address = (addressStr: string) => { - try { - // ip-address package treats cidr addresses as valid so need to verify it isn't a cidr - // Can't use Address6.isValid() - const address = new Address6(addressStr); - return !address.parsedSubnet; - } catch (e) { - return false; - } -}; - -const isValidAddress = (addressStr: string, protocolVersion?: ProtocolVersion) => { - if (protocolVersion === undefined) { - return isValidIPv4Address(addressStr) || isValidIPv6Address(addressStr); - } - return protocolVersion === ProtocolVersion.ipv4 - ? isValidIPv4Address(addressStr) - : isValidIPv6Address(addressStr); -}; - -const isReservedAddress = (addressStr: string, protocolVersion?: ProtocolVersion) => { - try { - if ( - (protocolVersion === undefined && isValidIPv4Address(addressStr)) || - protocolVersion === ProtocolVersion.ipv4 - ) { - return RESERVED_IPS.includes(addressStr); - } else { - return isReservedIpv6Address(new Address6(addressStr)); - } - } catch (e) { - return false; - } -}; - -export const getIpAddressValidationSchema = (protocolVersion: ProtocolVersion) => { - const protocolVersionLabel = protocolVersion === ProtocolVersion.ipv4 ? 'IPv4' : 'IPv6'; - return Yup.string().test( - protocolVersion, - `Value \${value} is not a valid ${protocolVersionLabel} address`, - (value?: string) => { - if (!value) { - return true; - } - return isValidAddress(value, protocolVersion); - }, - ); -}; - -export const getMultipleIpAddressValidationSchema = (protocolVersion?: ProtocolVersion) => { - const validationId = protocolVersion === undefined ? 'is-ipv4-or-ipv6-csv' : protocolVersion; - const protocolVersionLabel = - protocolVersion === undefined - ? 'IPv4 or IPv6' - : protocolVersion === ProtocolVersion.ipv4 - ? 'IPv4' - : 'IPv6'; - return Yup.string().test( - validationId, - ({ value }) => { - const addresses = (value as string).split(','); - const invalidAddresses = addresses.filter( - (address) => !isValidAddress(address, protocolVersion), - ); - const displayValue = invalidAddresses.join(', '); - if (invalidAddresses.length === 1) { - return `Value ${displayValue} is not a valid ${protocolVersionLabel} address`; - } else if (invalidAddresses.length > 1) { - return `The values ${displayValue} are not valid ${protocolVersionLabel} addresses`; - } - // If all addresses are valid, then there must be duplicated addresses - const duplicates = getDuplicates(addresses); - return `The following IP addresses are duplicated: ${duplicates.join(',')}`; - }, - (value?: string) => { - if (!value) { - return true; - } - const addresses: string[] = value.split(','); - const duplicates = getDuplicates(addresses); - if (duplicates.length !== 0) { - return false; - } - - return addresses.every((address) => isValidAddress(address, protocolVersion)); - }, - ); -}; - -export const isReservedIpv6Address = (ipv6Address: Address6) => { - return ipv6Address.isLoopback() || ipv6Address.isMulticast(); -}; - -function areNotReservedAdresses(value: unknown, protocolVersion?: ProtocolVersion): boolean { - if (!value) { - return true; - } - // The field may admit multiple values as a comma-separated string - const addresses = (value as string).split(','); - return addresses.every((address) => !isReservedAddress(address, protocolVersion)); -} - -export const isNotReservedHostIPAddress = (protocolVersion?: ProtocolVersion) => { - return Yup.string().test( - 'is-not-reserved-ip-address', - ({ value }) => { - const addresses = (value as string).split(','); - if (addresses.length === 1) { - return `The provided IP address is not a correct address for an interface.`; - } - - const reservedAddresses = addresses.filter((address) => { - return isReservedAddress(address, protocolVersion); - }); - return `The provided IP addresses ${reservedAddresses.join( - ', ', - )} are not correct addresses for an interface.`; - }, - (value) => areNotReservedAdresses(value, protocolVersion), - ); -}; - -export const isNotReservedHostDNSAddress = (protocolVersion?: ProtocolVersion) => { - return Yup.string().test( - 'is-not-reserved-dns-address', - ({ value }) => { - const addresses = (value as string).split(','); - if (addresses.length === 1) { - return `The provided IP address is not a valid DNS address.`; - } - - const reservedAddresses = addresses.filter((address) => { - return isReservedAddress(address, protocolVersion); - }); - return `The provided IP addresses ${reservedAddresses.join( - ', ', - )} are not valid DNS addresses.`; - }, - (value) => areNotReservedAdresses(value, protocolVersion), - ); -}; - -export const getIpAddressInSubnetValidationSchema = ( - protocolVersion: ProtocolVersion, - subnet: string, -) => { - return Yup.string().test( - 'is-in-subnet', - `IP Address is outside of the subnet ${subnet}`, - (value, testContext: Yup.TestContext) => { - if (!value) { - return true; - } - const ipValidationSchema = getIpAddressValidationSchema(protocolVersion); - try { - ipValidationSchema.validateSync(value); - } catch (err) { - const error = err as { message: string }; - return testContext.createError({ message: error.message }); - } - try { - const inSubnet = isInSubnet(value, subnet); - return inSubnet; - } catch (err) { - //if isInSubnet fails it means the machine network cidr isn't valid and this validation is irrelevant - return true; - } - }, - ); -}; - -export const getIpIsNotNetworkOrBroadcastAddressSchema = ( - protocolVersion: ProtocolVersion, - subnet: string, -) => { - return Yup.string().test( - 'is-not-network-or-broadcast', - `The IP address must not match the network or broadcast address`, - (value) => { - // Allow both addresses for IPv4 /31 subnets (RFC 3021) - if (protocolVersion === ProtocolVersion.ipv4 && subnet.endsWith('/31')) { - return true; - } - const subnetAddr = getAddressObject(subnet, protocolVersion); - if (!subnetAddr) { - return true; - } else { - const subnetStart = subnetAddr?.startAddress().correctForm(); - const subnetEnd = subnetAddr?.endAddress().correctForm(); - return !(value === subnetStart || value === subnetEnd); - } - }, - ); -}; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/FormViewHosts.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/FormViewHosts.tsx index 08d2684043..d14ce96f61 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/FormViewHosts.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/FormViewHosts.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { StaticIpForm } from '../StaticIpForm'; import { StaticIpFormProps, StaticIpViewProps } from '../propTypes'; -import { FormViewHostsValues, StaticProtocolType } from '../../data/dataTypes'; +import { StaticProtocolType, useTranslation } from '../../../../../../common'; +import { FormViewHostsValues } from '../../data/dataTypes'; import { FormViewHostsFields } from './FormViewHostsFields'; import { getFormViewHostsValidationSchema } from './formViewHostsValidationSchema'; import { formViewHostsToInfraEnvField } from '../../data/formDataToInfraEnvField'; @@ -11,6 +12,7 @@ import { getFormViewHostsValues, getFormViewNetworkWideValues } from '../../data import { InfraEnv } from '@openshift-assisted/types/assisted-installer-service'; export const FormViewHosts: React.FC = ({ infraEnv, ...props }) => { + const { t } = useTranslation(); const [protocolType, setProtocolType] = React.useState(); const [formProps, setFormProps] = React.useState>(); React.useEffect(() => { @@ -20,7 +22,7 @@ export const FormViewHosts: React.FC = ({ infraEnv, ...props if (networkWideValues) { setFormProps({ infraEnv, - validationSchema: getFormViewHostsValidationSchema(networkWideValues), + validationSchema: getFormViewHostsValidationSchema(networkWideValues, t), getInitialValues: (infraEnv: InfraEnv) => { return getFormViewHostsValues(infraEnv); }, diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/FormViewHostsFields.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/FormViewHostsFields.tsx index a06e0b6e58..c4df9b7e57 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/FormViewHostsFields.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/FormViewHostsFields.tsx @@ -2,10 +2,16 @@ import React from 'react'; import { FormGroup, Grid, TextContent, Text, TextVariants } from '@patternfly/react-core'; import { useField, useFormikContext } from 'formik'; import StaticIpHostsArray, { HostComponentProps } from '../StaticIpHostsArray'; -import { getFieldId, PopoverIcon } from '../../../../../../common'; +import { + getFieldId, + getProtocolVersionLabel, + PopoverIcon, + StaticProtocolType, + useTranslation, +} from '../../../../../../common'; import HostSummary from '../CollapsedHost'; -import { FormViewHost, StaticProtocolType } from '../../data/dataTypes'; -import { getProtocolVersionLabel, getShownProtocolVersions } from '../../data/protocolVersion'; +import { FormViewHost } from '../../data/dataTypes'; +import { getShownProtocolVersions } from '../../../../../../common/components/staticIP/protocolVersion'; import { getEmptyFormViewHost } from '../../data/emptyData'; import { OcmCheckboxField, OcmInputField } from '../../../../ui/OcmFormFields'; import '../staticIp.css'; @@ -14,6 +20,7 @@ import BondsConfirmationModal from './BondsConfirmationModal'; const getExpandedHostComponent = (protocolType: StaticProtocolType) => { const Component: React.FC = ({ fieldName, hostIdx }) => { + const { t } = useTranslation(); const [isModalOpen, setIsModalOpen] = React.useState(false); const { setFieldValue } = useFormikContext(); const [bondPrimaryField] = useField(`${fieldName}.bondPrimaryInterface`); @@ -92,7 +99,7 @@ const getExpandedHostComponent = (protocolType: StaticProtocolType) => { )} {getShownProtocolVersions(protocolType).map((protocolVersion) => ( diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/formViewHostsValidationSchema.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/formViewHostsValidationSchema.tsx index c32c943f07..ab78f3a1a6 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/formViewHostsValidationSchema.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewHosts/formViewHostsValidationSchema.tsx @@ -1,19 +1,19 @@ import * as Yup from 'yup'; +import { FormViewNetworkWideValues, FormViewHostsValues } from '../../data/dataTypes'; import { - FormViewNetworkWideValues, - FormViewHostsValues, + getInMachineNetworkValidationSchema, + getIpIsNotNetworkOrBroadcastAddressSchema, + macAddressValidationSchema, ProtocolVersion, -} from '../../data/dataTypes'; -import { macAddressValidationSchema } from '../../../../../../common'; -import { showIpv4, showIpv6 } from '../../data/protocolVersion'; -import { getInMachineNetworkValidationSchema } from '../FormViewNetworkWide/formViewNetworkWideValidationSchema'; +} from '../../../../../../common'; +import { showIpv4, showIpv6 } from '../../../../../../common/components/staticIP/protocolVersion'; import { getUniqueValidationSchema, UniqueStringArrayExtractor, - getIpIsNotNetworkOrBroadcastAddressSchema, } from '../../commonValidationSchemas'; -import { getMachineNetworkCidr } from '../../data/machineNetwork'; +import { getMachineNetworkCidr } from '../../../../../../common/components/staticIP/machineNetwork'; +import { TFunction } from 'i18next'; const requiredMsg = 'A value is required'; const getAllIpv4Addresses: UniqueStringArrayExtractor = ( @@ -39,12 +39,12 @@ const getAllBondInterfaces: UniqueStringArrayExtractor = ( ]); }; -const getHostValidationSchema = (networkWideValues: FormViewNetworkWideValues) => +const getHostValidationSchema = (networkWideValues: FormViewNetworkWideValues, t: TFunction) => Yup.object({ macAddress: Yup.mixed().when('useBond', { is: false, then: () => - macAddressValidationSchema + macAddressValidationSchema(t) .required(requiredMsg) .concat(getUniqueValidationSchema(getAllMacAddresses)), otherwise: () => Yup.mixed().notRequired(), @@ -54,6 +54,7 @@ const getHostValidationSchema = (networkWideValues: FormViewNetworkWideValues) = ? getInMachineNetworkValidationSchema( ProtocolVersion.ipv4, networkWideValues.ipConfigs['ipv4'].machineNetwork, + t, ) .required(requiredMsg) .concat(getUniqueValidationSchema(getAllIpv4Addresses)) @@ -61,6 +62,7 @@ const getHostValidationSchema = (networkWideValues: FormViewNetworkWideValues) = getIpIsNotNetworkOrBroadcastAddressSchema( ProtocolVersion.ipv4, getMachineNetworkCidr(networkWideValues.ipConfigs['ipv4'].machineNetwork), + t, ), ) : Yup.string(), @@ -68,6 +70,7 @@ const getHostValidationSchema = (networkWideValues: FormViewNetworkWideValues) = ? getInMachineNetworkValidationSchema( ProtocolVersion.ipv6, networkWideValues.ipConfigs['ipv6'].machineNetwork, + t, ) .required(requiredMsg) .concat(getUniqueValidationSchema(getAllIpv6Addresses)) @@ -75,6 +78,7 @@ const getHostValidationSchema = (networkWideValues: FormViewNetworkWideValues) = getIpIsNotNetworkOrBroadcastAddressSchema( ProtocolVersion.ipv6, getMachineNetworkCidr(networkWideValues.ipConfigs['ipv6'].machineNetwork), + t, ), ) : Yup.string(), @@ -82,7 +86,7 @@ const getHostValidationSchema = (networkWideValues: FormViewNetworkWideValues) = bondPrimaryInterface: Yup.mixed().when('useBond', { is: true, then: () => - macAddressValidationSchema + macAddressValidationSchema(t) .required(requiredMsg) .concat(getUniqueValidationSchema(getAllBondInterfaces)), otherwise: () => Yup.mixed().notRequired(), @@ -90,15 +94,18 @@ const getHostValidationSchema = (networkWideValues: FormViewNetworkWideValues) = bondSecondaryInterface: Yup.mixed().when('useBond', { is: true, then: () => - macAddressValidationSchema + macAddressValidationSchema(t) .required(requiredMsg) .concat(getUniqueValidationSchema(getAllBondInterfaces)), otherwise: () => Yup.mixed().notRequired(), }), }); -export const getFormViewHostsValidationSchema = (networkWideValues: FormViewNetworkWideValues) => { +export const getFormViewHostsValidationSchema = ( + networkWideValues: FormViewNetworkWideValues, + t: TFunction, +) => { return Yup.object().shape({ - hosts: Yup.array(getHostValidationSchema(networkWideValues)), + hosts: Yup.array(getHostValidationSchema(networkWideValues, t)), }); }; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewNetworkWide/FormViewNetworkWide.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewNetworkWide/FormViewNetworkWide.tsx index 47f7afd153..96d7c71322 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewNetworkWide/FormViewNetworkWide.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewNetworkWide/FormViewNetworkWide.tsx @@ -8,8 +8,10 @@ import { FormViewNetworkWideFields } from './FormViewNetworkWideFields'; import { getFormData, getFormViewNetworkWideValues } from '../../data/fromInfraEnv'; import { getEmptyNetworkWideConfigurations } from '../../data/emptyData'; import { InfraEnv } from '@openshift-assisted/types/assisted-installer-service'; +import { useTranslation } from '../../../../../../common'; export const FormViewNetworkWide: React.FC = ({ infraEnv, ...props }) => { + const { t } = useTranslation(); const [formProps, setFormProps] = React.useState>(); const [hosts, setHosts] = React.useState(); @@ -22,7 +24,7 @@ export const FormViewNetworkWide: React.FC = ({ infraEnv, ... setFormProps({ infraEnv, ...props, - validationSchema: networkWideValidationSchema, + validationSchema: networkWideValidationSchema(t), getInitialValues: (infraEnv: InfraEnv) => { return getFormViewNetworkWideValues(infraEnv); }, diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewNetworkWide/FormViewNetworkWideFields.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewNetworkWide/FormViewNetworkWideFields.tsx index b40f5af887..7599f844e0 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewNetworkWide/FormViewNetworkWideFields.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewNetworkWide/FormViewNetworkWideFields.tsx @@ -2,53 +2,34 @@ import React from 'react'; import { Text, TextVariants, - Grid, FormGroup, - TextInputTypes, Alert, AlertVariant, TextContent, - Flex, - FlexItem, ButtonVariant, - FormHelperText, - HelperText, - HelperTextItem, } from '@patternfly/react-core'; import { useField, useFormikContext } from 'formik'; +import { useSelector } from 'react-redux'; import { ConfirmationModal, getFieldId, - getHumanizedSubnetRange, - PopoverIcon, -} from '../../../../../../common'; -import { - getAddressObject, getProtocolVersionLabel, - getShownProtocolVersions, -} from '../../data/protocolVersion'; -import * as types from '../../data/dataTypes'; -import { getEmptyIpConfig } from '../../data/emptyData'; -import { - Cidr, - FormViewHost, - FormViewNetworkWideValues, IpConfig, - ProtocolVersion, - StaticProtocolType, -} from '../../data/dataTypes'; -import { getMachineNetworkCidr } from '../../data/machineNetwork'; -import useFieldErrorMsg from '../../../../../../common/hooks/useFieldErrorMsg'; -import { - MIN_PREFIX_LENGTH, - MAX_PREFIX_LENGTH, - MAX_VLAN_ID, + IpConfigFields, MIN_VLAN_ID, -} from './formViewNetworkWideValidationSchema'; + MAX_VLAN_ID, + PopoverIcon, + useTranslation, + StaticProtocolType, + ProtocolVersion, +} from '../../../../../../common'; +import { getShownProtocolVersions } from '../../../../../../common/components/staticIP/protocolVersion'; +import { getEmptyIpConfig } from '../../data/emptyData'; +import { FormViewHost, FormViewNetworkWideValues } from '../../data/dataTypes'; import { OcmCheckboxField, OcmInputField, OcmRadioField } from '../../../../ui/OcmFormFields'; +import { selectCurrentClusterPermissionsState } from '../../../../../store/slices/current-cluster/selectors'; import '../staticIp.css'; -import ExclamationCircleIcon from '@patternfly/react-icons/dist/js/icons/exclamation-circle-icon'; const hostsConfiguredAlert = ( ); -const MachineNetwork: React.FC<{ fieldName: string; protocolVersion: ProtocolVersion }> = ({ - fieldName, - protocolVersion, -}) => { - const [{ value }] = useField(fieldName); - const ipFieldName = `${fieldName}.ip`; - const prefixLengthFieldName = `${fieldName}.prefixLength`; - const ipErrorMessage = useFieldErrorMsg({ name: ipFieldName }); - const prefixLengthErrorMessage = useFieldErrorMsg({ name: prefixLengthFieldName }); - const errorMessage = ipErrorMessage || prefixLengthErrorMessage; - const machineNetworkHelptext = React.useMemo(() => { - if (errorMessage) { - return ''; - } - const cidr = getMachineNetworkCidr(value); - return getHumanizedSubnetRange(getAddressObject(cidr, protocolVersion)); - }, [value, protocolVersion, errorMessage]); - const fieldId = getFieldId(`${fieldName}`, 'input'); - return ( - - } - label="Subnet" - fieldId={fieldId} - isRequired - className="subnet" - > - - - - - {'/'} - - - - - {(errorMessage || machineNetworkHelptext) && ( - - - : null} - variant={errorMessage ? 'error' : 'default'} - id={errorMessage ? `${fieldId}-helper-error` : `${fieldId}-helper`} - > - {errorMessage ? errorMessage : machineNetworkHelptext} - - - - )} - - ); -}; - -const IpConfigFields: React.FC<{ - fieldName: string; - protocolVersion: ProtocolVersion; -}> = ({ protocolVersion, fieldName }) => { - return ( - - - - } - name={`${fieldName}.gateway`} - data-testid={`${protocolVersion}-gateway`} - /> - - ); -}; - const ipv6ValuesEmpty = (values: FormViewNetworkWideValues) => values.ipConfigs.ipv6.gateway === '' && values.ipConfigs.ipv6.machineNetwork.ip === '' && values.ipConfigs.ipv6.machineNetwork.prefixLength === ''; export const ProtocolTypeSelect = () => { + const { t } = useTranslation(); const selectFieldName = 'protocolType'; const [{ value: protocolType }, , { setValue: setProtocolType }] = useField(selectFieldName); @@ -167,7 +55,7 @@ export const ProtocolTypeSelect = () => { const { values } = useFormikContext(); const onChange = (e: React.ChangeEvent) => { - const newProtocolType = e.target.value as types.StaticProtocolType; + const newProtocolType = e.target.value as StaticProtocolType; if (newProtocolType === protocolType) { return; } @@ -180,6 +68,7 @@ export const ProtocolTypeSelect = () => { } }; const isIpv4Selected = protocolType === 'ipv4'; + return ( <> { - {getProtocolVersionLabel(ProtocolVersion.ipv4)}{' '} + {getProtocolVersionLabel(ProtocolVersion.ipv4, t)}{' '} { } onClose={() => { setConfirmModal(false); - setProtocolType('dualStack'); + setProtocolType(StaticProtocolType.dualStack); }} onConfirm={() => { setConfirmModal(false); - setProtocolType('ipv4'); + setProtocolType(StaticProtocolType.ipv4); }} /> )} ); }; + export const FormViewNetworkWideFields = ({ hosts }: { hosts: FormViewHost[] }) => { + const { isViewerMode } = useSelector(selectCurrentClusterPermissionsState); const { values, setFieldValue } = useFormikContext(); + return ( <> @@ -301,16 +193,12 @@ export const FormViewNetworkWideFields = ({ hosts }: { hosts: FormViewHost[] }) /> {getShownProtocolVersions(values.protocolType).map((protocolVersion) => ( - - - + isDisabled={isViewerMode} + /> ))} ); diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewNetworkWide/formViewNetworkWideValidationSchema.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewNetworkWide/formViewNetworkWideValidationSchema.tsx index 07d5fe3da7..70c30adaaa 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewNetworkWide/formViewNetworkWideValidationSchema.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/FormViewNetworkWide/formViewNetworkWideValidationSchema.tsx @@ -1,128 +1,23 @@ import * as Yup from 'yup'; +import { FormViewNetworkWideValues } from '../../data/dataTypes'; import { - IpConfig, - ProtocolVersion, - FormViewNetworkWideValues, - Cidr, - StaticProtocolType, -} from '../../data/dataTypes'; - -import { showProtocolVersion } from '../../data/protocolVersion'; -import { getMachineNetworkCidr } from '../../data/machineNetwork'; -import { - getIpAddressInSubnetValidationSchema, - getIpAddressValidationSchema, - getIpIsNotNetworkOrBroadcastAddressSchema, - getMultipleIpAddressValidationSchema, - isNotReservedHostIPAddress, - isNotReservedHostDNSAddress, -} from '../../commonValidationSchemas'; - -const REQUIRED_MESSAGE = 'A value is required'; -const MUST_BE_A_NUMBER = 'Must be a number'; -const ONLY_DIGITS_REGEX = /^\d+$/; - -export const MIN_PREFIX_LENGTH = 1; -export const MAX_PREFIX_LENGTH = { - ipv4: 32, - ipv6: 128, -}; - -export const MIN_VLAN_ID = 1; -export const MAX_VLAN_ID = 4094; - -const transformNumber = (originalValue: number) => { - return isNaN(originalValue) ? null : originalValue; -}; - -export const getInMachineNetworkValidationSchema = ( - protocolVersion: ProtocolVersion, - machineNetwork: Cidr, -) => { - return getIpAddressInSubnetValidationSchema( - protocolVersion, - getMachineNetworkCidr(machineNetwork), - ); -}; - -export const getIsNotNetworkOrBroadcastAddressSchema = ( - protocolVersion: ProtocolVersion, - machineNetwork: Cidr, -) => { - return getIpIsNotNetworkOrBroadcastAddressSchema( - protocolVersion, - getMachineNetworkCidr(machineNetwork), - ); -}; - -const getMachineNetworkValidationSchema = (protocolVersion: ProtocolVersion) => - Yup.object().shape({ - ip: getIPValidationSchema(protocolVersion), - prefixLength: Yup.number() - .required('Prefix length is required') - .min(1, `Prefix length must be more than or equal to 1`) - .max( - MAX_PREFIX_LENGTH[protocolVersion], - `Prefix length must be less than or equal to ${MAX_PREFIX_LENGTH[protocolVersion]}`, - ) - .transform(transformNumber) as Yup.NumberSchema, //add casting to not get typescript error caused by nullable + getDNSValidationSchema, + ipConfigsValidationSchemas, + VlanIdValidationSchema, +} from '../../../../../../common'; +import { TFunction } from 'i18next'; + +export const networkWideValidationSchema = (t: TFunction) => + Yup.lazy((values: FormViewNetworkWideValues) => { + return Yup.object({ + useVlan: Yup.boolean(), + vlanId: Yup.mixed().when('useVlan', { + is: (useVlan: boolean) => useVlan, + then: () => VlanIdValidationSchema(values.vlanId, t), + }), + protocolType: Yup.string(), + dns: getDNSValidationSchema(values.protocolType, t), + ipConfigs: ipConfigsValidationSchemas(values.ipConfigs, values.protocolType, t), + }); }); - -const getIPValidationSchema = (protocolVersion: ProtocolVersion) => { - return getIpAddressValidationSchema(protocolVersion) - .required(REQUIRED_MESSAGE) - .concat(isNotReservedHostIPAddress(protocolVersion)); -}; - -const getDNSValidationSchema = (protocolType: StaticProtocolType) => { - if (protocolType === 'dualStack') { - return getMultipleIpAddressValidationSchema() - .required(REQUIRED_MESSAGE) - .concat(isNotReservedHostDNSAddress()); - } - return getMultipleIpAddressValidationSchema(ProtocolVersion.ipv4) - .required(REQUIRED_MESSAGE) - .concat(isNotReservedHostDNSAddress(ProtocolVersion.ipv4)); -}; - -const getAddressDataValidationSchema = (protocolVersion: ProtocolVersion, ipConfig: IpConfig) => { - return Yup.object({ - machineNetwork: getMachineNetworkValidationSchema(protocolVersion), - gateway: getIPValidationSchema(protocolVersion) - .concat(getInMachineNetworkValidationSchema(protocolVersion, ipConfig.machineNetwork)) - .concat(getIsNotNetworkOrBroadcastAddressSchema(protocolVersion, ipConfig.machineNetwork)), - }); -}; - -export const networkWideValidationSchema = Yup.lazy((values: FormViewNetworkWideValues) => { - const ipConfigsValidationSchemas = Yup.object({ - ipv4: getAddressDataValidationSchema(ProtocolVersion.ipv4, values.ipConfigs.ipv4), - ipv6: showProtocolVersion(values.protocolType, ProtocolVersion.ipv6) - ? getAddressDataValidationSchema(ProtocolVersion.ipv6, values.ipConfigs.ipv6) - : Yup.object(), - }); - - return Yup.object({ - useVlan: Yup.boolean(), - vlanId: Yup.mixed().when('useVlan', { - is: (useVlan: boolean) => useVlan, - then: () => - Yup.number() - .required(MUST_BE_A_NUMBER) - .min(1, `Must be more than or equal to 1`) - .max(MAX_VLAN_ID, `Must be less than or equal to ${MAX_VLAN_ID}`) - .test('not-number', MUST_BE_A_NUMBER, () => validateNumber(values.vlanId)) - .nullable() - .transform(transformNumber) as Yup.NumberSchema, - }), - protocolType: Yup.string(), - dns: getDNSValidationSchema(values.protocolType), - ipConfigs: ipConfigsValidationSchemas, - }); -}); - -export const validateNumber = (vlanId: FormViewNetworkWideValues['vlanId']) => { - //We need to validate that value is a number(without letters) and is not an exponential number (ex: 1e2) - return new RegExp(ONLY_DIGITS_REGEX).test((vlanId || '').toString()); -}; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/StaticIpPage.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/StaticIpPage.tsx index f1a50f006c..aa53f1031b 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/StaticIpPage.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/StaticIpPage.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Text, TextContent, TextVariants, Alert, AlertVariant, Grid } from '@patternfly/react-core'; -import { StaticIpInfo, StaticIpView } from '../data/dataTypes'; +import { StaticIpView } from '../../../../../common'; +import { StaticIpInfo } from '../data/dataTypes'; import StaticIpViewRadioGroup from './StaticIpViewRadioGroup'; import { getStaticIpInfo } from '../data/fromInfraEnv'; import { StaticIpFormState, StaticIpPageProps, StaticIpViewProps } from './propTypes'; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/StaticIpViewRadioGroup.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/StaticIpViewRadioGroup.tsx index 6b4e92d285..3564556b53 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/StaticIpViewRadioGroup.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/StaticIpViewRadioGroup.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Alert, AlertVariant, ButtonVariant, Form, FormGroup } from '@patternfly/react-core'; -import { StaticIpView } from '../data/dataTypes'; -import { getFieldId, useAlerts } from '../../../../../common'; +import { getFieldId, StaticIpView, useAlerts } from '../../../../../common'; import ConfirmationModal from '../../../../../common/components/ui/ConfirmationModal'; import { OcmRadio } from '../../../ui/OcmFormFields'; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/YamlView.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/YamlView.tsx index 5a31950bf9..0fd0370426 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/YamlView.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/YamlView.tsx @@ -7,11 +7,13 @@ import { YamlViewFields } from './YamlViewFields'; import { getEmptyYamlValues } from '../../data/emptyData'; import { InfraEnv } from '@openshift-assisted/types/assisted-installer-service'; import { getYamlViewValues } from '../../data/fromInfraEnv'; +import { useTranslation } from '../../../../../../common'; export const YamlView: React.FC = ({ ...props }) => { + const { t } = useTranslation(); const formProps: StaticIpFormProps = { ...props, - validationSchema: yamlViewValidationSchema, + validationSchema: yamlViewValidationSchema(t), getInitialValues: (infraEnv: InfraEnv) => { return getYamlViewValues(infraEnv); }, diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/yamlViewValidationSchema.tsx b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/yamlViewValidationSchema.tsx index 3b03d0db91..9c2b170ae4 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/yamlViewValidationSchema.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/components/YamlView/yamlViewValidationSchema.tsx @@ -15,6 +15,7 @@ import { HostStaticNetworkConfig, MacInterfaceMap, } from '@openshift-assisted/types/assisted-installer-service'; +import { TFunction } from 'i18next'; const requiredMsg = 'A value is required'; @@ -59,24 +60,26 @@ const getInterfaceNamesInCurrentHost: UniqueStringArrayExtractor }); }; -const macInterfaceMapValidationSchema = Yup.array().of( - Yup.object({ - macAddress: macAddressValidationSchema - .required(requiredMsg) - .concat(getUniqueValidationSchema(getAllMacAddresses)), - logicalNicName: Yup.string() - .required(requiredMsg) - .concat(getUniqueValidationSchema(getInterfaceNamesInCurrentHost)) - .max(15, 'Interface name must be 15 characters at most.') - .matches(/^\S+$/, 'Interface name can not contain spaces.'), - }), -); - -export const yamlViewValidationSchema = Yup.object({ - hosts: Yup.array().of( - Yup.object().shape({ - networkYaml: networkYamlValidationSchema, - macInterfaceMap: macInterfaceMapValidationSchema, +const macInterfaceMapValidationSchema = (t: TFunction) => + Yup.array().of( + Yup.object({ + macAddress: macAddressValidationSchema(t) + .required(requiredMsg) + .concat(getUniqueValidationSchema(getAllMacAddresses)), + logicalNicName: Yup.string() + .required(requiredMsg) + .concat(getUniqueValidationSchema(getInterfaceNamesInCurrentHost)) + .max(15, 'Interface name must be 15 characters at most.') + .matches(/^\S+$/, 'Interface name can not contain spaces.'), }), - ), -}); + ); + +export const yamlViewValidationSchema = (t: TFunction) => + Yup.object({ + hosts: Yup.array().of( + Yup.object().shape({ + networkYaml: networkYamlValidationSchema, + macInterfaceMap: macInterfaceMapValidationSchema(t), + }), + ), + }); diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/dataTypes.ts b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/dataTypes.ts index dfd18a01a1..44d29d679e 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/dataTypes.ts +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/dataTypes.ts @@ -1,28 +1,10 @@ import { HostStaticNetworkConfig } from '@openshift-assisted/types/assisted-installer-service'; - -export enum StaticIpView { - YAML = 'yaml', - FORM = 'form', -} - -export enum ProtocolVersion { - ipv4 = 'ipv4', - ipv6 = 'ipv6', -} - -export type StaticProtocolType = 'ipv4' | 'dualStack'; - -export type Cidr = { - ip: string; - prefixLength: number | ''; -}; - -export type IpConfig = { - machineNetwork: Cidr; - gateway: string; -}; - -export type IpConfigs = { [protocolVersion in ProtocolVersion]: IpConfig }; +import { + IpConfigs, + ProtocolVersion, + StaticIpView, + StaticProtocolType, +} from '../../../../../common'; export type HostIps = { [protocolVersion in ProtocolVersion]: string }; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/dummyData.ts b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/dummyData.ts index d3d7cff52a..cfacffb306 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/dummyData.ts +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/dummyData.ts @@ -2,15 +2,10 @@ import { HostStaticNetworkConfig, MacInterfaceMap, } from '@openshift-assisted/types/assisted-installer-service'; -import { - FormViewNetworkWideValues, - ProtocolVersion, - StaticIpInfo, - StaticIpView, -} from './dataTypes'; +import { FormViewNetworkWideValues, StaticIpInfo } from './dataTypes'; +import { getShownProtocolVersions, ProtocolVersion, StaticIpView } from '../../../../../common'; import { NmstateEthernetInterface, NmstateInterfaceType } from './nmstateTypes'; import { FORM_VIEW_PREFIX, getNmstateProtocolConfig, toYamlWithComments } from './nmstateYaml'; -import { getShownProtocolVersions } from './protocolVersion'; const DUMMY_MAC_4 = '01:23:45:67:89:AB'; const DUMMY_MAC_6 = '01:23:45:67:89:AC'; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/emptyData.ts b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/emptyData.ts index aa089c8825..6f9915bda9 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/emptyData.ts +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/emptyData.ts @@ -1,12 +1,12 @@ import { HostStaticNetworkConfig } from '@openshift-assisted/types/assisted-installer-service'; import { FormViewHost, - IpConfig, FormViewNetworkWideValues, FormViewHostsValues, YamlViewValues, HostIps, } from './dataTypes'; +import { IpConfig, StaticProtocolType } from '../../../../../common'; export const getEmptyHostIps = (): HostIps => { return { @@ -49,7 +49,7 @@ export const getEmptyNetworkWideConfigurations = (): FormViewNetworkWideValues = ipv4: getEmptyIpConfig(), ipv6: getEmptyIpConfig(), }, - protocolType: 'ipv4', + protocolType: StaticProtocolType.ipv4, useVlan: false, vlanId: '', dns: '', diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/formDataFromInfraEnvField.ts b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/formDataFromInfraEnvField.ts index 180b764f7f..04c2f0331b 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/formDataFromInfraEnvField.ts +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/formDataFromInfraEnvField.ts @@ -1,12 +1,6 @@ import { HostStaticNetworkConfig } from '@openshift-assisted/types/assisted-installer-service'; -import { - FormViewHost, - FormViewNetworkWideValues, - ProtocolVersion, - StaticFormData, - StaticProtocolType, -} from './dataTypes'; +import { FormViewHost, FormViewNetworkWideValues, StaticFormData } from './dataTypes'; import findLastIndex from 'lodash-es/findLastIndex.js'; import { getProtocolVersionIdx, @@ -16,7 +10,7 @@ import { yamlToNmstateObject, } from './nmstateYaml'; -import { getShownProtocolVersions } from './protocolVersion'; +import { getShownProtocolVersions } from '../../../../../common/components/staticIP/protocolVersion'; import { getEmptyNetworkWideConfigurations } from './emptyData'; import { Nmstate, @@ -28,6 +22,7 @@ import { NmstateInterfaceType, } from './nmstateTypes'; import { isDummyInterface } from './dummyData'; +import { ProtocolVersion, StaticProtocolType } from '../../../../../common'; /* handle four cases: 1. right after create, there are no network wide configurations - yaml contains a dummy ipv4 interface and no machine network fields in the comments diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/formDataToInfraEnvField.ts b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/formDataToInfraEnvField.ts index da4d805592..c818e51020 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/formDataToInfraEnvField.ts +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/formDataToInfraEnvField.ts @@ -4,13 +4,7 @@ import { InfraEnv, MacInterfaceMap, } from '@openshift-assisted/types/assisted-installer-service'; -import { - ProtocolVersion, - FormViewHost, - FormViewNetworkWideValues, - StaticFormData, - HostIps, -} from './dataTypes'; +import { FormViewHost, FormViewNetworkWideValues, StaticFormData, HostIps } from './dataTypes'; import { FORM_VIEW_PREFIX, getDnsSection, @@ -21,7 +15,7 @@ import { getVlanNicName, YAML_COMMENT_CHAR, } from './nmstateYaml'; -import { getShownProtocolVersions } from './protocolVersion'; +import { getShownProtocolVersions } from '../../../../../common/components/staticIP/protocolVersion'; import { Nmstate, NmstateInterface, @@ -31,8 +25,9 @@ import { import { formDataFromInfraEnvField } from './formDataFromInfraEnvField'; import { getStaticNetworkConfig } from './fromInfraEnv'; import { DUMMY_NMSTATE_ADDRESSES, getDummyMacInterfaceMap, getDummyNicName } from './dummyData'; -import { getMachineNetworkCidr } from './machineNetwork'; +import { getMachineNetworkCidr } from '../../../../../common/components/staticIP/machineNetwork'; import { getEmptyHostIps } from './emptyData'; +import { ProtocolVersion } from '../../../../../common'; const REAL_NIC_NAME = 'eth0'; const REAL_NIC_NAME_1 = 'eth1'; diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/fromInfraEnv.ts b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/fromInfraEnv.ts index a615527c19..fc4f445552 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/fromInfraEnv.ts +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/fromInfraEnv.ts @@ -5,7 +5,6 @@ import { import { FORM_VIEW_PREFIX, getProtocolType, getYamlComments } from './nmstateYaml'; import { StaticIpInfo, - StaticIpView, StaticFormData, FormViewHostsValues, FormViewNetworkWideValues, @@ -15,6 +14,7 @@ import { isDummyYaml } from './dummyData'; import { formDataFromInfraEnvField } from './formDataFromInfraEnvField'; import { getEmptyFormViewHost } from './emptyData'; import { stringToJSON } from '../../../../../common/utils'; +import { StaticIpView } from '../../../../../common'; export const getStaticNetworkConfig = ( infraEnv: InfraEnv, diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/nmstateTypes.ts b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/nmstateTypes.ts index c4269b54dc..8f77a5cbac 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/nmstateTypes.ts +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/nmstateTypes.ts @@ -1,4 +1,4 @@ -import { ProtocolVersion } from './dataTypes'; +import { ProtocolVersion } from '../../../../../common'; export enum NmstateInterfaceType { ETHERNET = 'ethernet', diff --git a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/nmstateYaml.ts b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/nmstateYaml.ts index de449866fb..aab79c7abd 100644 --- a/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/nmstateYaml.ts +++ b/libs/ui-lib/lib/ocm/components/clusterConfiguration/staticIp/data/nmstateYaml.ts @@ -8,13 +8,9 @@ import { NmstateProtocolConfigs, NmstateRoutesConfig, } from './nmstateTypes'; -import { - FormViewNetworkWideValues, - MachineNetworks, - ProtocolVersion, - StaticProtocolType, -} from './dataTypes'; +import { FormViewNetworkWideValues, MachineNetworks } from './dataTypes'; import findLastIndex from 'lodash-es/findLastIndex.js'; +import { ProtocolVersion, StaticProtocolType } from '../../../../../common'; const REAL_NIC_NAME = 'eth0'; const REAL_NIC_NAME_1 = 'eth1'; @@ -69,10 +65,10 @@ export const getMachineNetworks = (comments: string[]): MachineNetworks => { export const getProtocolType = (comments: string[]): StaticProtocolType | null => { const machineNetworks = getMachineNetworks(comments); if (machineNetworks[ProtocolVersion.ipv4] && machineNetworks[ProtocolVersion.ipv6]) { - return 'dualStack'; + return StaticProtocolType.dualStack; } if (machineNetworks[ProtocolVersion.ipv4]) { - return 'ipv4'; + return StaticProtocolType.ipv4; } return null; }; diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/ClusterWizardContext.tsx b/libs/ui-lib/lib/ocm/components/clusterWizard/ClusterWizardContext.tsx index cf790da2b6..70a672d468 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/ClusterWizardContext.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/ClusterWizardContext.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { HostsNetworkConfigurationType } from '../../services'; -import { StaticIpView } from '../clusterConfiguration/staticIp/data/dataTypes'; import { ClusterWizardStepsType } from './wizardTransition'; -import { UISettingsValues } from '../../../common'; +import { StaticIpView, UISettingsValues } from '../../../common'; export type ClusterWizardContextType = { currentStepId: ClusterWizardStepsType; diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/ClusterWizardContextProvider.tsx b/libs/ui-lib/lib/ocm/components/clusterWizard/ClusterWizardContextProvider.tsx index 216e1860a9..a9a053aa81 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/ClusterWizardContextProvider.tsx +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/ClusterWizardContextProvider.tsx @@ -11,9 +11,12 @@ import { } from './wizardTransition'; import { HostsNetworkConfigurationType } from '../../services'; import { defaultWizardSteps, staticIpFormViewSubSteps } from './constants'; -import { StaticIpView } from '../clusterConfiguration/staticIp/data/dataTypes'; import { getStaticIpInfo } from '../clusterConfiguration/staticIp/data/fromInfraEnv'; -import { AssistedInstallerOCMPermissionTypesListType, useAlerts } from '../../../common'; +import { + AssistedInstallerOCMPermissionTypesListType, + StaticIpView, + useAlerts, +} from '../../../common'; import useSetClusterPermissions from '../../hooks/useSetClusterPermissions'; import { Cluster, InfraEnv } from '@openshift-assisted/types/assisted-installer-service'; import { useUISettings } from '../../hooks'; diff --git a/libs/ui-lib/lib/ocm/components/clusterWizard/wizardTransition.ts b/libs/ui-lib/lib/ocm/components/clusterWizard/wizardTransition.ts index 975858bc91..830f4f350f 100644 --- a/libs/ui-lib/lib/ocm/components/clusterWizard/wizardTransition.ts +++ b/libs/ui-lib/lib/ocm/components/clusterWizard/wizardTransition.ts @@ -6,7 +6,7 @@ import { WizardStepValidationMap, } from '../../../common/components/clusterWizard/validationsInfoUtils'; import { Day2WizardStepsType } from '../AddHosts/day2Wizard/constants'; -import { StaticIpInfo, StaticIpView } from '../clusterConfiguration/staticIp/data/dataTypes'; +import { StaticIpInfo } from '../clusterConfiguration/staticIp/data/dataTypes'; import { ValidationsInfo as HostValidationsInfo, Validation as HostValidation, @@ -17,6 +17,7 @@ import { Host, HostValidationId, } from '@openshift-assisted/types/assisted-installer-service'; +import { StaticIpView } from '../../../common'; export type ClusterWizardStepsType = | 'cluster-details'