-
- {children}
-
+
+ {iconName &&
}
+
{children}
);
};
diff --git a/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.module.css b/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.module.css
new file mode 100644
index 00000000..43aba004
--- /dev/null
+++ b/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.module.css
@@ -0,0 +1,15 @@
+.footer {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.metadataForm {
+ width: 50%;
+}
+
+.infoboxContainer {
+ width: 50%;
+ padding-right: 1rem;
+ padding-top: 2rem;
+}
diff --git a/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx b/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx
index 45744a5e..dbcc2057 100644
--- a/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx
+++ b/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx
@@ -10,16 +10,18 @@ import {
Bar,
Button,
Dialog,
+ FlexBox,
Form,
FormGroup,
Ui5CustomEvent,
Wizard,
WizardDomRef,
WizardStep,
+ Text,
} from '@ui5/webcomponents-react';
import { SummarizeStep } from './SummarizeStep.tsx';
-import { useTranslation } from 'react-i18next';
+import { Trans, useTranslation } from 'react-i18next';
import { useAuthOnboarding } from '../../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
import { ErrorDialog, ErrorDialogHandle } from '../../Shared/ErrorMessageBox.tsx';
import { CreateDialogProps } from '../../Dialogs/CreateWorkspaceDialogContainer.tsx';
@@ -56,19 +58,24 @@ import {
MCPSubject,
} from '../../../lib/api/types/mcpResource.ts';
import { stringify } from 'yaml';
+import { useComponentsSelectionData } from './useComponentsSelectionData.ts';
+import { Infobox } from '../../Ui/Infobox/Infobox.tsx';
+import styles from './CreateManagedControlPlaneWizardContainer.module.css';
type CreateManagedControlPlaneWizardContainerProps = {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
projectName?: string;
workspaceName?: string;
- isEditMode: boolean;
+ isEditMode?: boolean;
+ isDuplicateMode?: boolean;
initialTemplateName?: string;
initialData?: ManagedControlPlaneInterface;
isOnMcpPage?: boolean;
+ initialSection?: WizardStepType;
};
-type WizardStepType = 'metadata' | 'members' | 'componentSelection' | 'summarize' | 'success';
+export type WizardStepType = 'metadata' | 'members' | 'componentSelection' | 'summarize' | 'success';
const wizardStepOrder: WizardStepType[] = ['metadata', 'members', 'componentSelection', 'summarize', 'success'];
@@ -78,19 +85,27 @@ export const CreateManagedControlPlaneWizardContainer: FC
{
const { t } = useTranslation();
const { user } = useAuthOnboarding();
const errorDialogRef = useRef(null);
-
- const [selectedStep, setSelectedStep] = useState('metadata');
+ const [selectedStep, setSelectedStep] = useState(initialSection ?? 'metadata');
const [metadataFormKey, setMetadataFormKey] = useState(0);
const normalizeChargingTargetType = useCallback((val?: string | null) => (val ?? '').trim().toLowerCase(), []);
-
+ const [initialMcpDataWhenInEditMode, setInitialMcpDataWhenInEditMode] = useState({
+ name: '',
+ displayName: '',
+ chargingTarget: '',
+ chargingTargetType: '',
+ members: [],
+ componentsList: [],
+ });
// Here we will use OnboardingAPI to get all available templates
const templates = useMemo(() => [], []);
@@ -124,7 +139,6 @@ export const CreateManagedControlPlaneWizardContainer: FC createManagedControlPlaneSchema(t), [t]);
- const initializedComponents = useRef(false);
const {
register,
handleSubmit,
@@ -146,14 +160,7 @@ export const CreateManagedControlPlaneWizardContainer: FC({
- name: '',
- displayName: '',
- chargingTarget: '',
- chargingTargetType: '',
- members: [],
- componentsList: [],
- });
+
useEffect(() => {
if (selectedStep !== 'metadata') return;
@@ -215,7 +222,6 @@ export const CreateManagedControlPlaneWizardContainer: FC {
const list = (componentsList ?? []) as ComponentsListItem[];
return list.some(({ isSelected, selectedVersion }) => isSelected && !selectedVersion);
@@ -330,7 +336,7 @@ export const CreateManagedControlPlaneWizardContainer: FC {
- setValue('componentsList', components, { shouldValidate: false });
+ setValue('componentsList', components, { shouldValidate: true });
},
[setValue],
);
@@ -341,16 +347,17 @@ export const CreateManagedControlPlaneWizardContainer: FC {
@@ -368,9 +375,9 @@ export const CreateManagedControlPlaneWizardContainer: FC {
- if (!isEditMode) return undefined;
+ if (!isEditMode && !isDuplicateMode) return undefined;
const selection: Record = {};
const componentsMap: MCPComponentsSpec = initialData?.spec.components ?? {};
(Object.keys(componentsMap) as (keyof MCPComponentsSpec)[]).forEach((key) => {
@@ -390,12 +397,11 @@ export const CreateManagedControlPlaneWizardContainer: FC {
- if (!isOpen || !isEditMode) return;
+ if (!isOpen || !initialData) return;
const roleBindings = initialData?.spec?.authorization?.roleBindings ?? [];
const members: Member[] = roleBindings.flatMap((rb) =>
(rb.subjects ?? []).map((s: MCPSubject) => ({
@@ -405,10 +411,11 @@ export const CreateManagedControlPlaneWizardContainer: FC) ?? {};
const annotations = (initialData?.metadata?.annotations as unknown as Record) ?? {};
const data = {
- name: initialData?.metadata?.name ?? '',
+ name: isDuplicateMode && !!name ? `${name}${t('createMCP.copySuffix')}` : name,
displayName: annotations?.[DISPLAY_NAME_ANNOTATION] ?? '',
chargingTarget: labels?.[CHARGING_TARGET_LABEL] ?? '',
chargingTargetType: labels?.[CHARGING_TARGET_TYPE_LABEL]?.toLowerCase() ?? '',
@@ -416,10 +423,9 @@ export const CreateManagedControlPlaneWizardContainer: FC {
const normalizedKind = (kindInput ?? '').toString().trim().toLowerCase();
return normalizedKind === 'serviceaccount' ? 'ServiceAccount' : 'User';
@@ -476,42 +482,23 @@ export const CreateManagedControlPlaneWizardContainer: FC {
- if (!isEditMode) return;
- setInitialMcpDataWhenInEditMode({ ...initialMcpDataWhenInEditMode, componentsList: components });
- };
- useEffect(() => {
- if (selectedStep !== 'componentSelection') return;
- if (!selectedTemplate) return;
- if (appliedTemplateComponentsRef.current) return;
-
- const defaults = (selectedTemplate?.spec?.spec?.components?.defaultComponents ??
- []) as ManagedControlPlaneTemplate['spec']['spec']['components']['defaultComponents'];
- if (!defaults?.length) {
- appliedTemplateComponentsRef.current = true;
- return;
- }
-
- const current = (watch('componentsList') ?? []) as ComponentsListItem[];
- if (current.length > 0) {
- appliedTemplateComponentsRef.current = true;
- return;
- }
-
- const mapped = defaults
- .filter((c) => !!c?.name && !!c?.version)
- .map((c) => ({
- name: String(c.name),
- version: String(c.version),
- selectedVersion: String(c.version),
- selected: true,
- removable: Boolean(c.removable),
- versionChangeable: Boolean(c.versionChangeable),
- })) as unknown as ComponentsListItem[];
-
- setValue('componentsList', mapped, { shouldValidate: false });
- appliedTemplateComponentsRef.current = true;
- }, [selectedStep, selectedTemplate, watch, setValue]);
+ const {
+ isLoading: componentsLoading,
+ error: componentsError,
+ templateDefaultsError,
+ } = useComponentsSelectionData(
+ selectedTemplate,
+ initialSelection,
+ isOnMcpPage,
+ (name: 'componentsList', value: ComponentsListItem[], options?: { shouldValidate?: boolean }) =>
+ setValue(name, value, options),
+ (components) =>
+ setInitialMcpDataWhenInEditMode((prev) => ({
+ ...prev,
+ componentsList: components,
+ })),
+ );
+ // Template application for components is handled inside the hook
if (!isOpen) return null;
@@ -526,7 +513,7 @@ export const CreateManagedControlPlaneWizardContainer: FC
+
{selectedStep !== 'metadata' && isEditMode && (
{t('buttons.close')}
)}
@@ -555,19 +542,38 @@ export const CreateManagedControlPlaneWizardContainer: FC
-
+
+
+
+
+ {isDuplicateMode && (
+
+
+
+ }}
+ />
+
+
+ }} />
+
+
+
+ )}
+
)}
diff --git a/src/components/Wizards/CreateManagedControlPlane/EditManagedControlPlaneWizardDataLoader.tsx b/src/components/Wizards/CreateManagedControlPlane/EditManagedControlPlaneWizardDataLoader.tsx
index 3c973f9e..63eb373f 100644
--- a/src/components/Wizards/CreateManagedControlPlane/EditManagedControlPlaneWizardDataLoader.tsx
+++ b/src/components/Wizards/CreateManagedControlPlane/EditManagedControlPlaneWizardDataLoader.tsx
@@ -3,7 +3,10 @@ import { useApiResource } from '../../../lib/api/useApiResource.ts';
import { ResourceObject } from '../../../lib/api/types/crate/resourceObject.ts';
import styles from './EditManagedControlPlaneWizardDataLoader.module.css';
-import { CreateManagedControlPlaneWizardContainer } from './CreateManagedControlPlaneWizardContainer.tsx';
+import {
+ CreateManagedControlPlaneWizardContainer,
+ WizardStepType,
+} from './CreateManagedControlPlaneWizardContainer.tsx';
import { PROJECT_NAME_LABEL, WORKSPACE_LABEL } from '../../../lib/api/types/shared/keyNames.ts';
import { BusyIndicator } from '@ui5/webcomponents-react';
@@ -15,6 +18,8 @@ export type EditManagedControlPlaneWizardDataLoaderProps = {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
isOnMcpPage?: boolean;
+ initialSection?: WizardStepType;
+ mode?: 'edit' | 'duplicate';
};
export const EditManagedControlPlaneWizardDataLoader: FC = ({
@@ -23,6 +28,8 @@ export const EditManagedControlPlaneWizardDataLoader: FC {
const { isLoading, data, error } = useApiResource(
ResourceObject(workspaceName ?? '', 'managedcontrolplanes', resourceName),
@@ -50,9 +57,11 @@ export const EditManagedControlPlaneWizardDataLoader: FC
) : null}
>
diff --git a/src/components/Wizards/CreateManagedControlPlane/SummarizeStep.tsx b/src/components/Wizards/CreateManagedControlPlane/SummarizeStep.tsx
index 4bf6d4ae..c4c7ae98 100644
--- a/src/components/Wizards/CreateManagedControlPlane/SummarizeStep.tsx
+++ b/src/components/Wizards/CreateManagedControlPlane/SummarizeStep.tsx
@@ -79,7 +79,6 @@ export const SummarizeStep: React.FC = ({
/>
) : (
| undefined,
+ isOnMcpPage: boolean,
+ setValue: (name: 'componentsList', value: ComponentsListItem[], options?: { shouldValidate?: boolean }) => void,
+ onComponentsInitialized?: (components: ComponentsListItem[]) => void,
+): ComponentsHookResult => {
+ const { data, error, isLoading } = useApiResource(ListManagedComponents(), undefined, !!isOnMcpPage);
+
+ 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];
+ 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: '',
+ } as ComponentsListItem;
+ })
+ .filter((component) => !removeComponents.find((item) => item === component.name));
+
+ setValue('componentsList', newComponentsList, { shouldValidate: false });
+ if (onComponentsInitialized) {
+ onComponentsInitialized(newComponentsList);
+ }
+ // 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);
+ }, [data, selectedTemplate]);
+
+ return { isLoading: Boolean(isLoading), error, templateDefaultsError: defaultsError };
+};
diff --git a/src/spaces/mcp/pages/McpPage.module.css b/src/spaces/mcp/pages/McpPage.module.css
index 4d46900a..f6529c07 100644
--- a/src/spaces/mcp/pages/McpPage.module.css
+++ b/src/spaces/mcp/pages/McpPage.module.css
@@ -3,3 +3,14 @@
margin: 0.1em auto -8px auto;
width: 100%;
}
+
+.actionsBar {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ gap: 0.5rem;
+}
+
+.panelHeader {
+ width: 100%;
+}
diff --git a/src/spaces/mcp/pages/McpPage.tsx b/src/spaces/mcp/pages/McpPage.tsx
index 0fbaa19d..fbf3248f 100644
--- a/src/spaces/mcp/pages/McpPage.tsx
+++ b/src/spaces/mcp/pages/McpPage.tsx
@@ -1,4 +1,13 @@
-import { BusyIndicator, ObjectPage, ObjectPageSection, ObjectPageTitle, Panel, Title } from '@ui5/webcomponents-react';
+import {
+ BusyIndicator,
+ Button,
+ FlexBox,
+ ObjectPage,
+ ObjectPageSection,
+ ObjectPageTitle,
+ Panel,
+ Title,
+} from '@ui5/webcomponents-react';
import { useParams } from 'react-router-dom';
import CopyKubeconfigButton from '../../../components/ControlPlanes/CopyKubeconfigButton.tsx';
import styles from './McpPage.module.css';
@@ -32,17 +41,32 @@ import { useState } from 'react';
import { EditManagedControlPlaneWizardDataLoader } from '../../../components/Wizards/CreateManagedControlPlane/EditManagedControlPlaneWizardDataLoader.tsx';
import { ControlPlanePageMenu } from '../../../components/ControlPlanes/ControlPlanePageMenu.tsx';
import { DISPLAY_NAME_ANNOTATION } from '../../../lib/api/types/shared/keyNames.ts';
+import { WizardStepType } from '../../../components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx';
export default function McpPage() {
const { projectName, workspaceName, controlPlaneName } = useParams();
const { t } = useTranslation();
const [isEditManagedControlPlaneWizardOpen, setIsEditManagedControlPlaneWizardOpen] = useState(false);
+ const [editManagedControlPlaneWizardSection, setEditManagedControlPlaneWizardSection] = useState<
+ undefined | WizardStepType
+ >(undefined);
const {
data: mcp,
error,
isLoading,
} = useApiResource(ControlPlaneResource(projectName, workspaceName, controlPlaneName));
- const displayName = mcp?.metadata?.annotations?.[DISPLAY_NAME_ANNOTATION];
+ const displayName =
+ mcp?.metadata?.annotations && typeof mcp.metadata.annotations === 'object'
+ ? (mcp.metadata.annotations as Record)[DISPLAY_NAME_ANNOTATION]
+ : undefined;
+ const onEditComponents = () => {
+ setEditManagedControlPlaneWizardSection('componentSelection');
+ setIsEditManagedControlPlaneWizardOpen(true);
+ };
+ const handleEditManagedControlPlaneWizardClose = () => {
+ setIsEditManagedControlPlaneWizardOpen(false);
+ setEditManagedControlPlaneWizardSection(undefined);
+ };
if (isLoading) {
return ;
}
@@ -74,14 +98,7 @@ export default function McpPage() {
breadcrumbs={ }
//TODO: actionBar should use Toolbar and ToolbarButton for consistent design
actionsBar={
-
+
}
@@ -135,7 +153,12 @@ export default function McpPage() {
className={styles.panel}
headerLevel="H2"
headerText="Panel"
- header={
{t('McpPage.componentsTitle')} }
+ header={
+
+ {t('McpPage.componentsTitle')} {' '}
+
+
+ }
noAnimation
>