diff --git a/locales/en/plugin__kubevirt-plugin.json b/locales/en/plugin__kubevirt-plugin.json index 4ac928a504..e1d86397ec 100644 --- a/locales/en/plugin__kubevirt-plugin.json +++ b/locales/en/plugin__kubevirt-plugin.json @@ -297,6 +297,7 @@ "Container (Ephemeral)": "Container (Ephemeral)", "Container disk": "Container disk", "Container Image": "Container Image", + "Container image or PVC": "Container image or PVC", "Container is required.": "Container is required.", "Content from container registry.": "Content from container registry.", "Copied": "Copied", @@ -1286,6 +1287,7 @@ "URL": "URL", "URL (creates PVC)": "URL (creates PVC)", "URL is required": "URL is required", + "URL, Registry or Upload": "URL, Registry or Upload", "Usage": "Usage", "Use a persistent volume claim available on the cluster.": "Use a persistent volume claim available on the cluster.", "Use BIOS when bootloading the guest OS (Default)": "Use BIOS when bootloading the guest OS (Default)", diff --git a/src/utils/components/DiskModal/DiskModal.tsx b/src/utils/components/DiskModal/DiskModal.tsx index 1bbd1dac6a..efd0af5a77 100644 --- a/src/utils/components/DiskModal/DiskModal.tsx +++ b/src/utils/components/DiskModal/DiskModal.tsx @@ -5,7 +5,7 @@ import { useCDIUpload } from '@kubevirt-utils/hooks/useCDIUpload/useCDIUpload'; import { UPLOAD_STATUS } from '@kubevirt-utils/hooks/useCDIUpload/utils'; import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; import { getBootDisk } from '@kubevirt-utils/resources/vm'; -import { generatePrettyName, isEmpty, kubevirtConsole } from '@kubevirt-utils/utils/utils'; +import { generatePrettyName, kubevirtConsole } from '@kubevirt-utils/utils/utils'; import { Form } from '@patternfly/react-core'; import { isRunning } from '@virtualmachines/utils'; @@ -17,7 +17,7 @@ import BootSourceCheckbox from './components/BootSourceCheckbox/BootSourceCheckb import DiskInterfaceSelect from './components/DiskInterfaceSelect/DiskInterfaceSelect'; import DiskNameInput from './components/DiskNameInput/DiskNameInput'; import DiskSizeInput from './components/DiskSizeInput/DiskSizeInput'; -import DiskSourceSelect from './components/DiskSourceSelect/DiskSourceSelect'; +import { getSelectedDiskSourceComponent } from './components/DiskSourceSelect/utils/utils'; import DiskTypeSelect from './components/DiskTypeSelect/DiskTypeSelect'; import StorageClassAndPreallocation from './components/StorageClassAndPreallocation/StorageClassAndPreallocation'; import { getInitialStateDiskForm } from './utils/constants'; @@ -33,9 +33,9 @@ const DiskModal: FC = ({ createOwnerReference = true, headerText, initialFormData = null, + isEditDisk = false, isEditingCreatedDisk = false, isOpen, - isTemplate = false, onClose, onSubmit, onUploadedDataVolume, @@ -58,6 +58,8 @@ const DiskModal: FC = ({ handleSubmit, } = methods; + const diskSource = initialFormData?.diskSource; + return ( = ({ handleSubmit((data) => { if (isEditingCreatedDisk) return editVMDisk(vm, initialFormData, data, onSubmit); - if (!isEmpty(initialFormData)) + if (isEditDisk) return editDisk(initialFormData, data, uploadData, { createOwnerReference, onSubmit, @@ -105,12 +107,7 @@ const DiskModal: FC = ({ isDisabled={isVMRunning} /> - + {getSelectedDiskSourceComponent(vm, upload)[diskSource]} diff --git a/src/utils/components/DiskModal/components/DiskSourceSelect/DiskSourceSelect.tsx b/src/utils/components/DiskModal/components/DiskSourceSelect/DiskSourceSelect.tsx deleted file mode 100644 index 957ea18478..0000000000 --- a/src/utils/components/DiskModal/components/DiskSourceSelect/DiskSourceSelect.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { FC } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; - -import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; -import FormPFSelect from '@kubevirt-utils/components/FormPFSelect/FormPFSelect'; -import { DataUpload } from '@kubevirt-utils/hooks/useCDIUpload/useCDIUpload'; -import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; -import { FormGroup } from '@patternfly/react-core'; -import { isRunning } from '@virtualmachines/utils'; - -import { DEFAULT_DISK_SIZE } from '../../utils/constants'; -import { DiskFormState, SourceTypes } from '../../utils/types'; -import { diskSizeField, diskSourceField, DYNAMIC, OTHER } from '../utils/constants'; - -import DiskSourceSelectOptionGroups from './components/DiskSourceSelectOptions/DiskSourceSelectOptionGroups'; -import { diskSourceFieldID, optionLabelMapper } from './utils/constants'; -import { getSelectedDiskSourceComponent } from './utils/utils'; - -type DiskSourceSelectProps = { - isEditingCreatedDisk?: boolean; - isTemplate: boolean; - relevantUpload?: DataUpload; - vm: V1VirtualMachine; -}; - -const DiskSourceSelect: FC = ({ - isEditingCreatedDisk, - isTemplate, - relevantUpload, - vm, -}) => { - const { t } = useKubevirtTranslation(); - const { control, setValue, watch } = useFormContext(); - - const { diskSize, diskSource } = watch(); - - return ( - <> - ( - -
- { - if (diskSize === DYNAMIC && val !== SourceTypes.EPHEMERAL) { - setValue(diskSizeField, DEFAULT_DISK_SIZE); - } - - onChange(val); - }} - selected={value} - selectedLabel={optionLabelMapper[value]} - toggleProps={{ isDisabled: value === OTHER, isFullWidth: true }} - > - - -
-
- )} - control={control} - name={diskSourceField} - rules={{ required: true }} - /> - {getSelectedDiskSourceComponent(vm, relevantUpload)[diskSource]} - - ); -}; - -export default DiskSourceSelect; diff --git a/src/utils/components/DiskModal/components/DiskSourceSelect/components/DiskSourceSelectOptions/DiskSourceSelectOptions.tsx b/src/utils/components/DiskModal/components/DiskSourceSelect/components/DiskSourceSelectOptions/DiskSourceSelectOptions.tsx index c6a0b99374..2eecb738a5 100644 --- a/src/utils/components/DiskModal/components/DiskSourceSelect/components/DiskSourceSelectOptions/DiskSourceSelectOptions.tsx +++ b/src/utils/components/DiskModal/components/DiskSourceSelect/components/DiskSourceSelectOptions/DiskSourceSelectOptions.tsx @@ -1,8 +1,6 @@ import React, { FC } from 'react'; -import { useFormContext } from 'react-hook-form'; -import { DiskFormState, SourceTypes } from '@kubevirt-utils/components/DiskModal/utils/types'; -import { diskTypes } from '@kubevirt-utils/resources/vm/utils/disk/constants'; +import { SourceTypes } from '@kubevirt-utils/components/DiskModal/utils/types'; import { SelectList, SelectOption } from '@patternfly/react-core'; import { diskSourceFieldID } from '../../utils/constants'; @@ -17,20 +15,15 @@ type DiskSourceSelectOptionsProps = { const DiskSourceSelectOptions: FC = ({ isEditingCreatedDisk, - isTemplate, + isTemplate = false, isVMRunning, items, }) => { - const { watch } = useFormContext(); - - const diskType = watch('diskType'); - const isCDROMType = diskType === diskTypes.cdrom; return ( {items.map(({ description, id, label }) => { const isDisabled = (isVMRunning && id === SourceTypes.EPHEMERAL) || - (isCDROMType && id === SourceTypes.BLANK) || (isTemplate && id === SourceTypes.UPLOAD); return ( ({ + description: t('URL, Registry or Upload'), groupLabel: t('Import'), items: [ { @@ -60,10 +61,14 @@ export const getImportGroupOptions = (isTemplate: boolean): DiskSourceOptionGrou id: SourceTypes.REGISTRY, label: optionLabelMapper[SourceTypes.REGISTRY], }, - !isTemplate && { - id: SourceTypes.UPLOAD, - label: optionLabelMapper[SourceTypes.UPLOAD], - }, + ...(isTemplate + ? [] + : [ + { + id: SourceTypes.UPLOAD, + label: optionLabelMapper[SourceTypes.UPLOAD], + }, + ]), ], }); diff --git a/src/utils/components/DiskModal/utils/types.ts b/src/utils/components/DiskModal/utils/types.ts index 10a708dfe1..a948c84121 100644 --- a/src/utils/components/DiskModal/utils/types.ts +++ b/src/utils/components/DiskModal/utils/types.ts @@ -70,9 +70,9 @@ export type DiskModalProps = { createOwnerReference?: boolean; headerText: string; initialFormData?: DiskFormState; + isEditDisk?: boolean; isEditingCreatedDisk?: boolean; isOpen: boolean; - isTemplate?: boolean; onClose: () => void; onSubmit: (updatedVM: V1VirtualMachine) => Promise; onUploadedDataVolume?: (dataVolume: V1beta1DataVolume) => void; diff --git a/src/utils/components/DiskSourceFlyoutMenu/DiskSourceFlyoutMenu.tsx b/src/utils/components/DiskSourceFlyoutMenu/DiskSourceFlyoutMenu.tsx new file mode 100644 index 0000000000..939703dc23 --- /dev/null +++ b/src/utils/components/DiskSourceFlyoutMenu/DiskSourceFlyoutMenu.tsx @@ -0,0 +1,99 @@ +import React, { FC, useRef, useState } from 'react'; + +import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; +import { + Menu, + MenuContainer, + MenuContent, + MenuItem, + MenuList, + MenuToggle, +} from '@patternfly/react-core'; + +import { getDiskSourceOptionGroups } from '../DiskModal/components/DiskSourceSelect/utils/utils'; +import { SourceTypes } from '../DiskModal/utils/types'; + +import FlyoutItem from './FlyoutItem/FlyoutItem'; + +export type DiskSourceFlyoutMenuProps = { + className?: string; + isTemplate?: boolean; + onSelect: (value: SourceTypes) => void; +}; + +const DiskSourceFlyoutMenu: FC = ({ + className, + isTemplate = false, + onSelect, +}) => { + const { t } = useKubevirtTranslation(); + const [isOpen, setIsOpen] = useState(false); + const menuRef = useRef(null); + const toggleRef = useRef(null); + + const options = getDiskSourceOptionGroups(isTemplate); + + const onToggleClick = () => { + setIsOpen((open) => !open); + }; + + const onSelectSource = (value: SourceTypes) => { + onSelect(value); + setIsOpen(false); + }; + + const toggle = ( + + {t('Add disk')} + + ); + + const menu = ( + + + + {options?.map(({ description, groupLabel, items }) => { + const isFlyout = items?.length > 1; + return isFlyout ? ( + } + itemId={groupLabel} + > + {groupLabel} + + ) : ( + onSelectSource(items?.[0]?.id)} + > + {items?.[0]?.label} + + ); + })} + + + + ); + + return ( + setIsOpen(open)} + popperProps={{ width: '250px' }} + toggle={toggle} + toggleRef={toggleRef} + /> + ); +}; + +export default DiskSourceFlyoutMenu; diff --git a/src/utils/components/DiskSourceFlyoutMenu/FlyoutItem/FlyoutItem.scss b/src/utils/components/DiskSourceFlyoutMenu/FlyoutItem/FlyoutItem.scss new file mode 100644 index 0000000000..7ce0d2e636 --- /dev/null +++ b/src/utils/components/DiskSourceFlyoutMenu/FlyoutItem/FlyoutItem.scss @@ -0,0 +1,3 @@ +.flyout-item { + min-width: 250px; +} diff --git a/src/utils/components/DiskSourceFlyoutMenu/FlyoutItem/FlyoutItem.tsx b/src/utils/components/DiskSourceFlyoutMenu/FlyoutItem/FlyoutItem.tsx new file mode 100644 index 0000000000..d08ad918a2 --- /dev/null +++ b/src/utils/components/DiskSourceFlyoutMenu/FlyoutItem/FlyoutItem.tsx @@ -0,0 +1,28 @@ +import React, { FC } from 'react'; + +import { DiskSourceOptionGroupItem } from '@kubevirt-utils/components/DiskModal/components/DiskSourceSelect/utils/types'; +import { Menu, MenuContent, MenuItem, MenuList } from '@patternfly/react-core'; + +import { DiskSourceFlyoutMenuProps } from '../DiskSourceFlyoutMenu'; + +import './FlyoutItem.scss'; + +type FlyoutItemProps = { + items: DiskSourceOptionGroupItem[]; + onSelect: DiskSourceFlyoutMenuProps['onSelect']; +}; +const FlyoutItem: FC = ({ items, onSelect }) => ( + + + + {items?.map(({ description, id, label }) => ( + onSelect(id)}> + {label} + + ))} + + + +); + +export default FlyoutItem; diff --git a/src/views/catalog/wizard/tabs/disks/WizardDisksTab.tsx b/src/views/catalog/wizard/tabs/disks/WizardDisksTab.tsx index 1b4ed2a886..833b60c60a 100644 --- a/src/views/catalog/wizard/tabs/disks/WizardDisksTab.tsx +++ b/src/views/catalog/wizard/tabs/disks/WizardDisksTab.tsx @@ -2,15 +2,17 @@ import React from 'react'; import { WizardTab } from '@catalog/wizard/tabs'; import DiskModal from '@kubevirt-utils/components/DiskModal/DiskModal'; +import { getInitialStateDiskForm } from '@kubevirt-utils/components/DiskModal/utils/constants'; +import { DiskFormState, SourceTypes } from '@kubevirt-utils/components/DiskModal/utils/types'; +import DiskSourceFlyoutMenu from '@kubevirt-utils/components/DiskSourceFlyoutMenu/DiskSourceFlyoutMenu'; import { useModal } from '@kubevirt-utils/components/ModalProvider/ModalProvider'; import SidebarEditor from '@kubevirt-utils/components/SidebarEditor/SidebarEditor'; import WindowsDrivers from '@kubevirt-utils/components/WindowsDrivers/WindowsDrivers'; import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; import { PATHS_TO_HIGHLIGHT } from '@kubevirt-utils/resources/vm/utils/constants'; -import { ensurePath } from '@kubevirt-utils/utils/utils'; +import { ensurePath, generatePrettyName } from '@kubevirt-utils/utils/utils'; import { ListPageBody, - ListPageCreateButton, ListPageFilter, useListPageFilter, VirtualizedTable, @@ -24,7 +26,7 @@ import useWizardDisksTableData from './hooks/useWizardDisksTableData'; import './wizard-disk-tab.scss'; -const WizardDisksTab: WizardTab = ({ loaded, tabsData, updateTabsData, updateVM, vm }) => { +const WizardDisksTab: WizardTab = ({ tabsData, updateTabsData, updateVM, vm }) => { const { t } = useKubevirtTranslation(); const { createModal } = useModal(); const columns = useDiskColumns(); @@ -40,9 +42,15 @@ const WizardDisksTab: WizardTab = ({ loaded, tabsData, updateTabsData, updateVM, pathsToHighlight={PATHS_TO_HIGHLIGHT.DISKS_TAB} resource={vm} > - - createModal(({ isOpen, onClose }) => ( + { + const diskState: DiskFormState = { + ...getInitialStateDiskForm(), + diskName: generatePrettyName('disk'), + diskSource: diskSource, + }; + + return createModal(({ isOpen, onClose }) => ( updateTabsData((draft) => { @@ -57,19 +65,16 @@ const WizardDisksTab: WizardTab = ({ loaded, tabsData, updateTabsData, updateVM, } createOwnerReference={false} headerText={t('Add disk')} + initialFormData={diskState} isOpen={isOpen} onClose={onClose} onSubmit={updateVM} vm={vm} /> - )) - } + )); + }} className="list-page-create-button-margin" - isDisabled={!loaded} - > - {t('Add disk')} - - + /> = ({ diskName }) => { createOwnerReference={false} headerText={t('Edit disk')} initialFormData={initialFormState} + isEditDisk isOpen={isOpen} onClose={onClose} onSubmit={updateVM} diff --git a/src/views/templates/details/tabs/disks/TemplateDisksPage.tsx b/src/views/templates/details/tabs/disks/TemplateDisksPage.tsx index d23058ce42..0051d57f85 100644 --- a/src/views/templates/details/tabs/disks/TemplateDisksPage.tsx +++ b/src/views/templates/details/tabs/disks/TemplateDisksPage.tsx @@ -4,18 +4,21 @@ import { TemplateModel, V1Template } from '@kubevirt-ui/kubevirt-api/console'; import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; import DiskListTitle from '@kubevirt-utils/components/DiskListTitle/DiskListTitle'; import DiskModal from '@kubevirt-utils/components/DiskModal/DiskModal'; +import { getInitialStateDiskForm } from '@kubevirt-utils/components/DiskModal/utils/constants'; +import { DiskFormState, SourceTypes } from '@kubevirt-utils/components/DiskModal/utils/types'; +import DiskSourceFlyoutMenu from '@kubevirt-utils/components/DiskSourceFlyoutMenu/DiskSourceFlyoutMenu'; import { useModal } from '@kubevirt-utils/components/ModalProvider/ModalProvider'; import SidebarEditor from '@kubevirt-utils/components/SidebarEditor/SidebarEditor'; import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; import { replaceTemplateVM } from '@kubevirt-utils/resources/template'; +import { generatePrettyName } from '@kubevirt-utils/utils/utils'; import { k8sUpdate, - ListPageBody, ListPageFilter, useListPageFilter, VirtualizedTable, } from '@openshift-console/dynamic-plugin-sdk'; -import { Button } from '@patternfly/react-core'; +import { PageSection, PageSectionVariants } from '@patternfly/react-core'; import useEditTemplateAccessReview from '../../hooks/useIsTemplateEditable'; @@ -62,30 +65,32 @@ const TemplateDisksPage: FC = ({ obj: template }) => { return (
- + onResourceUpdate={onSubmitTemplate} resource={template}> + { + const diskState: DiskFormState = { + ...getInitialStateDiskForm(), + diskName: generatePrettyName('disk'), + diskSource: diskSource, + }; - - + )); + }} + className="list-page-create-button-margin" + isTemplate + /> = ({ obj: template }) => { unfilteredData={data} /> - +
); }; diff --git a/src/views/templates/details/tabs/disks/components/DiskRowActions.tsx b/src/views/templates/details/tabs/disks/components/DiskRowActions.tsx index ddf0f21a9f..e01d95610f 100644 --- a/src/views/templates/details/tabs/disks/components/DiskRowActions.tsx +++ b/src/views/templates/details/tabs/disks/components/DiskRowActions.tsx @@ -74,8 +74,8 @@ const DiskRowActions: FC = ({ diskName, isDisabled, onUpdat createOwnerReference={false} headerText={t('Edit disk')} initialFormData={initialFormState} + isEditDisk isOpen={isOpen} - isTemplate onClose={onClose} onSubmit={onUpdate} vm={vm} diff --git a/src/views/virtualmachines/details/tabs/configuration/storage/components/tables/disk/DiskList.tsx b/src/views/virtualmachines/details/tabs/configuration/storage/components/tables/disk/DiskList.tsx index 21e1d61360..a58eebb25d 100644 --- a/src/views/virtualmachines/details/tabs/configuration/storage/components/tables/disk/DiskList.tsx +++ b/src/views/virtualmachines/details/tabs/configuration/storage/components/tables/disk/DiskList.tsx @@ -4,13 +4,16 @@ import { printableVMStatus } from 'src/views/virtualmachines/utils'; import { V1VirtualMachine, V1VirtualMachineInstance } from '@kubevirt-ui/kubevirt-api/kubevirt'; import DiskListTitle from '@kubevirt-utils/components/DiskListTitle/DiskListTitle'; import DiskModal from '@kubevirt-utils/components/DiskModal/DiskModal'; +import { getInitialStateDiskForm } from '@kubevirt-utils/components/DiskModal/utils/constants'; +import { DiskFormState, SourceTypes } from '@kubevirt-utils/components/DiskModal/utils/types'; +import DiskSourceFlyoutMenu from '@kubevirt-utils/components/DiskSourceFlyoutMenu/DiskSourceFlyoutMenu'; import { useModal } from '@kubevirt-utils/components/ModalProvider/ModalProvider'; import WindowsDrivers from '@kubevirt-utils/components/WindowsDrivers/WindowsDrivers'; import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; import useDisksTableData from '@kubevirt-utils/resources/vm/hooks/disk/useDisksTableData'; import useProvisioningPercentage from '@kubevirt-utils/resources/vm/hooks/useProvisioningPercentage'; +import { generatePrettyName } from '@kubevirt-utils/utils/utils'; import { - ListPageCreateButton, ListPageFilter, useListPageFilter, VirtualizedTable, @@ -51,22 +54,26 @@ const DiskList: FC = ({ onDiskUpdate, vm, vmi }) => { return (
- - createModal(({ isOpen, onClose }) => ( + { + const diskState: DiskFormState = { + ...getInitialStateDiskForm(), + diskName: generatePrettyName('disk'), + diskSource: diskSource, + }; + + return createModal(({ isOpen, onClose }) => ( - )) - } - className="disk-list-page__list-page-create-button" - > - {t('Add disk')} - + )); + }} + /> = ({ obj, onDiskUpdate, vm, vmi })