diff --git a/src/components/ControlPlanes/List/ControlPlaneListWorkspaceGridTile.tsx b/src/components/ControlPlanes/List/ControlPlaneListWorkspaceGridTile.tsx index cf97ef31..e7039595 100644 --- a/src/components/ControlPlanes/List/ControlPlaneListWorkspaceGridTile.tsx +++ b/src/components/ControlPlanes/List/ControlPlaneListWorkspaceGridTile.tsx @@ -193,7 +193,6 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr projectName={projectNamespace} workspaceName={workspaceName} initialTemplateName={initialTemplateName} - isEditMode={false} /> ) : null} diff --git a/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.cy.tsx b/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.cy.tsx index 2b21da21..66a14930 100644 --- a/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.cy.tsx +++ b/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.cy.tsx @@ -1,17 +1,276 @@ import { CreateManagedControlPlaneWizardContainer } from './CreateManagedControlPlaneWizardContainer.tsx'; -import { useCreateManagedControlPlane } from '../../../hooks/useCreateManagedControlPlane.tsx'; +import { useCreateManagedControlPlane } from '../../../hooks/useCreateManagedControlPlane.ts'; import { CreateManagedControlPlaneType } from '../../../lib/api/types/crate/createManagedControlPlane.ts'; import { useAuthOnboarding } from '../../../spaces/onboarding/auth/AuthContextOnboarding.tsx'; +import { useComponentsQuery } from '../../../hooks/useComponentsQuery.ts'; +import '@ui5/webcomponents-cypress-commands'; +import { ManagedControlPlaneInterface } from '../../../lib/api/types/mcpResource.ts'; +import { useUpdateManagedControlPlane } from '../../../hooks/useUpdateManagedControlPlane.ts'; describe('CreateManagedControlPlaneWizardContainer', () => { let createMutationPayload: CreateManagedControlPlaneType | null = null; + let updateMutationPayload: CreateManagedControlPlaneType | null = null; + const fakeComponents = { + metadata: { + continue: '', + resourceVersion: '67156443', + }, + kind: 'ManagedComponentList', + items: [ + { + metadata: { + creationTimestamp: '2025-10-24T08:48:05Z', + generation: 1, + resourceVersion: '66348667', + managedFields: [ + { + fieldsType: 'FieldsV1', + manager: 'mcp-operator', + operation: 'Update', + fieldsV1: { + 'f:spec': {}, + }, + time: '2025-10-24T08:48:05Z', + apiVersion: 'core.openmcp.cloud/v1alpha1', + }, + { + operation: 'Update', + time: '2025-10-24T09:03:05Z', + fieldsType: 'FieldsV1', + subresource: 'status', + apiVersion: 'core.openmcp.cloud/v1alpha1', + fieldsV1: { + 'f:status': { + '.': {}, + 'f:versions': {}, + }, + }, + manager: 'mcp-operator', + }, + ], + name: 'crossplane', + uid: '9cb38a64-390b-4b98-a36a-b5cbba5344f1', + }, + spec: {}, + kind: 'ManagedComponent', + status: { + versions: [ + '1.15.0', + '1.15.5', + '1.16.0', + '1.16.1', + '1.16.2', + '1.17.0', + '1.17.1', + '1.17.2', + '1.17.3', + '1.18.0', + '1.18.1', + '1.18.2', + '1.18.3', + '1.19.0', + ], + }, + apiVersion: 'core.openmcp.cloud/v1alpha1', + }, + { + metadata: { + creationTimestamp: '2025-10-24T08:48:06Z', + generation: 1, + resourceVersion: '66348668', + managedFields: [ + { + fieldsType: 'FieldsV1', + manager: 'mcp-operator', + operation: 'Update', + fieldsV1: { + 'f:spec': {}, + }, + time: '2025-10-24T08:48:06Z', + apiVersion: 'core.openmcp.cloud/v1alpha1', + }, + { + operation: 'Update', + time: '2025-10-24T09:03:05Z', + fieldsType: 'FieldsV1', + subresource: 'status', + apiVersion: 'core.openmcp.cloud/v1alpha1', + fieldsV1: { + 'f:status': { + '.': {}, + 'f:versions': {}, + }, + }, + manager: 'mcp-operator', + }, + ], + name: 'external-secrets', + uid: '5eba35da-d213-4efd-9d58-2d1a6f08e400', + }, + spec: {}, + kind: 'ManagedComponent', + status: { + versions: [ + '0.10.7', + '0.11.0', + '0.12.1', + '0.13.0', + '0.14.4', + '0.15.1', + '0.16.2', + '0.17.0', + '0.18.2', + '0.19.2', + '0.20.1', + '0.8.0', + ], + }, + apiVersion: 'core.openmcp.cloud/v1alpha1', + }, + { + metadata: { + creationTimestamp: '2025-10-24T08:48:06Z', + generation: 1, + resourceVersion: '66348669', + managedFields: [ + { + fieldsType: 'FieldsV1', + manager: 'mcp-operator', + operation: 'Update', + fieldsV1: { + 'f:spec': {}, + }, + time: '2025-10-24T08:48:06Z', + apiVersion: 'core.openmcp.cloud/v1alpha1', + }, + { + operation: 'Update', + time: '2025-10-24T09:03:05Z', + fieldsType: 'FieldsV1', + subresource: 'status', + apiVersion: 'core.openmcp.cloud/v1alpha1', + fieldsV1: { + 'f:status': { + '.': {}, + 'f:versions': {}, + }, + }, + manager: 'mcp-operator', + }, + ], + name: 'flux', + uid: '68d7b8f4-651e-4d6b-b864-08194ab36862', + }, + spec: {}, + kind: 'ManagedComponent', + status: { + versions: ['2.15.0', '2.16.2'], + }, + apiVersion: 'core.openmcp.cloud/v1alpha1', + }, + { + metadata: { + creationTimestamp: '2025-10-24T08:48:07Z', + generation: 1, + resourceVersion: '66348674', + managedFields: [ + { + fieldsType: 'FieldsV1', + manager: 'mcp-operator', + operation: 'Update', + fieldsV1: { + 'f:spec': {}, + }, + time: '2025-10-24T08:48:07Z', + apiVersion: 'core.openmcp.cloud/v1alpha1', + }, + { + operation: 'Update', + time: '2025-10-24T09:03:06Z', + fieldsType: 'FieldsV1', + subresource: 'status', + apiVersion: 'core.openmcp.cloud/v1alpha1', + fieldsV1: { + 'f:status': { + '.': {}, + 'f:versions': {}, + }, + }, + manager: 'mcp-operator', + }, + ], + name: 'provider-btp', + uid: 'c889b1f7-8dfe-46f3-b8a5-9252587a2397', + }, + spec: {}, + kind: 'ManagedComponent', + status: { + versions: ['1.0.0', '1.0.1', '1.0.2', '1.0.3', '1.1.0', '1.1.1', '1.1.2', '1.2.0', '1.2.1', '1.2.2', '1.3.0'], + }, + apiVersion: 'core.openmcp.cloud/v1alpha1', + }, + { + metadata: { + creationTimestamp: '2025-10-24T08:48:07Z', + generation: 1, + resourceVersion: '66348675', + managedFields: [ + { + fieldsType: 'FieldsV1', + manager: 'mcp-operator', + operation: 'Update', + fieldsV1: { + 'f:spec': {}, + }, + time: '2025-10-24T08:48:07Z', + apiVersion: 'core.openmcp.cloud/v1alpha1', + }, + { + operation: 'Update', + time: '2025-10-24T09:03:06Z', + fieldsType: 'FieldsV1', + subresource: 'status', + apiVersion: 'core.openmcp.cloud/v1alpha1', + fieldsV1: { + 'f:status': { + '.': {}, + 'f:versions': {}, + }, + }, + manager: 'mcp-operator', + }, + ], + name: 'provider-btp-account', + uid: '96c96844-63da-4116-a2a3-35b2142cf765', + }, + spec: {}, + kind: 'ManagedComponent', + status: { + versions: ['0.7.5', '0.7.6'], + }, + apiVersion: 'core.openmcp.cloud/v1alpha1', + }, + ], + apiVersion: 'core.openmcp.cloud/v1alpha1', + }; const fakeUseCreateManagedControlPlane: typeof useCreateManagedControlPlane = () => ({ mutate: async (data: CreateManagedControlPlaneType): Promise => { createMutationPayload = data; return data; }, }); + const fakeUseUpdateManagedControlPlane: typeof useUpdateManagedControlPlane = () => ({ + mutate: async (data: CreateManagedControlPlaneType): Promise => { + updateMutationPayload = data; + return data; + }, + }); + const fakeUseComponentsQuery: typeof useComponentsQuery = () => ({ + components: fakeComponents, + error: undefined, + isLoading: false, + }); const fakeUseAuthOnboarding = (() => ({ user: { email: 'name@domain.com', @@ -22,11 +281,12 @@ describe('CreateManagedControlPlaneWizardContainer', () => { createMutationPayload = null; }); - it('creates a Managed Control Plane', () => { + it('creates an empty MCP', () => { cy.mount( {}} />, @@ -36,7 +296,7 @@ describe('CreateManagedControlPlaneWizardContainer', () => { apiVersion: 'core.openmcp.cloud/v1alpha1', kind: 'ManagedControlPlane', metadata: { - name: 'some-text', + name: 'mcp-empty', namespace: '--ws-', annotations: { 'openmcp.cloud/display-name': '', @@ -71,11 +331,373 @@ describe('CreateManagedControlPlaneWizardContainer', () => { }, }; - cy.get('#name').find(' input[id*="inner"]').type('some-text'); + cy.get('#name').typeIntoUi5Input('mcp-empty'); cy.get('ui5-button').contains('Next').click(); // navigate to Members cy.get('ui5-button').contains('Next').click(); // navigate to Component Selection cy.get('ui5-button').contains('Next').click(); // navigate to Summarize cy.get('ui5-button').contains('Create').click(); cy.then(() => cy.wrap(createMutationPayload).deepEqualJson(expMutationPayload)); }); + + it('creates an MCP with installed components, members, and optional fields', () => { + cy.mount( + {}} + />, + ); + + const expMutationPayload: CreateManagedControlPlaneType = { + apiVersion: 'core.openmcp.cloud/v1alpha1', + kind: 'ManagedControlPlane', + metadata: { + name: 'name', + namespace: '--ws-', + annotations: { + 'openmcp.cloud/display-name': 'displayName', + }, + labels: { + 'openmcp.cloud.sap/charging-target-type': 'BTP', + 'openmcp.cloud.sap/charging-target': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + }, + }, + spec: { + authentication: { + enableSystemIdentityProvider: true, + }, + components: { + apiServer: { + type: 'GardenerDedicated', + }, + flux: { + version: '2.15.0', + }, + crossplane: { + version: '1.19.0', + providers: [], + }, + }, + authorization: { + roleBindings: [ + { + role: 'admin', + subjects: [ + { + kind: 'User', + name: 'openmcp:name@domain.com', + }, + ], + }, + { + role: 'view', + subjects: [ + { + kind: 'User', + name: 'openmcp:additionalUser', + }, + ], + }, + ], + }, + }, + }; + + cy.get('#name').typeIntoUi5Input('name'); + cy.get('#displayName').typeIntoUi5Input('displayName'); + cy.get('#chargingTargetType').openDropDownByClick(); + cy.get('#chargingTargetType').clickDropdownMenuItemByText('BTP'); + cy.get('#chargingTarget').typeIntoUi5Input('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa').type('{enter}'); + + cy.get('ui5-button').contains('Next').click(); // navigate to Members + + cy.get('ui5-button').contains('Add User or ServiceAccount').click(); + cy.get('#member-email-input').typeIntoUi5Input('additionalUser'); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.press(Cypress.Keyboard.Keys.SPACE); // close Add Member dialog + + cy.get('ui5-button').contains('Next').click(); // navigate to Component Selection + + // Select Crossplane and Flux v2.15.0 + cy.get('[ui5-checkbox][aria-label*="crossplane"]').toggleUi5Checkbox(); + cy.get('[ui5-checkbox][aria-label*="flux"]').toggleUi5Checkbox(); + cy.get('[ui5-select][aria-label*="flux"]').openDropDownByClick(); + cy.get('[ui5-select][aria-label*="flux"]').clickDropdownMenuItemByText('2.15.0'); + cy.get('ui5-button').contains('Next').click(); // navigate to Summarize + cy.get('ui5-button').contains('Create').click(); + cy.then(() => cy.wrap(createMutationPayload).deepEqualJson(expMutationPayload)); + }); + + it('edits an existing MCP without any changes', () => { + const existingMcp: ManagedControlPlaneInterface = { + apiVersion: 'core.openmcp.cloud/v1alpha1', + kind: 'ManagedControlPlane', + metadata: { + name: 'existing-mcp', + namespace: 'project-existing-project--ws-existing-workspace', + annotations: { + 'openmcp.cloud/created-by': 'name@domain.com', + 'openmcp.cloud/display-name': 'displayName', + }, + labels: { + 'openmcp.cloud.sap/charging-target': 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + 'openmcp.cloud.sap/charging-target-type': 'BTP', + 'openmcp.cloud/mcp-project': 'existing-project', + 'openmcp.cloud/mcp-workspace': 'existing-workspace', + }, + }, + spec: { + authentication: { + enableSystemIdentityProvider: true, + }, + authorization: { + roleBindings: [ + { + role: 'admin', + subjects: [ + { + kind: 'User', + name: 'openmcp:name@domain.com', + }, + ], + }, + { + role: 'view', + subjects: [ + { + kind: 'User', + name: 'openmcp:additionalUser', + }, + ], + }, + ], + }, + components: { + apiServer: { + type: 'GardenerDedicated', + }, + flux: { + version: '2.15.0', + }, + crossplane: { + version: '1.19.0', + providers: [], + }, + }, + }, + }; + + const expMutationPayload: CreateManagedControlPlaneType = { + apiVersion: 'core.openmcp.cloud/v1alpha1', + kind: 'ManagedControlPlane', + metadata: { + name: 'existing-mcp', + namespace: '--ws-', + annotations: { + 'openmcp.cloud/display-name': 'displayName', + }, + labels: { + 'openmcp.cloud.sap/charging-target-type': 'btp', + 'openmcp.cloud.sap/charging-target': 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + }, + }, + spec: { + authentication: { + enableSystemIdentityProvider: true, + }, + components: { + apiServer: { + type: 'GardenerDedicated', + }, + flux: { + version: '2.15.0', + }, + crossplane: { + version: '1.19.0', + providers: [], + }, + }, + authorization: { + roleBindings: [ + { + role: 'admin', + subjects: [ + { + kind: 'User', + name: 'openmcp:name@domain.com', + }, + ], + }, + { + role: 'view', + subjects: [ + { + kind: 'User', + name: 'openmcp:additionalUser', + }, + ], + }, + ], + }, + }, + }; + + cy.mount( + {}} + />, + ); + + cy.get('ui5-button').contains('Next').click(); // navigate to Members + cy.get('ui5-button').contains('Next').click(); // navigate to Component Selection + cy.get('ui5-button').contains('Next').click(); // navigate to Summarize + cy.get('ui5-button').contains('Update').click(); + cy.then(() => cy.wrap(updateMutationPayload).deepEqualJson(expMutationPayload)); + }); + + it('edits an existing MCP with changes', () => { + const existingMcp: ManagedControlPlaneInterface = { + apiVersion: 'core.openmcp.cloud/v1alpha1', + kind: 'ManagedControlPlane', + metadata: { + name: 'existing-mcp', + namespace: 'project-existing-project--ws-existing-workspace', + annotations: { + 'openmcp.cloud/created-by': 'name@domain.com', + 'openmcp.cloud/display-name': '', + }, + labels: { + 'openmcp.cloud.sap/charging-target': '', + 'openmcp.cloud.sap/charging-target-type': '', + 'openmcp.cloud/mcp-project': 'existing-project', + 'openmcp.cloud/mcp-workspace': 'existing-workspace', + }, + }, + spec: { + authentication: { + enableSystemIdentityProvider: true, + }, + authorization: { + roleBindings: [ + { + role: 'admin', + subjects: [ + { + kind: 'User', + name: 'openmcp:name@domain.com', + }, + ], + }, + ], + }, + components: { + apiServer: { + type: 'GardenerDedicated', + }, + }, + }, + }; + + const expMutationPayload: CreateManagedControlPlaneType = { + apiVersion: 'core.openmcp.cloud/v1alpha1', + kind: 'ManagedControlPlane', + metadata: { + name: 'existing-mcp', + namespace: '--ws-', + annotations: { + 'openmcp.cloud/display-name': 'displayName', + }, + labels: { + 'openmcp.cloud.sap/charging-target-type': 'btp', + 'openmcp.cloud.sap/charging-target': 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', + }, + }, + spec: { + authentication: { + enableSystemIdentityProvider: true, + }, + components: { + apiServer: { + type: 'GardenerDedicated', + }, + flux: { + version: '2.15.0', + }, + crossplane: { + version: '1.19.0', + providers: [], + }, + }, + authorization: { + roleBindings: [ + { + role: 'admin', + subjects: [ + { + kind: 'User', + name: 'openmcp:name@domain.com', + }, + ], + }, + { + role: 'view', + subjects: [ + { + kind: 'User', + name: 'openmcp:additionalUser', + }, + ], + }, + ], + }, + }, + }; + + cy.mount( + {}} + />, + ); + + cy.get('#displayName').typeIntoUi5Input('displayName'); + cy.get('#chargingTargetType').openDropDownByClick(); + cy.get('#chargingTargetType').clickDropdownMenuItemByText('BTP'); + cy.get('#chargingTarget').typeIntoUi5Input('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb').type('{enter}'); + + cy.get('ui5-button').contains('Next').click(); // navigate to Members + + cy.get('ui5-button').contains('Add User or ServiceAccount').click(); + cy.get('#member-email-input').typeIntoUi5Input('additionalUser'); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.press(Cypress.Keyboard.Keys.TAB); + cy.press(Cypress.Keyboard.Keys.SPACE); // close Add Member dialog + + cy.get('ui5-button').contains('Next').click(); // navigate to Component Selection + + // Select Crossplane and Flux v2.15.0 + cy.get('[ui5-checkbox][aria-label*="crossplane"]').toggleUi5Checkbox(); + cy.get('[ui5-checkbox][aria-label*="flux"]').toggleUi5Checkbox(); + cy.get('[ui5-select][aria-label*="flux"]').openDropDownByClick(); + cy.get('[ui5-select][aria-label*="flux"]').clickDropdownMenuItemByText('2.15.0'); + cy.get('ui5-button').contains('Next').click(); // navigate to Summarize + cy.get('ui5-button').contains('Update').click(); + cy.then(() => cy.wrap(updateMutationPayload).deepEqualJson(expMutationPayload)); + }); }); diff --git a/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx b/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx index 2306e02d..aecfdff6 100644 --- a/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx +++ b/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx @@ -33,7 +33,6 @@ import { CreateManagedControlPlane, CreateManagedControlPlaneResource, CreateManagedControlPlaneType, - UpdateManagedControlPlaneResource, replaceComponentsName, } from '../../../lib/api/types/crate/createManagedControlPlane.ts'; import { @@ -62,7 +61,9 @@ import { stringify } from 'yaml'; import { useComponentsSelectionData } from './useComponentsSelectionData.ts'; import { Infobox } from '../../Ui/Infobox/Infobox.tsx'; import styles from './CreateManagedControlPlaneWizardContainer.module.css'; -import { useCreateManagedControlPlane as _useCreateManagedControlPlane } from '../../../hooks/useCreateManagedControlPlane.tsx'; +import { useCreateManagedControlPlane as _useCreateManagedControlPlane } from '../../../hooks/useCreateManagedControlPlane.ts'; +import { useUpdateManagedControlPlane as _useUpdateManagedControlPlane } from '../../../hooks/useUpdateManagedControlPlane.ts'; +import { useComponentsQuery as _useComponentsQuery } from '../../../hooks/useComponentsQuery.ts'; // Remap MCP components keys from internal replaceName back to originalName using replaceComponentsName mapping const remapComponentsKeysToOriginalNames = (components: MCPComponentsSpec = {}): MCPComponentsSpec => { @@ -83,10 +84,11 @@ type CreateManagedControlPlaneWizardContainerProps = { isDuplicateMode?: boolean; initialTemplateName?: string; initialData?: ManagedControlPlaneInterface; - isOnMcpPage?: boolean; initialSection?: WizardStepType; useCreateManagedControlPlane?: typeof _useCreateManagedControlPlane; + useUpdateManagedControlPlane?: typeof _useUpdateManagedControlPlane; useAuthOnboarding?: typeof _useAuthOnboarding; + useComponentsQuery?: typeof _useComponentsQuery; }; export type WizardStepType = 'metadata' | 'members' | 'componentSelection' | 'summarize' | 'success'; @@ -102,10 +104,11 @@ export const CreateManagedControlPlaneWizardContainer: FC { const { t } = useTranslation(); const { user } = useAuthOnboarding(); @@ -233,10 +236,10 @@ export const CreateManagedControlPlaneWizardContainer: FC( - UpdateManagedControlPlaneResource(projectName, workspaceName, initialData?.metadata?.name ?? ''), - undefined, - isOnMcpPage, + const { mutate: updateManagedControlPlane } = useUpdateManagedControlPlane( + projectName, + workspaceName, + initialData?.metadata?.name ?? '', ); const componentsList = watch('componentsList'); const hasMissingComponentVersions = useMemo(() => { @@ -252,7 +255,7 @@ export const CreateManagedControlPlaneWizardContainer: FC setValue(name, value, options), (components) => @@ -517,6 +519,7 @@ export const CreateManagedControlPlaneWizardContainer: FC void; - isOnMcpPage?: boolean; initialSection?: WizardStepType; mode?: 'edit' | 'duplicate'; }; @@ -27,7 +26,6 @@ export const EditManagedControlPlaneWizardDataLoader: FC { @@ -60,7 +58,6 @@ export const EditManagedControlPlaneWizardDataLoader: FC ) : null} diff --git a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts index f7d4f35e..5f21002a 100644 --- a/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts +++ b/src/components/Wizards/CreateManagedControlPlane/useComponentsSelectionData.ts @@ -1,9 +1,8 @@ import { useEffect, useState } from 'react'; import { ManagedControlPlaneTemplate } from '../../../lib/api/types/templates/mcpTemplate.ts'; import { ComponentsListItem, removeComponents } from '../../../lib/api/types/crate/createManagedControlPlane.ts'; -import { useApiResource } from '../../../lib/api/useApiResource.ts'; -import { ListManagedComponents } from '../../../lib/api/types/crate/listManagedComponents.ts'; import { sortVersions } from '../../../utils/componentsVersions.ts'; +import { useComponentsQuery as _useComponentsQuery } from '../../../hooks/useComponentsQuery.ts'; export type ComponentsHookResult = { isLoading: boolean; @@ -14,11 +13,11 @@ export type ComponentsHookResult = { export const useComponentsSelectionData = ( selectedTemplate: ManagedControlPlaneTemplate | undefined, initialSelection: Record | undefined, - isOnMcpPage: boolean, setValue: (name: 'componentsList', value: ComponentsListItem[], options?: { shouldValidate?: boolean }) => void, onComponentsInitialized?: (components: ComponentsListItem[]) => void, + useComponentsQuery: typeof _useComponentsQuery = _useComponentsQuery, ): ComponentsHookResult => { - const { data, error, isLoading } = useApiResource(ListManagedComponents(), undefined, !!isOnMcpPage); + const { components: data, error, isLoading } = useComponentsQuery(); useEffect(() => { const items = data?.items ?? []; diff --git a/src/hooks/useComponentsQuery.ts b/src/hooks/useComponentsQuery.ts new file mode 100644 index 00000000..9c39834c --- /dev/null +++ b/src/hooks/useComponentsQuery.ts @@ -0,0 +1,14 @@ +import { useApiResource } from '../lib/api/useApiResource.ts'; +import { APIError } from '../lib/api/error.ts'; +import { ListManagedComponents, ManagedComponentList } from '../lib/api/types/crate/listManagedComponents.ts'; + +export interface GetComponentsHookResult { + components: ManagedComponentList | undefined; + error: APIError | undefined; + isLoading: boolean; +} +export function useComponentsQuery(): GetComponentsHookResult { + const { data: components, error, isLoading } = useApiResource(ListManagedComponents(), undefined, true); + + return { components, error, isLoading }; +} diff --git a/src/hooks/useCreateManagedControlPlane.spec.ts b/src/hooks/useCreateManagedControlPlane.spec.ts index fdc67cbb..8ce1063e 100644 --- a/src/hooks/useCreateManagedControlPlane.spec.ts +++ b/src/hooks/useCreateManagedControlPlane.spec.ts @@ -1,5 +1,5 @@ import { act, renderHook } from '@testing-library/react'; -import { useCreateManagedControlPlane } from './useCreateManagedControlPlane.tsx'; +import { useCreateManagedControlPlane } from './useCreateManagedControlPlane.ts'; import { CreateManagedControlPlaneType } from '../lib/api/types/crate/createManagedControlPlane.ts'; import { describe, it, expect, vi, afterEach, Mock } from 'vitest'; diff --git a/src/hooks/useCreateManagedControlPlane.tsx b/src/hooks/useCreateManagedControlPlane.ts similarity index 100% rename from src/hooks/useCreateManagedControlPlane.tsx rename to src/hooks/useCreateManagedControlPlane.ts diff --git a/src/hooks/useUpdateManagedControlPlane.spec.ts b/src/hooks/useUpdateManagedControlPlane.spec.ts new file mode 100644 index 00000000..525ebae8 --- /dev/null +++ b/src/hooks/useUpdateManagedControlPlane.spec.ts @@ -0,0 +1,116 @@ +import { act, renderHook } from '@testing-library/react'; +import { describe, it, expect, vi, afterEach, Mock } from 'vitest'; +import { assertNonNullish, assertString } from '../utils/test/vitest-utils.ts'; + +import { useUpdateManagedControlPlane } from './useUpdateManagedControlPlane.ts'; +import { CreateManagedControlPlaneType } from '../lib/api/types/crate/createManagedControlPlane.ts'; + +describe('useUpdateManagedControlPlane', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should perform a valid update request', async () => { + // ARRANGE + const mockData: CreateManagedControlPlaneType = { + apiVersion: 'core.openmcp.cloud/v1alpha1', + kind: 'ManagedControlPlane', + metadata: { + name: 'name', + namespace: 'project-projectName--ws-workspaceName', + annotations: { + 'openmcp.cloud/display-name': 'display-name', + }, + labels: { + 'openmcp.cloud.sap/charging-target-type': 'BTP', + 'openmcp.cloud.sap/charging-target': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + }, + }, + spec: { + authentication: { + enableSystemIdentityProvider: true, + }, + components: { + externalSecretsOperator: { + version: '0.20.1', + }, + flux: { + version: '2.16.2', + }, + kyverno: { + version: '3.5.2', + }, + btpServiceOperator: { + version: '0.9.2', + }, + apiServer: { + type: 'GardenerDedicated', + }, + crossplane: { + version: '1.19.0', + providers: [ + { + name: 'provider-hana', + version: '0.2.0', + }, + ], + }, + }, + authorization: { + roleBindings: [ + { + role: 'admin', + subjects: [ + { + kind: 'User', + name: 'openmcp:user@domain.com', + }, + ], + }, + ], + }, + }, + }; + + const fetchMock: Mock = vi.fn(); + fetchMock.mockResolvedValue({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValue({}), + } as unknown as Response); + global.fetch = fetchMock; + + // ACT + const renderHookResult = renderHook(() => useUpdateManagedControlPlane('projectName', 'workspaceName', 'mcpName')); + const { mutate: update } = renderHookResult.result.current; + + await act(async () => { + await update(mockData); + }); + + // ASSERT + expect(fetchMock).toHaveBeenCalledTimes(1); + const call = fetchMock.mock.calls[0]; + const [url, init] = call; + assertNonNullish(init); + const { method, headers, body } = init; + + // URL should include namespace and the specific resource name + expect(url).toContain( + '/api/onboarding/apis/core.openmcp.cloud/v1alpha1/namespaces/projectName--ws-workspaceName/managedcontrolplanes/mcpName', + ); + + expect(method).toBe('PATCH'); + + expect(headers).toEqual( + expect.objectContaining({ + 'Content-Type': 'application/merge-patch+json', + 'X-use-crate': 'true', + }), + ); + + assertString(body); + const parsedBody = JSON.parse(body); + expect(parsedBody).toEqual(mockData); + }); +}); diff --git a/src/hooks/useUpdateManagedControlPlane.ts b/src/hooks/useUpdateManagedControlPlane.ts new file mode 100644 index 00000000..3d3ffc82 --- /dev/null +++ b/src/hooks/useUpdateManagedControlPlane.ts @@ -0,0 +1,19 @@ +import { + UpdateManagedControlPlaneResource, + CreateManagedControlPlaneType, +} from '../lib/api/types/crate/createManagedControlPlane.ts'; +import { useApiResourceMutation } from '../lib/api/useApiResource.ts'; + +export function useUpdateManagedControlPlane(projectName: string, workspaceName: string, mcpName: string) { + const { trigger } = useApiResourceMutation( + UpdateManagedControlPlaneResource(projectName, workspaceName, mcpName), + undefined, + true, + ); + + const mutate = async (data: CreateManagedControlPlaneType) => { + return trigger(data); + }; + + return { mutate }; +} diff --git a/src/spaces/mcp/pages/McpPage.tsx b/src/spaces/mcp/pages/McpPage.tsx index 9a82d2b2..b33e3bde 100644 --- a/src/spaces/mcp/pages/McpPage.tsx +++ b/src/spaces/mcp/pages/McpPage.tsx @@ -126,7 +126,6 @@ export default function McpPage() { setIsOpen={handleEditManagedControlPlaneWizardClose} workspaceName={mcp?.status?.access?.namespace} resourceName={controlPlaneName} - isOnMcpPage initialSection={editManagedControlPlaneWizardSection} />