From 4978f5ddf655860f6ecb341971006b16618a68c5 Mon Sep 17 00:00:00 2001 From: Yaacov Zamir Date: Wed, 21 Feb 2024 18:15:19 +0200 Subject: [PATCH] Storage map details page Signed-off-by: Yaacov Zamir --- .../en/plugin__forklift-console-plugin.json | 5 +- .../components/MapsSection/MapsSection.tsx | 7 +- .../modules/StorageMaps/StorageMappingRow.tsx | 57 -- .../StorageMaps/StorageMappingsPage.tsx | 119 --- .../StorageMaps/StorageMappingsWrapper.tsx | 8 - .../StorageMaps/UseStorageMappingActions.tsx | 3 - .../StorageMaps/__tests__/MappingRow.test.tsx | 40 - .../__snapshots__/MappingRow.test.tsx.snap | 830 ------------------ .../StorageMaps/__tests__/mergeData.test.ts | 70 -- .../__tests__/mergedStorageData.json | 411 --------- .../StorageMapActionsDropdown.style.css | 4 + .../actions/StorageMapActionsDropdown.tsx | 53 ++ .../StorageMapActionsDropdownItems.tsx | 40 + .../src/modules/StorageMaps/actions/index.ts | 4 + .../StorageMapCriticalConditions.tsx | 25 + .../componenets/StorageMapsAddButton.tsx | 33 + .../StorageMapsEmptyState.tsx} | 35 +- .../modules/StorageMaps/componenets/index.ts | 5 + .../src/modules/StorageMaps/dataForStorage.ts | 130 --- .../src/modules/StorageMaps/dynamic-plugin.ts | 39 +- .../modules/StorageMaps/mappingActions.tsx | 19 - .../src/modules/StorageMaps/styles.css | 10 - .../StorageMaps/utils/constants/index.ts | 3 + .../utils/constants/storage-map-status.ts | 5 + .../StorageMaps/utils/helpers/deepCopy.ts | 3 + .../utils/helpers/getStorageMapPhase.ts | 18 + .../StorageMaps/utils/helpers/index.ts | 4 + .../src/modules/StorageMaps/utils/index.ts | 5 + .../StorageMaps/utils/types/StorageMapData.ts | 8 + .../modules/StorageMaps/utils/types/index.ts | 3 + .../details/StorageMapDetailsPage.style.css | 67 ++ .../views/details/StorageMapDetailsPage.tsx | 55 ++ .../ConditionsSection/ConditionsSection.tsx | 70 ++ .../components/ConditionsSection/index.ts | 3 + .../DetailsSection/DetailsSection.tsx | 40 + .../components/CreatedAtDetailsItem.tsx | 32 + .../components/NameDetailsItem.tsx | 29 + .../components/NamespaceDetailsItem.tsx | 40 + .../components/OwnerDetailsItem.tsx | 32 + .../components/StorageDetailsItemProps.tsx | 10 + .../components/StorageMapPageHeadings.tsx | 64 ++ .../DetailsSection/components/index.ts | 7 + .../components/DetailsSection/index.ts | 4 + .../components/MapsSection/MapsSection.tsx | 197 +++++ .../MapsSection/components/MapsEdit.tsx | 102 +++ .../MapsSection/components/index.ts | 3 + .../details/components/MapsSection/index.ts | 5 + .../components/MapsSection/state/index.ts | 3 + .../components/MapsSection/state/reducer.ts | 35 + .../ProvidersSection/ProvidersSection.tsx | 118 +++ .../components/ProvidersEdit.tsx | 102 +++ .../ProvidersSection/components/index.ts | 3 + .../components/ProvidersSection/index.ts | 5 + .../ProvidersSection/state/index.ts | 3 + .../ProvidersSection/state/reducer.ts | 66 ++ .../views/details/components/index.ts | 6 + .../StorageMaps/views/details/index.ts | 4 + .../tabs/Details/StorageMapDetailsTab.tsx | 52 ++ .../views/details/tabs/Details/index.ts | 3 + .../details/tabs/YAML/StorageMapYAMLTab.tsx | 32 + .../views/details/tabs/YAML/index.ts | 3 + .../StorageMaps/views/details/tabs/index.ts | 4 + .../StorageMaps/views/list/StorageMapRow.tsx | 58 ++ .../views/list/StorageMapsListPage.style.css | 8 + .../views/list/StorageMapsListPage.tsx | 167 ++++ .../views/list/components/CellProps.tsx | 9 + .../views/list/components/NamespaceCell.tsx | 22 + .../views/list/components/PlanCell.tsx | 22 + .../list/components/ProviderLinkCell.tsx | 23 + .../views/list/components/StatusCell.tsx | 93 ++ .../list/components/StorageMapLinkCell.tsx | 19 + .../views/list/components/index.ts | 8 + .../modules/StorageMaps/views/list/index.ts | 4 + .../yamlTemplates/defaultYamlTemplate.ts | 20 + .../StorageMaps/yamlTemplates/index.ts | 3 + 75 files changed, 1919 insertions(+), 1732 deletions(-) delete mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/StorageMappingRow.tsx delete mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/StorageMappingsPage.tsx delete mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/StorageMappingsWrapper.tsx delete mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/UseStorageMappingActions.tsx delete mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/MappingRow.test.tsx delete mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/__snapshots__/MappingRow.test.tsx.snap delete mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/mergeData.test.ts delete mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/mergedStorageData.json create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/actions/StorageMapActionsDropdown.style.css create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/actions/StorageMapActionsDropdown.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/actions/StorageMapActionsDropdownItems.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/actions/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/componenets/StorageMapCriticalConditions.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/componenets/StorageMapsAddButton.tsx rename packages/forklift-console-plugin/src/modules/StorageMaps/{EmptyStateStorageMaps.tsx => componenets/StorageMapsEmptyState.tsx} (63%) create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/componenets/index.ts delete mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/dataForStorage.ts delete mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/mappingActions.tsx delete mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/styles.css create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/utils/constants/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/utils/constants/storage-map-status.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/utils/helpers/deepCopy.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/utils/helpers/getStorageMapPhase.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/utils/helpers/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/utils/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/utils/types/StorageMapData.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/utils/types/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/StorageMapDetailsPage.style.css create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/StorageMapDetailsPage.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ConditionsSection/ConditionsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ConditionsSection/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/DetailsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/CreatedAtDetailsItem.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/NameDetailsItem.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/NamespaceDetailsItem.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/OwnerDetailsItem.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/StorageDetailsItemProps.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/StorageMapPageHeadings.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/MapsSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/components/MapsEdit.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/components/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/state/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/state/reducer.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/ProvidersSection.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/components/ProvidersEdit.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/components/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/state/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/state/reducer.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/Details/StorageMapDetailsTab.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/Details/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/YAML/StorageMapYAMLTab.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/YAML/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/list/StorageMapRow.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/list/StorageMapsListPage.style.css create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/list/StorageMapsListPage.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/CellProps.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/NamespaceCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/PlanCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/ProviderLinkCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/StatusCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/StorageMapLinkCell.tsx create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/views/list/index.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/yamlTemplates/defaultYamlTemplate.ts create mode 100644 packages/forklift-console-plugin/src/modules/StorageMaps/yamlTemplates/index.ts diff --git a/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json b/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json index 2108bcefb..61d779eb9 100644 --- a/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json +++ b/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json @@ -99,6 +99,7 @@ "Delete NetworkMap?": "Delete NetworkMap?", "Delete Plan?": "Delete Plan?", "Delete Provider": "Delete Provider", + "Delete StorageMap": "Delete StorageMap", "Delete StorageMap?": "Delete StorageMap?", "Description": "Description", "Details": "Details", @@ -204,7 +205,6 @@ "Migration networks maps are used to map network interfaces between source and target workloads.": "Migration networks maps are used to map network interfaces between source and target workloads.", "Migration plans are used to plan migration or virtualization workloads from source providers to target providers.": "Migration plans are used to plan migration or virtualization workloads from source providers to target providers.", "Migration started": "Migration started", - "Migration storage maps are used to map storage interfaces between source and target workloads.": "Migration storage maps are used to map storage interfaces between source and target workloads.", "Migration Toolkit for Virtualization": "Migration Toolkit for Virtualization", "Migrations": "Migrations", "Migrations (last 24 hours)": "Migrations (last 24 hours)", @@ -375,6 +375,8 @@ "Storage Map name re-generated": "Storage Map name re-generated", "Storage map:": "Storage map:", "Storage mappings have been re-generated": "Storage mappings have been re-generated", + "StorageMap details": "StorageMap details", + "StorageMap YAML": "StorageMap YAML", "StorageMaps": "StorageMaps", "StorageMaps for virtualization": "StorageMaps for virtualization", "Storages used by the selected VMs": "Storages used by the selected VMs", @@ -410,7 +412,6 @@ "This plan cannot be edited because it is running must gather.": "This plan cannot be edited because it is running must gather.", "This plan cannot be edited because the inventory data for its associated providers is not ready.": "This plan cannot be edited because the inventory data for its associated providers is not ready.", "This plan cannot be restarted because it is running must gather service": "This plan cannot be restarted because it is running must gather service", - "To": "To", "To make changes to the plan, select Duplicate and edit the duplicate plan.": "To make changes to the plan, select Duplicate and edit the duplicate plan.", "To troubleshoot, check the Forklift controller pod events and logs.": "To troubleshoot, check the Forklift controller pod events and logs.", "To troubleshoot, check the Forklift controller pod logs.": "To troubleshoot, check the Forklift controller pod logs.", diff --git a/packages/forklift-console-plugin/src/modules/NetworkMaps/views/details/components/MapsSection/MapsSection.tsx b/packages/forklift-console-plugin/src/modules/NetworkMaps/views/details/components/MapsSection/MapsSection.tsx index 2f19855d6..b32c455c4 100644 --- a/packages/forklift-console-plugin/src/modules/NetworkMaps/views/details/components/MapsSection/MapsSection.tsx +++ b/packages/forklift-console-plugin/src/modules/NetworkMaps/views/details/components/MapsSection/MapsSection.tsx @@ -98,6 +98,11 @@ export const MapsSection: React.FC = ({ obj }) => { ); const nextSourceNet = sourceNetworks.find((n) => n?.name === next.source); + // sanity check, names may not be valid + if (!nextSourceNet) { + return; + } + const nextMap: V1beta1NetworkMapSpecMap = { source: convertInventoryNetworkToV1beta1NetworkMapSpecMapSource(nextSourceNet), destination: @@ -244,4 +249,4 @@ function convertOpenShiftNetworkAttachmentDefinitionToV1beta1NetworkMapSpecMapDe } const OpenShiftNetworkAttachmentDefinitionToName = (net) => - net?.namespace ? `${net?.namespace}/${net?.name}` : net?.name; + net?.namespace ? `${net?.namespace}/${net?.name}` : net?.name || 'Pod'; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/StorageMappingRow.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/StorageMappingRow.tsx deleted file mode 100644 index 9ef368b5d..000000000 --- a/packages/forklift-console-plugin/src/modules/StorageMaps/StorageMappingRow.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import MappingRow, { - CellCreator, - CellProps, - commonCells, - SourceCell, -} from 'src/components/mappings/MappingRow'; -import * as C from 'src/utils/constants'; - -import { RowProps } from '@kubev2v/common'; -import { MappingType } from '@kubev2v/legacy/queries/types'; -import { Label } from '@patternfly/react-core'; -import { StorageDomainIcon } from '@patternfly/react-icons'; - -import { FlatStorageMapping } from './dataForStorage'; -import { StorageMappingActions } from './mappingActions'; - -import './styles.css'; - -const SourceStorageCell = ({ resourceData }: CellProps) => { - return ( - - ); -}; - -const TargetStorageCell = ({ resourceData }: CellProps) => ( - - {resourceData.to.map(({ name }) => ( - - ))} - -); - -const storageCells: CellCreator = { - [C.FROM]: SourceStorageCell, - [C.TO]: TargetStorageCell, - [C.ACTIONS]: ({ resourceData: resourceData }: CellProps) => ( - - ), -}; - -const StorageMappingRow = (props: RowProps) => ( - -); - -export default StorageMappingRow; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/StorageMappingsPage.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/StorageMappingsPage.tsx deleted file mode 100644 index 2e6c0e036..000000000 --- a/packages/forklift-console-plugin/src/modules/StorageMaps/StorageMappingsPage.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React, { useState } from 'react'; -import { - AddMappingButton, - commonFieldsMetadataFactory, - StartWithEmptyColumnMapper, -} from 'src/components/mappings/MappingPage'; -import StandardPage from 'src/components/page/StandardPage'; -import * as C from 'src/utils/constants'; -import { useForkliftTranslation } from 'src/utils/i18n'; -import { ResourceConsolePageProps } from 'src/utils/types'; - -import { FreetextFilter, UserSettings } from '@kubev2v/common'; -import { ValueMatcher } from '@kubev2v/common'; -import { loadUserSettings } from '@kubev2v/common'; -import { ResourceFieldFactory } from '@kubev2v/common'; -import { MappingType } from '@kubev2v/legacy/queries/types'; -import { StorageMapModel } from '@kubev2v/types'; -import { useAccessReview } from '@openshift-console/dynamic-plugin-sdk'; - -import { FlatStorageMapping, Storage, useFlatStorageMappings } from './dataForStorage'; -import EmptyStateStorageMaps from './EmptyStateStorageMaps'; -import StorageMappingRow from './StorageMappingRow'; - -export const fieldsMetadataFactory: ResourceFieldFactory = (t) => [ - ...commonFieldsMetadataFactory(t), - { - resourceFieldId: C.TO, - label: t('To'), - isVisible: true, - filter: { - type: 'targetStorage', - placeholderLabel: t('Filter by name'), - }, - sortable: false, - }, - { - resourceFieldId: C.ACTIONS, - label: '', - isVisible: true, - isAction: true, - sortable: false, - }, -]; - -export const StorageMappingsPage = ({ namespace }: ResourceConsolePageProps) => { - const { t } = useForkliftTranslation(); - const [userSettings] = useState(() => loadUserSettings({ pageId: 'StorageMappings' })); - const dataSource = useFlatStorageMappings({ - namespace, - }); - - const [canCreate] = useAccessReview({ - group: StorageMapModel.apiGroup, - resource: StorageMapModel.plural, - verb: 'create', - namespace, - }); - - return ( - - ); -}; -StorageMappingsPage.displayName = 'StorageMappingsPage'; - -const Page = ({ - dataSource, - namespace, - title, - userSettings, - canCreate, -}: { - dataSource: [FlatStorageMapping[], boolean, boolean]; - namespace: string; - title: string; - userSettings: UserSettings; - canCreate: boolean; -}) => { - const { t } = useForkliftTranslation(); - - return ( - - addButton={ - canCreate && ( - - ) - } - dataSource={dataSource} - RowMapper={StorageMappingRow} - HeaderMapper={StartWithEmptyColumnMapper} - fieldsMetadata={fieldsMetadataFactory(t)} - namespace={namespace} - title={title} - userSettings={userSettings} - extraSupportedFilters={{ targetStorage: FreetextFilter }} - extraSupportedMatchers={[targetStorageMatcher]} - customNoResultsFound={} - /> - ); -}; - -const PageMemo = React.memo(Page); - -const targetStorageMatcher: ValueMatcher = { - filterType: 'targetStorage', - matchValue: (storages: Storage[]) => (filter: string) => - storages?.some((storage) => storage?.name?.includes(filter?.trim()) ?? false), -}; - -export default StorageMappingsPage; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/StorageMappingsWrapper.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/StorageMappingsWrapper.tsx deleted file mode 100644 index d3e31d817..000000000 --- a/packages/forklift-console-plugin/src/modules/StorageMaps/StorageMappingsWrapper.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { withQueryClient } from '@kubev2v/common'; - -import StorageMappingsPage from './StorageMappingsPage'; - -const StorageMappingsWrapper = withQueryClient(StorageMappingsPage); -StorageMappingsWrapper.displayName = 'StorageMappingsWrapper'; - -export default StorageMappingsWrapper; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/UseStorageMappingActions.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/UseStorageMappingActions.tsx deleted file mode 100644 index cd0787b06..000000000 --- a/packages/forklift-console-plugin/src/modules/StorageMaps/UseStorageMappingActions.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { useStorageMappingActions } from './mappingActions'; - -export default useStorageMappingActions; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/MappingRow.test.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/MappingRow.test.tsx deleted file mode 100644 index 14dbb9b5a..000000000 --- a/packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/MappingRow.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import { MemoryRouter } from 'react-router'; - -import { withQueryClient } from '@kubev2v/common'; -import { cleanup, render } from '@testing-library/react'; - -import { FlatStorageMapping } from '../dataForStorage'; -import StorageMappingRow from '../StorageMappingRow'; -import { fieldsMetadataFactory as storageFieldsFactory } from '../StorageMappingsPage'; - -import MERGED_STORAGE_DATA from './mergedStorageData.json'; - -// Mock translation function. -const t = (s) => s; -// Create a field metadata -const storageFields = storageFieldsFactory(t); - -afterEach(cleanup); - -describe('StorageMap rows', () => { - const storageTuples = MERGED_STORAGE_DATA.map((storage) => [storage.name, storage]); - test.each(storageTuples)('%s', (description, storage) => { - const Wrapped = withQueryClient(() => ( - - - - !f.isHidden)} - namespace={undefined} - resourceData={storage as FlatStorageMapping} - resourceIndex={0} - /> - -
-
- )); - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); -}); diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/__snapshots__/MappingRow.test.tsx.snap b/packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/__snapshots__/MappingRow.test.tsx.snap deleted file mode 100644 index 2a21a6282..000000000 --- a/packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/__snapshots__/MappingRow.test.tsx.snap +++ /dev/null @@ -1,830 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`StorageMap rows plantest1-generated-asdf 1`] = ` - - - - - - - - - - - - - - - - - -
- - - - - - - managed - - - - - - - - - - - - - - - - -
-
-
-`; - -exports[`StorageMap rows vcenter1-datastore-to-ocpv-storageclass1 1`] = ` - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - -
-
-
-`; - -exports[`StorageMap rows vcenter1-invalid-storage-mapping 1`] = ` - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - -
-
-
-`; - -exports[`StorageMap rows vcenter3-datastore-to-ocpv-storageclass2 1`] = ` - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - -
-
-
-`; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/mergeData.test.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/mergeData.test.ts deleted file mode 100644 index d84008b41..000000000 --- a/packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/mergeData.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { StorageMapResource } from 'src/utils/types'; - -import { MOCK_STORAGE_MAPPINGS } from '@kubev2v/legacy/queries/mocks/mappings.mock'; -import { MOCK_CLUSTER_PROVIDERS } from '@kubev2v/legacy/queries/mocks/providers.mock'; -import { V1beta1Provider } from '@kubev2v/types'; - -import { - groupByTarget as groupStorageByTarget, - mergeData as mergeStorageData, -} from '../dataForStorage'; - -import MERGED_STORAGE_DATA from './mergedStorageData.json'; - -describe('merging storage data', () => { - test('empty input', () => { - expect(mergeStorageData([], [])).toHaveLength(0); - }); - - test('standard mock data', () => { - const mappings = MOCK_STORAGE_MAPPINGS as StorageMapResource[]; - const providers = MOCK_CLUSTER_PROVIDERS as V1beta1Provider[]; - const merged = mergeStorageData(mappings, providers); - // do a stringify-parse run to remove undefined properties which clutter the results(if mismatch happens) - - /** - * Write test data to file. - * - * - import fs from 'fs'; - - try { - fs.writeFileSync('./mergedStorageData.json', JSON.stringify(merged, undefined, 4)); - } catch (err) { - console.error(err); - } - */ - - expect(JSON.parse(JSON.stringify(merged))).toEqual(MERGED_STORAGE_DATA); - }); -}); - -describe('grouping storage data', () => { - test('empty input', () => { - expect(groupStorageByTarget([])).toHaveLength(0); - }); - test('group with one element', () => { - expect(groupStorageByTarget([['large', { id: '123' }]])).toEqual([ - [{ name: 'large' }, [{ id: '123' }]], - ]); - }); - test('two elements sharing the same target storage class', () => { - expect( - groupStorageByTarget([ - ['large', { id: '123' }], - ['large', { name: 'foo' }], - ]), - ).toEqual([[{ name: 'large' }, [{ id: '123' }, { name: 'foo' }]]]); - }); - test('2 group with one element each', () => { - expect( - groupStorageByTarget([ - ['large', { id: '123' }], - ['small', { name: 'foo' }], - ]), - ).toEqual([ - [{ name: 'large' }, [{ id: '123' }]], - [{ name: 'small' }, [{ name: 'foo' }]], - ]); - }); -}); diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/mergedStorageData.json b/packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/mergedStorageData.json deleted file mode 100644 index 62593a277..000000000 --- a/packages/forklift-console-plugin/src/modules/StorageMaps/__tests__/mergedStorageData.json +++ /dev/null @@ -1,411 +0,0 @@ -[ - { - "name": "vcenter1-datastore-to-ocpv-storageclass1", - "namespace": "konveyor-forklift", - "template": true, - "gvk": { - "group": "forklift.konveyor.io", - "version": "v1beta1", - "kind": "StorageMap" - }, - "managed": false, - "source": "vcenter-1", - "sourceGvk": { - "group": "forklift.konveyor.io", - "version": "v1beta1", - "kind": "Provider" - }, - "sourceResolved": true, - "sourceReady": true, - "target": "ocpv-1", - "targetGvk": { - "group": "forklift.konveyor.io", - "version": "v1beta1", - "kind": "Provider" - }, - "targetResolved": true, - "targetReady": true, - "object": { - "apiVersion": "forklift.konveyor.io/v1beta1", - "kind": "StorageMap", - "metadata": { - "name": "vcenter1-datastore-to-ocpv-storageclass1", - "namespace": "konveyor-forklift", - "annotations": { - "forklift.konveyor.io/shared": "true" - } - }, - "spec": { - "provider": { - "source": { - "name": "vcenter-1", - "namespace": "openshift-migration" - }, - "destination": { - "name": "ocpv-1", - "namespace": "openshift-migration" - } - }, - "map": [ - { - "source": { - "id": "1" - }, - "destination": { - "storageClass": "large" - } - } - ] - }, - "status": { - "conditions": [ - { - "category": "Required", - "lastTransitionTime": "2023-03-20T20:36:23Z", - "message": "The storage map is ready.", - "status": "True", - "type": "Ready" - } - ] - } - }, - "to": [ - { - "name": "large" - } - ], - "from": [ - [ - { - "name": "large" - }, - [ - { - "id": "1" - } - ] - ] - ], - "status": "Ready", - "conditions": [ - { - "category": "Required", - "lastTransitionTime": "2023-03-20T20:36:23Z", - "message": "The storage map is ready.", - "status": "True", - "type": "Ready" - } - ] - }, - { - "name": "plantest1-generated-asdf", - "namespace": "konveyor-forklift", - "template": false, - "gvk": { - "group": "forklift.konveyor.io", - "version": "v1beta1", - "kind": "StorageMap" - }, - "owner": "plantest-01", - "ownerGvk": { - "group": "forklift.konveyor.io", - "version": "v1beta1", - "kind": "Plan" - }, - "managed": true, - "source": "vcenter-1", - "sourceGvk": { - "group": "forklift.konveyor.io", - "version": "v1beta1", - "kind": "Provider" - }, - "sourceResolved": true, - "sourceReady": true, - "target": "ocpv-1", - "targetGvk": { - "group": "forklift.konveyor.io", - "version": "v1beta1", - "kind": "Provider" - }, - "targetResolved": true, - "targetReady": true, - "object": { - "apiVersion": "forklift.konveyor.io/v1beta1", - "kind": "StorageMap", - "metadata": { - "name": "plantest1-generated-asdf", - "namespace": "konveyor-forklift", - "annotations": { - "forklift.konveyor.io/shared": "false" - }, - "ownerReferences": [ - { - "apiVersion": "forklift.konveyor.io/v1beta1", - "kind": "Plan", - "name": "plantest-01", - "namespace": "openshift-migration", - "uid": "28fde094-b667-4d21-8f29-27c18f22178c" - } - ] - }, - "spec": { - "provider": { - "source": { - "name": "vcenter-1", - "namespace": "openshift-migration" - }, - "destination": { - "name": "ocpv-1", - "namespace": "openshift-migration" - } - }, - "map": [ - { - "source": { - "id": "1" - }, - "destination": { - "storageClass": "large" - } - } - ] - }, - "status": { - "conditions": [ - { - "category": "Required", - "lastTransitionTime": "2023-03-20T20:36:23Z", - "message": "The storage map is ready.", - "status": "True", - "type": "Ready" - } - ] - } - }, - "to": [ - { - "name": "large" - } - ], - "from": [ - [ - { - "name": "large" - }, - [ - { - "id": "1" - } - ] - ] - ], - "status": "Ready", - "conditions": [ - { - "category": "Required", - "lastTransitionTime": "2023-03-20T20:36:23Z", - "message": "The storage map is ready.", - "status": "True", - "type": "Ready" - } - ] - }, - { - "name": "vcenter3-datastore-to-ocpv-storageclass2", - "namespace": "konveyor-forklift", - "template": true, - "gvk": { - "group": "forklift.konveyor.io", - "version": "v1beta1", - "kind": "StorageMap" - }, - "managed": false, - "source": "vcenter-3", - "sourceGvk": { - "group": "forklift.konveyor.io", - "version": "v1beta1", - "kind": "Provider" - }, - "sourceResolved": true, - "sourceReady": false, - "target": "ocpv-1", - "targetGvk": { - "group": "forklift.konveyor.io", - "version": "v1beta1", - "kind": "Provider" - }, - "targetResolved": true, - "targetReady": true, - "object": { - "apiVersion": "forklift.konveyor.io/v1beta1", - "kind": "StorageMap", - "metadata": { - "name": "vcenter3-datastore-to-ocpv-storageclass2", - "namespace": "konveyor-forklift", - "annotations": { - "forklift.konveyor.io/shared": "true" - } - }, - "spec": { - "provider": { - "source": { - "name": "vcenter-3", - "namespace": "openshift-migration" - }, - "destination": { - "name": "ocpv-1", - "namespace": "openshift-migration" - } - }, - "map": [ - { - "source": { - "name": "vmware-datastore-2" - }, - "destination": { - "storageClass": "large" - } - } - ] - }, - "status": { - "conditions": [ - { - "category": "Required", - "lastTransitionTime": "2023-03-20T20:36:23Z", - "message": "The storage map is ready.", - "status": "True", - "type": "Ready" - } - ] - } - }, - "to": [ - { - "name": "large" - } - ], - "from": [ - [ - { - "name": "large" - }, - [ - { - "name": "vmware-datastore-2" - } - ] - ] - ], - "status": "Ready", - "conditions": [ - { - "category": "Required", - "lastTransitionTime": "2023-03-20T20:36:23Z", - "message": "The storage map is ready.", - "status": "True", - "type": "Ready" - } - ] - }, - { - "name": "vcenter1-invalid-storage-mapping", - "namespace": "konveyor-forklift", - "template": true, - "gvk": { - "group": "forklift.konveyor.io", - "version": "v1beta1", - "kind": "StorageMap" - }, - "managed": false, - "source": "vcenter-3", - "sourceGvk": { - "group": "forklift.konveyor.io", - "version": "v1beta1", - "kind": "Provider" - }, - "sourceResolved": true, - "sourceReady": false, - "target": "ocpv-1", - "targetGvk": { - "group": "forklift.konveyor.io", - "version": "v1beta1", - "kind": "Provider" - }, - "targetResolved": true, - "targetReady": true, - "object": { - "apiVersion": "forklift.konveyor.io/v1beta1", - "kind": "StorageMap", - "metadata": { - "name": "vcenter1-invalid-storage-mapping", - "namespace": "konveyor-forklift", - "annotations": { - "forklift.konveyor.io/shared": "true" - } - }, - "spec": { - "provider": { - "source": { - "name": "vcenter-3", - "namespace": "openshift-migration" - }, - "destination": { - "name": "ocpv-1", - "namespace": "openshift-migration" - } - }, - "map": [ - { - "source": { - "id": "invalid-id" - }, - "destination": { - "storageClass": "large" - } - } - ] - }, - "status": { - "conditions": [ - { - "category": "Critical", - "lastTransitionTime": "2023-03-20T21:33:45Z", - "message": "Source storage not found.", - "reason": "NotFound", - "status": "True", - "type": "SourceStorageNotValid" - } - ] - } - }, - "to": [ - { - "name": "large" - } - ], - "from": [ - [ - { - "name": "large" - }, - [ - { - "id": "invalid-id" - } - ] - ] - ], - "status": "NotReady", - "conditions": [ - { - "category": "Critical", - "lastTransitionTime": "2023-03-20T21:33:45Z", - "message": "Source storage not found.", - "reason": "NotFound", - "status": "True", - "type": "SourceStorageNotValid" - } - ] - } -] \ No newline at end of file diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/actions/StorageMapActionsDropdown.style.css b/packages/forklift-console-plugin/src/modules/StorageMaps/actions/StorageMapActionsDropdown.style.css new file mode 100644 index 000000000..3cced821c --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/actions/StorageMapActionsDropdown.style.css @@ -0,0 +1,4 @@ +.forklift-dropdown { + margin: 0; + padding: 0; +} diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/actions/StorageMapActionsDropdown.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/actions/StorageMapActionsDropdown.tsx new file mode 100644 index 000000000..656d44383 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/actions/StorageMapActionsDropdown.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { useToggle } from 'src/modules/Providers/hooks'; +import { ModalHOC } from 'src/modules/Providers/modals'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { Dropdown, DropdownPosition, DropdownToggle, KebabToggle } from '@patternfly/react-core'; + +import { CellProps } from '../views/list/components'; + +import { StorageMapActionsDropdownItems } from './StorageMapActionsDropdownItems'; + +import './StorageMapActionsDropdown.style.css'; + +const StorageMapActionsKebabDropdown_: React.FC = ({ + data, + isKebab, +}) => { + const { t } = useForkliftTranslation(); + + // Hook for managing the open/close state of the dropdown + const [isDropdownOpen, toggle] = useToggle(); + + // Returning the Dropdown component from PatternFly library + return ( + + ) : ( + + {t('Actions')} + + ) + } + dropdownItems={StorageMapActionsDropdownItems({ data })} + /> + ); +}; + +export const StorageMapActionsDropdown: React.FC = (props) => ( + + + +); + +export interface StorageMapActionsDropdownProps extends CellProps { + isKebab?: boolean; +} diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/actions/StorageMapActionsDropdownItems.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/actions/StorageMapActionsDropdownItems.tsx new file mode 100644 index 000000000..8b5c2844b --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/actions/StorageMapActionsDropdownItems.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { DropdownItemLink } from 'src/components/actions/DropdownItemLink'; +import { DeleteModal, useModal } from 'src/modules/Providers/modals'; +import { getResourceUrl } from 'src/modules/Providers/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { StorageMapModel, StorageMapModelRef } from '@kubev2v/types'; +import { DropdownItem } from '@patternfly/react-core'; + +import { StorageMapData } from '../utils'; + +export const StorageMapActionsDropdownItems = ({ data }: StorageMapActionsDropdownItemsProps) => { + const { t } = useForkliftTranslation(); + const { showModal } = useModal(); + + const { obj: StorageMap } = data; + + const StorageMapURL = getResourceUrl({ + reference: StorageMapModelRef, + name: StorageMap?.metadata?.name, + namespace: StorageMap?.metadata?.namespace, + }); + + return [ + + {t('Edit StorageMap')} + , + showModal()} + > + {t('Delete StorageMap')} + , + ]; +}; + +interface StorageMapActionsDropdownItemsProps { + data: StorageMapData; +} diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/actions/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/actions/index.ts new file mode 100644 index 000000000..6c14cadda --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/actions/index.ts @@ -0,0 +1,4 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './StorageMapActionsDropdown'; +export * from './StorageMapActionsDropdownItems'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/componenets/StorageMapCriticalConditions.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/componenets/StorageMapCriticalConditions.tsx new file mode 100644 index 000000000..8175b4f0b --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/componenets/StorageMapCriticalConditions.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Linkify from 'react-linkify'; + +import { Alert, Text, TextContent, TextVariants } from '@patternfly/react-core'; + +export const StorageMapCriticalConditions: React.FC<{ type: string; message: string }> = ({ + type, + message, +}) => { + const { t } = useTranslation(); + return ( + + + + {message || '-'} +
+ {t('To troubleshoot, check the Forklift controller pod logs.')} +
+
+
+ ); +}; + +export default StorageMapCriticalConditions; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/componenets/StorageMapsAddButton.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/componenets/StorageMapsAddButton.tsx new file mode 100644 index 000000000..20f77c3a7 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/componenets/StorageMapsAddButton.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { useHistory } from 'react-router'; +import { getResourceUrl } from 'src/modules/Providers/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { StorageMapModelRef } from '@kubev2v/types'; +import { Button } from '@patternfly/react-core'; + +export const StorageMapsAddButton: React.FC<{ namespace: string; dataTestId?: string }> = ({ + namespace, + dataTestId, +}) => { + const { t } = useForkliftTranslation(); + const history = useHistory(); + + const StorageMapsListURL = getResourceUrl({ + reference: StorageMapModelRef, + namespace: namespace, + namespaced: namespace !== undefined, + }); + + return ( + + ); +}; + +export default StorageMapsAddButton; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/EmptyStateStorageMaps.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/componenets/StorageMapsEmptyState.tsx similarity index 63% rename from packages/forklift-console-plugin/src/modules/StorageMaps/EmptyStateStorageMaps.tsx rename to packages/forklift-console-plugin/src/modules/StorageMaps/componenets/StorageMapsEmptyState.tsx index 9902b389c..95c982728 100644 --- a/packages/forklift-console-plugin/src/modules/StorageMaps/EmptyStateStorageMaps.tsx +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/componenets/StorageMapsEmptyState.tsx @@ -2,17 +2,16 @@ import React from 'react'; import { Link } from 'react-router-dom'; import ForkliftEmptyState from 'src/components/empty-states/ForkliftEmptyState'; import automationIcon from 'src/components/empty-states/images/automation.svg'; -import { AddMappingButton } from 'src/components/mappings/MappingPage'; +import { useHasSufficientProviders } from 'src/modules/Plans/data'; +import { getResourceUrl } from 'src/modules/Providers/utils'; import { HELP_LINK_HREF } from 'src/utils/constants'; import { ForkliftTrans, useForkliftTranslation } from 'src/utils/i18n'; import { ExternalLink } from '@kubev2v/common'; -import { PROVIDERS_REFERENCE } from '@kubev2v/legacy/common/constants'; -import { createK8sPath } from '@kubev2v/legacy/queries/helpers'; -import { MappingType } from '@kubev2v/legacy/queries/types'; +import { ProviderModelRef } from '@kubev2v/types'; import { Button, Flex, FlexItem } from '@patternfly/react-core'; -import { useHasSufficientProviders } from '../Plans/data'; +import { StorageMapsAddButton } from './StorageMapsAddButton'; const AutomationIcon = () => ; @@ -21,6 +20,12 @@ const EmptyStatePlans: React.FC<{ namespace: string }> = ({ namespace }) => { const hasSufficientProviders = useHasSufficientProviders(namespace); + const ProvidersListURL = getResourceUrl({ + reference: ProviderModelRef, + namespace: namespace, + namespaced: namespace !== undefined, + }); + return ( = ({ namespace }) => { - Migration storage maps are used to map storage interfaces between source and target - workloads, at least one source and one target provider must be available in order to - create a migration plan,{' '} + Migration network maps are used to map network interfaces between source and target + virtualization providers, at least one source and one target provider must be + available in order to create a migration storage map,{' '} Learn more @@ -49,26 +54,18 @@ const EmptyStatePlans: React.FC<{ namespace: string }> = ({ namespace }) => { ) : ( t( - 'Migration storage maps are used to map storage interfaces between source and target workloads.', + 'Migration networks maps are used to map network interfaces between source and target workloads.', ) ) } callForActionButtons={ - hasSufficientProviders && ( - - ) + hasSufficientProviders && } /> ); diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/componenets/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/componenets/index.ts new file mode 100644 index 000000000..a335b50c2 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/componenets/index.ts @@ -0,0 +1,5 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './StorageMapCriticalConditions'; +export * from './StorageMapsAddButton'; +export * from './StorageMapsEmptyState'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/dataForStorage.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/dataForStorage.ts deleted file mode 100644 index f0f90f3c7..000000000 --- a/packages/forklift-console-plugin/src/modules/StorageMaps/dataForStorage.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - CommonMapping, - OwnerRef, - resolveOwnerRef, - toStatus, - useMappings, -} from 'src/components/mappings/data'; -import * as C from 'src/utils/constants'; -import { useStorageMappings } from 'src/utils/fetch'; -import { groupVersionKindForObj, resolveProviderRef } from 'src/utils/resources'; -import { ProviderRef, StorageMapResource } from 'src/utils/types'; - -import { IdNameNamespaceTypeRef, IStorageMapping } from '@kubev2v/legacy/queries/types'; -import { V1beta1Provider, V1beta1StorageMapStatusConditions } from '@kubev2v/types'; -import { K8sGroupVersionKind } from '@openshift-console/dynamic-plugin-sdk'; - -export interface Storage { - name: string; -} - -export interface FlatStorageMapping extends CommonMapping { - [C.FROM]: [Storage, IdNameNamespaceTypeRef[]][]; - [C.TO]: Storage[]; - [C.OBJECT]: IStorageMapping; -} - -/** - * Group networks by target network. It's many(sources)-to-one(target) mapping. - * - * @example - * Input: [ - * ["large", { id: "123" }], - * ["large", { name: "foo" }] - * ] - * Output: [ - * [{ name: "large"} , [ - * { id: "123" }, - * { name: "foo" } - * ] - * ] - * ] - */ -export const groupByTarget = ( - tuples: [string, IdNameNamespaceTypeRef][], -): [Storage, IdNameNamespaceTypeRef[]][] => - Object.entries( - tuples.reduce( - (acc, [targetStorageName, sourceStorage]) => ({ - ...acc, - [targetStorageName]: [...(acc[targetStorageName] ?? []), sourceStorage], - }), - {} as { [k: string]: IdNameNamespaceTypeRef[] }, - ), - ).map(([targetStorageName, sources]) => [{ name: targetStorageName }, sources]); - -export const mergeData = ( - mappings: StorageMapResource[], - providers: V1beta1Provider[], -): FlatStorageMapping[] => { - return mappings - .map( - ( - mapping, - ): [ - StorageMapResource, - StorageMapResource, - K8sGroupVersionKind, - ProviderRef, - ProviderRef, - OwnerRef, - [Storage, IdNameNamespaceTypeRef[]][], - ] => [ - mapping, // to extract props - mapping, // to pass as object blob - groupVersionKindForObj(mapping), - resolveProviderRef(mapping.spec.provider.source, providers), - resolveProviderRef(mapping.spec.provider.destination, providers), - resolveOwnerRef(mapping.metadata.ownerReferences), - groupByTarget( - mapping.spec.map.map(({ destination, source }) => [destination.storageClass, source]), - ), - ], - ) - .map( - ([ - { - metadata: { name, namespace, annotations = [] }, - status: { conditions = [] } = {}, - }, - mapping, - gvk, - sourceProvider, - targetProvider, - owner, - groupedStorages, - ]): FlatStorageMapping => ({ - name, - namespace, - template: annotations?.[C.SHARED_MAPPING_ANNOTATION] !== 'false', - gvk, - owner: owner.name, - ownerGvk: owner.gvk, - managed: !!owner.name, - source: sourceProvider.name, - sourceGvk: sourceProvider.gvk, - sourceResolved: sourceProvider.resolved, - sourceReady: sourceProvider.ready, - target: targetProvider.name, - targetGvk: targetProvider.gvk, - targetResolved: targetProvider.resolved, - targetReady: targetProvider.ready, - object: mapping, - to: groupedStorages.map(([storage]) => storage), - from: groupedStorages, - status: toStatus(conditions), - conditions: conditions as V1beta1StorageMapStatusConditions[], - }), - ); -}; - -export const useFlatStorageMappings = ({ - namespace, - name = undefined, -}): [FlatStorageMapping[], boolean, boolean] => { - return useMappings( - { namespace, name }, - useStorageMappings, - mergeData, - ); -}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/dynamic-plugin.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/dynamic-plugin.ts index 9247a5b11..aa9bfc9be 100644 --- a/packages/forklift-console-plugin/src/modules/StorageMaps/dynamic-plugin.ts +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/dynamic-plugin.ts @@ -1,32 +1,33 @@ import { StorageMapModel, StorageMapModelGroupVersionKind } from '@kubev2v/types'; import { EncodedExtension } from '@openshift/dynamic-plugin-sdk'; import { - ActionProvider, ModelMetadata, + ResourceDetailsPage, ResourceListPage, ResourceNSNavItem, } from '@openshift-console/dynamic-plugin-sdk'; import type { ConsolePluginMetadata } from '@openshift-console/dynamic-plugin-sdk-webpack/lib/schema/plugin-package'; export const exposedModules: ConsolePluginMetadata['exposedModules'] = { - StorageMappingsPage: './modules/StorageMaps/StorageMappingsWrapper', - useStorageMappingActions: './modules/StorageMaps/UseStorageMappingActions', + StorageMapsListPage: './modules/StorageMaps/views/list/StorageMapsListPage', + StorageMapDetailsPage: './modules/StorageMaps/views/details/StorageMapDetailsPage', + yamlTemplates: './modules/StorageMaps/yamlTemplates', }; export const extensions: EncodedExtension[] = [ { type: 'console.navigation/resource-ns', properties: { - id: 'storageMappings', - insertAfter: 'networkMappings', + id: 'StorageMappings', + insertAfter: 'plans', perspective: 'admin', section: 'migration', // t('plugin__forklift-console-plugin~StorageMaps for virtualization') name: '%plugin__forklift-console-plugin~StorageMaps for virtualization%', model: StorageMapModelGroupVersionKind, dataAttributes: { - 'data-quickstart-id': 'qs-nav-storage-mappings', - 'data-testid': 'storage-mappings-nav-item', + 'data-quickstart-id': 'qs-nav-network-mappings', + 'data-testid': 'network-mappings-nav-item', }, }, } as EncodedExtension, @@ -35,20 +36,22 @@ export const extensions: EncodedExtension[] = [ type: 'console.page/resource/list', properties: { component: { - $codeRef: 'StorageMappingsPage', + $codeRef: 'StorageMapsListPage', }, model: StorageMapModelGroupVersionKind, }, } as EncodedExtension, + { - type: 'console.action/provider', + type: 'console.page/resource/details', properties: { - contextId: 'forklift-flat-storage-mapping', - provider: { - $codeRef: 'useStorageMappingActions', + component: { + $codeRef: 'StorageMapDetailsPage', }, + model: StorageMapModelGroupVersionKind, }, - } as EncodedExtension, + } as EncodedExtension, + { type: 'console.model-metadata', properties: { @@ -56,4 +59,14 @@ export const extensions: EncodedExtension[] = [ ...StorageMapModel, }, } as EncodedExtension, + + { + type: 'console.yaml-template', + properties: { + name: 'default', + model: StorageMapModelGroupVersionKind, + ...StorageMapModel, + template: { $codeRef: 'yamlTemplates.defaultYamlTemplate' }, + }, + }, ]; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/mappingActions.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/mappingActions.tsx deleted file mode 100644 index 758ba693e..000000000 --- a/packages/forklift-console-plugin/src/modules/StorageMaps/mappingActions.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useMappingActions } from 'src/components/mappings/mappingActions'; - -import { withActionServiceContext } from '@kubev2v/common'; -import { MappingType } from '@kubev2v/legacy/queries/types'; - -import { FlatStorageMapping } from './dataForStorage'; - -export const useStorageMappingActions = ({ resourceData }: { resourceData: FlatStorageMapping }) => - useMappingActions({ resourceData, mappingType: MappingType.Storage }); - -/** - * Use the `console.action/provider` extension named `forklift-flat-storage-mapping` to render - * a set of actions in a kebab menu. - */ -export const StorageMappingActions = withActionServiceContext({ - contextId: 'forklift-flat-storage-mapping', - variant: 'kebab', -}); -StorageMappingActions.displayName = 'StorageMappingActions'; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/styles.css b/packages/forklift-console-plugin/src/modules/StorageMaps/styles.css deleted file mode 100644 index 04d61e90e..000000000 --- a/packages/forklift-console-plugin/src/modules/StorageMaps/styles.css +++ /dev/null @@ -1,10 +0,0 @@ -.forklift-table__flex-labels-with-gaps { - display: inline-flex; - flex-wrap: wrap; - gap: var(--pf-global--spacer--sm); -} - -.forklift-empty-state__icon { - max-width: 14rem; - max-height: 14rem; -} diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/utils/constants/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/constants/index.ts new file mode 100644 index 000000000..3026bfe66 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/constants/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './storage-map-status'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/utils/constants/storage-map-status.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/constants/storage-map-status.ts new file mode 100644 index 000000000..686dd5c22 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/constants/storage-map-status.ts @@ -0,0 +1,5 @@ +export const STORAGE_MAP_STATUS: Record = { + Ready: 'Ready', + 'Not Ready': 'Not Ready', + Critical: 'Critical', +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/utils/helpers/deepCopy.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/helpers/deepCopy.ts new file mode 100644 index 000000000..2376783e5 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/helpers/deepCopy.ts @@ -0,0 +1,3 @@ +export function deepCopy(obj: T): T { + return JSON.parse(JSON.stringify(obj)); +} diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/utils/helpers/getStorageMapPhase.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/helpers/getStorageMapPhase.ts new file mode 100644 index 000000000..50b437770 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/helpers/getStorageMapPhase.ts @@ -0,0 +1,18 @@ +import { StorageMapData } from '../types'; + +export const getStorageMapPhase = (data: StorageMapData) => { + const conditions = data?.obj?.status?.conditions; + + const isCritical = conditions?.find((c) => c.category === 'Critical' && c.status === 'True'); + const isReady = conditions?.find((c) => c.type === 'Ready' && c.status === 'True'); + + if (isCritical) { + return 'Critical'; + } + + if (isReady) { + return 'Ready'; + } + + return 'Not Ready'; +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/utils/helpers/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/helpers/index.ts new file mode 100644 index 000000000..1dc991c2a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/helpers/index.ts @@ -0,0 +1,4 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './deepCopy'; +export * from './getStorageMapPhase'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/utils/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/index.ts new file mode 100644 index 000000000..6cc78e687 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/index.ts @@ -0,0 +1,5 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './constants'; +export * from './helpers'; +export * from './types'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/utils/types/StorageMapData.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/types/StorageMapData.ts new file mode 100644 index 000000000..396279fb0 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/types/StorageMapData.ts @@ -0,0 +1,8 @@ +import { ProvidersPermissionStatus } from 'src/modules/Providers/utils'; + +import { V1beta1StorageMap } from '@kubev2v/types'; + +export interface StorageMapData { + obj?: V1beta1StorageMap; + permissions?: ProvidersPermissionStatus; +} diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/utils/types/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/types/index.ts new file mode 100644 index 000000000..a51f32f39 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/utils/types/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './StorageMapData'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/StorageMapDetailsPage.style.css b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/StorageMapDetailsPage.style.css new file mode 100644 index 000000000..7e17bad97 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/StorageMapDetailsPage.style.css @@ -0,0 +1,67 @@ +.forklift-page-headings-alerts { + padding-left: 0; +} + +.forklift-page-section { + border-top: 1px solid var(--pf-global--BorderColor--100); +} + +.forklift-title { + font-size: 1rem; +} + +.forklift-welcome__flex-text { + align-self: normal; +} + +.forklift-welcome-text { + padding-bottom: var(--pf-global--spacer--sm); +} + +.forklift-welcome__flex-icon { + align-self: center; +} + +.forklift-welcome__icon { + max-width: 18rem; + max-height: 18rem; + padding-right: var(--pf-global--spacer--lg); +} + +.forklift-welcome-header-badge { + min-width: 768px; + margin-left: var(--pf-global--spacer--sm); +} + +.forklift-status-migration-running { + color: var(--pf-global--info-color--100); +} + +.forklift-status-migration-failed { + color: var(--pf-global--danger-color--100); +} + +.forklift-status-migration-succeeded { + color: var(--pf-global--success-color--100); +} + +.forklift-status-migration { + padding-right: var(--pf-global--spacer--xl); +} + +.forklift-status-migration-chart { + height: 100%; + width: 100%; +} + +.forklift-overview__controller-card__status-icon { + padding-right: var(--pf-global--spacer--sm); +} + +.forklift-overview__pods-tab { + background: var(--pf-c-drawer__section--BackgroundColor); +} + +.forklift-network-map__details-tab--update-button { + margin-bottom: var(--pf-global--spacer--md); +} \ No newline at end of file diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/StorageMapDetailsPage.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/StorageMapDetailsPage.tsx new file mode 100644 index 000000000..57122b550 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/StorageMapDetailsPage.tsx @@ -0,0 +1,55 @@ +import React, { memo } from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { HorizontalNav, K8sModel } from '@openshift-console/dynamic-plugin-sdk'; + +import { StorageMapPageHeadings } from './components/DetailsSection/components/StorageMapPageHeadings'; +import { StorageMapDetailsTab, StorageMapYAMLTab } from './tabs'; + +import './StorageMapDetailsPage.style.css'; + +const StorageMapDetailsPageInternal: React.FC<{ + name: string; + namespace: string; +}> = ({ name, namespace }) => { + const { t } = useForkliftTranslation(); + + const pages = [ + { + href: '', + name: t('Details'), + component: () => , + }, + { + href: 'yaml', + name: t('YAML'), + component: () => , + }, + ]; + + return ( + <> + + + + ); +}; +const StorageMapDetailsPageInternalMemo = memo(StorageMapDetailsPageInternal); + +export const StorageMapDetailsPage: React.FC = ({ + name, + namespace, +}) => { + return ; +}; +StorageMapDetailsPage.displayName = 'StorageMapDetailsPage'; + +type StorageMapDetailsPageProps = { + kind: string; + kindObj: K8sModel; + match: { path: string; url: string; isExact: boolean; params: unknown }; + name: string; + namespace?: string; +}; + +export default StorageMapDetailsPage; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ConditionsSection/ConditionsSection.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ConditionsSection/ConditionsSection.tsx new file mode 100644 index 000000000..f03b6647c --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ConditionsSection/ConditionsSection.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { K8sResourceCondition } from '@kubev2v/types'; +import { Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; + +/** + * React Component to display a table of conditions. + * + * @component + * @param {ConditionsSectionProps} props - Props for the Conditions component. + * @param {K8sResourceCondition[]} props.conditions - Array of conditions to be displayed. + * @returns {ReactElement} A table displaying the provided conditions. + */ +export const ConditionsSection: React.FC = ({ conditions }) => { + const { t } = useForkliftTranslation(); + + if (!conditions) { + return <>; + } + + const getStatusLabel = (status: string) => { + switch (status) { + case 'True': + return t('True'); + case 'False': + return t('False'); + default: + return status; + } + }; + + return ( + + + + {t('Type')} + {t('Status')} + {t('Updated')} + {t('Reason')} + {t('Message')} + + + + {conditions.map((condition) => ( + + {condition.type} + {getStatusLabel(condition.status)} + + + + {condition.reason} + {condition?.message || '-'} + + ))} + + + ); +}; + +/** + * Type for the props of the Conditions component. + * + * @typedef {Object} ConditionsProps + * @property {K8sResourceCondition[]} conditions - The conditions to be displayed. + */ +export type ConditionsSectionProps = { + conditions: K8sResourceCondition[]; +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ConditionsSection/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ConditionsSection/index.ts new file mode 100644 index 000000000..75876a3f6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ConditionsSection/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './ConditionsSection'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/DetailsSection.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/DetailsSection.tsx new file mode 100644 index 000000000..20e526973 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/DetailsSection.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { ModalHOC } from 'src/modules/Providers/modals'; + +import { V1beta1StorageMap } from '@kubev2v/types'; +import { DescriptionList } from '@patternfly/react-core'; + +import { + CreatedAtDetailsItem, + NameDetailsItem, + NamespaceDetailsItem, + OwnerDetailsItem, +} from './components'; + +export const DetailsSection: React.FC = (props) => ( + + + +); + +export type DetailsSectionProps = { + obj: V1beta1StorageMap; +}; + +export const DetailsSectionInternal: React.FC = ({ obj }) => { + return ( + + + + + + + + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/CreatedAtDetailsItem.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/CreatedAtDetailsItem.tsx new file mode 100644 index 000000000..3f2520e15 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/CreatedAtDetailsItem.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { DetailsItem } from 'src/modules/Providers/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { Timestamp } from '@openshift-console/dynamic-plugin-sdk'; + +import { StorageDetailsItemProps } from './StorageDetailsItemProps'; + +export const CreatedAtDetailsItem: React.FC = ({ + resource, + moreInfoLink, + helpContent, +}) => { + const { t } = useForkliftTranslation(); + + const defaultMoreInfoLink = 'https://kubernetes.io/docs/reference/using-api/api-concepts'; + const defaultHelpContent = t( + `CreationTimestamp is a timestamp representing the server time when this object was created. + It is not guaranteed to be set in happens-before order across separate operations. + Clients may not set this value. It is represented in RFC3339 form and is in UTC.`, + ); + + return ( + } + moreInfoLink={moreInfoLink ?? defaultMoreInfoLink} + helpContent={helpContent ?? defaultHelpContent} + crumbs={['metadata', 'creationTimestamp']} + /> + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/NameDetailsItem.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/NameDetailsItem.tsx new file mode 100644 index 000000000..95648990e --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/NameDetailsItem.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { DetailsItem } from 'src/modules/Providers/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { StorageDetailsItemProps } from './StorageDetailsItemProps'; + +export const NameDetailsItem: React.FC = ({ + resource, + moreInfoLink, + helpContent, +}) => { + const { t } = useForkliftTranslation(); + + const defaultMoreInfoLink = + 'https://kubernetes.io/docs/concepts/overview/working-with-objects/names'; + const defaultHelpContent = t( + 'Name is primarily intended for creation idempotence and configuration definition. Cannot be updated.', + ); + + return ( + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/NamespaceDetailsItem.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/NamespaceDetailsItem.tsx new file mode 100644 index 000000000..5633c8754 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/NamespaceDetailsItem.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { DetailsItem } from 'src/modules/Providers/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; + +import { StorageDetailsItemProps } from './StorageDetailsItemProps'; + +export const NamespaceDetailsItem: React.FC = ({ + resource, + moreInfoLink, + helpContent, +}) => { + const { t } = useForkliftTranslation(); + + const defaultMoreInfoLink = + 'https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces'; + const defaultHelpContent = t( + `Namespace defines the space within which each name must be unique. + An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. + Not all objects are required to be scoped to a namespace - + the value of this field for those objects will be empty.`, + ); + + return ( + + } + moreInfoLink={moreInfoLink ?? defaultMoreInfoLink} + helpContent={helpContent ?? defaultHelpContent} + crumbs={['metadata', 'namespace']} + /> + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/OwnerDetailsItem.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/OwnerDetailsItem.tsx new file mode 100644 index 000000000..9bc9d51ac --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/OwnerDetailsItem.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { DetailsItem, OwnerReferencesItem } from 'src/modules/Providers/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { StorageDetailsItemProps } from './StorageDetailsItemProps'; + +export const OwnerDetailsItem: React.FC = ({ + resource, + moreInfoLink, + helpContent, +}) => { + const { t } = useForkliftTranslation(); + + const defaultMoreInfoLink = + 'https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/'; + const defaultHelpContent = t( + `List of objects depended by this object. If ALL objects in the list have been deleted, + this object will be garbage collected. If this object is managed by a controller, + then an entry in this list will point to this controller, with the controller field set to true. + There cannot be more than one managing controller.`, + ); + + return ( + } + moreInfoLink={moreInfoLink ?? defaultMoreInfoLink} + helpContent={helpContent ?? defaultHelpContent} + crumbs={['metadata', 'ownerReferences']} + /> + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/StorageDetailsItemProps.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/StorageDetailsItemProps.tsx new file mode 100644 index 000000000..8d05b834f --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/StorageDetailsItemProps.tsx @@ -0,0 +1,10 @@ +import { ReactNode } from 'react'; + +import { V1beta1StorageMap } from '@kubev2v/types'; + +export interface StorageDetailsItemProps { + resource: V1beta1StorageMap; + canPatch?: boolean; + moreInfoLink?: string; + helpContent?: ReactNode; +} diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/StorageMapPageHeadings.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/StorageMapPageHeadings.tsx new file mode 100644 index 000000000..480362eb8 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/StorageMapPageHeadings.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { useGetDeleteAndEditAccessReview } from 'src/modules/Providers/hooks'; +import { PageHeadings } from 'src/modules/Providers/utils'; +import { StorageMapActionsDropdown } from 'src/modules/StorageMaps/actions'; +import { StorageMapCriticalConditions } from 'src/modules/StorageMaps/componenets'; + +import { + StorageMapModel, + StorageMapModelGroupVersionKind, + V1beta1StorageMap, +} from '@kubev2v/types'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { PageSection } from '@patternfly/react-core'; + +export const StorageMapPageHeadings: React.FC<{ name: string; namespace: string }> = ({ + name, + namespace, +}) => { + const [obj, loaded, loadError] = useK8sWatchResource({ + groupVersionKind: StorageMapModelGroupVersionKind, + namespaced: true, + name, + namespace, + }); + + const permissions = useGetDeleteAndEditAccessReview({ + model: StorageMapModel, + namespace, + }); + + const alerts = []; + + const criticalCondition = + loaded && + !loadError && + obj?.status?.conditions.find((condition) => condition?.category === 'Critical'); + + if (criticalCondition) { + alerts.push( + , + ); + } + + return ( + <> + } + > + {alerts && alerts.length > 0 && ( + + {alerts} + + )} + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/index.ts new file mode 100644 index 000000000..325aa3b35 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/components/index.ts @@ -0,0 +1,7 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './CreatedAtDetailsItem'; +export * from './NameDetailsItem'; +export * from './NamespaceDetailsItem'; +export * from './OwnerDetailsItem'; +export * from './StorageDetailsItemProps'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/index.ts new file mode 100644 index 000000000..86a1ade75 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/DetailsSection/index.ts @@ -0,0 +1,4 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './components'; +export * from './DetailsSection'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/MapsSection.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/MapsSection.tsx new file mode 100644 index 000000000..a24d1e25b --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/MapsSection.tsx @@ -0,0 +1,197 @@ +import React, { useReducer } from 'react'; +import { Suspend } from 'src/modules/Plans/views/details/components'; +import { useOpenShiftStorages, useSourceStorages } from 'src/modules/Providers/hooks/useStorages'; +import { MappingList } from 'src/modules/Providers/views/migrate/components/MappingList'; +import { Mapping } from 'src/modules/Providers/views/migrate/types'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { + ProviderModelGroupVersionKind, + StorageMapModel, + V1beta1Provider, + V1beta1StorageMap, + V1beta1StorageMapSpecMap, +} from '@kubev2v/types'; +import { k8sUpdate, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { + Button, + DescriptionListDescription, + Flex, + FlexItem, + Spinner, +} from '@patternfly/react-core'; + +import { mapsSectionReducer, MapsSectionState } from './state/reducer'; + +const initialState: MapsSectionState = { + StorageMap: null, + hasChanges: false, + updating: false, +}; + +export const MapsSection: React.FC = ({ obj }) => { + const { t } = useForkliftTranslation(); + const [state, dispatch] = useReducer(mapsSectionReducer, initialState); + + // Initialize the state with the prop obj + React.useEffect(() => { + dispatch({ type: 'INIT', payload: obj }); + }, [obj]); + + const [providers, providersLoaded, providersLoadError] = useK8sWatchResource({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + isList: true, + namespace: obj.metadata.namespace, + }); + + const sourceProvider = providers.find( + (p) => p?.metadata?.uid === obj?.spec?.provider?.source?.uid, + ); + const [sourceStorages] = useSourceStorages(sourceProvider); + + const destinationProvider = providers.find( + (p) => p?.metadata?.uid === obj?.spec?.provider?.destination?.uid, + ); + const [destinationStorages] = useOpenShiftStorages(destinationProvider); + + const onUpdate = async () => { + dispatch({ type: 'SET_UPDATING', payload: true }); + await k8sUpdate({ model: StorageMapModel, data: state.StorageMap }); + dispatch({ type: 'SET_UPDATING', payload: false }); + }; + + const isStorageMapped = (StorageMapID: string) => { + return state.StorageMap.spec.map.find((m) => StorageMapID === m?.source?.id) !== undefined; + }; + + const availableSources = sourceStorages?.filter((n) => !isStorageMapped(n?.id)); + + const getInventoryStorageName = (id: string) => sourceStorages.find((s) => s.id === id)?.name; + + const onAdd = () => + availableSources.length > 0 && + dispatch({ + type: 'SET_MAP', + payload: [ + ...(state.StorageMap?.spec?.map || []), + { + source: availableSources[0], + destination: { storageClass: destinationStorages?.[0].name }, + }, + ], + }); + + const onReplace = ({ current, next }) => { + const currentDestinationStorage = destinationStorages.find( + (n) => n.name == current.destination, + ); + const currentSourceStorage = sourceStorages.find((n) => n?.name === current.source); + + const nextDestinationStorage = destinationStorages.find((n) => n.name == next.destination); + const nextSourceStorage = sourceStorages.find((n) => n?.name === next.source); + + // sanity check, names may not be valid + if (!nextSourceStorage || !nextDestinationStorage) { + return; + } + + const nextMap: V1beta1StorageMapSpecMap = { + source: { id: nextSourceStorage.id }, + destination: { storageClass: nextDestinationStorage.name }, + }; + + const payload = state?.StorageMap?.spec?.map?.map((map) => { + return map?.source?.id === currentSourceStorage?.id && + map.destination?.storageClass === currentDestinationStorage?.name + ? nextMap + : map; + }); + + dispatch({ + type: 'SET_MAP', + payload: payload || [], + }); + }; + + const onDelete = (current: Mapping) => { + const references = storageNameToIDReference(state?.StorageMap?.status?.references || []); + const currentSourceStorage = sourceStorages.find((n) => n.name === current.source); + + dispatch({ + type: 'SET_MAP', + payload: [ + ...(state?.StorageMap?.spec?.map.filter( + (map) => + !( + (map?.source?.id === currentSourceStorage?.id || + map?.source?.id === references[current.source]) && + map.destination?.storageClass === current.destination + ), + ) || []), + ], + }); + }; + + return ( + + + + + + + + + + + + + s?.name)]} + sources={sourceStorages.map((n) => ({ + label: n.name, + usedBySelectedVms: false, + isMapped: isStorageMapped(n?.id), + }))} + mappings={state?.StorageMap?.spec?.map.map((m) => ({ + source: getInventoryStorageName(m.source.id), + destination: m.destination.storageClass, + }))} + generalSourcesLabel={t('Other storages present on the source provider ')} + usedSourcesLabel={t('Storages used by the selected VMs')} + noSourcesLabel={t('No storages in this category')} + isDisabled={false} + /> + + + ); +}; + +export type MapsSectionProps = { + obj: V1beta1StorageMap; +}; + +function storageNameToIDReference(array: { id?: string; name?: string }[]): Record { + return array.reduce((accumulator, current) => { + if (current?.id && current?.name) { + accumulator[current.name] = current.id; + } + return accumulator; + }, {} as Record); +} diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/components/MapsEdit.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/components/MapsEdit.tsx new file mode 100644 index 000000000..98bd73c1a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/components/MapsEdit.tsx @@ -0,0 +1,102 @@ +import React, { ReactNode, useState } from 'react'; +import { DetailsItem } from 'src/modules/Providers/utils'; + +import { ProviderModelGroupVersionKind, V1beta1Provider } from '@kubev2v/types'; +import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; +import { Form, FormGroup, FormSelect, FormSelectOption } from '@patternfly/react-core'; + +export const MapsEdit: React.FC = ({ + providers, + selectedProviderName, + onChange, + label, + placeHolderLabel, + invalidLabel, + helpContent, + mode, +}) => { + const [isEdit, setEdit] = useState(mode === 'edit'); + + const ProviderOption = (provider, index) => ( + + ); + + const targetProvider = fineProvider({ providers, name: selectedProviderName }); + + const validated = targetProvider !== undefined ? 'success' : 'error'; + const hasProviders = providers?.length > 0; + + if (isEdit) { + return ( +
+ + + {[ + , + ...providers.map(ProviderOption), + ]} + + +
+ ); + } else { + return ( + + } + onEdit={hasProviders ? () => setEdit(true) : undefined} + helpContent={helpContent} + crumbs={['spec', 'providers']} + /> + ); + } +}; + +export type MapsEditProps = { + providers: V1beta1Provider[]; + selectedProviderName: string; + onChange: (value: string) => void; + label: string; + placeHolderLabel: string; + invalidLabel: string; + helpContent: ReactNode; + mode: 'edit' | 'view'; +}; + +type FindProviderFunction = (args: { + providers: V1beta1Provider[]; + name: string; +}) => V1beta1Provider; + +const fineProvider: FindProviderFunction = ({ providers, name }) => + providers.find((p) => p.metadata.name === name); diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/components/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/components/index.ts new file mode 100644 index 000000000..92c790144 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/components/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './MapsEdit'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/index.ts new file mode 100644 index 000000000..489298036 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/index.ts @@ -0,0 +1,5 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './components'; +export * from './MapsSection'; +export * from './state'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/state/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/state/index.ts new file mode 100644 index 000000000..d79f304c6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/state/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './reducer'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/state/reducer.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/state/reducer.ts new file mode 100644 index 000000000..1fd1bfe4d --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/MapsSection/state/reducer.ts @@ -0,0 +1,35 @@ +import { deepCopy } from 'src/modules/StorageMaps/utils'; + +import { V1beta1StorageMap, V1beta1StorageMapSpecMap } from '@kubev2v/types'; + +export interface MapsSectionState { + StorageMap: V1beta1StorageMap | null; + hasChanges: boolean; + updating: boolean; +} + +export type MapsAction = + | { type: 'SET_MAP'; payload: V1beta1StorageMapSpecMap[] } + | { type: 'SET_UPDATING'; payload: boolean } + | { type: 'INIT'; payload: V1beta1StorageMap }; + +export function mapsSectionReducer(state: MapsSectionState, action: MapsAction): MapsSectionState { + let newState: MapsSectionState; + + switch (action.type) { + case 'SET_MAP': + newState = { ...state, hasChanges: true }; + newState.StorageMap.spec.map = action.payload; + return newState; + case 'SET_UPDATING': + return { ...state, updating: action.payload }; + case 'INIT': + return { + StorageMap: deepCopy(action.payload), + hasChanges: false, + updating: false, + }; + default: + return state; + } +} diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/ProvidersSection.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/ProvidersSection.tsx new file mode 100644 index 000000000..e84d03640 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/ProvidersSection.tsx @@ -0,0 +1,118 @@ +import React, { useReducer } from 'react'; +import { Suspend } from 'src/modules/Plans/views/details/components'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { + ProviderModelGroupVersionKind, + StorageMapModel, + V1beta1Provider, + V1beta1StorageMap, +} from '@kubev2v/types'; +import { k8sUpdate, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { Button, DescriptionList, Flex, FlexItem, Spinner } from '@patternfly/react-core'; + +import { ProvidersEdit } from './components'; +import { providersSectionReducer, ProvidersSectionState } from './state'; + +const initialState: ProvidersSectionState = { + StorageMap: null, + sourceProviderMode: 'view', + targetProviderMode: 'view', + hasChanges: false, + updating: false, +}; + +export const ProvidersSection: React.FC = ({ obj }) => { + const { t } = useForkliftTranslation(); + const [state, dispatch] = useReducer(providersSectionReducer, initialState); + + // Initialize the state with the prop obj + React.useEffect(() => { + dispatch({ type: 'INIT', payload: obj }); + }, [obj]); + + const [providers, providersLoaded, providersLoadError] = useK8sWatchResource({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + isList: true, + namespace: obj.metadata.namespace, + }); + + const targetProviders = providers.filter((p) => ['openshift'].includes(p?.spec?.type)); + + const onUpdate = async () => { + dispatch({ type: 'SET_UPDATING', payload: true }); + await k8sUpdate({ model: StorageMapModel, data: state.StorageMap }); + }; + + return ( + + + + + + + + + + + + + + dispatch({ + type: 'SET_SOURCE_PROVIDER', + payload: providers.find((p) => p?.metadata?.name === value), + }) + } + invalidLabel={t('The chosen provider is no longer available.')} + mode={state.sourceProviderMode} + helpContent="source provider" + setMode={() => dispatch({ type: 'SET_SOURCE_PROVIDER_MODE', payload: 'edit' })} + /> + + + dispatch({ + type: 'SET_TARGET_PROVIDER', + payload: providers.find((p) => p?.metadata?.name === value), + }) + } + invalidLabel={t('The chosen provider is no longer available.')} + mode={state.targetProviderMode} + helpContent="Target provider" + setMode={() => dispatch({ type: 'SET_TARGET_PROVIDER_MODE', payload: 'edit' })} + /> + + + ); +}; + +export type ProvidersSectionProps = { + obj: V1beta1StorageMap; +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/components/ProvidersEdit.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/components/ProvidersEdit.tsx new file mode 100644 index 000000000..d88b7c78c --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/components/ProvidersEdit.tsx @@ -0,0 +1,102 @@ +import React, { ReactNode } from 'react'; +import { DetailsItem } from 'src/modules/Providers/utils'; + +import { ProviderModelGroupVersionKind, V1beta1Provider } from '@kubev2v/types'; +import { ResourceLink } from '@openshift-console/dynamic-plugin-sdk'; +import { Form, FormGroup, FormSelect, FormSelectOption } from '@patternfly/react-core'; + +export const ProvidersEdit: React.FC = ({ + providers, + selectedProviderName, + onChange, + label, + placeHolderLabel, + invalidLabel, + helpContent, + mode, + setMode, +}) => { + const ProviderOption = (provider, index) => ( + + ); + + const targetProvider = fineProvider({ providers, name: selectedProviderName }); + + const validated = targetProvider !== undefined ? 'success' : 'error'; + const hasProviders = providers?.length > 0; + + if (mode === 'edit') { + return ( +
+ + + {[ + , + ...providers.map(ProviderOption), + ]} + + +
+ ); + } else { + return ( + + } + onEdit={hasProviders ? () => setMode('edit') : undefined} + helpContent={helpContent} + crumbs={['spec', 'providers']} + /> + ); + } +}; + +export type ProvidersEditProps = { + providers: V1beta1Provider[]; + selectedProviderName: string; + onChange: (value: string) => void; + label: string; + placeHolderLabel: string; + invalidLabel: string; + helpContent: ReactNode; + mode: 'edit' | 'view'; + setMode: (mode: 'edit' | 'view') => void; +}; + +type FindProviderFunction = (args: { + providers: V1beta1Provider[]; + name: string; +}) => V1beta1Provider; + +const fineProvider: FindProviderFunction = ({ providers, name }) => + providers.find((p) => p.metadata.name === name); diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/components/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/components/index.ts new file mode 100644 index 000000000..a64b2a035 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/components/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './ProvidersEdit'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/index.ts new file mode 100644 index 000000000..ed872e101 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/index.ts @@ -0,0 +1,5 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './components'; +export * from './ProvidersSection'; +export * from './state'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/state/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/state/index.ts new file mode 100644 index 000000000..d79f304c6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/state/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './reducer'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/state/reducer.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/state/reducer.ts new file mode 100644 index 000000000..993b06464 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/ProvidersSection/state/reducer.ts @@ -0,0 +1,66 @@ +import { deepCopy } from 'src/modules/StorageMaps/utils'; + +import { V1beta1Provider, V1beta1StorageMap } from '@kubev2v/types'; + +export interface ProvidersSectionState { + StorageMap: V1beta1StorageMap | null; + sourceProviderMode: 'view' | 'edit'; + targetProviderMode: 'view' | 'edit'; + hasChanges: boolean; + updating: boolean; +} + +export type ProvidersAction = + | { type: 'SET_SOURCE_PROVIDER'; payload: V1beta1Provider } + | { type: 'SET_TARGET_PROVIDER'; payload: V1beta1Provider } + | { type: 'SET_SOURCE_PROVIDER_MODE'; payload: 'view' | 'edit' } + | { type: 'SET_TARGET_PROVIDER_MODE'; payload: 'view' | 'edit' } + | { type: 'SET_UPDATING'; payload: boolean } + | { type: 'INIT'; payload: V1beta1StorageMap }; + +export function providersSectionReducer( + state: ProvidersSectionState, + action: ProvidersAction, +): ProvidersSectionState { + let newState: ProvidersSectionState; + + switch (action.type) { + case 'SET_SOURCE_PROVIDER': + newState = { ...state, hasChanges: true }; + newState.StorageMap.spec.provider.source = { + apiVersion: action.payload?.apiVersion, + kind: action.payload?.kind, + name: action.payload?.metadata?.name, + namespace: action.payload?.metadata?.namespace, + uid: action.payload?.metadata?.uid, + }; + return newState; + case 'SET_TARGET_PROVIDER': + newState = { ...state, hasChanges: true }; + newState.StorageMap.spec.provider.destination = { + apiVersion: action.payload?.apiVersion, + kind: action.payload?.kind, + name: action.payload?.metadata?.name, + namespace: action.payload?.metadata?.namespace, + uid: action.payload?.metadata?.uid, + }; + return newState; + case 'SET_SOURCE_PROVIDER_MODE': + return { ...state, sourceProviderMode: action.payload }; + case 'SET_TARGET_PROVIDER_MODE': + return { ...state, targetProviderMode: action.payload }; + + case 'SET_UPDATING': + return { ...state, updating: action.payload }; + case 'INIT': + return { + StorageMap: deepCopy(action.payload), + targetProviderMode: 'view', + sourceProviderMode: 'view', + hasChanges: false, + updating: false, + }; + default: + return state; + } +} diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/index.ts new file mode 100644 index 000000000..6235321ca --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/components/index.ts @@ -0,0 +1,6 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './ConditionsSection'; +export * from './DetailsSection'; +export * from './MapsSection'; +export * from './ProvidersSection'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/index.ts new file mode 100644 index 000000000..7b4ceba0e --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/index.ts @@ -0,0 +1,4 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './StorageMapDetailsPage'; +export * from './tabs'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/Details/StorageMapDetailsTab.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/Details/StorageMapDetailsTab.tsx new file mode 100644 index 000000000..c8248ce6c --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/Details/StorageMapDetailsTab.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { SectionHeading } from 'src/components/headers/SectionHeading'; +import { Suspend } from 'src/modules/Plans/views/details/components'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { StorageMapModelGroupVersionKind, V1beta1StorageMap } from '@kubev2v/types'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { PageSection } from '@patternfly/react-core'; + +import { ConditionsSection, DetailsSection, MapsSection, ProvidersSection } from '../../components'; + +interface StorageMapDetailsTabProps extends RouteComponentProps { + name: string; + namespace: string; +} + +export const StorageMapDetailsTab: React.FC = ({ name, namespace }) => { + const { t } = useForkliftTranslation(); + + const [obj, loaded, loadError] = useK8sWatchResource({ + groupVersionKind: StorageMapModelGroupVersionKind, + namespaced: true, + isList: false, + namespace, + name, + }); + + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/Details/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/Details/index.ts new file mode 100644 index 000000000..d636a3c1c --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/Details/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './StorageMapDetailsTab'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/YAML/StorageMapYAMLTab.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/YAML/StorageMapYAMLTab.tsx new file mode 100644 index 000000000..fd01fc1bd --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/YAML/StorageMapYAMLTab.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { Suspend } from 'src/modules/Plans/views/details/components'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { StorageMapModelGroupVersionKind, V1beta1StorageMap } from '@kubev2v/types'; +import { ResourceYAMLEditor, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; + +interface StorageMapYAMLTabProps extends RouteComponentProps { + name: string; + namespace: string; +} + +export const StorageMapYAMLTab: React.FC = ({ name, namespace }) => { + const { t } = useForkliftTranslation(); + + const [obj, loaded, loadError] = useK8sWatchResource({ + groupVersionKind: StorageMapModelGroupVersionKind, + namespaced: true, + isList: false, + namespace, + name, + }); + + return ( + + + + ); +}; + +export default StorageMapYAMLTab; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/YAML/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/YAML/index.ts new file mode 100644 index 000000000..cd65de51d --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/YAML/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './StorageMapYAMLTab'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/index.ts new file mode 100644 index 000000000..118bb9635 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/details/tabs/index.ts @@ -0,0 +1,4 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './Details'; +export * from './YAML'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/StorageMapRow.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/StorageMapRow.tsx new file mode 100644 index 000000000..a9c0eefd1 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/StorageMapRow.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +import { ResourceField, RowProps } from '@kubev2v/common'; +import { Td, Tr } from '@patternfly/react-table'; + +import { StorageMapActionsDropdown } from '../../actions'; +import { StorageMapData } from '../../utils'; + +import { + CellProps, + NamespaceCell, + PlanCell, + ProviderLinkCell, + StatusCell, + StorageMapLinkCell, +} from './components'; + +export const ProviderRow: React.FC> = ({ + resourceFields, + resourceData, +}) => { + return ( + + {resourceFields.map(({ resourceFieldId }) => + renderTd({ resourceData, resourceFieldId, resourceFields }), + )} + + ); +}; + +const renderTd = ({ resourceData, resourceFieldId, resourceFields }: RenderTdProps) => { + const fieldId = resourceFieldId; + + const CellRenderer = cellRenderers?.[fieldId] ?? (() => <>); + return ( + + + + ); +}; + +const cellRenderers: Record> = { + ['name']: StorageMapLinkCell, + ['namespace']: NamespaceCell, + ['owner']: PlanCell, + ['phase']: StatusCell, + ['destination']: ProviderLinkCell, + ['source']: ProviderLinkCell, + ['actions']: (props) => StorageMapActionsDropdown({ isKebab: true, ...props }), +}; + +interface RenderTdProps { + resourceData: StorageMapData; + resourceFieldId: string; + resourceFields: ResourceField[]; +} + +export default ProviderRow; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/StorageMapsListPage.style.css b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/StorageMapsListPage.style.css new file mode 100644 index 000000000..c16b57bf5 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/StorageMapsListPage.style.css @@ -0,0 +1,8 @@ +.forklift-empty-state__icon { + max-width: 14rem; + max-height: 14rem; +} + +.forklift-providers-list-header__alert { + padding-top: var(--pf-global--spacer--sm); +} \ No newline at end of file diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/StorageMapsListPage.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/StorageMapsListPage.tsx new file mode 100644 index 000000000..8dd214092 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/StorageMapsListPage.tsx @@ -0,0 +1,167 @@ +import React, { useState } from 'react'; +import StandardPage from 'src/components/page/StandardPage'; +import { useGetDeleteAndEditAccessReview } from 'src/modules/Providers/hooks'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { EnumToTuple, loadUserSettings, ResourceFieldFactory } from '@kubev2v/common'; +import { + StorageMapModel, + StorageMapModelGroupVersionKind, + V1beta1StorageMap, +} from '@kubev2v/types'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; + +import { StorageMapsAddButton } from '../../componenets'; +import StorageMapsEmptyState from '../../componenets/StorageMapsEmptyState'; +import { getStorageMapPhase, STORAGE_MAP_STATUS, StorageMapData } from '../../utils'; + +import StorageMapRow from './StorageMapRow'; + +import './StorageMapsListPage.style.css'; + +export const fieldsMetadataFactory: ResourceFieldFactory = (t) => [ + { + resourceFieldId: 'name', + jsonPath: '$.obj.metadata.name', + label: t('Name'), + isVisible: true, + isIdentity: true, // Name is sufficient ID when Namespace is pre-selected + filter: { + type: 'freetext', + placeholderLabel: t('Filter by name'), + }, + sortable: true, + }, + { + resourceFieldId: 'namespace', + jsonPath: '$.obj.metadata.namespace', + label: t('Namespace'), + isVisible: true, + isIdentity: true, + filter: { + type: 'freetext', + placeholderLabel: t('Filter by namespace'), + }, + sortable: true, + }, + { + resourceFieldId: 'phase', + jsonPath: getStorageMapPhase, + label: t('Status'), + isVisible: true, + filter: { + type: 'enum', + primary: true, + placeholderLabel: t('Status'), + values: EnumToTuple(STORAGE_MAP_STATUS), + }, + sortable: true, + }, + { + resourceFieldId: 'source', + jsonPath: '$.obj.spec.provider.source.name', + label: t('Source provider'), + isVisible: true, + filter: { + type: 'freetext', + placeholderLabel: t('Filter by source'), + }, + sortable: true, + }, + { + resourceFieldId: 'destination', + jsonPath: '$.obj.spec.provider.destination.name', + label: t('Target provider'), + isVisible: true, + filter: { + type: 'freetext', + placeholderLabel: t('Filter by target'), + }, + sortable: true, + }, + { + resourceFieldId: 'owner', + jsonPath: '$.obj.metadata.ownerReferences[0].name', + label: t('Owner'), + isVisible: true, + filter: { + type: 'freetext', + placeholderLabel: t('Filter by namespace'), + }, + sortable: true, + }, + { + resourceFieldId: 'actions', + label: '', + isAction: true, + isVisible: true, + sortable: false, + }, +]; + +const StorageMapsListPage: React.FC<{ + namespace: string; +}> = ({ namespace }) => { + const { t } = useForkliftTranslation(); + const [userSettings] = useState(() => loadUserSettings({ pageId: 'StorageMaps' })); + + const [StorageMaps, StorageMapsLoaded, StorageMapsLoadError] = useK8sWatchResource< + V1beta1StorageMap[] + >({ + groupVersionKind: StorageMapModelGroupVersionKind, + namespaced: true, + isList: true, + namespace, + }); + + const permissions = useGetDeleteAndEditAccessReview({ + model: StorageMapModel, + namespace, + }); + + const data: StorageMapData[] = StorageMaps.map((obj) => ({ + obj, + permissions, + })); + + const EmptyState = ( + + } + namespace={namespace} + /> + ); + + return ( + + data-testid="network-maps-list" + addButton={ + permissions.canCreate && ( + + ) + } + dataSource={[data || [], StorageMapsLoaded, StorageMapsLoadError]} + RowMapper={StorageMapRow} + fieldsMetadata={fieldsMetadataFactory(t)} + namespace={namespace} + title={t('StorageMaps')} + userSettings={userSettings} + customNoResultsFound={EmptyState} + /> + ); +}; + +interface EmptyStateProps { + AddButton: JSX.Element; + namespace?: string; +} + +const EmptyState_: React.FC = ({ namespace }) => { + return ; +}; + +export default StorageMapsListPage; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/CellProps.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/CellProps.tsx new file mode 100644 index 000000000..1b1c7c775 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/CellProps.tsx @@ -0,0 +1,9 @@ +import { StorageMapData } from 'src/modules/StorageMaps/utils'; + +import { ResourceField } from '@kubev2v/common'; + +export type CellProps = { + data: StorageMapData; + fieldId: string; + fields: ResourceField[]; +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/NamespaceCell.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/NamespaceCell.tsx new file mode 100644 index 000000000..ac84940f6 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/NamespaceCell.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { TableLinkCell } from 'src/modules/Providers/utils'; + +import { CellProps } from './CellProps'; + +/** + * NamespaceCell component, used for displaying a link cell with information about the namespace. + * @param {CellProps} props - The props for the component. + * @returns {JSX.Element} - The rendered component. + */ +export const NamespaceCell: React.FC = ({ data }) => { + const { obj: StorageMap } = data; + const { namespace } = StorageMap?.metadata || {}; + + return ( + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/PlanCell.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/PlanCell.tsx new file mode 100644 index 000000000..e7731836b --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/PlanCell.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { TableLinkCell } from 'src/modules/Providers/utils'; + +import { PlanModelGroupVersionKind } from '@kubev2v/types'; + +import { CellProps } from './CellProps'; + +export const PlanCell: React.FC = ({ data }) => { + const plan = data?.obj?.metadata?.ownerReferences?.[0]; + + if (!plan) { + return <>-; + } + + const { obj: StorageMap } = data; + const { namespace } = StorageMap?.metadata || {}; + const { name } = plan || {}; + + return ( + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/ProviderLinkCell.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/ProviderLinkCell.tsx new file mode 100644 index 000000000..06032d87e --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/ProviderLinkCell.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { TableLinkCell } from 'src/modules/Providers/utils'; + +import { ProviderModelGroupVersionKind } from '@kubev2v/types'; + +import { CellProps } from './CellProps'; + +export const ProviderLinkCell: React.FC = ({ data, fieldId }) => { + const provider = data.obj.spec.provider[fieldId]; + const { name, namespace } = provider || {}; + + if (!provider) { + return <>-; + } + + return ( + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/StatusCell.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/StatusCell.tsx new file mode 100644 index 000000000..69099c248 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/StatusCell.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { TFunction } from 'react-i18next'; +import Linkify from 'react-linkify'; +import { Link } from 'react-router-dom'; +import { getResourceUrl, TableIconCell } from 'src/modules/Providers/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { getResourceFieldValue } from '@kubev2v/common'; +import { StorageMapModelRef } from '@kubev2v/types'; +import { + GreenCheckCircleIcon, + RedExclamationCircleIcon, +} from '@openshift-console/dynamic-plugin-sdk'; +import { Button, Popover, Spinner, Text, TextContent, TextVariants } from '@patternfly/react-core'; + +import { CellProps } from './CellProps'; + +export const StatusCell: React.FC = ({ data, fields, fieldId }) => { + const { t } = useForkliftTranslation(); + + const phase = getResourceFieldValue(data, 'phase', fields); + const phaseLabel = phaseLabels[phase] ? t(phaseLabels[phase]) : t('Undefined'); + + switch (phase) { + case 'Critical': + return ErrorStatusCell({ + t, + data, + fields, + fieldId, + }); + default: + return {phaseLabel}; + } +}; + +export const ErrorStatusCell: React.FC = ({ t, data, fields }) => { + const { obj: StorageMap } = data; + const phase = getResourceFieldValue(data, 'phase', fields); + const phaseLabel = phaseLabels[phase] ? t(phaseLabels[phase]) : t('Undefined'); + const StorageMapURL = getResourceUrl({ + reference: StorageMapModelRef, + name: StorageMap?.metadata?.name, + namespace: StorageMap?.metadata?.namespace, + }); + + // Find the error message from the status conditions + const bodyContent = StorageMap?.status?.conditions.find( + (condition) => condition?.category === 'Critical', + )?.message; + + // Set the footer content + const footerContent = ( + + + {t( + `To troubleshoot, view the network map details page + and check the Forklift controller pod logs.`, + )} + + + {t('View network map details')} + + + ); + + return ( + {bodyContent}} + footerContent={footerContent} + > + + + ); +}; + +const statusIcons = { + Ready: , + 'Not Ready': , + Critical: , +}; + +const phaseLabels = { + // t('Ready') + Ready: 'Ready', + // t('Not Ready') + 'Not Ready': 'Not Ready', + // t('Critical') + Critical: 'Critical', +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/StorageMapLinkCell.tsx b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/StorageMapLinkCell.tsx new file mode 100644 index 000000000..78944ece7 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/StorageMapLinkCell.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { TableLinkCell } from 'src/modules/Providers/utils'; + +import { StorageMapModelGroupVersionKind } from '@kubev2v/types'; + +import { CellProps } from './CellProps'; + +export const StorageMapLinkCell: React.FC = ({ data }) => { + const { obj: StorageMap } = data; + const { name, namespace } = StorageMap?.metadata || {}; + + return ( + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/index.ts new file mode 100644 index 000000000..d0950ce55 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/components/index.ts @@ -0,0 +1,8 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './CellProps'; +export * from './NamespaceCell'; +export * from './PlanCell'; +export * from './ProviderLinkCell'; +export * from './StatusCell'; +export * from './StorageMapLinkCell'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/index.ts new file mode 100644 index 000000000..2df6d64ff --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/views/list/index.ts @@ -0,0 +1,4 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './StorageMapRow'; +export * from './StorageMapsListPage'; +// @endindex diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/yamlTemplates/defaultYamlTemplate.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/yamlTemplates/defaultYamlTemplate.ts new file mode 100644 index 000000000..d3eeb5186 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/yamlTemplates/defaultYamlTemplate.ts @@ -0,0 +1,20 @@ +import { Map as ImmutableMap } from 'immutable'; + +import { StorageMapModel } from '@kubev2v/types'; + +export const StorageMapModelYAMLTemplates = ImmutableMap().setIn( + ['default'], + ` +apiVersion: ${StorageMapModel.apiGroup}/${StorageMapModel.apiVersion} +kind: ${StorageMapModel.kind} +metadata: + name: example +spec: + map: [] + provider: + source: {} + destination: {} +`, +); + +export const defaultYamlTemplate = StorageMapModelYAMLTemplates.getIn(['default']); diff --git a/packages/forklift-console-plugin/src/modules/StorageMaps/yamlTemplates/index.ts b/packages/forklift-console-plugin/src/modules/StorageMaps/yamlTemplates/index.ts new file mode 100644 index 000000000..f8a5433ab --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/StorageMaps/yamlTemplates/index.ts @@ -0,0 +1,3 @@ +// @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './defaultYamlTemplate'; +// @endindex