From 795d3b3d130af1240161228e0daae33959740405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Fri, 19 Dec 2025 08:59:09 +0100 Subject: [PATCH 01/10] fix --- .../ComponentsSelection.tsx | 48 ++++++------------- .../useComponentsSelectionData.ts | 33 ++++++++++++- .../types/crate/createManagedControlPlane.ts | 8 ++-- 3 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/components/ComponentsSelection/ComponentsSelection.tsx b/src/components/ComponentsSelection/ComponentsSelection.tsx index fc31c988..32576934 100644 --- a/src/components/ComponentsSelection/ComponentsSelection.tsx +++ b/src/components/ComponentsSelection/ComponentsSelection.tsx @@ -1,21 +1,21 @@ -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { + Button, CheckBox, - Select, - Option, + CheckBoxDomRef, FlexBox, - Title, - Text, - Input, - Button, Grid, + Icon, + Input, + InputDomRef, List, ListItemStandard, - Icon, - Ui5CustomEvent, - CheckBoxDomRef, + Option, + Select, SelectDomRef, - InputDomRef, + Text, + Title, + Ui5CustomEvent, } from '@ui5/webcomponents-react'; import styles from './ComponentsSelection.module.css'; import { Infobox } from '../Ui/Infobox/Infobox.tsx'; @@ -40,31 +40,11 @@ export const ComponentsSelection: React.FC = ({ const selectedComponents = useMemo(() => getSelectedComponents(componentsList), [componentsList]); - const isProvider = useCallback((componentName: string) => { - return componentName.includes('provider') && componentName !== 'crossplane'; - }, []); - const searchResults = useMemo(() => { const lowerSearch = searchTerm.toLowerCase(); - const filtered = componentsList.filter(({ name }) => name.toLowerCase().includes(lowerSearch)); - - // Sort components: crossplane first, then providers, then rest - return filtered.sort((a, b) => { - const isCrossplaneA = a.name === 'crossplane'; - const isCrossplaneB = b.name === 'crossplane'; - - if (isCrossplaneA && !isCrossplaneB) return -1; - if (isCrossplaneB && !isCrossplaneA) return 1; - - const isProviderA = isProvider(a.name); - const isProviderB = isProvider(b.name); - - if (isProviderA && !isProviderB) return -1; - if (isProviderB && !isProviderA) return 1; - return a.name.localeCompare(b.name); - }); - }, [componentsList, searchTerm, isProvider]); + return componentsList.filter(({ name }) => name.toLowerCase().includes(lowerSearch)); + }, [componentsList, searchTerm]); const handleSelectionChange = useCallback( (e: Ui5CustomEvent) => { @@ -126,7 +106,7 @@ export const ComponentsSelection: React.FC = ({ {searchResults.length > 0 ? ( searchResults.map((component) => { const providerDisabled = isProviderDisabled(component); - const isProviderComponent = isProvider(component.name); + const isProviderComponent = component.isProvider; return ( !removeComponents.find((item) => item === component.name)); - setValue('componentsList', newComponentsList, { shouldValidate: false }); + // Add providers from initialSelection that don't exist in the available components list + if (initialSelection) { + const existingNames = new Set(newComponentsList.map((c) => c.name)); + Object.entries(initialSelection).forEach(([name, selection]) => { + if (!existingNames.has(name) && selection.isSelected && selection.version) { + newComponentsList.push({ + name, + versions: [selection.version], + selectedVersion: selection.version, + isSelected: true, + documentationUrl: '', + isProvider: true, + }); + } + }); + } + + // Sort the components list: alphabetically, but providers come after 'crossplane' + const components = newComponentsList.filter((c) => !c.isProvider).sort((a, b) => a.name.localeCompare(b.name)); + const providers = newComponentsList.filter((c) => c.isProvider).sort((a, b) => a.name.localeCompare(b.name)); + + // Find crossplane index in nonProviders and insert providers after it + const crossplaneIndex = components.findIndex((c) => c.name === 'crossplane'); + const sortedList = + crossplaneIndex !== -1 + ? [...components.slice(0, crossplaneIndex + 1), ...providers, ...components.slice(crossplaneIndex + 1)] + : [...components, ...providers]; + + setValue('componentsList', sortedList, { shouldValidate: false }); if (onComponentsInitialized) { - onComponentsInitialized(newComponentsList); + onComponentsInitialized(sortedList); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(data?.items), selectedTemplate, initialSelection]); diff --git a/src/lib/api/types/crate/createManagedControlPlane.ts b/src/lib/api/types/crate/createManagedControlPlane.ts index 25873ef4..ae21aabc 100644 --- a/src/lib/api/types/crate/createManagedControlPlane.ts +++ b/src/lib/api/types/crate/createManagedControlPlane.ts @@ -12,6 +12,7 @@ export interface ComponentsListItem { isSelected: boolean; selectedVersion: string; documentationUrl: string; + isProvider: boolean; } interface RoleBinding { @@ -78,10 +79,7 @@ export const CreateManagedControlPlane = ( ): CreateManagedControlPlaneType => { const selectedComponents: Components = optional?.componentsList - ?.filter( - (component) => - component.isSelected && !component.name.includes('provider') && !component.name.includes('crossplane'), - ) + ?.filter((component) => component.isSelected && !component.isProvider && !component.name.includes('crossplane')) .map((component) => { const mapping = replaceComponentsName.find((m) => m.originalName === component.name); return { @@ -99,7 +97,7 @@ export const CreateManagedControlPlane = ( const selectedProviders: Provider[] = optional?.componentsList - ?.filter(({ name, isSelected }) => name.includes('provider') && isSelected) + ?.filter(({ isSelected, isProvider }) => isProvider && isSelected) .map(({ name, selectedVersion }) => ({ name: name, version: selectedVersion, From 414b6dd0953f4863e4bc5b15ea308a3c00df158a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Fri, 19 Dec 2025 09:19:11 +0100 Subject: [PATCH 02/10] Update useComponentsSelectionData.ts --- .../useComponentsSelectionData.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts index 17dc2650..ef57aa18 100644 --- a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts +++ b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts @@ -55,7 +55,7 @@ export const useComponentsSelectionData = ( }) .filter((component) => !removeComponents.find((item) => item === component.name)); - // Add providers from initialSelection that don't exist in the available components list + // Add custom providers from initialSelection that don't exist in the available components list if (initialSelection) { const existingNames = new Set(newComponentsList.map((c) => c.name)); Object.entries(initialSelection).forEach(([name, selection]) => { @@ -72,16 +72,15 @@ export const useComponentsSelectionData = ( }); } - // Sort the components list: alphabetically, but providers come after 'crossplane' + // Sort components alphabetically, then crossplane providers alphabetically after 'crossplane' const components = newComponentsList.filter((c) => !c.isProvider).sort((a, b) => a.name.localeCompare(b.name)); - const providers = newComponentsList.filter((c) => c.isProvider).sort((a, b) => a.name.localeCompare(b.name)); + const crossplaneProviders = newComponentsList + .filter((c) => c.isProvider) + .sort((a, b) => a.name.localeCompare(b.name)); - // Find crossplane index in nonProviders and insert providers after it const crossplaneIndex = components.findIndex((c) => c.name === 'crossplane'); - const sortedList = - crossplaneIndex !== -1 - ? [...components.slice(0, crossplaneIndex + 1), ...providers, ...components.slice(crossplaneIndex + 1)] - : [...components, ...providers]; + const insertIndex = crossplaneIndex !== -1 ? crossplaneIndex + 1 : components.length; + const sortedList = [...components.slice(0, insertIndex), ...crossplaneProviders, ...components.slice(insertIndex)]; setValue('componentsList', sortedList, { shouldValidate: false }); if (onComponentsInitialized) { From 59c09164d6f5a3360d5ff6dca2b79fd1c1aabb8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Fri, 19 Dec 2025 10:40:45 +0100 Subject: [PATCH 03/10] explicit landscaper fix --- ...eateManagedControlPlaneWizardContainer.tsx | 3 +++ .../SummarizeStep.tsx | 4 ++++ .../types/crate/createManagedControlPlane.ts | 24 ++++++++++++++----- src/lib/api/types/mcpResource.ts | 1 + 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx b/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx index aecfdff6..12d221d2 100644 --- a/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx +++ b/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx @@ -267,6 +267,7 @@ export const CreateManagedControlPlaneWizardContainer: FC ; projectName: string; @@ -22,6 +23,7 @@ interface SummarizeStepProps { componentsList?: ComponentsListItem[]; originalYamlString?: string; isEditMode?: boolean; + initialData?: ManagedControlPlaneInterface; } export const SummarizeStep: React.FC = ({ @@ -31,6 +33,7 @@ export const SummarizeStep: React.FC = ({ workspaceName, componentsList, isEditMode = false, + initialData, }) => { const { t } = useTranslation(); const resource = CreateManagedControlPlane( @@ -91,6 +94,7 @@ export const SummarizeStep: React.FC = ({ chargingTargetType: watch('chargingTargetType'), }, idpPrefix, + initialData, ), )} /> diff --git a/src/lib/api/types/crate/createManagedControlPlane.ts b/src/lib/api/types/crate/createManagedControlPlane.ts index ae21aabc..7d084b7d 100644 --- a/src/lib/api/types/crate/createManagedControlPlane.ts +++ b/src/lib/api/types/crate/createManagedControlPlane.ts @@ -2,6 +2,7 @@ import { Resource } from '../resource'; import { CHARGING_TARGET_LABEL, CHARGING_TARGET_TYPE_LABEL, DISPLAY_NAME_ANNOTATION } from '../shared/keyNames'; import { Member } from '../shared/members'; import { AccountType } from '../../../../components/Members/EditMembers.tsx'; +import { ManagedControlPlaneInterface } from '../mcpResource.ts'; export type Annotations = Record; export type Labels = Record; @@ -43,7 +44,8 @@ interface Components { version: string; } | { type: 'GardenerDedicated' } - | { version: string; providers: Provider[] }; + | { version: string; providers: Provider[] } + | undefined; } export interface CreateManagedControlPlaneType { @@ -76,6 +78,7 @@ export const CreateManagedControlPlane = ( componentsList?: ComponentsListItem[]; }, idpPrefix?: string, + initialData?: ManagedControlPlaneInterface, ): CreateManagedControlPlaneType => { const selectedComponents: Components = optional?.componentsList @@ -109,6 +112,19 @@ export const CreateManagedControlPlane = ( }, }; + // Preserve landscaper from initialData if present (edit mode) + const landscaperFromInitialData = initialData?.spec?.components?.landscaper; + + const components: Components = { + ...selectedComponents, + apiServer: { type: 'GardenerDedicated' }, + ...(crossplaneComponent ? crossplaneWithProviders : {}), + }; + + if (landscaperFromInitialData) { + components.landscaper = landscaperFromInitialData; + } + return { apiVersion: 'core.openmcp.cloud/v1alpha1', kind: 'ManagedControlPlane', @@ -125,11 +141,7 @@ export const CreateManagedControlPlane = ( }, spec: { authentication: { enableSystemIdentityProvider: true }, - components: { - ...selectedComponents, - apiServer: { type: 'GardenerDedicated' }, - ...(crossplaneComponent ? crossplaneWithProviders : {}), - }, + components, authorization: { roleBindings: optional?.members?.map((member) => ({ diff --git a/src/lib/api/types/mcpResource.ts b/src/lib/api/types/mcpResource.ts index fa92973e..f0f248ac 100644 --- a/src/lib/api/types/mcpResource.ts +++ b/src/lib/api/types/mcpResource.ts @@ -59,6 +59,7 @@ export interface MCPComponentsSpec { crossplane?: MCPCrossplaneComponent; externalSecretsOperator?: MCPVersionedComponent; flux?: MCPVersionedComponent; + landscaper?: MCPVersionedComponent; } export interface MCPApiServerComponent { From 921d1609d92d0f1b81e4ee8461384e113235fca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Fri, 19 Dec 2025 10:50:43 +0100 Subject: [PATCH 04/10] fix --- src/lib/api/types/crate/createManagedControlPlane.ts | 9 +++++---- src/lib/api/types/mcpResource.ts | 6 +++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/lib/api/types/crate/createManagedControlPlane.ts b/src/lib/api/types/crate/createManagedControlPlane.ts index 7d084b7d..bd16a13c 100644 --- a/src/lib/api/types/crate/createManagedControlPlane.ts +++ b/src/lib/api/types/crate/createManagedControlPlane.ts @@ -2,7 +2,7 @@ import { Resource } from '../resource'; import { CHARGING_TARGET_LABEL, CHARGING_TARGET_TYPE_LABEL, DISPLAY_NAME_ANNOTATION } from '../shared/keyNames'; import { Member } from '../shared/members'; import { AccountType } from '../../../../components/Members/EditMembers.tsx'; -import { ManagedControlPlaneInterface } from '../mcpResource.ts'; +import { ManagedControlPlaneInterface, MCPVersionedComponent } from '../mcpResource.ts'; export type Annotations = Record; export type Labels = Record; @@ -41,10 +41,11 @@ interface Spec { interface Components { [key: string]: | { - version: string; + version?: string; + providers?: Provider[]; } | { type: 'GardenerDedicated' } - | { version: string; providers: Provider[] } + | { deployers?: string[] } | undefined; } @@ -122,7 +123,7 @@ export const CreateManagedControlPlane = ( }; if (landscaperFromInitialData) { - components.landscaper = landscaperFromInitialData; + components.landscaper = landscaperFromInitialData as { deployers?: string[] }; } return { diff --git a/src/lib/api/types/mcpResource.ts b/src/lib/api/types/mcpResource.ts index f0f248ac..63141c19 100644 --- a/src/lib/api/types/mcpResource.ts +++ b/src/lib/api/types/mcpResource.ts @@ -59,7 +59,11 @@ export interface MCPComponentsSpec { crossplane?: MCPCrossplaneComponent; externalSecretsOperator?: MCPVersionedComponent; flux?: MCPVersionedComponent; - landscaper?: MCPVersionedComponent; + landscaper?: MCPLandscaperComponent; +} + +export interface MCPLandscaperComponent { + deployers?: string[]; } export interface MCPApiServerComponent { From cce705146566903991f5dcffc5d36410df1950ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Fri, 19 Dec 2025 10:58:46 +0100 Subject: [PATCH 05/10] Update createManagedControlPlane.ts --- src/lib/api/types/crate/createManagedControlPlane.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/api/types/crate/createManagedControlPlane.ts b/src/lib/api/types/crate/createManagedControlPlane.ts index bd16a13c..e1f957a6 100644 --- a/src/lib/api/types/crate/createManagedControlPlane.ts +++ b/src/lib/api/types/crate/createManagedControlPlane.ts @@ -2,7 +2,7 @@ import { Resource } from '../resource'; import { CHARGING_TARGET_LABEL, CHARGING_TARGET_TYPE_LABEL, DISPLAY_NAME_ANNOTATION } from '../shared/keyNames'; import { Member } from '../shared/members'; import { AccountType } from '../../../../components/Members/EditMembers.tsx'; -import { ManagedControlPlaneInterface, MCPVersionedComponent } from '../mcpResource.ts'; +import { ManagedControlPlaneInterface } from '../mcpResource.ts'; export type Annotations = Record; export type Labels = Record; From a1c140eb3d81bb78660bd7980cbc9ebd3ef17d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Mon, 22 Dec 2025 12:42:08 +0100 Subject: [PATCH 06/10] fix --- .../useComponentsSelectionData.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts index ef57aa18..ca99a74f 100644 --- a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts +++ b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts @@ -25,12 +25,13 @@ export const useComponentsSelectionData = ( setValue('componentsList', [], { shouldValidate: false }); return; } + const newComponentsList: ComponentsListItem[] = items .map((item) => { const rawVersions = Array.isArray(item.status?.versions) ? (item.status?.versions as string[]) : []; const versions = sortVersions(rawVersions); const name = item.metadata?.name ?? ''; - const initSel = initialSelection?.[name]; + const initSel = initialSelection?.[name] ?? initialSelection?.[name.replace('provider-', '')]; const templateDefault = selectedTemplate?.spec?.spec?.components?.defaultComponents?.find( (dc) => dc.name === name, ); @@ -59,7 +60,12 @@ export const useComponentsSelectionData = ( if (initialSelection) { const existingNames = new Set(newComponentsList.map((c) => c.name)); Object.entries(initialSelection).forEach(([name, selection]) => { - if (!existingNames.has(name) && selection.isSelected && selection.version) { + if ( + !existingNames.has(name) && + !existingNames.has(`provider-${name}`) && + selection.isSelected && + selection.version + ) { newComponentsList.push({ name, versions: [selection.version], From d16d0f075c893b8821ab1e1b6830318e0f880256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Mon, 22 Dec 2025 13:37:10 +0100 Subject: [PATCH 07/10] refactor --- .../useComponentsSelectionData.spec.ts | 272 ++++++++++++++++++ .../useComponentsSelectionData.ts | 271 +++++++++++------ 2 files changed, 456 insertions(+), 87 deletions(-) create mode 100644 src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.spec.ts diff --git a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.spec.ts b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.spec.ts new file mode 100644 index 00000000..099292c2 --- /dev/null +++ b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.spec.ts @@ -0,0 +1,272 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { renderHook, waitFor } from '@testing-library/react'; +import { + isProviderComponent, + findInitialSelection, + determineSelectedVersion, + mapToComponentsListItem, + addCustomProviders, + sortComponents, + buildComponentsList, + validateTemplateDefaults, + useComponentsSelectionData, + InitialSelection, +} from './useComponentsSelectionData.ts'; +import { ManagedControlPlaneTemplate } from '../../../lib/api/types/templates/mcpTemplate.ts'; +import { ManagedComponent } from '../../../lib/api/types/crate/listManagedComponents.ts'; +import { ComponentsListItem } from '../../../lib/api/types/crate/createManagedControlPlane.ts'; + +const createManagedComponent = (name: string, versions: string[]): ManagedComponent => ({ + apiVersion: 'core.openmcp.cloud/v1alpha1', + kind: 'ManagedComponent', + metadata: { name }, + spec: {}, + status: { versions }, +}); + +const createTemplate = (defaultComponents: { name: string; version: string }[]): ManagedControlPlaneTemplate => + ({ + apiVersion: 'core.openmcp.cloud/v1alpha1', + kind: 'ManagedControlPlaneTemplate', + metadata: { name: 'test-template', namespace: 'default' }, + spec: { + meta: { + chargingTarget: { type: 'BTP', value: '' }, + displayName: {}, + name: {}, + }, + spec: { + authentication: { system: { changeable: true, enabled: true } }, + authorization: { defaultMembers: [] }, + components: { defaultComponents }, + }, + }, + }) as ManagedControlPlaneTemplate; + +const createComponentsListItem = (name: string, isProvider = false): ComponentsListItem => ({ + name, + versions: [], + selectedVersion: '', + isSelected: false, + documentationUrl: '', + isProvider, +}); + +const sampleInitialSelection: InitialSelection = { + crossplane: { isSelected: true, version: '1.20.1' }, + 'custom-provider': { isSelected: true, version: '1.0.0' }, + kubernetes: { isSelected: true, version: '0.15.0' }, + btp: { isSelected: true, version: '1.2.2' }, + flux: { isSelected: true, version: '2.16.2' }, +}; + +const sampleItems: ManagedComponent[] = [ + createManagedComponent('cert-manager', ['1.13.1', '1.16.1']), + createManagedComponent('crossplane', ['1.15.0', '1.19.0', '1.20.1']), + createManagedComponent('flux', ['2.15.0', '2.16.2']), + createManagedComponent('kyverno', ['3.2.4', '3.5.2']), + createManagedComponent('provider-btp', ['1.0.0', '1.2.2', '1.4.0']), + createManagedComponent('provider-kubernetes', ['0.14.0', '0.15.0']), +]; + +describe('isProviderComponent', () => { + it('returns true for provider-* names, false for crossplane and others', () => { + expect(isProviderComponent('provider-btp')).toBe(true); + expect(isProviderComponent('crossplane')).toBe(false); + expect(isProviderComponent('flux')).toBe(false); + }); +}); + +describe('findInitialSelection', () => { + it('finds by exact name or without provider- prefix', () => { + expect(findInitialSelection('crossplane', sampleInitialSelection)?.version).toBe('1.20.1'); + expect(findInitialSelection('provider-btp', sampleInitialSelection)?.version).toBe('1.2.2'); + expect(findInitialSelection('non-existent', sampleInitialSelection)).toBeUndefined(); + }); +}); + +describe('determineSelectedVersion', () => { + const versions = ['1.20.1', '1.19.0', '1.18.0']; + + it('prioritizes: initial selection > template default > first available', () => { + const initSel = { isSelected: true, version: '1.19.0' }; + const templateDefault = { name: 'test', version: '1.18.0' }; + + expect(determineSelectedVersion(versions, initSel, templateDefault)).toBe('1.19.0'); + expect(determineSelectedVersion(versions, undefined, templateDefault)).toBe('1.18.0'); + expect(determineSelectedVersion(versions, undefined, undefined)).toBe('1.20.1'); + }); + + it('returns empty string if selected version not available', () => { + const initSel = { isSelected: true, version: '9.9.9' }; + expect(determineSelectedVersion(versions, initSel, undefined)).toBe(''); + }); +}); + +describe('mapToComponentsListItem', () => { + it('maps component with sorted versions and applies initial selection', () => { + const item = createManagedComponent('crossplane', ['1.15.0', '1.20.1', '1.19.0']); + const initialSelection: InitialSelection = { + crossplane: { isSelected: true, version: '1.19.0' }, + }; + + const result = mapToComponentsListItem(item, initialSelection, undefined); + + expect(result.versions).toEqual(['1.20.1', '1.19.0', '1.15.0']); + expect(result.selectedVersion).toBe('1.19.0'); + expect(result.isSelected).toBe(true); + expect(result.isProvider).toBe(false); + }); + + it('applies template default when no initial selection', () => { + const item = createManagedComponent('flux', ['2.15.0', '2.16.2']); + const template = createTemplate([{ name: 'flux', version: '2.15.0' }]); + + const result = mapToComponentsListItem(item, undefined, template); + + expect(result.selectedVersion).toBe('2.15.0'); + expect(result.isSelected).toBe(true); + }); +}); + +describe('addCustomProviders', () => { + it('adds providers from initial selection not in available components', () => { + const components = [createComponentsListItem('crossplane')]; + const initialSelection: InitialSelection = { + 'custom-provider': { isSelected: true, version: '1.0.0' }, + }; + + const result = addCustomProviders(components, initialSelection); + + expect(result).toHaveLength(2); + expect(result[1].name).toBe('custom-provider'); + expect(result[1].isProvider).toBe(true); + }); + + it('skips if provider already exists or is not selected', () => { + const components = [createComponentsListItem('provider-btp', true)]; + + expect(addCustomProviders(components, { 'provider-btp': { isSelected: true, version: '1.0.0' } })).toHaveLength(1); + expect(addCustomProviders(components, { btp: { isSelected: true, version: '1.0.0' } })).toHaveLength(1); + expect(addCustomProviders([], { test: { isSelected: false, version: '1.0.0' } })).toHaveLength(0); + }); +}); + +describe('sortComponents', () => { + it('places providers after crossplane, both groups alphabetically', () => { + const components = [ + createComponentsListItem('flux'), + createComponentsListItem('crossplane'), + createComponentsListItem('provider-kubernetes', true), + createComponentsListItem('provider-btp', true), + ]; + + const result = sortComponents(components); + + expect(result.map((c) => c.name)).toEqual(['crossplane', 'provider-btp', 'provider-kubernetes', 'flux']); + }); +}); + +describe('buildComponentsList', () => { + it('builds complete list: maps, filters cert-manager, adds custom providers, sorts', () => { + const result = buildComponentsList(sampleItems, sampleInitialSelection, undefined); + + expect(result.find((c) => c.name === 'cert-manager')).toBeUndefined(); + expect(result.find((c) => c.name === 'custom-provider')).toBeDefined(); + expect(result[0].name).toBe('crossplane'); + + const crossplane = result.find((c) => c.name === 'crossplane'); + expect(crossplane?.isSelected).toBe(true); + expect(crossplane?.selectedVersion).toBe('1.20.1'); + + const providerBtp = result.find((c) => c.name === 'provider-btp'); + expect(providerBtp?.selectedVersion).toBe('1.2.2'); + }); + + it('returns empty array for empty items', () => { + expect(buildComponentsList([], undefined, undefined)).toEqual([]); + }); +}); + +describe('validateTemplateDefaults', () => { + it('returns null for valid defaults or empty inputs', () => { + const validTemplate = createTemplate([{ name: 'crossplane', version: '1.20.1' }]); + + expect(validateTemplateDefaults(sampleItems, validTemplate)).toBeNull(); + expect(validateTemplateDefaults([], validTemplate)).toBeNull(); + expect(validateTemplateDefaults(sampleItems, undefined)).toBeNull(); + }); + + it('returns error messages for missing component or version', () => { + const missingComponent = createTemplate([{ name: 'non-existent', version: '1.0.0' }]); + const missingVersion = createTemplate([{ name: 'crossplane', version: '99.99.99' }]); + + expect(validateTemplateDefaults(sampleItems, missingComponent)).toContain('non-existent'); + expect(validateTemplateDefaults(sampleItems, missingVersion)).toContain('99.99.99'); + }); +}); + +describe('useComponentsSelectionData', () => { + const mockSetValue = vi.fn(); + const mockOnComponentsInitialized = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('calls setValue with built components list and onComponentsInitialized callback', async () => { + const mockUseComponentsQuery = vi.fn().mockReturnValue({ + components: { items: sampleItems }, + error: undefined, + isLoading: false, + }); + + renderHook(() => + useComponentsSelectionData( + undefined, + sampleInitialSelection, + mockSetValue, + mockOnComponentsInitialized, + mockUseComponentsQuery, + ), + ); + + await waitFor(() => { + expect(mockSetValue).toHaveBeenCalledWith('componentsList', expect.any(Array), { shouldValidate: false }); + expect(mockOnComponentsInitialized).toHaveBeenCalled(); + }); + }); + + it('returns loading and error states from query', () => { + const mockError = new Error('Test error'); + const mockUseComponentsQuery = vi.fn().mockReturnValue({ + components: undefined, + error: mockError, + isLoading: true, + }); + + const { result } = renderHook(() => + useComponentsSelectionData(undefined, undefined, mockSetValue, undefined, mockUseComponentsQuery), + ); + + expect(result.current.isLoading).toBe(true); + expect(result.current.error).toBe(mockError); + }); + + it('returns templateDefaultsError for invalid template defaults', async () => { + const invalidTemplate = createTemplate([{ name: 'non-existent', version: '1.0.0' }]); + const mockUseComponentsQuery = vi.fn().mockReturnValue({ + components: { items: sampleItems }, + error: undefined, + isLoading: false, + }); + + const { result } = renderHook(() => + useComponentsSelectionData(invalidTemplate, undefined, mockSetValue, undefined, mockUseComponentsQuery), + ); + + await waitFor(() => { + expect(result.current.templateDefaultsError).toContain('non-existent'); + }); + }); +}); diff --git a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts index ca99a74f..6c802762 100644 --- a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts +++ b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts @@ -3,6 +3,7 @@ import { ManagedControlPlaneTemplate } from '../../../lib/api/types/templates/mc import { ComponentsListItem, removeComponents } from '../../../lib/api/types/crate/createManagedControlPlane.ts'; import { sortVersions } from '../../../utils/componentsVersions.ts'; import { useComponentsQuery as _useComponentsQuery } from '../../../hooks/useComponentsQuery.ts'; +import { ManagedComponent } from '../../../lib/api/types/crate/listManagedComponents.ts'; export type ComponentsHookResult = { isLoading: boolean; @@ -10,9 +11,185 @@ export type ComponentsHookResult = { templateDefaultsError: string | null; }; +export type InitialSelection = Record; + +export type DefaultComponent = { + name: string; + version?: string; +}; + +export const isProviderComponent = (name: string): boolean => { + return name.includes('provider') && name !== 'crossplane'; +}; + +// Checks both exact name and without 'provider-' prefix +export const findInitialSelection = ( + name: string, + initialSelection: InitialSelection | undefined, +): { isSelected: boolean; version: string } | undefined => { + if (!initialSelection) return undefined; + return initialSelection[name] ?? initialSelection[name.replace('provider-', '')]; +}; + +export const findTemplateDefault = ( + name: string, + selectedTemplate: ManagedControlPlaneTemplate | undefined, +): DefaultComponent | undefined => { + return selectedTemplate?.spec?.spec?.components?.defaultComponents?.find((dc) => dc.name === name); +}; + +// Priority: initial selection > template default > first available version +export const determineSelectedVersion = ( + versions: string[], + initSel: { isSelected: boolean; version: string } | undefined, + templateDefault: DefaultComponent | undefined, +): string => { + if (initSel?.version && versions.includes(initSel.version)) { + return initSel.version; + } + + if (!initSel && templateDefault?.version && versions.includes(templateDefault.version)) { + return templateDefault.version; + } + + if (!initSel && !templateDefault) { + return versions[0] ?? ''; + } + + return ''; +}; + +export const determineIsSelected = ( + initSel: { isSelected: boolean; version: string } | undefined, + templateDefault: DefaultComponent | undefined, +): boolean => { + if (initSel) { + return Boolean(initSel.isSelected); + } + return Boolean(templateDefault); +}; + +export const mapToComponentsListItem = ( + item: ManagedComponent, + initialSelection: InitialSelection | undefined, + selectedTemplate: ManagedControlPlaneTemplate | undefined, +): ComponentsListItem => { + const rawVersions = Array.isArray(item.status?.versions) ? (item.status?.versions as string[]) : []; + const versions = sortVersions(rawVersions); + const name = item.metadata?.name ?? ''; + + const initSel = findInitialSelection(name, initialSelection); + const templateDefault = findTemplateDefault(name, selectedTemplate); + + const isSelected = determineIsSelected(initSel, templateDefault); + const selectedVersion = determineSelectedVersion(versions, initSel, templateDefault); + + return { + name, + versions, + selectedVersion, + isSelected, + documentationUrl: '', + isProvider: isProviderComponent(name), + }; +}; + +export const filterRemovedComponents = (components: ComponentsListItem[]): ComponentsListItem[] => { + return components.filter((component) => !removeComponents.includes(component.name)); +}; + +// Adds providers from initial selection that don't exist in the available components list +export const addCustomProviders = ( + componentsList: ComponentsListItem[], + initialSelection: InitialSelection | undefined, +): ComponentsListItem[] => { + if (!initialSelection) return componentsList; + + const result = [...componentsList]; + const existingNames = new Set(result.map((c) => c.name)); + + Object.entries(initialSelection).forEach(([name, selection]) => { + const hasExactMatch = existingNames.has(name); + const hasProviderPrefixMatch = existingNames.has(`provider-${name}`); + + if (!hasExactMatch && !hasProviderPrefixMatch && selection.isSelected && selection.version) { + result.push({ + name, + versions: [selection.version], + selectedVersion: selection.version, + isSelected: true, + documentationUrl: '', + isProvider: true, + }); + } + }); + + return result; +}; + +// Sorts: non-providers alphabetically, then providers after 'crossplane' +export const sortComponents = (componentsList: ComponentsListItem[]): ComponentsListItem[] => { + const nonProviders = componentsList.filter((c) => !c.isProvider).sort((a, b) => a.name.localeCompare(b.name)); + + const crossplaneProviders = componentsList.filter((c) => c.isProvider).sort((a, b) => a.name.localeCompare(b.name)); + + const crossplaneIndex = nonProviders.findIndex((c) => c.name === 'crossplane'); + const insertIndex = crossplaneIndex !== -1 ? crossplaneIndex + 1 : nonProviders.length; + + return [...nonProviders.slice(0, insertIndex), ...crossplaneProviders, ...nonProviders.slice(insertIndex)]; +}; + +export const buildComponentsList = ( + items: ManagedComponent[], + initialSelection: InitialSelection | undefined, + selectedTemplate: ManagedControlPlaneTemplate | undefined, +): ComponentsListItem[] => { + if (!items || items.length === 0) { + return []; + } + + const mappedComponents = items.map((item) => mapToComponentsListItem(item, initialSelection, selectedTemplate)); + const filteredComponents = filterRemovedComponents(mappedComponents); + const withCustomProviders = addCustomProviders(filteredComponents, initialSelection); + + return sortComponents(withCustomProviders); +}; + +export const validateTemplateDefaults = ( + items: ManagedComponent[], + selectedTemplate: ManagedControlPlaneTemplate | undefined, +): string | null => { + const defaults = selectedTemplate?.spec?.spec?.components?.defaultComponents ?? []; + + if (!items.length || !defaults.length) { + return null; + } + + const errors: string[] = []; + + defaults.forEach((dc) => { + if (!dc?.name) return; + + const item = items.find((it) => it.metadata?.name === dc.name); + + if (!item) { + errors.push(`Component "${dc.name}" from template is not available.`); + return; + } + + const versions: string[] = Array.isArray(item.status?.versions) ? (item.status?.versions as string[]) : []; + + if (dc.version && !versions.includes(dc.version)) { + errors.push(`Component "${dc.name}" version "${dc.version}" from template is not available.`); + } + }); + + return errors.length ? errors.join('\n') : null; +}; + export const useComponentsSelectionData = ( selectedTemplate: ManagedControlPlaneTemplate | undefined, - initialSelection: Record | undefined, + initialSelection: InitialSelection | undefined, setValue: (name: 'componentsList', value: ComponentsListItem[], options?: { shouldValidate?: boolean }) => void, onComponentsInitialized?: (components: ComponentsListItem[]) => void, useComponentsQuery: typeof _useComponentsQuery = _useComponentsQuery, @@ -21,102 +198,22 @@ export const useComponentsSelectionData = ( useEffect(() => { const items = data?.items ?? []; - if (!items || items.length === 0) { - setValue('componentsList', [], { shouldValidate: false }); - return; - } - - const newComponentsList: ComponentsListItem[] = items - .map((item) => { - const rawVersions = Array.isArray(item.status?.versions) ? (item.status?.versions as string[]) : []; - const versions = sortVersions(rawVersions); - const name = item.metadata?.name ?? ''; - const initSel = initialSelection?.[name] ?? initialSelection?.[name.replace('provider-', '')]; - const templateDefault = selectedTemplate?.spec?.spec?.components?.defaultComponents?.find( - (dc) => dc.name === name, - ); - let isSelected = Boolean(initSel?.isSelected); - let selectedVersion = initSel?.version && versions.includes(initSel.version) ? initSel.version : ''; - if (!initSel) { - isSelected = Boolean(templateDefault); - const templateVersion = templateDefault?.version; - selectedVersion = templateVersion && versions.includes(templateVersion) ? templateVersion : ''; - } - if (!initSel && !templateDefault) { - selectedVersion = versions[0] ?? ''; - } - return { - name, - versions, - selectedVersion, - isSelected, - documentationUrl: '', - isProvider: name.includes('provider') && name !== 'crossplane', - } as ComponentsListItem; - }) - .filter((component) => !removeComponents.find((item) => item === component.name)); - - // Add custom providers from initialSelection that don't exist in the available components list - if (initialSelection) { - const existingNames = new Set(newComponentsList.map((c) => c.name)); - Object.entries(initialSelection).forEach(([name, selection]) => { - if ( - !existingNames.has(name) && - !existingNames.has(`provider-${name}`) && - selection.isSelected && - selection.version - ) { - newComponentsList.push({ - name, - versions: [selection.version], - selectedVersion: selection.version, - isSelected: true, - documentationUrl: '', - isProvider: true, - }); - } - }); - } - - // Sort components alphabetically, then crossplane providers alphabetically after 'crossplane' - const components = newComponentsList.filter((c) => !c.isProvider).sort((a, b) => a.name.localeCompare(b.name)); - const crossplaneProviders = newComponentsList - .filter((c) => c.isProvider) - .sort((a, b) => a.name.localeCompare(b.name)); - - const crossplaneIndex = components.findIndex((c) => c.name === 'crossplane'); - const insertIndex = crossplaneIndex !== -1 ? crossplaneIndex + 1 : components.length; - const sortedList = [...components.slice(0, insertIndex), ...crossplaneProviders, ...components.slice(insertIndex)]; + const sortedList = buildComponentsList(items, initialSelection, selectedTemplate); setValue('componentsList', sortedList, { shouldValidate: false }); - if (onComponentsInitialized) { + + if (onComponentsInitialized && sortedList.length > 0) { onComponentsInitialized(sortedList); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(data?.items), selectedTemplate, initialSelection]); const [defaultsError, setDefaultsError] = useState(null); + useEffect(() => { const items = data?.items ?? []; - const defaults = selectedTemplate?.spec?.spec?.components?.defaultComponents ?? []; - if (!items.length || !defaults.length) { - setDefaultsError(null); - return; - } - const errors: string[] = []; - defaults.forEach((dc) => { - if (!dc?.name) return; - const item = items.find((it) => it.metadata?.name === dc.name); - if (!item) { - errors.push(`Component "${dc.name}" from template is not available.`); - return; - } - const versions: string[] = Array.isArray(item.status?.versions) ? (item.status?.versions as string[]) : []; - if (dc.version && !versions.includes(dc.version)) { - errors.push(`Component "${dc.name}" version "${dc.version}" from template is not available.`); - } - }); - setDefaultsError(errors.length ? errors.join('\n') : null); + const error = validateTemplateDefaults(items, selectedTemplate); + setDefaultsError(error); }, [data, selectedTemplate]); return { isLoading: Boolean(isLoading), error, templateDefaultsError: defaultsError }; From 1e6389a8e10f94a89f5de552663d00b20209849a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Mon, 22 Dec 2025 14:03:41 +0100 Subject: [PATCH 08/10] adds custom versions support --- .../useComponentsSelectionData.spec.ts | 17 +++++++++++++++-- .../useComponentsSelectionData.ts | 17 +++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.spec.ts b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.spec.ts index 099292c2..b0797cab 100644 --- a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.spec.ts +++ b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.spec.ts @@ -97,9 +97,9 @@ describe('determineSelectedVersion', () => { expect(determineSelectedVersion(versions, undefined, undefined)).toBe('1.20.1'); }); - it('returns empty string if selected version not available', () => { + it('returns initial selection version even if not in versions array', () => { const initSel = { isSelected: true, version: '9.9.9' }; - expect(determineSelectedVersion(versions, initSel, undefined)).toBe(''); + expect(determineSelectedVersion(versions, initSel, undefined)).toBe('9.9.9'); }); }); @@ -118,6 +118,19 @@ describe('mapToComponentsListItem', () => { expect(result.isProvider).toBe(false); }); + it('adds initial selection version to versions array if not present', () => { + const item = createManagedComponent('crossplane', ['1.15.0', '1.19.0']); + const initialSelection: InitialSelection = { + crossplane: { isSelected: true, version: '1.20.1' }, + }; + + const result = mapToComponentsListItem(item, initialSelection, undefined); + + expect(result.versions).toContain('1.20.1'); + expect(result.versions).toEqual(['1.20.1', '1.19.0', '1.15.0']); + expect(result.selectedVersion).toBe('1.20.1'); + }); + it('applies template default when no initial selection', () => { const item = createManagedComponent('flux', ['2.15.0', '2.16.2']); const template = createTemplate([{ name: 'flux', version: '2.15.0' }]); diff --git a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts index 6c802762..3dc278ba 100644 --- a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts +++ b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts @@ -44,19 +44,15 @@ export const determineSelectedVersion = ( initSel: { isSelected: boolean; version: string } | undefined, templateDefault: DefaultComponent | undefined, ): string => { - if (initSel?.version && versions.includes(initSel.version)) { + if (initSel?.version) { return initSel.version; } - if (!initSel && templateDefault?.version && versions.includes(templateDefault.version)) { + if (templateDefault?.version && versions.includes(templateDefault.version)) { return templateDefault.version; } - if (!initSel && !templateDefault) { - return versions[0] ?? ''; - } - - return ''; + return versions[0] ?? ''; }; export const determineIsSelected = ( @@ -75,12 +71,17 @@ export const mapToComponentsListItem = ( selectedTemplate: ManagedControlPlaneTemplate | undefined, ): ComponentsListItem => { const rawVersions = Array.isArray(item.status?.versions) ? (item.status?.versions as string[]) : []; - const versions = sortVersions(rawVersions); + let versions = sortVersions(rawVersions); const name = item.metadata?.name ?? ''; const initSel = findInitialSelection(name, initialSelection); const templateDefault = findTemplateDefault(name, selectedTemplate); + // Add initial selection version if not in available versions + if (initSel?.version && !versions.includes(initSel.version)) { + versions = sortVersions([...rawVersions, initSel.version]); + } + const isSelected = determineIsSelected(initSel, templateDefault); const selectedVersion = determineSelectedVersion(versions, initSel, templateDefault); From bab2e66cee47e1e032013c9fe3c3d90dc07e1241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Tue, 23 Dec 2025 10:15:16 +0100 Subject: [PATCH 09/10] refactor --- .../useComponentsSelectionData.ts | 10 ++++++++++ src/lib/api/types/crate/createManagedControlPlane.ts | 5 +++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts index 3dc278ba..c41cb1d2 100644 --- a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts +++ b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts @@ -31,6 +31,14 @@ export const findInitialSelection = ( return initialSelection[name] ?? initialSelection[name.replace('provider-', '')]; }; +// Gets the original name from initial selection, considering 'provider-' prefix +export const findOriginalName = (name: string, initialSelection: InitialSelection | undefined): string | undefined => { + if (!initialSelection) return undefined; + if (initialSelection[name]) return name; + if (initialSelection[name.replace('provider-', '')]) return name.replace('provider-', ''); + return undefined; +}; + export const findTemplateDefault = ( name: string, selectedTemplate: ManagedControlPlaneTemplate | undefined, @@ -75,6 +83,7 @@ export const mapToComponentsListItem = ( const name = item.metadata?.name ?? ''; const initSel = findInitialSelection(name, initialSelection); + const templateDefault = findTemplateDefault(name, selectedTemplate); // Add initial selection version if not in available versions @@ -91,6 +100,7 @@ export const mapToComponentsListItem = ( selectedVersion, isSelected, documentationUrl: '', + originalName: findOriginalName(name, initialSelection), isProvider: isProviderComponent(name), }; }; diff --git a/src/lib/api/types/crate/createManagedControlPlane.ts b/src/lib/api/types/crate/createManagedControlPlane.ts index e1f957a6..26033424 100644 --- a/src/lib/api/types/crate/createManagedControlPlane.ts +++ b/src/lib/api/types/crate/createManagedControlPlane.ts @@ -14,6 +14,7 @@ export interface ComponentsListItem { selectedVersion: string; documentationUrl: string; isProvider: boolean; + originalName?: string; } interface RoleBinding { @@ -102,8 +103,8 @@ export const CreateManagedControlPlane = ( const selectedProviders: Provider[] = optional?.componentsList ?.filter(({ isSelected, isProvider }) => isProvider && isSelected) - .map(({ name, selectedVersion }) => ({ - name: name, + .map(({ name, selectedVersion, originalName }) => ({ + name: originalName ?? name, version: selectedVersion, })) ?? []; const crossplaneWithProviders = { From c61739ced1accaca5e816802b869fd0991e3490d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Goral?= Date: Tue, 23 Dec 2025 10:31:39 +0100 Subject: [PATCH 10/10] refactor --- .../useComponentsSelectionData.spec.ts | 37 +++++++++++++++++++ .../useComponentsSelectionData.ts | 10 ++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.spec.ts b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.spec.ts index b0797cab..9329a998 100644 --- a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.spec.ts +++ b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.spec.ts @@ -3,6 +3,7 @@ import { renderHook, waitFor } from '@testing-library/react'; import { isProviderComponent, findInitialSelection, + findOriginalName, determineSelectedVersion, mapToComponentsListItem, addCustomProviders, @@ -85,6 +86,42 @@ describe('findInitialSelection', () => { }); }); +describe('findOriginalName', () => { + it('returns undefined when initialSelection is undefined', () => { + expect(findOriginalName('crossplane', undefined)).toBeUndefined(); + }); + + it('returns the exact name when it exists in initialSelection', () => { + expect(findOriginalName('crossplane', sampleInitialSelection)).toBe('crossplane'); + expect(findOriginalName('flux', sampleInitialSelection)).toBe('flux'); + }); + + it('returns name without provider- prefix when that exists in initialSelection', () => { + expect(findOriginalName('provider-btp', sampleInitialSelection)).toBe('btp'); + expect(findOriginalName('provider-kubernetes', sampleInitialSelection)).toBe('kubernetes'); + }); + + it('returns undefined when name does not exist in initialSelection', () => { + expect(findOriginalName('non-existent', sampleInitialSelection)).toBeUndefined(); + expect(findOriginalName('provider-unknown', sampleInitialSelection)).toBeUndefined(); + }); + + it('returns exact name when both exact and prefixed versions could match', () => { + const selectionWithBoth: InitialSelection = { + 'provider-test': { isSelected: true, version: '1.0.0' }, + test: { isSelected: true, version: '2.0.0' }, + }; + // Exact match takes priority + expect(findOriginalName('provider-test', selectionWithBoth)).toBe('provider-test'); + }); + + it('handles names that do not start with provider- prefix', () => { + // When the name doesn't have provider- prefix, nameWithoutPrefix equals name + // so the second condition is skipped + expect(findOriginalName('crossplane', sampleInitialSelection)).toBe('crossplane'); + }); +}); + describe('determineSelectedVersion', () => { const versions = ['1.20.1', '1.19.0', '1.18.0']; diff --git a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts index c41cb1d2..413f752c 100644 --- a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts +++ b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts @@ -34,8 +34,14 @@ export const findInitialSelection = ( // Gets the original name from initial selection, considering 'provider-' prefix export const findOriginalName = (name: string, initialSelection: InitialSelection | undefined): string | undefined => { if (!initialSelection) return undefined; - if (initialSelection[name]) return name; - if (initialSelection[name.replace('provider-', '')]) return name.replace('provider-', ''); + + if (name in initialSelection) return name; + + const nameWithoutPrefix = name.replace('provider-', ''); + if (nameWithoutPrefix !== name && nameWithoutPrefix in initialSelection) { + return nameWithoutPrefix; + } + return undefined; };