diff --git a/public/locales/en.json b/public/locales/en.json index 1db60e95..69abf57c 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -59,8 +59,9 @@ }, "ControlPlaneCard": { "deleteConfirmationDialog": "MCP deletion triggered. The list will refresh automatically once completed.", - "editMCP": "Edit Managed Control Plane", - "deleteMCP": "Delete Managed Control Plane" + "editMCP": "Edit ManagedControlPlane", + "duplicateMCP": "Duplicate ManagedControlPlane", + "deleteMCP": "Delete ManagedControlPlane" }, "ControlPlaneListAllWorkspaces": { @@ -375,18 +376,23 @@ "createMCP": { "dialogTitle": "Create Managed Control Plane", "titleText": "Managed Control Plane Created Successfully!", - "subtitleText": "Your Managed Control Plane is being set up. It will be ready to use in just a few minutes. You can safely close this window." + "subtitleText": "Your Managed Control Plane is being set up. It will be ready to use in just a few minutes. You can safely close this window.", + "copySuffix": "-copy" }, "editMCP": { "dialogTitle": "Edit Managed Control Plane", "titleText": "Managed Control Plane Updated Successfully!", - "subtitleText": "Your Managed Control Plane is being updated. It will be ready to use in just a few minutes. You can safely close this window." + "subtitleText": "Your Managed Control Plane is being updated. It will be ready to use in just a few minutes. You can safely close this window.", + "editComponents": "Edit components", + "duplicatingMCPInfo1": "Duplicating a ManagedControlPlane will only create a ManagedControlPlane with the same configuration. ", + "duplicatingMCPInfo2": "It will NOT copy the managed resources inside" }, "componentsSelection": { "selectComponents": "Select Components", "selectedComponents": "Selected Components", "pleaseSelectComponents": "Choose the components you want to add to your Managed Control Plane.", - "cannotLoad": "Cannot load components list" + "cannotLoad": "Cannot load components list", + "noComponentsFound": "No components found matching your search." }, "Hints": { "CrossplaneHint": { diff --git a/src/components/ComponentsSelection/ComponentsSelection.tsx b/src/components/ComponentsSelection/ComponentsSelection.tsx index b08fbbec..fbe4ca3f 100644 --- a/src/components/ComponentsSelection/ComponentsSelection.tsx +++ b/src/components/ComponentsSelection/ComponentsSelection.tsx @@ -101,7 +101,7 @@ export const ComponentsSelection: React.FC = ({ /> -
+
{searchResults.length > 0 ? ( searchResults.map((component) => { const providerDisabled = isProviderDisabled(component); @@ -167,13 +167,13 @@ export const ComponentsSelection: React.FC = ({ ); }) ) : ( - - {t('componentsSelection.pleaseSelectComponents')} + + {t('componentsSelection.noComponentsFound')} )}
-
+
{templateDefaultsError ? (
@@ -191,7 +191,7 @@ export const ComponentsSelection: React.FC = ({ ))} ) : ( - + {t('componentsSelection.pleaseSelectComponents')} )} diff --git a/src/components/ComponentsSelection/ComponentsSelectionContainer.tsx b/src/components/ComponentsSelection/ComponentsSelectionContainer.tsx index 16aeb250..9d6a3435 100644 --- a/src/components/ComponentsSelection/ComponentsSelectionContainer.tsx +++ b/src/components/ComponentsSelection/ComponentsSelectionContainer.tsx @@ -1,24 +1,16 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React from 'react'; import { ComponentsSelection } from './ComponentsSelection.tsx'; - import IllustratedError from '../Shared/IllustratedError.tsx'; -import { sortVersions } from '../../utils/componentsVersions.ts'; - -import { ListManagedComponents } from '../../lib/api/types/crate/listManagedComponents.ts'; -import { useApiResource } from '../../lib/api/useApiResource.ts'; import Loading from '../Shared/Loading.tsx'; -import { ComponentsListItem, removeComponents } from '../../lib/api/types/crate/createManagedControlPlane.ts'; +import { ComponentsListItem } from '../../lib/api/types/crate/createManagedControlPlane.ts'; import { useTranslation } from 'react-i18next'; -import { ManagedControlPlaneTemplate } from '../../lib/api/types/templates/mcpTemplate.ts'; export interface ComponentsSelectionProps { componentsList: ComponentsListItem[]; setComponentsList: (components: ComponentsListItem[]) => void; - setInitialComponentsList: (components: ComponentsListItem[]) => void; - managedControlPlaneTemplate?: ManagedControlPlaneTemplate; - initialSelection?: Record; - isOnMcpPage?: boolean; - initializedComponents: React.RefObject; + isLoading: boolean; + error: unknown; + templateDefaultsError?: string; } /** @@ -34,124 +26,15 @@ export const getSelectedComponents = (components: ComponentsListItem[]) => { }); }; -type TemplateDefaultComponent = { - name: string; - version: string; - removable?: boolean; - versionChangeable?: boolean; -}; - export const ComponentsSelectionContainer: React.FC = ({ setComponentsList, componentsList, - managedControlPlaneTemplate, - initialSelection, - isOnMcpPage, - setInitialComponentsList, - initializedComponents, + isLoading, + error, + templateDefaultsError, }) => { - const { - data: availableManagedComponentsListData, - error, - isLoading, - } = useApiResource(ListManagedComponents(), undefined, !!isOnMcpPage); const { t } = useTranslation(); - const [templateDefaultsError, setTemplateDefaultsError] = useState(null); - const defaultComponents = useMemo( - () => managedControlPlaneTemplate?.spec?.spec?.components?.defaultComponents ?? [], - [managedControlPlaneTemplate], - ); - - useEffect(() => { - if ( - initializedComponents.current || - !availableManagedComponentsListData?.items || - availableManagedComponentsListData.items.length === 0 - ) { - return; - } - - const newComponentsList = availableManagedComponentsListData.items - .map((item) => { - const versions = sortVersions(item.status?.versions ?? []); - const template = defaultComponents.find((dc) => dc.name === (item.metadata?.name ?? '')); - const templateVersion = template?.version; - let selectedVersion = template - ? templateVersion && versions.includes(templateVersion) - ? templateVersion - : '' - : (versions[0] ?? ''); - let isSelected = !!template; - - const initSel = initialSelection?.[item.metadata?.name ?? '']; - if (initSel) { - // Override selection and version from initial selection if provided - isSelected = Boolean(initSel.isSelected); - selectedVersion = initSel.version && versions.includes(initSel.version) ? initSel.version : ''; - } - return { - name: item.metadata?.name ?? '', - versions, - selectedVersion, - isSelected, - documentationUrl: '', - }; - }) - .filter((component) => !removeComponents.find((item) => item === component.name)); - setInitialComponentsList(newComponentsList); - setComponentsList(newComponentsList); - initializedComponents.current = true; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setComponentsList, defaultComponents, initialSelection, availableManagedComponentsListData?.items]); - - useEffect(() => { - const items = availableManagedComponentsListData?.items ?? []; - if (items.length === 0 || !defaultComponents.length) { - setTemplateDefaultsError(null); - return; - } - - const errors: string[] = []; - defaultComponents.forEach((dc: TemplateDefaultComponent) => { - 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.`); - } - }); - - setTemplateDefaultsError(errors.length ? errors.join('\n') : null); - }, [availableManagedComponentsListData, defaultComponents]); - - useEffect(() => { - if (!initializedComponents.current) return; - if (!defaultComponents?.length) return; - if (!componentsList?.length) return; - // If initialSelection is provided, do not auto-apply template defaults - if (initialSelection && Object.keys(initialSelection).length > 0) return; - - const anySelected = componentsList.some((c) => c.isSelected); - if (anySelected) return; - - const updated = componentsList.map((c) => { - const template = defaultComponents.find((dc) => dc.name === c.name); - if (!template) return c; - const templateVersion = template.version; - const selectedVersion = - templateVersion && Array.isArray(c.versions) && c.versions.includes(templateVersion) ? templateVersion : ''; - return { ...c, isSelected: true, selectedVersion }; - }); - - setComponentsList(updated); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultComponents, componentsList, setComponentsList, initialSelection]); - if (isLoading) { return ; } diff --git a/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx b/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx index 00e339f8..c5564ef2 100644 --- a/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx +++ b/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx @@ -38,11 +38,22 @@ interface Props { projectName: string; } -export function ControlPlaneCard({ controlPlane, workspace, projectName }: Props) { +type MCPWizardState = { + isOpen: boolean; + mode?: 'edit' | 'duplicate'; +}; +export const ControlPlaneCard = ({ controlPlane, workspace, projectName }: Props) => { const [dialogDeleteMcpIsOpen, setDialogDeleteMcpIsOpen] = useState(false); const toast = useToast(); const { t } = useTranslation(); - const [isEditManagedControlPlaneWizardOpen, setIsEditManagedControlPlaneWizardOpen] = useState(false); + const [managedControlPlaneWizardState, setManagedControlPlaneWizardState] = useState({ + isOpen: false, + mode: undefined, + }); + + const handleIsManagedControlPlaneWizardOpen = (isOpen: boolean, mode?: 'edit' | 'duplicate') => { + setManagedControlPlaneWizardState({ isOpen, mode }); + }; const { trigger: patchTrigger } = useApiResourceMutation( PatchMCPResourceForDeletion(controlPlane.metadata.namespace, controlPlane.metadata.name), ); @@ -85,7 +96,7 @@ export function ControlPlaneCard({ controlPlane, workspace, projectName }: Props handleIsManagedControlPlaneWizardOpen(isOpen)} workspaceName={namespace} resourceName={name} + mode={managedControlPlaneWizardState.mode} /> ); -} +}; diff --git a/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCardMenu.tsx b/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCardMenu.tsx index 6af2ea65..e85cc714 100644 --- a/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCardMenu.tsx +++ b/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCardMenu.tsx @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'; type ControlPlanesListMenuProps = { setDialogDeleteMcpIsOpen: Dispatch>; isDeleteMcpButtonDisabled: boolean; - setIsEditManagedControlPlaneWizardOpen: Dispatch>; + setIsEditManagedControlPlaneWizardOpen: (isOpen: boolean, mode?: 'edit' | 'duplicate') => void; }; export const ControlPlaneCardMenu: FC = ({ @@ -34,7 +34,10 @@ export const ControlPlaneCardMenu: FC = ({ onItemClick={(event) => { const action = (event.detail.item as HTMLElement).dataset.action; if (action === 'editMcp') { - setIsEditManagedControlPlaneWizardOpen(true); + setIsEditManagedControlPlaneWizardOpen(true, 'edit'); + } + if (action === 'duplicateMcp') { + setIsEditManagedControlPlaneWizardOpen(true, 'duplicate'); } if (action === 'deleteMcp') { setDialogDeleteMcpIsOpen(true); @@ -53,6 +56,7 @@ export const ControlPlaneCardMenu: FC = ({ icon="delete" disabled={isDeleteMcpButtonDisabled} /> + *:not(:last-child) { + margin-bottom: 1rem; +} + +.content > * { + color: var(--sapBackgroundColor); + line-height: 1.2rem; } diff --git a/src/components/Ui/Infobox/Infobox.tsx b/src/components/Ui/Infobox/Infobox.tsx index 132e6e36..e0eb4e3f 100644 --- a/src/components/Ui/Infobox/Infobox.tsx +++ b/src/components/Ui/Infobox/Infobox.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from 'react'; import cx from 'clsx'; +import { Icon } from '@ui5/webcomponents-react'; import styles from './Infobox.module.css'; @@ -10,15 +11,24 @@ interface LabelProps { children: ReactNode; fullWidth?: boolean; className?: string; + icon?: string; } +const variantIcons = { + normal: 'message-information', + success: 'message-success', + warning: 'message-warning', + danger: 'message-error', +}; + export const Infobox: React.FC = ({ id, - size = 'md', // Default to medium size - variant = 'normal', // Default to normal variant + size = 'md', + variant = 'normal', children, fullWidth = false, className, + icon, }) => { const infoboxClasses = cx( styles.infobox, @@ -35,11 +45,12 @@ export const Infobox: React.FC = ({ className, ); + const iconName = icon || variantIcons[variant]; + return ( -
- - {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 && ( )} @@ -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')}{' '} +