From a8928d211182d70657b4e827342cb3e8889995af Mon Sep 17 00:00:00 2001 From: suomiy Date: Tue, 10 Sep 2019 16:22:58 +0200 Subject: [PATCH] kubevirt: refactor createNicRow to NICModal - add edit nic functionality - refactor nic patches (create PatchBuilder utility) --- .../create-vm-wizard-footer.tsx | 20 +- .../networks-tab-initial-state.ts | 4 +- .../form/form-select-placeholder-option.tsx | 2 + .../components/modals/modal/modal-footer.scss | 3 + .../components/modals/modal/modal-footer.tsx | 73 +++++ .../src/components/modals/nic-modal/index.ts | 1 + .../modals/nic-modal/nic-modal-enhanced.tsx | 133 ++++++++ .../components/modals/nic-modal/nic-modal.tsx | 301 ++++++++++++++++++ .../src/components/vm-disks/vm-disks.tsx | 6 +- .../src/components/vm-nics/create-nic-row.tsx | 218 ------------- .../src/components/vm-nics/nic-row.tsx | 50 +-- .../src/components/vm-nics/types.ts | 31 +- .../src/components/vm-nics/utils.ts | 50 --- .../src/components/vm-nics/vm-nics.tsx | 212 +++++------- .../kubevirt-plugin/src/constants/index.ts | 1 + .../src/constants/value-enum.ts | 20 ++ .../src/constants/vm/constants.ts | 1 + .../kubevirt-plugin/src/constants/vm/index.ts | 2 +- .../src/constants/vm/network/constants.ts | 1 + .../src/constants/vm/network/index.ts | 4 + .../vm/network/network-interface-model.ts | 39 +++ .../vm/network/network-interface-type.ts | 29 ++ .../src/constants/vm/network/network-type.ts | 55 ++++ .../kubevirt-plugin/src/constants/vm/nic.ts | 12 - .../src/hooks/use-show-error-toggler.ts | 29 ++ .../src/k8s/patches/vm-template/index.ts | 3 +- .../src/k8s/patches/vm/utils.ts | 26 ++ .../src/k8s/patches/vm/vm-boot-patches.ts | 2 +- .../src/k8s/patches/vm/vm-disk-patches.ts | 4 +- .../src/k8s/patches/vm/vm-nic-patches.ts | 100 ++++-- .../kubevirt-plugin/src/k8s/utils/index.ts | 1 - .../kubevirt-plugin/src/k8s/utils/patch.ts | 100 ++++++ .../object-with-type-property-wrapper.ts | 77 +++++ .../src/k8s/wrapper/common/wrapper.ts | 26 ++ .../wrapper/vm/network-interface-wrapper.ts | 57 ++++ .../src/k8s/wrapper/vm/network-wrapper.ts | 58 ++++ .../src/selectors/nad/combined.ts | 25 ++ .../src/selectors/nad/index.ts | 1 + .../kubevirt-plugin/src/selectors/utils.ts | 2 + .../src/selectors/vm/combined.ts | 31 +- .../src/selectors/vm/devices.ts | 3 + .../kubevirt-plugin/src/selectors/vm/disk.ts | 3 - .../kubevirt-plugin/src/selectors/vm/nic.ts | 3 - .../src/selectors/vm/selectors.ts | 57 ++-- .../kubevirt-plugin/src/selectors/vm/types.ts | 6 - .../kubevirt-plugin/src/types/vm/index.ts | 20 ++ .../kubevirt-plugin/src/utils/grammar.ts | 14 +- .../kubevirt-plugin/src/utils/index.ts | 3 + .../kubevirt-plugin/src/utils/strings.ts | 19 ++ .../src/utils/validations/common.ts | 7 +- .../src/utils/validations/vm/nic.ts | 51 ++- 51 files changed, 1401 insertions(+), 595 deletions(-) create mode 100644 frontend/packages/kubevirt-plugin/src/components/modals/modal/modal-footer.scss create mode 100644 frontend/packages/kubevirt-plugin/src/components/modals/modal/modal-footer.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/index.ts create mode 100644 frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal-enhanced.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal.tsx delete mode 100644 frontend/packages/kubevirt-plugin/src/components/vm-nics/create-nic-row.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/constants/value-enum.ts create mode 100644 frontend/packages/kubevirt-plugin/src/constants/vm/network/constants.ts create mode 100644 frontend/packages/kubevirt-plugin/src/constants/vm/network/index.ts create mode 100644 frontend/packages/kubevirt-plugin/src/constants/vm/network/network-interface-model.ts create mode 100644 frontend/packages/kubevirt-plugin/src/constants/vm/network/network-interface-type.ts create mode 100644 frontend/packages/kubevirt-plugin/src/constants/vm/network/network-type.ts delete mode 100644 frontend/packages/kubevirt-plugin/src/constants/vm/nic.ts create mode 100644 frontend/packages/kubevirt-plugin/src/hooks/use-show-error-toggler.ts create mode 100644 frontend/packages/kubevirt-plugin/src/k8s/patches/vm/utils.ts delete mode 100644 frontend/packages/kubevirt-plugin/src/k8s/utils/index.ts create mode 100644 frontend/packages/kubevirt-plugin/src/k8s/utils/patch.ts create mode 100644 frontend/packages/kubevirt-plugin/src/k8s/wrapper/common/object-with-type-property-wrapper.ts create mode 100644 frontend/packages/kubevirt-plugin/src/k8s/wrapper/common/wrapper.ts create mode 100644 frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/network-interface-wrapper.ts create mode 100644 frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/network-wrapper.ts create mode 100644 frontend/packages/kubevirt-plugin/src/selectors/nad/combined.ts create mode 100644 frontend/packages/kubevirt-plugin/src/selectors/nad/index.ts delete mode 100644 frontend/packages/kubevirt-plugin/src/selectors/vm/types.ts diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard-footer.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard-footer.tsx index cd33d1b40d1..f7341039c93 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard-footer.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard-footer.tsx @@ -10,6 +10,8 @@ import { WizardStep, } from '@patternfly/react-core'; import * as _ from 'lodash'; +import { useShowErrorToggler } from '../../hooks/use-show-error-toggler'; +import { getDialogUIError } from '../../utils/strings'; import { ALL_VM_WIZARD_TABS, VMWizardProps, VMWizardTab } from './types'; import { hasStepAllRequiredFilled, @@ -37,22 +39,14 @@ const CreateVMWizardFooterComponent: React.FC { - const [showError, setShowError] = React.useState(false); - const [prevIsValid, setPrevIsValid] = React.useState(false); - + const [showError, setShowError, checkValidity] = useShowErrorToggler(); return ( {({ onNext, onBack, onClose, activeStep, goToStepById }: WizardContext) => { const activeStepID = activeStep.id as VMWizardTab; const isLocked = _.some(ALL_VM_WIZARD_TABS, (id) => isStepLocked(stepData, id)); const isValid = isStepValid(stepData, activeStepID); - - if (isValid !== prevIsValid) { - setPrevIsValid(isValid); - if (isValid) { - setShowError(false); - } - } + checkValidity(isValid); const isFirstStep = activeStepID === VMWizardTab.VM_SETTINGS; const isFinishingStep = [VMWizardTab.REVIEW, VMWizardTab.RESULT].includes(activeStepID); @@ -65,11 +59,7 @@ const CreateVMWizardFooterComponent: React.FC {!isValid && showError && ( ({ diff --git a/frontend/packages/kubevirt-plugin/src/components/form/form-select-placeholder-option.tsx b/frontend/packages/kubevirt-plugin/src/components/form/form-select-placeholder-option.tsx index 006c920d70d..8e820c0e838 100644 --- a/frontend/packages/kubevirt-plugin/src/components/form/form-select-placeholder-option.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/form/form-select-placeholder-option.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; import { FormSelectOption } from '@patternfly/react-core'; +export const asFormSelectValue = (value) => value || ''; + // renders only when props change (shallow compare) export const FormSelectPlaceholderOption: React.FC = ({ placeholder, diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/modal/modal-footer.scss b/frontend/packages/kubevirt-plugin/src/components/modals/modal/modal-footer.scss new file mode 100644 index 00000000000..3e49a8c9cd9 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/modal/modal-footer.scss @@ -0,0 +1,3 @@ +.kubevirt-create-nic-modal__buttons { + text-align: left; +} diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/modal/modal-footer.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/modal/modal-footer.tsx new file mode 100644 index 00000000000..f722fe126e7 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/modal/modal-footer.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { Alert, Button, ButtonVariant } from '@patternfly/react-core'; +import { LoadingInline } from '@console/internal/components/utils'; +import { prefixedID } from '../../../utils'; + +import './modal-footer.scss'; + +type ModalErrorMessageProps = { + message: string; +}; + +export const ModalErrorMessage: React.FC = ({ message }) => ( + +
{message}
+
+); + +type ModalSimpleErrorMessageProps = { + message: string; +}; + +export const ModalSimpleErrorMessage: React.FC = ({ message }) => ( + +); + +type ModalFooterProps = { + id?: string; + errorMessage?: string; + isSimpleError?: boolean; + onSubmit: (e) => void; + onCancel: (e) => void; + isDisabled?: boolean; + submitButtonText?: string; + cancelButtonText?: string; +}; + +export const ModalFooter: React.FC = ({ + id, + errorMessage = null, + isDisabled = false, + isSimpleError = false, + onSubmit, + onCancel, + submitButtonText = 'Add', + cancelButtonText = 'Cancel', +}) => ( +
+ {errorMessage && isSimpleError && } + {errorMessage && !isSimpleError && } + + + {isDisabled && } +
+); diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/index.ts b/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/index.ts new file mode 100644 index 00000000000..c571da89260 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/index.ts @@ -0,0 +1 @@ +export * from './nic-modal'; diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal-enhanced.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal-enhanced.tsx new file mode 100644 index 00000000000..496a4612f31 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal-enhanced.tsx @@ -0,0 +1,133 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Firehose, FirehoseResult } from '@console/internal/components/utils'; +import { createModalLauncher, ModalComponentProps } from '@console/internal/components/factory'; +import { k8sPatch, K8sResourceKind } from '@console/internal/module/k8s'; +import { getName, getNamespace } from '@console/shared/src'; +import { getLoadedData, getResource } from '../../../utils'; +import { NetworkAttachmentDefinitionModel } from '../../../models'; +import { NetworkType } from '../../../constants/vm'; +import { getInterfaces, getUsedNetworks, asVM, getVMLikeModel } from '../../../selectors/vm'; +import { NetworkInterfaceWrapper } from '../../../k8s/wrapper/vm/network-interface-wrapper'; +import { VMLikeEntityKind } from '../../../types'; +import { getAddNicPatches } from '../../../k8s/patches/vm/vm-nic-patches'; +import { getSimpleName } from '../../../selectors/utils'; +import { NetworkWrapper } from '../../../k8s/wrapper/vm/network-wrapper'; +import { NICModal } from './nic-modal'; + +const NICModalFirehoseComponent: React.FC = (props) => { + const { nic, network, vmLikeEntity, vmLikeEntityLoading, ...restProps } = props; + + const vmLikeFinal = getLoadedData(vmLikeEntityLoading, vmLikeEntity); // default old snapshot before loading a new one + const vm = asVM(vmLikeFinal); + + const nicWrapper = nic ? NetworkInterfaceWrapper.initialize(nic) : NetworkInterfaceWrapper.EMPTY; + const networkWrapper = network ? NetworkWrapper.initialize(network) : NetworkWrapper.EMPTY; + + const usedNetworksChoices = getUsedNetworks(vm); + + const usedInterfacesNames: Set = new Set( + getInterfaces(vm) + .map(getSimpleName) + .filter((n) => n && n !== nicWrapper.getName()), + ); + + const usedMultusNetworkNames: Set = new Set( + usedNetworksChoices + .filter( + (usedNetwork) => + usedNetwork.getType() === NetworkType.MULTUS && + usedNetwork.getMultusNetworkName() !== networkWrapper.getMultusNetworkName(), + ) + .map((usedNetwork) => usedNetwork.getMultusNetworkName()), + ); + + const allowPodNetwork = + networkWrapper.isPodNetwork() || + !usedNetworksChoices.find((usedNetwork) => usedNetwork.isPodNetwork()); + + const onSubmit = (resultNetworkInterfaceWrapper, resultNetworkWrapper) => + k8sPatch( + getVMLikeModel(vmLikeEntity), + vmLikeEntity, + getAddNicPatches(vmLikeEntity, { + nic: NetworkInterfaceWrapper.mergeWrappers( + nicWrapper, + resultNetworkInterfaceWrapper, + ).asResource(), + network: NetworkWrapper.mergeWrappers(networkWrapper, resultNetworkWrapper).asResource(), + oldNICName: nicWrapper.getName(), + oldNetworkName: networkWrapper.getName(), + }), + ); + + return ( + + ); +}; + +type NICModalFirehoseComponentProps = ModalComponentProps & { + nic?: any; + network?: any; + nads?: FirehoseResult; + vmLikeEntityLoading?: FirehoseResult; + vmLikeEntity: VMLikeEntityKind; +}; + +const NICModalFirehose: React.FC = (props) => { + const { hasNADs, vmLikeEntity, ...restProps } = props; + + const namespace = getNamespace(vmLikeEntity); + const name = getName(vmLikeEntity); + + const resources = [ + getResource(getVMLikeModel(vmLikeEntity), { + name, + namespace, + prop: 'vmLikeEntityLoading', + isList: false, + }), + ]; + + if (hasNADs) { + resources.push( + getResource(NetworkAttachmentDefinitionModel, { + namespace, + prop: 'nads', + optional: true, + }), + ); + } + + return ( + + + + ); +}; + +type NICModalFirehoseProps = ModalComponentProps & { + vmLikeEntity: VMLikeEntityKind; + nic?: any; + network?: any; + hasNADs: boolean; +}; + +const cloneVMModalStateToProps = ({ k8s }) => { + const hasNADs = !!k8s.getIn(['RESOURCES', 'models', NetworkAttachmentDefinitionModel.kind]); + return { + hasNADs, + }; +}; + +const NICModalConnected = connect(cloneVMModalStateToProps)(NICModalFirehose); + +export const nicModalEnhanced = createModalLauncher(NICModalConnected); diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal.tsx new file mode 100644 index 00000000000..46a15f25cfd --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal.tsx @@ -0,0 +1,301 @@ +import * as React from 'react'; +import { Form, FormSelect, FormSelectOption, TextInput } from '@patternfly/react-core'; +import { + FirehoseResult, + HandlePromiseProps, + withHandlePromise, +} from '@console/internal/components/utils'; +import { + createModalLauncher, + ModalBody, + ModalComponentProps, + ModalTitle, +} from '@console/internal/components/factory'; +import { K8sResourceKind } from '@console/internal/module/k8s'; +import { getLoadedData, getLoadError, isLoaded, prefixedID } from '../../../utils'; +import { NetworkAttachmentDefinitionModel } from '../../../models'; +import { validateNIC } from '../../../utils/validations/vm'; +import { isValidationError } from '../../../utils/validations/common'; +import { FormRow } from '../../form/form-row'; +import { ValidationErrorType } from '../../../utils/validations/types'; +import { ignoreCaseSort } from '../../../utils/sort'; +import { + asFormSelectValue, + FormSelectPlaceholderOption, +} from '../../form/form-select-placeholder-option'; +import { + NetworkInterfaceType, + NetworkInterfaceModel, + NetworkType, +} from '../../../constants/vm/network'; +import { getNetworkChoices } from '../../../selectors/nad'; +import { NetworkInterfaceWrapper } from '../../../k8s/wrapper/vm/network-interface-wrapper'; +import { NetworkWrapper } from '../../../k8s/wrapper/vm/network-wrapper'; +import { getDialogUIError, getSequenceName } from '../../../utils/strings'; +import { ModalFooter } from '../modal/modal-footer'; +import { useShowErrorToggler } from '../../../hooks/use-show-error-toggler'; + +export type NetworkProps = { + id: string; + isDisabled: boolean; + nads?: FirehoseResult; + usedMultusNetworkNames: Set; + allowPodNetwork: boolean; + network?: NetworkWrapper; + onChange: (networkChoice: NetworkWrapper) => void; +}; + +export const Network: React.FC = ({ + id, + isDisabled, + network, + onChange, + nads, + usedMultusNetworkNames, + allowPodNetwork, +}) => { + const nadsLoading = !isLoaded(nads); + const nadsLoadError = getLoadError(nads, NetworkAttachmentDefinitionModel); + const networkChoices = getNetworkChoices( + getLoadedData(nads, []), + usedMultusNetworkNames, + allowPodNetwork, + ); + + return ( + + { + const target = event.target as HTMLSelectElement; + const newNetworkType = target[target.selectedIndex].getAttribute('data-network-type'); + onChange( + NetworkWrapper.initializeFromSimpleData({ + type: NetworkType.fromString(newNetworkType), + multusNetworkName: net, + }), + ); + }} + value={asFormSelectValue(network && network.getReadableName())} + id={id} + isDisabled={isDisabled || nadsLoading || nadsLoadError} + > + + {ignoreCaseSort(networkChoices, ['readableName']).map((networkWrapper: NetworkWrapper) => { + const readableName = networkWrapper.getReadableName(); + return ( + + ); + })} + + + ); +}; + +export const NICModal = withHandlePromise((props: NICModalProps) => { + const { + network, + nads, + usedInterfacesNames, + usedMultusNetworkNames, + allowPodNetwork, + onSubmit, + inProgress, + errorMessage, + handlePromise, + close, + cancel, + } = props; + const asId = prefixedID.bind(null, 'nic'); + const nic = props.nic || NetworkInterfaceWrapper.EMPTY; + const isEditing = nic !== NetworkInterfaceWrapper.EMPTY; + + const [name, setName] = React.useState( + nic.getName() || getSequenceName('nic', usedInterfacesNames), + ); + const [model, setModel] = React.useState( + nic.getModel() || (isEditing ? null : NetworkInterfaceModel.VIRTIO), + ); + const [resultNetwork, setResultNetwork] = React.useState(network); + const [interfaceType, setInterfaceType] = React.useState(nic.getType()); + const [macAddress, setMacAddress] = React.useState(nic.getMACAddress() || ''); + + const resultNIC = NetworkInterfaceWrapper.initializeFromSimpleData({ + name, + model, + interfaceType, + macAddress, + }); + + const { + validations: { name: nameValidation, macAddress: macAddressValidation }, + isValid, + hasAllRequiredFilled, + } = validateNIC(resultNIC, resultNetwork, { usedInterfacesNames }); + + const [showUIError, setShowUIError] = useShowErrorToggler(false, isValid, isValid); + + const onNetworkChoiceChange = (newNetworkChoice: NetworkWrapper) => { + if (newNetworkChoice.isPodNetwork()) { + setMacAddress(''); + } + + if (!interfaceType || !newNetworkChoice.getType().allowsInterfaceType(interfaceType)) { + setInterfaceType(newNetworkChoice.getType().getDefaultInterfaceType()); + } + setResultNetwork(newNetworkChoice); + }; + + const submit = (e) => { + e.preventDefault(); + + if (isValid) { + // eslint-disable-next-line promise/catch-or-return + handlePromise( + onSubmit( + resultNIC, + NetworkWrapper.mergeWrappers( + resultNetwork, + NetworkWrapper.initializeFromSimpleData({ name: resultNIC.getName() }), + ), + ), + ).then(close); + } else { + setShowUIError(true); + } + }; + + return ( +
+ {isEditing ? 'Edit' : 'Add'} Network Interface + +
+ + setName(v)} + /> + + + + setModel(NetworkInterfaceModel.fromString(networkInterfaceModel)) + } + value={asFormSelectValue(model)} + id={asId('model')} + isDisabled={inProgress} + > + + {NetworkInterfaceModel.getAll().map((ifaceModel) => { + return ( + + ); + })} + + + + + setInterfaceType(NetworkInterfaceType.fromString(iType))} + value={asFormSelectValue(interfaceType)} + id={asId('type')} + isDisabled={inProgress} + > + + {(resultNetwork.getType() + ? resultNetwork.getType().getAllowedInterfaceTypes() + : NetworkInterfaceType.getAll() + ).map((iType) => ( + + ))} + + + + setMacAddress(v)} + /> + + +
+ { + e.stopPropagation(); + cancel(); + }} + /> +
+ ); +}); + +export type NICModalProps = { + nic: NetworkInterfaceWrapper; + network: NetworkWrapper; + onSubmit: (networkInterface: NetworkInterfaceWrapper, network: NetworkWrapper) => Promise; + nads?: FirehoseResult; + usedInterfacesNames: Set; + usedMultusNetworkNames: Set; + allowPodNetwork: boolean; +} & ModalComponentProps & + HandlePromiseProps; + +export const nicModal = createModalLauncher(NICModal); diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx index 13e0e596e20..88050672717 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx @@ -1,4 +1,3 @@ -import * as _ from 'lodash'; import * as React from 'react'; import { Button } from 'patternfly-react'; import { Alert, AlertActionCloseButton } from '@patternfly/react-core'; @@ -26,6 +25,7 @@ import { } from '../../selectors/dv/selectors'; import { VMLikeEntityTabProps } from '../vms/types'; import { getResource } from '../../utils'; +import { getSimpleName } from '../../selectors/utils'; import { DiskRow } from './disk-row'; import { StorageBundle, StorageRowType, VMDiskRowProps } from './types'; import { CreateDiskRowFirehose } from './create-disk-row'; @@ -58,7 +58,7 @@ const getStoragesData = ( const pvcLookup = createLookup(pvcs, getName); const datavolumeLookup = createLookup(datavolumes, getName); - const volumeLookup = createBasicLookup(getVolumes(vm), (volume) => _.get(volume, 'name')); + const volumeLookup = createBasicLookup(getVolumes(vm), getSimpleName); const datavolumeTemplatesLookup = createBasicLookup(getDataVolumeTemplates(vm), getName); const disksWithType = getDisks(vm).map((disk) => { @@ -177,7 +177,7 @@ export const VMDisks: React.FC = ({ vmLikeEntity, pvcs, datavolume customData={{ vmLikeEntity, vm, - diskLookup: createBasicLookup(getDisks(vm), (disk) => _.get(disk, 'name')), + diskLookup: createBasicLookup(getDisks(vm), getSimpleName), onCreateRowDismiss: () => { setIsCreating(false); }, diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-nics/create-nic-row.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-nics/create-nic-row.tsx deleted file mode 100644 index 0bb147be2dd..00000000000 --- a/frontend/packages/kubevirt-plugin/src/components/vm-nics/create-nic-row.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import * as React from 'react'; -import * as _ from 'lodash'; -import { CancelAcceptButtons, Dropdown, Text } from 'kubevirt-web-ui-components'; -import { connect } from 'react-redux'; -import { TableData, TableRow } from '@console/internal/components/factory'; -import { Firehose, FirehoseResult, LoadingInline } from '@console/internal/components/utils'; -import { FormGroup, HelpBlock } from 'patternfly-react'; -import { useSafetyFirst } from '@console/internal/components/safety-first'; -import { k8sPatch, K8sResourceKind } from '@console/internal/module/k8s'; -import { getNamespace } from '@console/shared'; -import { NetworkAttachmentDefinitionModel } from '../../models'; -import { getAddNicPatches } from '../../k8s/patches/vm/vm-nic-patches'; -import { VMKind, VMLikeEntityKind } from '../../types'; -import { getNetworkChoices, getVMLikeModel } from '../../selectors/vm'; -import { dimensifyRow } from '../../utils/table'; -import { NetworkType } from '../../constants/vm'; -import { getValidationErrorMessage, getValidationErrorType } from '../../utils/validations/common'; -import { validateMACAddress, validateNicName } from '../../utils/validations/vm'; -import { GENERAL_ERROR_MSG } from '../../utils/validations/strings'; -import { getResource } from '../../utils'; -import { getDefaultNetworkBinding, getNetworkBindings, nicTableColumnClasses } from './utils'; -import { VMNicRowProps } from './types'; -import '../vm-disks/_create-device-row.scss'; - -const createNic = ({ - vmLikeEntity, - nic, -}: { - vmLikeEntity: VMLikeEntityKind; - nic: any; -}): Promise => - k8sPatch(getVMLikeModel(vmLikeEntity), vmLikeEntity, getAddNicPatches(vmLikeEntity, nic)); - -type NetworkColumn = { - network: string; - onChange: (string) => void; - hasNADs: boolean; - nads: FirehoseResult; - vm: VMKind; - creating: boolean; -}; - -const NetworkColumn: React.FC = ({ - network, - vm, - onChange, - nads, - hasNADs, - creating, -}) => { - if (!hasNADs || nads.loadError || nads.loaded) { - const loadedNads = _.get(nads, 'data') || []; - const networkChoices = getNetworkChoices(vm, loadedNads); - const networkValue = - network || - (networkChoices.length === 0 - ? '--- No Network Definition Available ---' - : '--- Select Network Definition ---'); - return ( - - ); - } - return ; -}; - -type CreateNicRowProps = VMNicRowProps & { nads?: FirehoseResult }; - -export const CreateNicRow: React.FC = ({ - nads, - hasNADs, - customData: { - vm, - preferableNicBus, - vmLikeEntity, - interfaceLookup, - onCreateRowDismiss, - onCreateRowError, - forceRerender, - }, - index, - style, -}) => { - const [creating, setCreating] = useSafetyFirst(false); - const [name, setName] = React.useState(''); - const [model] = React.useState(preferableNicBus); - const [network, setNetwork] = React.useState(null); - const [binding, setBinding] = React.useState(null); - const [macAddress, setMacAddress] = React.useState(''); - const networkType = _.get(network, 'networkType'); - - const dimensify = dimensifyRow(nicTableColumnClasses); - const id = 'create-nic-row'; - - const nameError = validateNicName(name, interfaceLookup); - const macAddressError = validateMACAddress(macAddress); - const isValid = !nameError && !macAddressError && network && binding; - - return ( - - - - { - setName(v); - forceRerender(); - }} - value={name} - /> - {getValidationErrorMessage(nameError)} - - - - {model} - - - { - const { networkType: newNetworkType } = net; - if (newNetworkType === NetworkType.POD) { - setMacAddress(''); - } - - if (!binding || !getNetworkBindings(newNetworkType).includes(binding)) { - setBinding(getDefaultNetworkBinding(newNetworkType)); - } - - setNetwork(net); - }} - hasNADs={hasNADs} - nads={nads} - vm={vm} - creating={creating} - /> - - - - - - - { - setMacAddress(v); - forceRerender(); - }} - value={macAddress} - disabled={creating || networkType === NetworkType.POD} - /> - {getValidationErrorMessage(macAddressError)} - - - - { - setCreating(true); - createNic({ vmLikeEntity, nic: { name, model, network, binding, mac: macAddress } }) - .then(onCreateRowDismiss) - .catch((error) => { - onCreateRowError((error && error.message) || GENERAL_ERROR_MSG); - setCreating(false); - }); - }} - disabled={!isValid} - /> - - - ); -}; - -const CreateNicRowFirehose: React.FC = (props) => { - if (props.hasNADs) { - const resources = [ - getResource(NetworkAttachmentDefinitionModel, { - namespace: getNamespace(props.customData.vmLikeEntity), - prop: 'nads', - optional: true, - }), - ]; - - return ( - - - - ); - } - - return ; -}; - -const stateToProps = ({ k8s }) => { - return { - hasNADs: !!k8s.getIn(['RESOURCES', 'models', NetworkAttachmentDefinitionModel.kind]), - }; -}; - -export const CreateNicRowConnected = connect(stateToProps)(CreateNicRowFirehose); diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx index 875d5c7385c..d405086bb55 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx @@ -1,18 +1,33 @@ import * as React from 'react'; import { TableData, TableRow } from '@console/internal/components/factory'; import { asAccessReview, Kebab, KebabOption } from '@console/internal/components/utils'; -import { getDeletetionTimestamp, DASH } from '@console/shared'; +import { DASH, getDeletetionTimestamp } from '@console/shared/src'; import { TemplateModel } from '@console/internal/models'; -import { BUS_VIRTIO } from '../../constants/vm'; import { deleteDeviceModal, DeviceType } from '../modals/delete-device-modal'; -import { VMLikeEntityKind } from '../../types'; import { VirtualMachineModel } from '../../models'; import { isVM } from '../../selectors/vm'; import { dimensifyRow } from '../../utils/table'; +import { VMLikeEntityKind } from '../../types'; +import { nicModalEnhanced } from '../modals/nic-modal/nic-modal-enhanced'; import { nicTableColumnClasses } from './utils'; import { VMNicRowProps } from './types'; -const menuActionDelete = (vmLikeEntity: VMLikeEntityKind, nic): KebabOption => ({ +const menuActionEdit = (nic, network, vmLikeEntity: VMLikeEntityKind): KebabOption => ({ + label: 'Edit', + callback: () => + nicModalEnhanced({ + vmLikeEntity, + nic, + network, + }), + accessReview: asAccessReview( + isVM(vmLikeEntity) ? VirtualMachineModel : TemplateModel, + vmLikeEntity, + 'patch', + ), +}); + +const menuActionDelete = (nic, network, vmLikeEntity: VMLikeEntityKind): KebabOption => ({ label: 'Delete', callback: () => deleteDeviceModal({ @@ -27,33 +42,32 @@ const menuActionDelete = (vmLikeEntity: VMLikeEntityKind, nic): KebabOption => ( ), }); -const getActions = (vmLikeEntity: VMLikeEntityKind, nic) => { - const actions = [menuActionDelete]; - return actions.map((a) => a(vmLikeEntity, nic)); +const getActions = (nic, network, vmLikeEntity: VMLikeEntityKind) => { + const actions = [menuActionEdit, menuActionDelete]; + return actions.map((a) => a(nic, network, vmLikeEntity)); }; export const NicRow: React.FC = ({ - obj: { networkName, binding, nic }, + obj: { name, model, networkName, interfaceType, macAddress, nic, network }, customData: { vmLikeEntity }, index, style, }) => { - const nicName = nic.name; const dimensify = dimensifyRow(nicTableColumnClasses); return ( - - {nicName} - {nic.model || BUS_VIRTIO} - {networkName} - {binding || DASH} - {nic.macAddress || DASH} + + {name} + {model || DASH} + {networkName || DASH} + {interfaceType || DASH} + {macAddress || DASH} diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-nics/types.ts b/frontend/packages/kubevirt-plugin/src/components/vm-nics/types.ts index 0d71c685c8a..eff2354fcd9 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-nics/types.ts +++ b/frontend/packages/kubevirt-plugin/src/components/vm-nics/types.ts @@ -1,31 +1,22 @@ -import { EntityMap } from '@console/shared'; -import { VMKind, VMLikeEntityKind } from '../../types'; - -export enum NetworkRowType { - NETWORK_TYPE_VM = 'network-type-vm', - NETWORK_TYPE_CREATE = 'network-type-create', -} +import { VMLikeEntityKind } from '../../types'; export type NetworkBundle = { name?: string; + model?: string; networkName: string; - binding: string; - networkType: NetworkRowType; - nic?: any; + interfaceType?: string; + macAddress?: string; + nic: any; + network: any; +}; + +export type VMNicRowCustomData = { + vmLikeEntity: VMLikeEntityKind; }; export type VMNicRowProps = { obj: NetworkBundle; + customData: VMNicRowCustomData; index: number; style: object; - hasNADs?: boolean; - customData: { - vmLikeEntity: VMLikeEntityKind; - vm: VMKind; - interfaceLookup: EntityMap; - preferableNicBus: string; - onCreateRowDismiss: () => void; - onCreateRowError: (error: string) => void; - forceRerender: () => void; - }; }; diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-nics/utils.ts b/frontend/packages/kubevirt-plugin/src/components/vm-nics/utils.ts index 50531786d30..51645b9ab8a 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-nics/utils.ts +++ b/frontend/packages/kubevirt-plugin/src/components/vm-nics/utils.ts @@ -1,55 +1,5 @@ import * as classNames from 'classnames'; import { Kebab } from '@console/internal/components/utils'; -import { DASH } from '@console/shared'; -import { NetworkBinding, NetworkType, POD_NETWORK } from '../../constants/vm'; - -export const getNetworkBindings = (networkType) => { - switch (networkType) { - case NetworkType.MULTUS: - return [NetworkBinding.BRIDGE, NetworkBinding.SRIOV]; - case NetworkType.POD: - default: - return [NetworkBinding.MASQUERADE, NetworkBinding.BRIDGE, NetworkBinding.SRIOV]; - } -}; - -export const getDefaultNetworkBinding = (networkType) => { - switch (networkType) { - case NetworkType.MULTUS: - return NetworkBinding.BRIDGE; - case NetworkType.POD: - return NetworkBinding.MASQUERADE; - default: - return null; - } -}; - -export const getInterfaceBinding = (intface) => { - if (intface.bridge) { - return NetworkBinding.BRIDGE; - } - if (intface.sriov) { - return NetworkBinding.SRIOV; - } - if (intface.masquerade) { - return NetworkBinding.MASQUERADE; - } - return null; -}; - -export const getNetworkName = (network) => { - if (network) { - // eslint-disable-next-line no-prototype-builtins - if (network.hasOwnProperty('pod')) { - return POD_NETWORK; - } - // eslint-disable-next-line no-prototype-builtins - if (network.hasOwnProperty('multus')) { - return network.multus.networkName; - } - } - return DASH; -}; export const nicTableColumnClasses = [ classNames('col-lg-3'), diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-nics/vm-nics.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-nics/vm-nics.tsx index 0c65f8ebed8..920f74a5898 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-nics/vm-nics.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-nics/vm-nics.tsx @@ -1,142 +1,104 @@ import * as React from 'react'; import { Button } from 'patternfly-react'; -import { Alert, AlertActionCloseButton } from '@patternfly/react-core'; -import * as _ from 'lodash'; import { Table } from '@console/internal/components/factory'; -import { useSafetyFirst } from '@console/internal/components/safety-first'; import { sortable } from '@patternfly/react-table'; import { createBasicLookup } from '@console/shared'; import { VMLikeEntityKind } from '../../types'; -import { getInterfaces, getNetworks, getVmPreferableNicBus, asVM } from '../../selectors/vm'; +import { getInterfaces, getNetworks, asVM } from '../../selectors/vm'; import { dimensifyHeader } from '../../utils/table'; import { VMLikeEntityTabProps } from '../vms/types'; +import { NetworkInterfaceWrapper } from '../../k8s/wrapper/vm/network-interface-wrapper'; +import { nicModalEnhanced } from '../modals/nic-modal/nic-modal-enhanced'; +import { getSimpleName } from '../../selectors/utils'; +import { NetworkWrapper } from '../../k8s/wrapper/vm/network-wrapper'; import { NicRow } from './nic-row'; -import { NetworkBundle, NetworkRowType, VMNicRowProps } from './types'; -import { CreateNicRowConnected } from './create-nic-row'; -import { getInterfaceBinding, getNetworkName, nicTableColumnClasses } from './utils'; +import { NetworkBundle } from './types'; +import { nicTableColumnClasses } from './utils'; -export const VMNicRow: React.FC = (props) => { - switch (props.obj.networkType) { - case NetworkRowType.NETWORK_TYPE_VM: - return ; - case NetworkRowType.NETWORK_TYPE_CREATE: - return ; - default: - return null; - } -}; - -const getNicsData = ( - vmLikeEntity: VMLikeEntityKind, - addNewNic: boolean, - rerenderFlag: boolean, -): NetworkBundle[] => { +const getNicsData = (vmLikeEntity: VMLikeEntityKind): NetworkBundle[] => { const vm = asVM(vmLikeEntity); - const networkLookup = createBasicLookup(getNetworks(vm), (network) => _.get(network, 'name')); - - const nicsWithType = getInterfaces(vm).map((nic) => ({ - ...nic, // for sorting - networkType: NetworkRowType.NETWORK_TYPE_VM, - networkName: getNetworkName(networkLookup[nic.name]), - binding: getInterfaceBinding(nic), - nic, - })); + const networkLookup = createBasicLookup(getNetworks(vm), getSimpleName); - return addNewNic - ? [{ networkType: NetworkRowType.NETWORK_TYPE_CREATE, rerenderFlag }, ...nicsWithType] - : nicsWithType; + return getInterfaces(vm).map((nic) => { + const network = networkLookup[nic.name]; + const interfaceWrapper = NetworkInterfaceWrapper.initialize(nic); + const networkWrapper = NetworkWrapper.initialize(network); + return { + nic, + network, + // for sorting + name: interfaceWrapper.getName(), + model: interfaceWrapper.getReadableModel(), + networkName: networkWrapper.getReadableName(), + interfaceType: interfaceWrapper.getTypeValue(), + macAddress: interfaceWrapper.getMACAddress(), + }; + }); }; -export const VMNics: React.FC = ({ obj: vmLikeEntity }) => { - const [isCreating, setIsCreating] = useSafetyFirst(false); - const [createError, setCreateError] = useSafetyFirst(null); - const [rerenderFlag, setRerenderFlag] = useSafetyFirst(false); // TODO: HACK: fire changes in Virtualize Table for CreateNicRow. Remove after deprecating CreateNicRow - - const vm = asVM(vmLikeEntity); - const preferableNicBus = getVmPreferableNicBus(vm); - - return ( -
-
-
- -
-
-
- {createError && ( - setCreateError(null)} />} - /> - )} - - dimensifyHeader( - [ - { - title: 'Name', - sortField: 'name', - transforms: [sortable], - }, - { - title: 'Model', - sortField: 'model', - transforms: [sortable], - }, - { - title: 'Network', - sortField: 'networkName', - transforms: [sortable], - }, - { - title: 'Binding Method', - sortField: 'binding', - transforms: [sortable], - }, - { - title: 'MAC Address', - sortField: 'macAddress', - transforms: [sortable], - }, - { - title: '', - }, - ], - nicTableColumnClasses, - ) +export const VMNics: React.FC = ({ obj: vmLikeEntity }) => ( +
+
+
+
- ); -}; +
+
+ dimensifyHeader( + [ + { + title: 'Name', + sortField: 'name', + transforms: [sortable], + }, + { + title: 'Model', + sortField: 'model', + transforms: [sortable], + }, + { + title: 'Network', + sortField: 'networkName', + transforms: [sortable], + }, + { + title: 'Type', + sortField: 'interfaceType', + transforms: [sortable], + }, + { + title: 'MAC Address', + sortField: 'macAddress', + transforms: [sortable], + }, + { + title: '', + }, + ], + nicTableColumnClasses, + ) + } + Row={NicRow} + customData={{ + vmLikeEntity, + }} + virtualize + loaded + /> + + +); diff --git a/frontend/packages/kubevirt-plugin/src/constants/index.ts b/frontend/packages/kubevirt-plugin/src/constants/index.ts index 772837351d0..f7c7de6422e 100644 --- a/frontend/packages/kubevirt-plugin/src/constants/index.ts +++ b/frontend/packages/kubevirt-plugin/src/constants/index.ts @@ -2,3 +2,4 @@ export * from './vm'; export * from './vm-templates'; export * from './cdi'; export * from './namespace'; +export * from './value-enum'; diff --git a/frontend/packages/kubevirt-plugin/src/constants/value-enum.ts b/frontend/packages/kubevirt-plugin/src/constants/value-enum.ts new file mode 100644 index 00000000000..b6e0c9380e2 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/value-enum.ts @@ -0,0 +1,20 @@ +import * as _ from 'lodash'; + +export abstract class ValueEnum { + protected static getAllClassEnumProperties = (Clazz: Function) => + Object.keys(Clazz) + .filter((value) => Clazz[value] instanceof Clazz) + .map((key) => Clazz[key]) as A[]; + + static getAll = () => Object.freeze([]); + + protected readonly value: T; + + protected constructor(value: T) { + this.value = value; + } + + getValue = () => this.value; + + toString = () => _.toString(this.value); +} diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/constants.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/constants.ts index 854f3c3ccc7..ce03f827767 100644 --- a/frontend/packages/kubevirt-plugin/src/constants/vm/constants.ts +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/constants.ts @@ -1,5 +1,6 @@ export const VIRT_LAUNCHER_POD_PREFIX = 'virt-launcher-'; export const BUS_VIRTIO = 'virtio'; +export const READABLE_VIRTIO = 'VirtIO'; export const ANNOTATION_FIRST_BOOT = 'kubevirt.ui/firstBoot'; export const CUSTOM_FLAVOR = 'Custom'; diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/index.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/index.ts index 479d4d72cfa..6af1fb13e01 100644 --- a/frontend/packages/kubevirt-plugin/src/constants/vm/index.ts +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/index.ts @@ -1,2 +1,2 @@ export * from './constants'; -export * from './nic'; +export * from './network'; diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/network/constants.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/network/constants.ts new file mode 100644 index 00000000000..f7a90ddd983 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/network/constants.ts @@ -0,0 +1 @@ +export const POD_NETWORK = 'Pod Networking'; diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/network/index.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/network/index.ts new file mode 100644 index 00000000000..3669207179c --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/network/index.ts @@ -0,0 +1,4 @@ +export * from './constants'; +export * from './network-interface-type'; +export * from './network-interface-model'; +export * from './network-type'; diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/network/network-interface-model.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/network/network-interface-model.ts new file mode 100644 index 00000000000..cb61ea7ecbe --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/network/network-interface-model.ts @@ -0,0 +1,39 @@ +/* eslint-disable lines-between-class-members */ +import { ValueEnum } from '../../value-enum'; +import { READABLE_VIRTIO } from '../constants'; + +export class NetworkInterfaceModel extends ValueEnum { + static readonly VIRTIO = new NetworkInterfaceModel('virtio'); + static readonly E1000 = new NetworkInterfaceModel('e1000'); + static readonly E1000E = new NetworkInterfaceModel('e1000e'); + static readonly NE2kPCI = new NetworkInterfaceModel('ne2kPCI'); + static readonly PCNET = new NetworkInterfaceModel('pcnet'); + static readonly RTL8139 = new NetworkInterfaceModel('rtl8139'); + + private static readonly ALL = Object.freeze( + ValueEnum.getAllClassEnumProperties(NetworkInterfaceModel), + ); + + private static readonly stringMapper = NetworkInterfaceModel.ALL.reduce( + (accumulator, networkInterfaceModel: NetworkInterfaceModel) => ({ + ...accumulator, + [networkInterfaceModel.value]: networkInterfaceModel, + }), + {}, + ); + + static getAll = () => NetworkInterfaceModel.ALL; + + static fromString = (model: string): NetworkInterfaceModel => + NetworkInterfaceModel.stringMapper[model]; + + static fromSerialized = (networkInterfaceModel: { value: string }): NetworkInterfaceModel => + NetworkInterfaceModel.fromString(networkInterfaceModel && networkInterfaceModel.value); + + toString = () => { + if (this === NetworkInterfaceModel.VIRTIO) { + return READABLE_VIRTIO; + } + return this.value; + }; +} diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/network/network-interface-type.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/network/network-interface-type.ts new file mode 100644 index 00000000000..39c05b49b58 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/network/network-interface-type.ts @@ -0,0 +1,29 @@ +/* eslint-disable lines-between-class-members */ +import { ValueEnum } from '../../value-enum'; + +export class NetworkInterfaceType extends ValueEnum { + static readonly MASQUERADE = new NetworkInterfaceType('masquerade'); + static readonly BRIDGE = new NetworkInterfaceType('bridge'); + static readonly SRIOV = new NetworkInterfaceType('sriov'); + static readonly SLIRP = new NetworkInterfaceType('slirp'); + + private static readonly ALL = Object.freeze( + ValueEnum.getAllClassEnumProperties(NetworkInterfaceType), + ); + + private static readonly stringMapper = NetworkInterfaceType.ALL.reduce( + (accumulator, networkType: NetworkInterfaceType) => ({ + ...accumulator, + [networkType.value]: networkType, + }), + {}, + ); + + static getAll = () => NetworkInterfaceType.ALL; + + static fromString = (model: string): NetworkInterfaceType => + NetworkInterfaceType.stringMapper[model]; + + static fromSerialized = (networkInterfaceType: { value: string }): NetworkInterfaceType => + NetworkInterfaceType.fromString(networkInterfaceType && networkInterfaceType.value); +} diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/network/network-type.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/network/network-type.ts new file mode 100644 index 00000000000..df5a192273d --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/network/network-type.ts @@ -0,0 +1,55 @@ +/* eslint-disable lines-between-class-members */ +import { ValueEnum } from '../../value-enum'; +import { NetworkInterfaceType } from './network-interface-type'; + +export class NetworkType extends ValueEnum { + static readonly MULTUS = new NetworkType('multus', NetworkInterfaceType.BRIDGE, [ + NetworkInterfaceType.BRIDGE, + NetworkInterfaceType.SRIOV, + ]); + static readonly POD = new NetworkType('pod', NetworkInterfaceType.MASQUERADE, [ + NetworkInterfaceType.MASQUERADE, + NetworkInterfaceType.BRIDGE, + NetworkInterfaceType.SRIOV, + ]); + static readonly GENIE = new NetworkType('genie', NetworkInterfaceType.BRIDGE, [ + NetworkInterfaceType.BRIDGE, + ]); + + private static ALL = Object.freeze(ValueEnum.getAllClassEnumProperties(NetworkType)); + + private static stringMapper = NetworkType.ALL.reduce( + (accumulator, networkType: NetworkType) => ({ + ...accumulator, + [networkType.value]: networkType, + }), + {}, + ); + + static getAll = () => NetworkType.ALL; + + static fromString = (model: string): NetworkType => NetworkType.stringMapper[model]; + + static fromSerialized = (networkType: { value: string }): NetworkType => + NetworkType.fromString(networkType && networkType.value); + + private readonly defaultInterfaceType: NetworkInterfaceType; + private readonly allowedInterfaceTypes: Readonly; + private readonly allowedInterfaceTypesSet: Set; + + private constructor( + value?: string, + defaultInterfaceType?: NetworkInterfaceType, + allowedInterfaceTypes?: NetworkInterfaceType[], + ) { + super(value); + this.defaultInterfaceType = defaultInterfaceType; + this.allowedInterfaceTypes = Object.freeze(allowedInterfaceTypes); + this.allowedInterfaceTypesSet = new Set(allowedInterfaceTypes); + } + + getDefaultInterfaceType = () => this.defaultInterfaceType; + getAllowedInterfaceTypes = () => this.allowedInterfaceTypes; + allowsInterfaceType = (interfaceType: NetworkInterfaceType) => + this.allowedInterfaceTypesSet.has(interfaceType); +} diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/nic.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/nic.ts deleted file mode 100644 index e6a3d8ac275..00000000000 --- a/frontend/packages/kubevirt-plugin/src/constants/vm/nic.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const POD_NETWORK = 'Pod Networking'; - -export enum NetworkType { - MULTUS = 'multus', // compatible with web-ui-components constants - POD = 'pod', -} - -export enum NetworkBinding { - MASQUERADE = 'masquerade', - BRIDGE = 'bridge', - SRIOV = 'sriov', -} diff --git a/frontend/packages/kubevirt-plugin/src/hooks/use-show-error-toggler.ts b/frontend/packages/kubevirt-plugin/src/hooks/use-show-error-toggler.ts new file mode 100644 index 00000000000..b78d0207c8e --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/hooks/use-show-error-toggler.ts @@ -0,0 +1,29 @@ +import * as React from 'react'; + +export const useShowErrorToggler = ( + initialShowError: boolean = false, + initialIsValid: boolean = false, + checkIsValid?: boolean, +) => { + const [showError, setShowError] = React.useState(initialShowError); + const [prevIsValid, setPrevIsValid] = React.useState(initialIsValid); + + const checkValidity = (isValid: boolean) => { + if (isValid !== prevIsValid) { + setPrevIsValid(isValid); + if (isValid) { + setShowError(false); + } + } + }; + + if (checkIsValid != null) { + checkValidity(checkIsValid); + } + + return [showError, setShowError, checkValidity] as [ + boolean, + React.Dispatch>, + (isValid: boolean) => void + ]; +}; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm-template/index.ts b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm-template/index.ts index 46d21c55694..6257d61da31 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm-template/index.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm-template/index.ts @@ -1,3 +1,4 @@ +import * as _ from 'lodash'; import { TemplateKind, Patch } from '@console/internal/module/k8s'; import { VMLikeEntityKind, VMKind } from '../../../types'; import { isVM } from '../../../selectors/vm'; @@ -26,7 +27,7 @@ export const getVMLikePatches = ( templatePrefix = getTemplatePatchPrefix(vmLikeEntity as TemplateKind, vm); } - const patches = vm ? patchesSupplier(vm) : []; + const patches = _.compact(vm ? patchesSupplier(vm) : []); return templatePrefix ? patches.map((p) => addPrefixToPatch(templatePrefix, p)) : patches; }; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/utils.ts b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/utils.ts new file mode 100644 index 00000000000..c63a27da8d7 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/utils.ts @@ -0,0 +1,26 @@ +import * as _ from 'lodash'; +import { getSimpleName } from '../../../selectors/utils'; +import { getDeviceBootOrder } from '../../../selectors/vm'; +import { PatchBuilder } from '../../utils/patch'; + +export const getShiftBootOrderPatches = ( + path: string, + devices: any[], + removedDeviceName: string, + removedDeviceBootOrder: number = -1, +) => { + const devicesWithoutRemovedDevice = + removedDeviceName == null + ? devices + : devices.filter((device) => getSimpleName(device) !== removedDeviceName); + + return devicesWithoutRemovedDevice + .filter((device) => getDeviceBootOrder(device, -1) > removedDeviceBootOrder) + .map((device) => { + const patchedDevice = _.cloneDeep(device); + patchedDevice.bootOrder = getDeviceBootOrder(patchedDevice) - 1; + return new PatchBuilder(path) + .setListUpdate(patchedDevice, devicesWithoutRemovedDevice, getSimpleName) + .build(); + }); +}; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-boot-patches.ts b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-boot-patches.ts index 82d989c426c..545610c89c6 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-boot-patches.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-boot-patches.ts @@ -2,7 +2,7 @@ import * as _ from 'lodash'; import { ANNOTATION_FIRST_BOOT, BOOT_ORDER_FIRST, BOOT_ORDER_SECOND } from '../../../constants/vm'; import { getBootDeviceIndex, getDisks, getInterfaces } from '../../../selectors/vm'; import { VMKind } from '../../../types/vm'; -import { patchSafeValue } from '../../utils'; +import { patchSafeValue } from '../../utils/patch'; export const getPxeBootPatch = (vm: VMKind) => { const patches = []; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-disk-patches.ts b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-disk-patches.ts index 52ccac734aa..9fa3c22bd53 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-disk-patches.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-disk-patches.ts @@ -3,7 +3,7 @@ import { getAddDiskPatch, getDeviceBootOrderPatch } from 'kubevirt-web-ui-compon import { ConfigMapKind, Patch } from '@console/internal/module/k8s'; import { getDataVolumeTemplates, - getDiskBootOrder, + getDeviceBootOrder, getDisks, getVolumeDataVolumeName, getVolumes, @@ -50,7 +50,7 @@ export const getRemoveDiskPatches = (vmLikeEntity: VMLikeEntityKind, disk): Patc } } - const bootOrderIndex = getDiskBootOrder(disk); + const bootOrderIndex = getDeviceBootOrder(disk); if (bootOrderIndex != null) { return [...patches, ...getDeviceBootOrderPatch(vm, 'disks', diskName)]; } diff --git a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-nic-patches.ts b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-nic-patches.ts index 323cce6768b..e57395328ba 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-nic-patches.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-nic-patches.ts @@ -1,55 +1,89 @@ import * as _ from 'lodash'; -import { getAddNicPatch, getDeviceBootOrderPatch } from 'kubevirt-web-ui-components'; import { Patch } from '@console/internal/module/k8s'; -import { getInterfaces, getNetworks, getNicBootOrder } from '../../../selectors/vm'; +import { getDisks, getInterfaces, getNetworks } from '../../../selectors/vm'; import { getVMLikePatches } from '../vm-template'; -import { VMLikeEntityKind } from '../../../types'; +import { V1Network, V1NetworkInterface, VMLikeEntityKind } from '../../../types'; +import { getSimpleName } from '../../../selectors/utils'; +import { PatchBuilder, PatchOperation } from '../../utils/patch'; +import { NetworkWrapper } from '../../wrapper/vm/network-wrapper'; +import { NetworkInterfaceWrapper } from '../../wrapper/vm/network-interface-wrapper'; +import { getShiftBootOrderPatches } from './utils'; export const getRemoveNicPatches = (vmLikeEntity: VMLikeEntityKind, nic: any): Patch[] => { return getVMLikePatches(vmLikeEntity, (vm) => { const nicName = nic.name; const nics = getInterfaces(vm); const networks = getNetworks(vm); + const network = networks.find((n) => getSimpleName(n) === nicName); + const networkInterfaceWrapper = NetworkInterfaceWrapper.initialize(nic); + const networkChoice = NetworkWrapper.initialize(network); - const nicIndex = nics.findIndex((d) => d.name === nicName); - const networkIndex = networks.findIndex((v) => v.name === nicName); - - const patches: Patch[] = []; - if (nicIndex >= 0) { - patches.push({ - op: 'remove', - path: `/spec/template/spec/domain/devices/interfaces/${nicIndex}`, - }); - } - - if (networkIndex >= 0) { - patches.push({ - op: 'remove', - path: `/spec/template/spec/networks/${networkIndex}`, - }); - } + const patches = [ + new PatchBuilder('/spec/template/spec/domain/devices/interfaces') + .setListRemove(nic, nics, getSimpleName) + .build(), + new PatchBuilder('/spec/template/spec/networks') + .setListRemove(network, networks, getSimpleName) + .build(), + ]; // if pod network is deleted, we need to set autoattachPodInterface to false - if (_.get(nic, 'network.pod')) { - const op = _.has(vm, 'spec.domain.devices.autoattachPodInterface') ? 'replace' : 'add'; - patches.push({ - op, - path: '/spec/template/spec/domain/devices/autoattachPodInterface', - value: false, - }); + if (networkChoice.isPodNetwork()) { + patches.push( + new PatchBuilder('/spec/template/spec/domain/devices/autoattachPodInterface') + .setOperation(PatchOperation.ADD) + .setValue(false) + .build(), + ); } - const bootOrderIndex = getNicBootOrder(nic); - if (bootOrderIndex != null) { - return [...patches, ...getDeviceBootOrderPatch(vm, 'interfaces', nicName)]; + if (networkInterfaceWrapper.hasBootOrder()) { + patches.push( + ...[ + ...getShiftBootOrderPatches( + '/spec/template/spec/domain/devices/disks', + getDisks(vm), + null, + networkInterfaceWrapper.getBootOrder(), + ), + ...getShiftBootOrderPatches( + '/spec/template/spec/domain/devices/interfaces', + nics, + nicName, + networkInterfaceWrapper.getBootOrder(), + ), + ], + ); } - return patches; + return _.compact(patches); }); }; -export const getAddNicPatches = (vmLikeEntity: VMLikeEntityKind, nic: any): Patch[] => { +export const getAddNicPatches = ( + vmLikeEntity: VMLikeEntityKind, + { + nic, + network, + oldNICName, + oldNetworkName, + }: { nic: V1NetworkInterface; network: V1Network; oldNICName: string; oldNetworkName: string }, +): Patch[] => { return getVMLikePatches(vmLikeEntity, (vm) => { - return getAddNicPatch(vm, nic); + const nics = getInterfaces(vm, null); + const networks = getNetworks(vm, null); + + return [ + new PatchBuilder('/spec/template/spec/domain/devices/interfaces') + .setListUpdate(nic, nics, (currentNIC) => + currentNIC === nic ? oldNICName : getSimpleName(currentNIC), + ) + .build(), + new PatchBuilder('/spec/template/spec/networks') + .setListUpdate(network, networks, (currentNetwork) => + currentNetwork === network ? oldNetworkName : getSimpleName(currentNetwork), + ) + .build(), + ].filter((patch) => patch); }); }; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/utils/index.ts b/frontend/packages/kubevirt-plugin/src/k8s/utils/index.ts deleted file mode 100644 index 860977b8da6..00000000000 --- a/frontend/packages/kubevirt-plugin/src/k8s/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export const patchSafeValue = (value: string): string => value.replace('/', '~1'); diff --git a/frontend/packages/kubevirt-plugin/src/k8s/utils/patch.ts b/frontend/packages/kubevirt-plugin/src/k8s/utils/patch.ts new file mode 100644 index 00000000000..6b90a6c8dcc --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/k8s/utils/patch.ts @@ -0,0 +1,100 @@ +import { Patch } from '@console/internal/module/k8s'; +import { assureEndsWith } from '../../utils/grammar'; + +export const patchSafeValue = (value: string): string => value.replace('/', '~1'); + +export enum PatchOperation { + ADD = 'add', + REMOVE = 'remove', + REPLACE = 'replace', +} + +export class PatchBuilder { + private readonly path: string; + + private value: any; + + private valueIndex: number = -1; + + private operation: PatchOperation; + + private valid = true; + + constructor(path: string) { + this.path = path; + } + + setValue = (value) => { + this.value = value; + return this; + }; + + setOperation = (operation: PatchOperation) => { + this.operation = operation; + return this; + }; + + setListIndex = (index: number) => { + this.valueIndex = index; + return this; + }; + + setListRemove = (value: T, items: T[], compareGetter?: (t: T) => any) => { + this.value = undefined; + this.operation = PatchOperation.REMOVE; + if (items) { + const foundIndex = items.findIndex((t) => + compareGetter ? compareGetter(t) === compareGetter(value) : t === value, + ); + if (foundIndex < 0) { + this.valid = false; // do not do anything + } else { + this.valueIndex = items.length === 1 ? -1 : foundIndex; // delete the whole list if last value there + } + } else { + this.valueIndex = -1; // remove the empty list + } + return this; + }; + + setListUpdate = (value: T, items?: T[], compareGetter?: (t: T) => any) => { + if (items) { + this.value = value; + const foundIndex = items.findIndex((t) => + compareGetter ? compareGetter(t) === compareGetter(value) : t === value, + ); + if (foundIndex < 0) { + this.valueIndex = items.length; + this.operation = PatchOperation.ADD; + } else { + this.valueIndex = foundIndex; + this.operation = PatchOperation.REPLACE; + } + } else { + // list is missing - add the whole list + this.value = [value]; + this.valueIndex = -1; + this.operation = PatchOperation.ADD; + } + return this; + }; + + isPatchValid = () => this.valid && !!(this.path && this.operation); + + build = (): Patch => { + if (!this.isPatchValid()) { + return null; + } + + const result: any = { + op: this.operation, + path: this.valueIndex < 0 ? this.path : `${assureEndsWith(this.path, '/')}${this.valueIndex}`, + }; + + if (this.operation !== PatchOperation.REMOVE) { + result.value = this.value; + } + + return result; + }; +} diff --git a/frontend/packages/kubevirt-plugin/src/k8s/wrapper/common/object-with-type-property-wrapper.ts b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/common/object-with-type-property-wrapper.ts new file mode 100644 index 00000000000..cde7042bb5c --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/common/object-with-type-property-wrapper.ts @@ -0,0 +1,77 @@ +import * as _ from 'lodash'; +import { ValueEnum } from '../../../constants'; +import { Wrapper } from './wrapper'; + +export abstract class ObjectWithTypePropertyWrapper< + RESOURCE, + TYPE extends ValueEnum +> extends Wrapper { + private readonly TypeClass: { getAll: () => TYPE[] | Readonly }; + + private readonly typeDataPath: string[]; + + protected static defaultMergeWrappersWithType = < + A, + B extends ObjectWithTypePropertyWrapper + >( + Clazz, + wrappers: B[], + ): B => { + const result = Wrapper.defaultMergeWrappers(Clazz, wrappers); + const lastWithType = _.last(wrappers.filter((wrapper) => wrapper && wrapper.getType())); + + if (lastWithType) { + result.setType(lastWithType.getType(), result.getTypeData(lastWithType.getType())); + } + return result; + }; + + constructor( + data: RESOURCE, + opts: { initializeWithType?: TYPE; initializeWithTypeData?: any; copy?: boolean }, + typeClass: { getAll: () => TYPE[] | Readonly }, + typeDataPath: string[] = [], + ) { + super(data, opts); + this.TypeClass = typeClass; + this.typeDataPath = typeDataPath; + + if (opts && opts.initializeWithType) { + const { initializeWithTypeData, initializeWithType, copy } = opts; + + const resultTypeData = initializeWithTypeData + ? copy + ? _.cloneDeep(initializeWithTypeData) + : initializeWithTypeData + : this.getTypeData(initializeWithType); + this.setType(initializeWithType, resultTypeData); + } + } + + getType = (): TYPE => + this.TypeClass.getAll().find((type) => this.getIn([...this.typeDataPath, type.getValue()])); + + getTypeValue = (): string => { + const type = this.getType(); + return type && type.getValue(); + }; + + hasType = (): boolean => !!this.getType(); + + protected getTypeData = (type: TYPE) => + this.getIn([...this.typeDataPath, (type || this.getType()).getValue()]); + + protected setType = (type: TYPE, typeData?: any) => { + const typeDataParent = + this.typeDataPath.length === 0 ? this.data : this.getIn(this.typeDataPath); + if (!typeDataParent) { + return; + } + this.TypeClass.getAll().forEach( + (superflousProperty) => delete typeDataParent[superflousProperty.getValue()], + ); + if (type) { + typeDataParent[type.getValue()] = typeData ? _.cloneDeep(typeData) : {}; + } + }; +} diff --git a/frontend/packages/kubevirt-plugin/src/k8s/wrapper/common/wrapper.ts b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/common/wrapper.ts new file mode 100644 index 00000000000..5aed85b3eab --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/common/wrapper.ts @@ -0,0 +1,26 @@ +import * as _ from 'lodash'; + +export class Wrapper { + protected data: RESOURCE; + + protected static defaultMergeWrappers = >(Clazz, wrappers: B[]): B => { + const nonEmptyWrappers = wrappers.filter((i) => i); + if (nonEmptyWrappers.length === 0) { + return new Clazz(); + } + + const mergedWrappers: A = _.merge({}, ...nonEmptyWrappers.map((i) => i.data)); + + return new Clazz(mergedWrappers); + }; + + constructor(data: RESOURCE, opts: { copy?: boolean }) { + this.data = (data && opts && opts.copy ? _.cloneDeep(data) : data || {}) as any; + } + + asResource = (): RESOURCE => _.cloneDeep(this.data); + + protected get = (key: string) => (this.data && key ? this.data[key] : null); + + protected getIn = (path: string[]) => (this.data && path ? _.get(this.data, path) : null); +} diff --git a/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/network-interface-wrapper.ts b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/network-interface-wrapper.ts new file mode 100644 index 00000000000..425b29f6f6d --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/network-interface-wrapper.ts @@ -0,0 +1,57 @@ +import { V1NetworkInterface } from '../../../types/vm'; +import { NetworkInterfaceModel, NetworkInterfaceType } from '../../../constants/vm'; +import { ObjectWithTypePropertyWrapper } from '../common/object-with-type-property-wrapper'; + +export class NetworkInterfaceWrapper extends ObjectWithTypePropertyWrapper< + V1NetworkInterface, + NetworkInterfaceType +> { + static readonly EMPTY = new NetworkInterfaceWrapper(); + + static mergeWrappers = (...interfaces: NetworkInterfaceWrapper[]): NetworkInterfaceWrapper => + ObjectWithTypePropertyWrapper.defaultMergeWrappersWithType(NetworkInterfaceWrapper, interfaces); + + static initializeFromSimpleData = (params?: { + name?: string; + model?: NetworkInterfaceModel; + interfaceType?: NetworkInterfaceType; + macAddress?: string; + bootOrder?: number; + }) => { + if (!params) { + return NetworkInterfaceWrapper.EMPTY; + } + const { name, model, macAddress, interfaceType, bootOrder } = params; + return new NetworkInterfaceWrapper( + { name, model: model && model.getValue(), macAddress, bootOrder }, + { initializeWithType: interfaceType }, + ); + }; + + static initialize = (nic?: V1NetworkInterface, copy?: boolean) => + new NetworkInterfaceWrapper(nic, copy && { copy }); + + protected constructor( + nic?: V1NetworkInterface, + opts?: { initializeWithType?: NetworkInterfaceType; copy?: boolean }, + ) { + super(nic, opts, NetworkInterfaceType); + } + + getName = (): string => this.get('name'); + + getModel = (): NetworkInterfaceModel => NetworkInterfaceModel.fromString(this.get('model')); + + getReadableModel = () => { + const model = this.getModel(); + return model && model.toString(); + }; + + getMACAddress = () => this.get('macAddress'); + + getBootOrder = () => this.get('bootOrder'); + + isFirstBootableDevice = () => this.getBootOrder() === 1; + + hasBootOrder = () => this.getBootOrder() != null; +} diff --git a/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/network-wrapper.ts b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/network-wrapper.ts new file mode 100644 index 00000000000..ee4f3009349 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/network-wrapper.ts @@ -0,0 +1,58 @@ +import { NetworkType, POD_NETWORK } from '../../../constants'; +import { V1Network } from '../../../types/vm'; +import { ObjectWithTypePropertyWrapper } from '../common/object-with-type-property-wrapper'; + +export class NetworkWrapper extends ObjectWithTypePropertyWrapper { + static readonly EMPTY = new NetworkWrapper(); + + static mergeWrappers = (...networks: NetworkWrapper[]): NetworkWrapper => + ObjectWithTypePropertyWrapper.defaultMergeWrappersWithType(NetworkWrapper, networks); + + static initializeFromSimpleData = (params?: { + name?: string; + type?: NetworkType; + multusNetworkName?: string; + }) => { + if (!params) { + return NetworkWrapper.EMPTY; + } + const { name, type, multusNetworkName } = params; + return new NetworkWrapper( + { name }, + { + initializeWithType: type, + initializeWithTypeData: + type === NetworkType.MULTUS + ? { networkName: multusNetworkName, test: undefined } + : undefined, + }, + ); + }; + + static initialize = (network?: V1Network, copy?: boolean) => + new NetworkWrapper(network, copy && { copy }); + + protected constructor( + network?: V1Network, + opts?: { initializeWithType?: NetworkType; initializeWithTypeData?: any; copy?: boolean }, + ) { + super(network, opts, NetworkType); + } + + getName = () => this.get('name'); + + getMultusNetworkName = () => this.getIn(['multus', 'networkName']); + + isPodNetwork = () => this.getType() === NetworkType.POD; + + getReadableName = () => { + switch (this.getType()) { + case NetworkType.MULTUS: + return this.getMultusNetworkName(); + case NetworkType.POD: + return POD_NETWORK; + default: + return null; + } + }; +} diff --git a/frontend/packages/kubevirt-plugin/src/selectors/nad/combined.ts b/frontend/packages/kubevirt-plugin/src/selectors/nad/combined.ts new file mode 100644 index 00000000000..92d7e3bd1ef --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/selectors/nad/combined.ts @@ -0,0 +1,25 @@ +import { K8sResourceKind } from '@console/internal/module/k8s'; +import { getName } from '@console/shared/src'; +import { NetworkType } from '../../constants/vm'; +import { NetworkWrapper } from '../../k8s/wrapper/vm/network-wrapper'; + +export const getNetworkChoices = ( + nads: K8sResourceKind[], + usedNetworkNames: Set, + allowPodNetwork, +): NetworkWrapper[] => { + const networkChoices = nads + .map((nad) => getName(nad)) + .filter((nadName) => !(usedNetworkNames && usedNetworkNames.has(nadName))) + .map((name) => + NetworkWrapper.initializeFromSimpleData({ + multusNetworkName: name, + type: NetworkType.MULTUS, + }), + ); + + if (allowPodNetwork) { + networkChoices.push(NetworkWrapper.initializeFromSimpleData({ type: NetworkType.POD })); + } + return networkChoices; +}; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/nad/index.ts b/frontend/packages/kubevirt-plugin/src/selectors/nad/index.ts new file mode 100644 index 00000000000..4203546e835 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/selectors/nad/index.ts @@ -0,0 +1 @@ +export * from './combined'; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/utils.ts b/frontend/packages/kubevirt-plugin/src/selectors/utils.ts index 104568029f4..6fca265a73e 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/utils.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/utils.ts @@ -15,3 +15,5 @@ export const findKeySuffixValue = (obj: StringHashMap, keyPrefix: string) => { const index = key ? key.lastIndexOf('/') : -1; return index > 0 ? key.substring(index + 1) : null; }; + +export const getSimpleName = (obj) => obj && obj.name; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm/combined.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm/combined.ts index 3f492148ab2..51f8ca9804e 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm/combined.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm/combined.ts @@ -1,7 +1,6 @@ import * as _ from 'lodash'; import { getName, getNamespace, getOwnerReferences } from '@console/shared/src/selectors'; -import { createBasicLookup } from '@console/shared/src/utils/utils'; -import { K8sResourceKind, PodKind } from '@console/internal/module/k8s'; +import { PodKind } from '@console/internal/module/k8s'; import { buildOwnerReference, compareOwnerReference } from '../../utils'; import { VMIKind, VMKind } from '../../types/vm'; import { VMMultiStatus } from '../../types'; @@ -9,9 +8,7 @@ import { VM_STATUS_IMPORTING, VM_STATUS_V2V_CONVERSION_IN_PROGRESS, } from '../../statuses/vm/constants'; -import { NetworkType, POD_NETWORK } from '../../constants/vm'; -import { getUsedNetworks, isVMRunning } from './selectors'; -import { Network } from './types'; +import { isVMRunning } from './selectors'; const IMPORTING_STATUSES = new Set([VM_STATUS_IMPORTING, VM_STATUS_V2V_CONVERSION_IN_PROGRESS]); @@ -21,30 +18,6 @@ export const isVMImporting = (status: VMMultiStatus): boolean => export const isVMRunningWithVMI = ({ vm, vmi }: { vm: VMKind; vmi: VMIKind }): boolean => isVMRunning(vm) && !_.isEmpty(vmi); -export const getNetworkChoices = (vm: VMKind, nads: K8sResourceKind[]): Network[] => { - const usedNetworks = getUsedNetworks(vm); - const usedMultuses = usedNetworks.filter( - (usedNetwork) => usedNetwork.networkType === NetworkType.MULTUS, - ); - const usedMultusesLookup = createBasicLookup(usedMultuses, (multus) => _.get(multus, 'name')); - - const networkChoices = nads - .map((nad) => getName(nad)) - .filter((nadName) => !usedMultusesLookup[nadName]) - .map((name) => ({ - name, - networkType: NetworkType.MULTUS, - })); - - if (!usedNetworks.find((usedNetwork) => usedNetwork.networkType === NetworkType.POD)) { - networkChoices.push({ - name: POD_NETWORK, - networkType: NetworkType.POD, - }); - } - return networkChoices; -}; - export const findConversionPod = (vm: VMKind, pods: PodKind[]) => { if (!pods) { return null; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm/devices.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm/devices.ts index 42336ae9c21..cbded8b794a 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm/devices.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm/devices.ts @@ -1,2 +1,5 @@ export const getBootDeviceIndex = (devices, bootOrder) => devices.findIndex((device) => device.bootOrder === bootOrder); + +export const getDeviceBootOrder = (device, defaultValue?): number => + device && device.bootOrder === undefined ? defaultValue : device.bootOrder; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm/disk.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm/disk.ts index e8a7e2097e9..82fcde83774 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm/disk.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm/disk.ts @@ -1,6 +1,3 @@ import * as _ from 'lodash'; export const getDiskBus = (disk, defaultValue?): string => _.get(disk, 'disk.bus', defaultValue); - -export const getDiskBootOrder = (disk, defaultValue?): number => - _.get(disk, 'bootOrder', defaultValue); diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm/nic.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm/nic.ts index 6915ea6b115..33bf4f8abc6 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm/nic.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm/nic.ts @@ -1,6 +1,3 @@ import * as _ from 'lodash'; -export const getNicBootOrder = (nic, defaultValue?): number => - _.get(nic, 'bootOrder', defaultValue); - export const getNicBus = (nic, defaultValue?): string => _.get(nic, 'model', defaultValue); diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts index 04298a04100..5817a53ab93 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts @@ -2,31 +2,37 @@ import * as _ from 'lodash'; import { createBasicLookup } from '@console/shared/src/utils/utils'; import { BUS_VIRTIO, - NetworkType, TEMPLATE_FLAVOR_LABEL, TEMPLATE_OS_LABEL, TEMPLATE_OS_NAME_ANNOTATION, TEMPLATE_WORKLOAD_LABEL, } from '../../constants/vm'; -import { VMKind, VMLikeEntityKind, CPURaw } from '../../types'; -import { findKeySuffixValue, getValueByPrefix } from '../utils'; +import { V1NetworkInterface, VMKind, VMLikeEntityKind, CPURaw } from '../../types'; +import { findKeySuffixValue, getSimpleName, getValueByPrefix } from '../utils'; import { getAnnotations, getLabels } from '../selectors'; +import { NetworkWrapper } from '../../k8s/wrapper/vm/network-wrapper'; import { getDiskBus } from './disk'; -import { getNicBus } from './nic'; -import { Network } from './types'; import { getVolumeCloudInitUserData } from './volume'; import { vCPUCount } from './cpu'; export const getMemory = (vm: VMKind) => _.get(vm, 'spec.template.spec.domain.resources.requests.memory'); export const getCPU = (vm: VMKind): CPURaw => _.get(vm, 'spec.template.spec.domain.cpu'); -export const getDisks = (vm: VMKind) => _.get(vm, 'spec.template.spec.domain.devices.disks', []); -export const getInterfaces = (vm: VMKind) => - _.get(vm, 'spec.template.spec.domain.devices.interfaces') || []; - -export const getNetworks = (vm: VMKind) => _.get(vm, 'spec.template.spec.networks') || []; -export const getVolumes = (vm: VMKind) => _.get(vm, 'spec.template.spec.volumes') || []; -export const getDataVolumeTemplates = (vm: VMKind) => _.get(vm, 'spec.dataVolumeTemplates') || []; +export const getDisks = (vm: VMKind, defaultValue = []) => + _.get(vm, 'spec.template.spec.domain.devices.disks') == null + ? defaultValue + : vm.spec.template.spec.domain.devices.disks; +export const getInterfaces = (vm: VMKind, defaultValue = []): V1NetworkInterface[] => + _.get(vm, 'spec.template.spec.domain.devices.interfaces') == null + ? defaultValue + : vm.spec.template.spec.domain.devices.interfaces; + +export const getNetworks = (vm: VMKind, defaultValue = []) => + _.get(vm, 'spec.template.spec.networks') == null ? defaultValue : vm.spec.template.spec.networks; +export const getVolumes = (vm: VMKind, defaultValue = []) => + _.get(vm, 'spec.template.spec.volumes') == null ? defaultValue : vm.spec.template.spec.volumes; +export const getDataVolumeTemplates = (vm: VMKind, defaultValue = []) => + _.get(vm, 'spec.dataVolumeTemplates') == null ? defaultValue : vm.spec.dataVolumeTemplate; export const getOperatingSystem = (vm: VMLikeEntityKind) => findKeySuffixValue(getLabels(vm), TEMPLATE_OS_LABEL); @@ -51,32 +57,13 @@ export const getVmPreferableDiskBus = (vm: VMKind) => .map((disk) => getDiskBus(disk)) .find((bus) => bus) || BUS_VIRTIO; -export const getVmPreferableNicBus = (vm: VMKind) => - getNetworks(vm) - .map((nic) => getNicBus(nic)) - .find((bus) => bus) || BUS_VIRTIO; - -export const getUsedNetworks = (vm: VMKind): Network[] => { +export const getUsedNetworks = (vm: VMKind): NetworkWrapper[] => { const interfaces = getInterfaces(vm); - const networkLookup = createBasicLookup(getNetworks(vm), (network) => - _.get(network, 'name'), - ); + const networkLookup = createBasicLookup(getNetworks(vm), getSimpleName); return interfaces - .map((i) => { - const network = networkLookup[i.name]; - if (_.get(network, 'multus')) { - return { - networkType: NetworkType.MULTUS, - name: network.multus.networkName, - }; - } - if (_.get(network, 'pod')) { - return { name: network.name, networkType: NetworkType.POD }; - } - return null; - }) - .filter((i) => i); + .map((i) => NetworkWrapper.initialize(networkLookup[i.name])) + .filter((i) => i.getType()); }; export const getFlavorDescription = (vm: VMKind) => { diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm/types.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm/types.ts deleted file mode 100644 index f1738f1d5de..00000000000 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { NetworkType } from '../../constants/vm'; - -export type Network = { - name?: string; - networkType: NetworkType; -}; diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/index.ts b/frontend/packages/kubevirt-plugin/src/types/vm/index.ts index b5a78ea6b0f..0167b02723e 100644 --- a/frontend/packages/kubevirt-plugin/src/types/vm/index.ts +++ b/frontend/packages/kubevirt-plugin/src/types/vm/index.ts @@ -73,6 +73,26 @@ export type CPURaw = } | string; +export type V1NetworkInterface = { + name?: string; + model?: string; + macAddress?: string; + bootOrder?: number; + bridge?: {}; + masquerade?: {}; + sriov?: {}; + slirp?: {}; +}; + +export type V1Network = { + name?: string; + multus?: { + networkName: string; + }; + pod?: {}; + genie?: {}; +}; + export enum ProvisionSource { PXE = 'PXE', CONTAINER = 'Container', diff --git a/frontend/packages/kubevirt-plugin/src/utils/grammar.ts b/frontend/packages/kubevirt-plugin/src/utils/grammar.ts index 779a455f1b0..e46a015954d 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/grammar.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/grammar.ts @@ -9,9 +9,17 @@ export const joinGrammaticallyListOfItems = (items: string[]) => { : result; }; +export const assureEndsWith = (sentence: string, appendix: string) => { + if (!sentence || !appendix || sentence.endsWith(appendix)) { + return sentence; + } + + return `${sentence}${appendix}`; +}; + export const makeSentence = (sentence: string, capitalize = true) => { - const result = capitalize ? _.capitalize(sentence) : sentence; - return !result || result.charAt(result.length) === '.' ? result : `${result}.`; + const result = capitalize ? _.upperFirst(sentence) : sentence; + return assureEndsWith(result, '.'); }; export const addMissingSubject = (sentence: string, subject: string) => { @@ -20,5 +28,5 @@ export const addMissingSubject = (sentence: string, subject: string) => { // c is an upper case letter return sentence; } - return subject ? `${_.capitalize(subject)} ${sentence}` : sentence; + return subject ? `${_.upperFirst(subject)} ${sentence}` : sentence; }; diff --git a/frontend/packages/kubevirt-plugin/src/utils/index.ts b/frontend/packages/kubevirt-plugin/src/utils/index.ts index 4d029263cc3..747c63682be 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/index.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/index.ts @@ -29,6 +29,9 @@ export const prefixedID = (idPrefix: string, id: string) => export const joinIDs = (...ids: string[]) => ids.join('-'); +export const isLoaded = (result: FirehoseResult) => + result && result.loaded; + export const getLoadedData = ( result: FirehoseResult, defaultValue = null, diff --git a/frontend/packages/kubevirt-plugin/src/utils/strings.ts b/frontend/packages/kubevirt-plugin/src/utils/strings.ts index 823a1519aa1..d850fc787e3 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/strings.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/strings.ts @@ -6,4 +6,23 @@ export const CREATED_WITH_CLEANUP = 'created & cleaned up'; export const FAILED_TO_CREATE = 'failed to create'; export const FAILED_TO_PATCH = 'failed to patch'; +export const getDialogUIError = (hasAllRequiredFilled) => + hasAllRequiredFilled + ? 'Please correct the invalid fields.' + : 'Please fill in all required fields.'; + export const getCheckboxReadableValue = (value: boolean) => (value ? 'yes' : 'no'); + +export const getSequenceName = (name: string, usedSequenceNames?: Set) => { + if (!usedSequenceNames) { + return `${name}${0}`; + } + + for (let i = 0; i < usedSequenceNames.size + 1; i++) { + const sequenceName = `${name}${i}`; + if (!usedSequenceNames.has(sequenceName)) { + return sequenceName; + } + } + return null; +}; diff --git a/frontend/packages/kubevirt-plugin/src/utils/validations/common.ts b/frontend/packages/kubevirt-plugin/src/utils/validations/common.ts index 2d5373de38d..2e64ff95d84 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/validations/common.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/common.ts @@ -30,11 +30,8 @@ export const getValidationObject = ( type, }); -export const getValidationErrorType = (validationObject: ValidationObject): ValidationErrorType => { - return ( - validationObject && validationObject.type === ValidationErrorType.Error && validationObject.type - ); -}; +export const isValidationError = (validationObject: ValidationObject) => + !!validationObject && validationObject.type === ValidationErrorType.Error; export const getValidationErrorMessage = (validationObject: ValidationObject): string => { return ( diff --git a/frontend/packages/kubevirt-plugin/src/utils/validations/vm/nic.ts b/frontend/packages/kubevirt-plugin/src/utils/validations/vm/nic.ts index 621509898e3..70e6241eda4 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/validations/vm/nic.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/vm/nic.ts @@ -1,21 +1,19 @@ -import { EntityMap } from '@console/shared'; import { getValidationObject, validateDNS1123SubdomainValue } from '../common'; -import { addMissingSubject, makeSentence } from '../../grammar'; +import { makeSentence } from '../../grammar'; import { MAC_ADDRESS_INVALID_ERROR, NIC_NAME_EXISTS } from '../strings'; import { ValidationObject } from '../types'; +import { NetworkInterfaceWrapper } from '../../../k8s/wrapper/vm/network-interface-wrapper'; +import { NetworkWrapper } from '../../../k8s/wrapper/vm/network-wrapper'; import { isValidMAC } from './validations'; export const validateNicName = ( name: string, - interfaceLookup: EntityMap, + usedInterfacesNames: Set, + { subject } = { subject: 'Name' }, ): ValidationObject => { - let validation = validateDNS1123SubdomainValue(name); + let validation = validateDNS1123SubdomainValue(name, { subject }); - if (validation) { - validation.message = addMissingSubject(validation.message, 'Name'); - } - - if (!validation && interfaceLookup[name]) { + if (!validation && usedInterfacesNames && usedInterfacesNames.has(name)) { validation = getValidationObject(NIC_NAME_EXISTS); } @@ -26,3 +24,38 @@ export const validateMACAddress = (mac: string): ValidationObject => { const isValid = mac === '' || (mac && isValidMAC(mac)); return isValid ? null : getValidationObject(makeSentence(MAC_ADDRESS_INVALID_ERROR)); }; + +export const validateNIC = ( + interfaceWrapper: NetworkInterfaceWrapper, + network: NetworkWrapper, + { usedInterfacesNames }: { usedInterfacesNames?: Set }, +): UINetworkInterfaceValidation => { + const validations = { + name: validateNicName(interfaceWrapper && interfaceWrapper.getName(), usedInterfacesNames), + macAddress: validateMACAddress(interfaceWrapper && interfaceWrapper.getMACAddress()), + }; + + const hasAllRequiredFilled = + interfaceWrapper && + interfaceWrapper.getName() && + interfaceWrapper.getModel() && + interfaceWrapper.hasType() && + network && + network.getReadableName() && + network.hasType(); + + return { + validations, + hasAllRequiredFilled: !!hasAllRequiredFilled, + isValid: !!hasAllRequiredFilled && !Object.keys(validations).find((key) => validations[key]), + }; +}; + +export type UINetworkInterfaceValidation = { + validations: { + name?: ValidationObject; + macAddress?: ValidationObject; + }; + isValid: boolean; + hasAllRequiredFilled: boolean; +};