From 1c777cd64e606747eccb2f0494ad838109b859f7 Mon Sep 17 00:00:00 2001 From: Ugo Palatucci Date: Mon, 9 Oct 2023 10:55:34 +0200 Subject: [PATCH] CNV-33095: add volumesnapshot option in the add bootable volume modal --- locales/en/plugin__kubevirt-plugin.json | 8 +- .../AddBootableVolumeModal.tsx | 4 +- .../SourceTypeSelection.tsx | 7 +- .../components/VolumeSource/VolumeSource.tsx | 70 ++---- .../VolumeSource/components/PVCSource.tsx | 54 +++++ .../components/SnapshotSource.tsx | 26 +++ .../AddBootableVolumeModal/utils/constants.ts | 5 + .../AddBootableVolumeModal/utils/utils.ts | 205 ++++++++++++------ .../SelectSnapshot/SelectSnapshot.tsx | 57 +++++ .../SelectSnapshot/SelectSnapshotName.tsx | 87 ++++++++ .../SelectSnapshotNamespace.tsx | 89 ++++++++ .../components/SelectSnapshot/useSnapshots.ts | 70 ++++++ .../hooks/useCanCreateBootableVolume.ts | 13 +- .../AddBootableVolumeButton.tsx | 5 +- .../BootableVolumeList/BootableVolumeList.tsx | 4 +- 15 files changed, 582 insertions(+), 122 deletions(-) create mode 100644 src/utils/components/AddBootableVolumeModal/components/VolumeSource/components/PVCSource.tsx create mode 100644 src/utils/components/AddBootableVolumeModal/components/VolumeSource/components/SnapshotSource.tsx create mode 100644 src/utils/components/SelectSnapshot/SelectSnapshot.tsx create mode 100644 src/utils/components/SelectSnapshot/SelectSnapshotName.tsx create mode 100644 src/utils/components/SelectSnapshot/SelectSnapshotNamespace.tsx create mode 100644 src/utils/components/SelectSnapshot/useSnapshots.ts diff --git a/locales/en/plugin__kubevirt-plugin.json b/locales/en/plugin__kubevirt-plugin.json index 28e8e37f42..10c2d70004 100644 --- a/locales/en/plugin__kubevirt-plugin.json +++ b/locales/en/plugin__kubevirt-plugin.json @@ -10,6 +10,8 @@ "--- Select PVC project ---": "--- Select PVC project ---", "--- Select secret ---": "--- Select secret ---", "--- Select sysprep ---": "--- Select sysprep ---", + "--- Select VolumeSnapshot name ---": "--- Select VolumeSnapshot name ---", + "--- Select VolumeSnapshot project ---": "--- Select VolumeSnapshot project ---", ", {{prefferedQualifiedNodesSize}} matching preferred Nodes found": ", {{prefferedQualifiedNodesSize}} matching preferred Nodes found", "(default)": "(default)", "(default) | ": "(default) | ", @@ -594,6 +596,7 @@ "Loading Templates with available boot source": "Loading Templates with available boot source", "Local storage (LSO)": "Local storage (LSO)", "Location of the existing PVC": "Location of the existing PVC", + "Location of the existing Snapshot": "Location of the existing Snapshot", "Log into <2>Hybrid Cloud Console to track your Organization ID.": "Log into <2>Hybrid Cloud Console to track your Organization ID.", "M series": "M series", "M Series": "M Series", @@ -1121,7 +1124,7 @@ "Upload a boot source (Quick start)": "Upload a boot source (Quick start)", "Upload a new file to a PVC. A new PVC will be created.": "Upload a new file to a PVC. A new PVC will be created.", "Upload a new file to PVC. a new PVC will be created.": "Upload a new file to PVC. a new PVC will be created.", - "Upload a new volume, or use an existing PersistentVolumeClaim (PVC) or DataSource.": "Upload a new volume, or use an existing PersistentVolumeClaim (PVC) or DataSource.", + "Upload a new volume, or use an existing PersistentVolumeClaim (PVC), VolumeSnapshot or DataSource.": "Upload a new volume, or use an existing PersistentVolumeClaim (PVC), VolumeSnapshot or DataSource.", "Upload canceled": "Upload canceled", "Upload cancelled": "Upload cancelled", "Upload content from a container located in a registry accessible from the cluster. The container disk is meant to be used only for read-only filesystems such as CD-ROMs or for small short-lived throw-away VMs.": "Upload content from a container located in a registry accessible from the cluster. The container disk is meant to be used only for read-only filesystems such as CD-ROMs or for small short-lived throw-away VMs.", @@ -1145,6 +1148,7 @@ "Use cron formatting to set when and how often to look for new imports.": "Use cron formatting to set when and how often to look for new imports.", "Use existing": "Use existing", "Use existing volume": "Use existing volume", + "Use existing volume snapshot": "Use existing volume snapshot", "Use optimized access mode & volume mode settings from StorageProfile resource.": "Use optimized access mode & volume mode settings from StorageProfile resource.", "Use our collection of resources to help you get started with virtualization.": "Use our collection of resources to help you get started with virtualization.", "Use template size PVC": "Use template size PVC", @@ -1230,6 +1234,8 @@ "Volume snapshot status": "Volume snapshot status", "Volume Snapshot Status is a mechanism for reporting if a volume can be snapshotted or not.": "Volume Snapshot Status is a mechanism for reporting if a volume can be snapshotted or not.", "Volumes project": "Volumes project", + "VolumeSnapshot name": "VolumeSnapshot name", + "VolumeSnapshot project": "VolumeSnapshot project", "Warning": "Warning", "Warnings": "Warnings", "Weight": "Weight", diff --git a/src/utils/components/AddBootableVolumeModal/AddBootableVolumeModal.tsx b/src/utils/components/AddBootableVolumeModal/AddBootableVolumeModal.tsx index 1f49339961..cf0e6b975a 100644 --- a/src/utils/components/AddBootableVolumeModal/AddBootableVolumeModal.tsx +++ b/src/utils/components/AddBootableVolumeModal/AddBootableVolumeModal.tsx @@ -108,7 +108,9 @@ const AddBootableVolumeModal: FC = ({ obj={emptyDataSource} submitBtnText={t('Save')} > - {t('Upload a new volume, or use an existing PersistentVolumeClaim (PVC) or DataSource.')} + {t( + 'Upload a new volume, or use an existing PersistentVolumeClaim (PVC), VolumeSnapshot or DataSource.', + )}
= ({ setFormSelection, }) => { const [isOpen, setIsOpen] = useState(false); - const { canCreateDS, canCreatePVC, loading } = useCanCreateBootableVolume(namespace); + const { canCreateDS, canCreatePVC, canCreateSnapshots, loading } = + useCanCreateBootableVolume(namespace); const onSelect = useCallback( (event, value) => { @@ -53,6 +54,10 @@ const SourceTypeSelection: FC = ({ {t('Use existing volume')} + + {t('Use existing volume snapshot')} + + = ({ upload, }) => { const { t } = useKubevirtTranslation(); - const { pvcName, pvcNamespace, registryURL, uploadFile, uploadFilename } = bootableVolume || {}; + const { registryURL, uploadFile, uploadFilename } = bootableVolume || {}; - if (sourceType === DROPDOWN_FORM_SELECTION.UPLOAD_IMAGE) - return ( + const sourceComponentByType = { + [DROPDOWN_FORM_SELECTION.UPLOAD_IMAGE]: ( = ({ uploadFile={uploadFile} uploadFileName={uploadFilename} /> - ); - - if (sourceType === DROPDOWN_FORM_SELECTION.USE_REGISTRY) - return ( + ), + [DROPDOWN_FORM_SELECTION.USE_EXISTING_PVC]: ( + + ), + [DROPDOWN_FORM_SELECTION.USE_REGISTRY]: ( = ({ value={registryURL} /> - ); - - return ( - <> - - setBootableVolumeField('size')( - hasSizeUnit(newSize) - ? removeByteSuffix(newSize) - : removeByteSuffix(xbytes(Number(newSize), { iec: true, space: false })), - ) - } - pvcNameSelected={pvcName} - pvcNamespaceSelected={pvcNamespace} - selectPVCName={setBootableVolumeField('pvcName')} - selectPVCNamespace={setBootableVolumeField('pvcNamespace')} + ), + [DROPDOWN_FORM_SELECTION.USE_SNAPSHOT]: ( + - - - - - - - - - - ); + ), + }; + + return sourceComponentByType[sourceType]; }; export default VolumeSource; diff --git a/src/utils/components/AddBootableVolumeModal/components/VolumeSource/components/PVCSource.tsx b/src/utils/components/AddBootableVolumeModal/components/VolumeSource/components/PVCSource.tsx new file mode 100644 index 0000000000..5e0669916c --- /dev/null +++ b/src/utils/components/AddBootableVolumeModal/components/VolumeSource/components/PVCSource.tsx @@ -0,0 +1,54 @@ +import React, { FC } from 'react'; +import xbytes from 'xbytes'; + +import { + AddBootableVolumeState, + SetBootableVolumeFieldType, +} from '@kubevirt-utils/components/AddBootableVolumeModal/utils/constants'; +import { removeByteSuffix } from '@kubevirt-utils/components/CapacityInput/utils'; +import DiskSourcePVCSelect from '@kubevirt-utils/components/DiskModal/DiskFormFields/DiskSourceFormSelect/components/DiskSourcePVCSelect'; +import HelpTextIcon from '@kubevirt-utils/components/HelpTextIcon/HelpTextIcon'; +import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; +import { hasSizeUnit } from '@kubevirt-utils/resources/vm/utils/disk/size'; +import { Checkbox, PopoverPosition, Split, SplitItem } from '@patternfly/react-core'; + +type PVCSourceProps = { + bootableVolume: AddBootableVolumeState; + setBootableVolumeField: SetBootableVolumeFieldType; +}; + +const PVCSource: FC = ({ bootableVolume, setBootableVolumeField }) => { + const { t } = useKubevirtTranslation(); + const { pvcName, pvcNamespace } = bootableVolume || {}; + + return ( + <> + + setBootableVolumeField('size')( + hasSizeUnit(newSize) + ? removeByteSuffix(newSize) + : removeByteSuffix(xbytes(Number(newSize), { iec: true, space: false })), + ) + } + pvcNameSelected={pvcName} + pvcNamespaceSelected={pvcNamespace} + selectPVCName={setBootableVolumeField('pvcName')} + selectPVCNamespace={setBootableVolumeField('pvcNamespace')} + /> + + + + + + + + + + ); +}; + +export default PVCSource; diff --git a/src/utils/components/AddBootableVolumeModal/components/VolumeSource/components/SnapshotSource.tsx b/src/utils/components/AddBootableVolumeModal/components/VolumeSource/components/SnapshotSource.tsx new file mode 100644 index 0000000000..e83e8370f7 --- /dev/null +++ b/src/utils/components/AddBootableVolumeModal/components/VolumeSource/components/SnapshotSource.tsx @@ -0,0 +1,26 @@ +import React, { FC } from 'react'; + +import { + AddBootableVolumeState, + SetBootableVolumeFieldType, +} from '@kubevirt-utils/components/AddBootableVolumeModal/utils/constants'; +import SelectSnapshot from '@kubevirt-utils/components/SelectSnapshot/SelectSnapshot'; + +type SnapshotSourceProps = { + bootableVolume: AddBootableVolumeState; + setBootableVolumeField: SetBootableVolumeFieldType; +}; + +const SnapshotSource: FC = ({ bootableVolume, setBootableVolumeField }) => { + const { snapshotName, snapshotNamespace } = bootableVolume || {}; + return ( + + ); +}; + +export default SnapshotSource; diff --git a/src/utils/components/AddBootableVolumeModal/utils/constants.ts b/src/utils/components/AddBootableVolumeModal/utils/constants.ts index 7123d7a907..25065c74c1 100644 --- a/src/utils/components/AddBootableVolumeModal/utils/constants.ts +++ b/src/utils/components/AddBootableVolumeModal/utils/constants.ts @@ -14,6 +14,7 @@ export enum DROPDOWN_FORM_SELECTION { UPLOAD_IMAGE = 'upload', USE_EXISTING_PVC = 'pvc', USE_REGISTRY = 'registry', + USE_SNAPSHOT = 'snapshot', } export type AddBootableVolumeState = { @@ -26,6 +27,8 @@ export type AddBootableVolumeState = { registryURL: string; retainRevisions: number; size: string; + snapshotName: string; + snapshotNamespace: string; storageClassName: string; storageClassProvisioner: string; uploadFile: File | string; @@ -47,6 +50,8 @@ export const initialBootableVolumeState: AddBootableVolumeState = { registryURL: null, retainRevisions: 3, size: DEFAULT_DISK_SIZE, + snapshotName: null, + snapshotNamespace: null, storageClassName: null, storageClassProvisioner: null, uploadFile: null, diff --git a/src/utils/components/AddBootableVolumeModal/utils/utils.ts b/src/utils/components/AddBootableVolumeModal/utils/utils.ts index de8d2be596..5922e6cc96 100644 --- a/src/utils/components/AddBootableVolumeModal/utils/utils.ts +++ b/src/utils/components/AddBootableVolumeModal/utils/utils.ts @@ -45,71 +45,33 @@ export const createBootableVolume: createBootableVolumeType = uploadData, }) => async (dataSource: V1beta1DataSource) => { - const { - annotations, - bootableVolumeName, - labels, - pvcName, - pvcNamespace, - size, - storageClassName, - uploadFile, - } = bootableVolume || {}; - - const draftDataSource = produce(dataSource, (draftDS) => { - draftDS.metadata.name = bootableVolumeName; - draftDS.metadata.namespace = namespace; - draftDS.metadata.annotations = annotations; - draftDS.metadata.labels = labels; - }); - - if (sourceType === DROPDOWN_FORM_SELECTION.USE_REGISTRY) { - const newDataSource = await createDataSourceWithImportCron(bootableVolume, draftDataSource); - - onCreateVolume?.(newDataSource); - - return newDataSource; - } - - const isUploadForm = sourceType === DROPDOWN_FORM_SELECTION.UPLOAD_IMAGE; - - const bootableVolumeToCreate = produce(emptySourceDataVolume, (draftBootableVolume) => { - draftBootableVolume.metadata.name = bootableVolumeName; - draftBootableVolume.metadata.namespace = namespace; - draftBootableVolume.spec.storage.resources.requests.storage = size; - draftBootableVolume.metadata.labels = labels; - - if (storageClassName) { - draftBootableVolume.spec.storage.storageClassName = storageClassName; - } - - if (!applyStorageProfileSettings) { - draftBootableVolume.spec.storage.accessModes = claimPropertySets?.[0]?.accessModes; - draftBootableVolume.spec.storage.volumeMode = claimPropertySets?.[0]?.volumeMode; - } - - draftBootableVolume.spec.source = isUploadForm - ? { upload: {} } - : { pvc: { name: pvcName, namespace: pvcNamespace } }; - }); - - const dataSourceToCreate = produce(draftDataSource, (draftDS) => { - draftDS.spec.source = { - pvc: { - name: bootableVolumeToCreate.metadata.name, - namespace: bootableVolumeToCreate.metadata.namespace, - }, - }; - }); + const draftDataSource = setDataSourceMetadata(bootableVolume, namespace, dataSource); - isUploadForm - ? await uploadData({ - dataVolume: bootableVolumeToCreate, - file: uploadFile as File, - }) - : await k8sCreate({ data: bootableVolumeToCreate, model: DataVolumeModel }); + const actionBySourceType: Record Promise> = { + [DROPDOWN_FORM_SELECTION.UPLOAD_IMAGE]: () => + createBootableVolumeFromUpload( + bootableVolume, + namespace, + applyStorageProfileSettings, + claimPropertySets, + draftDataSource, + uploadData, + ), + [DROPDOWN_FORM_SELECTION.USE_EXISTING_PVC]: () => + createPVCBootableVolume( + bootableVolume, + namespace, + applyStorageProfileSettings, + claimPropertySets, + draftDataSource, + ), + [DROPDOWN_FORM_SELECTION.USE_REGISTRY]: () => + createDataSourceWithImportCron(bootableVolume, draftDataSource), + [DROPDOWN_FORM_SELECTION.USE_SNAPSHOT]: () => + createSnapshotDataSource(bootableVolume, draftDataSource), + }; - const newDataSource = await k8sCreate({ data: dataSourceToCreate, model: DataSourceModel }); + const newDataSource = await actionBySourceType[sourceType](); onCreateVolume?.(newDataSource); @@ -124,6 +86,123 @@ type CreateDataSourceWithImportCronType = ( initialDataSource: V1beta1DataSource, ) => Promise; +const setDataSourceMetadata = ( + bootableVolume: AddBootableVolumeState, + namespace: string, + dataSource: V1beta1DataSource, +): V1beta1DataSource => { + const { annotations, bootableVolumeName, labels } = bootableVolume || {}; + + return produce(dataSource, (draftDS) => { + draftDS.metadata.name = bootableVolumeName; + draftDS.metadata.namespace = namespace; + draftDS.metadata.annotations = annotations; + draftDS.metadata.labels = labels; + }); +}; + +const createBootableVolumeFromUpload = async ( + bootableVolume: AddBootableVolumeState, + namespace: string, + applyStorageProfileSettings: boolean, + claimPropertySets: ClaimPropertySets, + draftDataSource: V1beta1DataSource, + uploadData: ({ dataVolume, file }: UploadDataProps) => Promise, +) => { + const { bootableVolumeName, labels, size, storageClassName, uploadFile } = bootableVolume || {}; + + const bootableVolumeToCreate = produce(emptySourceDataVolume, (draftBootableVolume) => { + draftBootableVolume.metadata.name = bootableVolumeName; + draftBootableVolume.metadata.namespace = namespace; + draftBootableVolume.spec.storage.resources.requests.storage = size; + draftBootableVolume.metadata.labels = labels; + + if (storageClassName) { + draftBootableVolume.spec.storage.storageClassName = storageClassName; + } + + if (!applyStorageProfileSettings) { + draftBootableVolume.spec.storage.accessModes = claimPropertySets?.[0]?.accessModes; + draftBootableVolume.spec.storage.volumeMode = claimPropertySets?.[0]?.volumeMode; + } + + draftBootableVolume.spec.source = { upload: {} }; + }); + + const dataSourceToCreate = produce(draftDataSource, (draftDS) => { + draftDS.spec.source = { + pvc: { + name: bootableVolumeToCreate.metadata.name, + namespace: bootableVolumeToCreate.metadata.namespace, + }, + }; + }); + await uploadData({ + dataVolume: bootableVolumeToCreate, + file: uploadFile as File, + }); + + return await k8sCreate({ data: dataSourceToCreate, model: DataSourceModel }); +}; + +const createSnapshotDataSource = async ( + bootableVolume: AddBootableVolumeState, + draftDataSource: V1beta1DataSource, +) => { + const dataSourceToCreate = produce(draftDataSource, (draftDS) => { + draftDS.spec.source = { + snapshot: { + name: bootableVolume.snapshotName, + namespace: bootableVolume.snapshotNamespace, + }, + }; + }); + + return await k8sCreate({ data: dataSourceToCreate, model: DataSourceModel }); +}; + +const createPVCBootableVolume = async ( + bootableVolume: AddBootableVolumeState, + namespace: string, + applyStorageProfileSettings: boolean, + claimPropertySets: ClaimPropertySets, + draftDataSource: V1beta1DataSource, +) => { + const { bootableVolumeName, labels, pvcName, pvcNamespace, size, storageClassName } = + bootableVolume || {}; + + const bootableVolumeToCreate = produce(emptySourceDataVolume, (draftBootableVolume) => { + draftBootableVolume.metadata.name = bootableVolumeName; + draftBootableVolume.metadata.namespace = namespace; + draftBootableVolume.spec.storage.resources.requests.storage = size; + draftBootableVolume.metadata.labels = labels; + + if (storageClassName) { + draftBootableVolume.spec.storage.storageClassName = storageClassName; + } + + if (!applyStorageProfileSettings) { + draftBootableVolume.spec.storage.accessModes = claimPropertySets?.[0]?.accessModes; + draftBootableVolume.spec.storage.volumeMode = claimPropertySets?.[0]?.volumeMode; + } + + draftBootableVolume.spec.source = { pvc: { name: pvcName, namespace: pvcNamespace } }; + }); + + const dataSourceToCreate = produce(draftDataSource, (draftDS) => { + draftDS.spec.source = { + pvc: { + name: bootableVolumeToCreate.metadata.name, + namespace: bootableVolumeToCreate.metadata.namespace, + }, + }; + }); + + await k8sCreate({ data: bootableVolumeToCreate, model: DataVolumeModel }); + + return await k8sCreate({ data: dataSourceToCreate, model: DataSourceModel }); +}; + export const createDataSourceWithImportCron: CreateDataSourceWithImportCronType = async ( bootableVolume, initialDataSource, diff --git a/src/utils/components/SelectSnapshot/SelectSnapshot.tsx b/src/utils/components/SelectSnapshot/SelectSnapshot.tsx new file mode 100644 index 0000000000..ce3374bf0c --- /dev/null +++ b/src/utils/components/SelectSnapshot/SelectSnapshot.tsx @@ -0,0 +1,57 @@ +import React, { FC, useCallback, useMemo } from 'react'; + +import SelectSnapshotName from './SelectSnapshotName'; +import SelectSnapshotNamespace from './SelectSnapshotNamespace'; +import useSnapshots from './useSnapshots'; + +type SelectSnapshotProps = { + selectSnapshotName: (value: string) => void; + selectSnapshotNamespace?: (value: string) => void; + snapshotNameSelected: string; + snapshotNamespaceSelected: string; +}; + +const SelectSnapshot: FC = ({ + selectSnapshotName, + selectSnapshotNamespace, + snapshotNameSelected, + snapshotNamespaceSelected, +}) => { + const { projectsLoaded, projectsNames, snapshots, snapshotsLoaded } = + useSnapshots(snapshotNamespaceSelected); + + const onSelectProject = useCallback( + (newProject) => { + selectSnapshotNamespace && selectSnapshotNamespace(newProject); + selectSnapshotName(undefined); + }, + [selectSnapshotNamespace, selectSnapshotName], + ); + + const snapshotNames = useMemo( + () => + snapshots?.map((snapshot) => snapshot?.metadata?.name)?.sort((a, b) => a?.localeCompare(b)), + [snapshots], + ); + + return ( +
+ + +
+ ); +}; + +export default SelectSnapshot; diff --git a/src/utils/components/SelectSnapshot/SelectSnapshotName.tsx b/src/utils/components/SelectSnapshot/SelectSnapshotName.tsx new file mode 100644 index 0000000000..76df5c03f8 --- /dev/null +++ b/src/utils/components/SelectSnapshot/SelectSnapshotName.tsx @@ -0,0 +1,87 @@ +import React, { Dispatch, FC, ReactElement, SetStateAction, useCallback, useState } from 'react'; + +import { modelToGroupVersionKind, VolumeSnapshotModel } from '@kubevirt-ui/kubevirt-api/console'; +import Loading from '@kubevirt-utils/components/Loading/Loading'; +import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; +import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; +import { FormGroup, Select, SelectOption, SelectVariant, Truncate } from '@patternfly/react-core'; + +type SelectSnapshotNameProps = { + isDisabled?: boolean; + onChange: Dispatch>; + snapshotNames: string[]; + snapshotNameSelected: string; + snapshotsLoaded: boolean; +}; + +export const FilterSnapshotSelect = (options: string[]) => { + return (_, value: string): ReactElement[] => { + let newOptions = options; + + if (value) { + const regex = new RegExp(value, 'i'); + newOptions = options.filter((namespace) => regex.test(namespace)); + } + + return newOptions.map((namespace) => ( + + )) as ReactElement[]; + }; +}; + +const SelectSnapshotName: FC = ({ + isDisabled, + onChange, + snapshotNames, + snapshotNameSelected, + snapshotsLoaded, +}) => { + const { t } = useKubevirtTranslation(); + const [isOpen, setSelectOpen] = useState(false); + + const onSelect = useCallback( + (event, selection) => { + onChange(selection); + setSelectOpen(false); + }, + [onChange], + ); + + const fieldId = 'snapshot-name-select'; + + return ( + + {snapshotsLoaded ? ( + + ) : ( + + )} + + ); +}; + +export default SelectSnapshotName; diff --git a/src/utils/components/SelectSnapshot/SelectSnapshotNamespace.tsx b/src/utils/components/SelectSnapshot/SelectSnapshotNamespace.tsx new file mode 100644 index 0000000000..2bef265f98 --- /dev/null +++ b/src/utils/components/SelectSnapshot/SelectSnapshotNamespace.tsx @@ -0,0 +1,89 @@ +import React, { Dispatch, FC, ReactElement, SetStateAction, useCallback, useState } from 'react'; + +import { ProjectModel } from '@kubevirt-ui/kubevirt-api/console'; +import Loading from '@kubevirt-utils/components/Loading/Loading'; +import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; +import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; +import { FormGroup, Select, SelectOption, SelectVariant } from '@patternfly/react-core'; + +type SelectSnapshotNamespaceProps = { + isDisabled?: boolean; + onChange: Dispatch>; + projectsLoaded: boolean; + projectsName: string[]; + selectedProject: string; +}; + +export const FilterSnapshotSelect = (options: string[]) => { + return (_, value: string): ReactElement[] => { + let newOptions = options; + + if (value) { + const regex = new RegExp(value, 'i'); + newOptions = options.filter((namespace) => regex.test(namespace)); + } + + return newOptions.map((namespace) => ( + + )) as ReactElement[]; + }; +}; + +const SelectSnapshotNamespace: FC = ({ + isDisabled, + onChange, + projectsLoaded, + projectsName, + selectedProject, +}) => { + const { t } = useKubevirtTranslation(); + const [isNamespaceSnapshotOpen, setNamespaceOpen] = useState(false); + + const onSelect = useCallback( + (event, selection) => { + onChange(selection); + setNamespaceOpen(false); + }, + [onChange], + ); + + const fieldId = 'snapshot-project-select'; + + return ( + + {projectsLoaded ? ( + + ) : ( + + )} + + ); +}; + +export default SelectSnapshotNamespace; diff --git a/src/utils/components/SelectSnapshot/useSnapshots.ts b/src/utils/components/SelectSnapshot/useSnapshots.ts new file mode 100644 index 0000000000..31cace31b3 --- /dev/null +++ b/src/utils/components/SelectSnapshot/useSnapshots.ts @@ -0,0 +1,70 @@ +import { useMemo } from 'react'; + +import { + modelToGroupVersionKind, + ProjectModel, + VolumeSnapshotModel, +} from '@kubevirt-ui/kubevirt-api/console'; +import { V1VolumeSnapshotStatus } from '@kubevirt-ui/kubevirt-api/kubevirt'; +import { K8sResourceCommon, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; + +type UseSnapshotsReturnType = { + error: Error; + projectsLoaded: boolean; + projectsNames: string[]; + snapshots: VolumeSnapshotKind[]; + snapshotsLoaded: boolean; +}; + +export type VolumeSnapshotKind = K8sResourceCommon & { + spec: { + source: { + persistentVolumeClaimName?: string; + volumeSnapshotContentName?: string; + }; + volumeSnapshotClassName: string; + }; + status?: V1VolumeSnapshotStatus & { + boundVolumeSnapshotContentName?: string; + }; +}; + +const useSnapshots = (projectSelected: string): UseSnapshotsReturnType => { + const [projects, projectsLoaded, projectsErrors] = useK8sWatchResource({ + groupVersionKind: modelToGroupVersionKind(ProjectModel), + isList: true, + namespaced: false, + }); + + const projectsNames = useMemo( + () => projects?.map((project) => project?.metadata?.name)?.sort((a, b) => a?.localeCompare(b)), + [projects], + ); + + const snapshotWathcResource = projectSelected + ? { + groupVersionKind: modelToGroupVersionKind(VolumeSnapshotModel), + isList: true, + namespace: projectSelected, + namespaced: true, + } + : null; + + const [snapshotsRaw, snapshotsLoaded, snapshotsErrors] = + useK8sWatchResource(snapshotWathcResource); + + const snapshots = useMemo( + () => (snapshotsRaw || [])?.sort((a, b) => a?.metadata?.name?.localeCompare(b?.metadata?.name)), + [snapshotsRaw], + ); + + return { + error: projectsErrors || snapshotsErrors, + projectsLoaded, + projectsNames, + snapshots, + snapshotsLoaded, + }; +}; + +export default useSnapshots; diff --git a/src/utils/resources/bootableresources/hooks/useCanCreateBootableVolume.ts b/src/utils/resources/bootableresources/hooks/useCanCreateBootableVolume.ts index 8b94c6064b..b990274866 100644 --- a/src/utils/resources/bootableresources/hooks/useCanCreateBootableVolume.ts +++ b/src/utils/resources/bootableresources/hooks/useCanCreateBootableVolume.ts @@ -1,4 +1,4 @@ -import { PersistentVolumeClaimModel } from '@kubevirt-ui/kubevirt-api/console'; +import { PersistentVolumeClaimModel, VolumeSnapshotModel } from '@kubevirt-ui/kubevirt-api/console'; import DataImportCronModel from '@kubevirt-ui/kubevirt-api/console/models/DataImportCronModel'; import DataSourceModel from '@kubevirt-ui/kubevirt-api/console/models/DataSourceModel'; import VirtualMachineClusterPreferenceModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineClusterPreferenceModel'; @@ -7,6 +7,7 @@ import { K8sVerb, useAccessReview } from '@openshift-console/dynamic-plugin-sdk' type UseCanCreateBootableVolume = (namespace: string) => { canCreateDS: boolean; canCreatePVC: boolean; + canCreateSnapshots: boolean; canListInstanceTypesPrefernce: boolean; loading: boolean; }; @@ -18,6 +19,12 @@ const useCanCreateBootableVolume: UseCanCreateBootableVolume = (namespace) => { resource: PersistentVolumeClaimModel.plural, verb: 'create' as K8sVerb, }); + const [canCreateSnapshots, loadingShapshots] = useAccessReview({ + group: VolumeSnapshotModel.apiGroup, + namespace: namespace, + resource: VolumeSnapshotModel.plural, + verb: 'create' as K8sVerb, + }); const [canCreateDS, loadingDS] = useAccessReview({ group: DataSourceModel.apiGroup, @@ -42,8 +49,10 @@ const useCanCreateBootableVolume: UseCanCreateBootableVolume = (namespace) => { return { canCreateDS: canCreateDS && canCreateDIC, canCreatePVC, + canCreateSnapshots, canListInstanceTypesPrefernce, - loading: loadingPVC || loadingDS || loadingDIC || loadingInstanceTypesPrefernce, + loading: + loadingPVC || loadingDS || loadingDIC || loadingInstanceTypesPrefernce || loadingShapshots, }; }; diff --git a/src/views/catalog/CreateFromInstanceTypes/components/AddBootableVolumeButton/AddBootableVolumeButton.tsx b/src/views/catalog/CreateFromInstanceTypes/components/AddBootableVolumeButton/AddBootableVolumeButton.tsx index c2d417e976..ed751c1454 100644 --- a/src/views/catalog/CreateFromInstanceTypes/components/AddBootableVolumeButton/AddBootableVolumeButton.tsx +++ b/src/views/catalog/CreateFromInstanceTypes/components/AddBootableVolumeButton/AddBootableVolumeButton.tsx @@ -1,12 +1,11 @@ import React, { FC } from 'react'; +import { getOSImagesNS } from 'src/views/clusteroverview/OverviewTab/inventory-card/utils/utils'; import { useInstanceTypeVMStore } from '@catalog/CreateFromInstanceTypes/state/useInstanceTypeVMStore'; import AddBootableVolumeModal from '@kubevirt-utils/components/AddBootableVolumeModal/AddBootableVolumeModal'; import { useModal } from '@kubevirt-utils/components/ModalProvider/ModalProvider'; -import { KUBEVIRT_OS_IMAGES_NS, OPENSHIFT_OS_IMAGES_NS } from '@kubevirt-utils/constants/constants'; import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; import useCanCreateBootableVolume from '@kubevirt-utils/resources/bootableresources/hooks/useCanCreateBootableVolume'; -import { isUpstream } from '@kubevirt-utils/utils/utils'; import { Button, ButtonVariant } from '@patternfly/react-core'; export type AddBootableVolumeButtonProps = { @@ -17,7 +16,7 @@ const AddBootableVolumeButton: FC = ({ loadError } const { t } = useKubevirtTranslation(); const { createModal } = useModal(); - const sourceNamespace = isUpstream ? KUBEVIRT_OS_IMAGES_NS : OPENSHIFT_OS_IMAGES_NS; + const sourceNamespace = getOSImagesNS(); const { canCreateDS, canCreatePVC } = useCanCreateBootableVolume(sourceNamespace); const canCreate = canCreateDS || canCreatePVC; diff --git a/src/views/catalog/CreateFromInstanceTypes/components/BootableVolumeList/BootableVolumeList.tsx b/src/views/catalog/CreateFromInstanceTypes/components/BootableVolumeList/BootableVolumeList.tsx index be270c663e..68505784c3 100644 --- a/src/views/catalog/CreateFromInstanceTypes/components/BootableVolumeList/BootableVolumeList.tsx +++ b/src/views/catalog/CreateFromInstanceTypes/components/BootableVolumeList/BootableVolumeList.tsx @@ -1,9 +1,9 @@ import React, { FC, useEffect, useMemo, useState } from 'react'; +import { getOSImagesNS } from 'src/views/clusteroverview/OverviewTab/inventory-card/utils/utils'; import { useInstanceTypeVMStore } from '@catalog/CreateFromInstanceTypes/state/useInstanceTypeVMStore'; import { UseBootableVolumesValues } from '@catalog/CreateFromInstanceTypes/state/utils/types'; import { V1beta1VirtualMachineClusterPreference } from '@kubevirt-ui/kubevirt-api/kubevirt'; -import { OPENSHIFT_OS_IMAGES_NS } from '@kubevirt-utils/constants/constants'; import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; import { getBootableVolumePVCSource } from '@kubevirt-utils/resources/bootableresources/helpers'; import { BootableVolume } from '@kubevirt-utils/resources/bootableresources/types'; @@ -99,7 +99,7 @@ const BootableVolumeList: FC = ({ aria-label="bootable volume list" className="bootable-volume-list-bar__volume-namespace" isDisabled - value={OPENSHIFT_OS_IMAGES_NS} + value={getOSImagesNS()} />