diff --git a/package-lock.json b/package-lock.json index be9917e9..ba4988bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@xyflow/react": "12.8.4", "clsx": "2.1.1", "dagre": "0.8.5", + "diff": "^8.0.2", "dotenv": "17.2.2", "fastify": "5.6.0", "fastify-plugin": "5.0.1", @@ -8776,6 +8777,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz", + "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", diff --git a/package.json b/package.json index 8ad44d63..afe1a1b0 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@xyflow/react": "12.8.4", "clsx": "2.1.1", "dagre": "0.8.5", + "diff": "^8.0.2", "dotenv": "17.2.2", "fastify": "5.6.0", "fastify-plugin": "5.0.1", diff --git a/public/locales/en.json b/public/locales/en.json index e24cb086..57a90b6f 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -58,7 +58,10 @@ "loadingErrorMessage": "Failed to list mcps in workspace" }, "ControlPlaneCard": { - "deleteConfirmationDialog": "MCP deletion triggered. The list will refresh automatically once completed." + "deleteConfirmationDialog": "MCP deletion triggered. The list will refresh automatically once completed.", + "editMCP": "Edit Managed Control Plane", + "deleteMCP": "Delete Managed Control Plane" + }, "ControlPlaneListAllWorkspaces": { "emptyListTitleMessage": "No Workspaces created yet", @@ -370,7 +373,8 @@ "create": "Create", "close": "Close", "back": "Back", - "cancel": "Cancel" + "cancel": "Cancel", + "update": "Update" }, "yaml": { "copiedToClipboard": "YAML copied to clipboard!", @@ -381,6 +385,11 @@ "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." }, + "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." + }, "componentsSelection": { "selectComponents": "Select Components", "selectedComponents": "Selected Components", diff --git a/src/components/ComponentsSelection/ComponentsSelectionContainer.tsx b/src/components/ComponentsSelection/ComponentsSelectionContainer.tsx index 998f8b7b..16aeb250 100644 --- a/src/components/ComponentsSelection/ComponentsSelectionContainer.tsx +++ b/src/components/ComponentsSelection/ComponentsSelectionContainer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { ComponentsSelection } from './ComponentsSelection.tsx'; import IllustratedError from '../Shared/IllustratedError.tsx'; @@ -14,7 +14,11 @@ import { ManagedControlPlaneTemplate } from '../../lib/api/types/templates/mcpTe export interface ComponentsSelectionProps { componentsList: ComponentsListItem[]; setComponentsList: (components: ComponentsListItem[]) => void; + setInitialComponentsList: (components: ComponentsListItem[]) => void; managedControlPlaneTemplate?: ManagedControlPlaneTemplate; + initialSelection?: Record; + isOnMcpPage?: boolean; + initializedComponents: React.RefObject; } /** @@ -25,9 +29,7 @@ export const getSelectedComponents = (components: ComponentsListItem[]) => { const isCrossplaneSelected = components.some(({ name, isSelected }) => name === 'crossplane' && isSelected); return components.filter((component) => { if (!component.isSelected) return false; - if (component.name?.includes('provider') && !isCrossplaneSelected) { - return false; - } + if (component.name?.includes('provider') && !isCrossplaneSelected) return false; return true; }); }; @@ -43,10 +45,18 @@ export const ComponentsSelectionContainer: React.FC = setComponentsList, componentsList, managedControlPlaneTemplate, + initialSelection, + isOnMcpPage, + setInitialComponentsList, + initializedComponents, }) => { - const { data: availableManagedComponentsListData, error, isLoading } = useApiResource(ListManagedComponents()); + const { + data: availableManagedComponentsListData, + error, + isLoading, + } = useApiResource(ListManagedComponents(), undefined, !!isOnMcpPage); const { t } = useTranslation(); - const initialized = useRef(false); + const [templateDefaultsError, setTemplateDefaultsError] = useState(null); const defaultComponents = useMemo( () => managedControlPlaneTemplate?.spec?.spec?.components?.defaultComponents ?? [], @@ -54,40 +64,50 @@ export const ComponentsSelectionContainer: React.FC = ); useEffect(() => { - const items = availableManagedComponentsListData?.items ?? []; - - if (!items.length) { - if (!initialized.current) return; - setTemplateDefaultsError(null); + if ( + initializedComponents.current || + !availableManagedComponentsListData?.items || + availableManagedComponentsListData.items.length === 0 + ) { return; } - if (!initialized.current) { - const newComponentsList = items - .map((item) => { - const versions = sortVersions(item.status.versions); - const template = defaultComponents.find((dc) => dc.name === item.metadata.name); - const templateVersion = template?.version; - const selectedVersion = template - ? templateVersion && versions.includes(templateVersion) - ? templateVersion - : '' - : (versions[0] ?? ''); - return { - name: item.metadata.name, - versions, - selectedVersion, - isSelected: !!template, - documentationUrl: '', - }; - }) - .filter((component) => !removeComponents.find((item) => item === component.name)); - - setComponentsList(newComponentsList); - initialized.current = true; - } + 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]); - if (!defaultComponents.length) { + useEffect(() => { + const items = availableManagedComponentsListData?.items ?? []; + if (items.length === 0 || !defaultComponents.length) { setTemplateDefaultsError(null); return; } @@ -95,19 +115,42 @@ export const ComponentsSelectionContainer: React.FC = const errors: string[] = []; defaultComponents.forEach((dc: TemplateDefaultComponent) => { if (!dc?.name) return; - const item = items.find((it) => it.metadata.name === dc.name); + 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 : []; + 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, setComponentsList]); + }, [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 ; @@ -117,7 +160,6 @@ export const ComponentsSelectionContainer: React.FC = return ; } - // Defensive: If the API returned no items, show error if (!componentsList || componentsList.length === 0) { return ; } diff --git a/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx b/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx index 19de48d8..00e339f8 100644 --- a/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx +++ b/src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx @@ -1,4 +1,4 @@ -import { Button, Card, FlexBox, Label, Title } from '@ui5/webcomponents-react'; +import { Card, FlexBox, Label, Title } from '@ui5/webcomponents-react'; import '@ui5/webcomponents-fiori/dist/illustrations/NoData.js'; import '@ui5/webcomponents-fiori/dist/illustrations/EmptyList.js'; import '@ui5/webcomponents-icons/dist/delete'; @@ -27,6 +27,11 @@ import { useToast } from '../../../context/ToastContext.tsx'; import { canConnectToMCP } from '../controlPlanes.ts'; import { Infobox } from '../../Ui/Infobox/Infobox.tsx'; +import { ControlPlaneCardMenu } from './ControlPlaneCardMenu.tsx'; + +import { EditManagedControlPlaneWizardDataLoader } from '../../Wizards/CreateManagedControlPlane/EditManagedControlPlaneWizardDataLoader.tsx'; +import { DISPLAY_NAME_ANNOTATION } from '../../../lib/api/types/shared/keyNames.ts'; + interface Props { controlPlane: ListControlPlanesType; workspace: ListWorkspacesType; @@ -37,7 +42,7 @@ export function ControlPlaneCard({ controlPlane, workspace, projectName }: Props const [dialogDeleteMcpIsOpen, setDialogDeleteMcpIsOpen] = useState(false); const toast = useToast(); const { t } = useTranslation(); - + const [isEditManagedControlPlaneWizardOpen, setIsEditManagedControlPlaneWizardOpen] = useState(false); const { trigger: patchTrigger } = useApiResourceMutation( PatchMCPResourceForDeletion(controlPlane.metadata.namespace, controlPlane.metadata.name), ); @@ -46,6 +51,9 @@ export function ControlPlaneCard({ controlPlane, workspace, projectName }: Props ); const name = controlPlane.metadata.name; + const displayName = + controlPlane?.metadata?.annotations?.[DISPLAY_NAME_ANNOTATION as keyof typeof controlPlane.metadata.annotations]; + const namespace = controlPlane.metadata.namespace; const isSystemIdentityProviderEnabled = Boolean(controlPlane.spec?.authentication?.enableSystemIdentityProvider); @@ -61,7 +69,7 @@ export function ControlPlaneCard({ controlPlane, workspace, projectName }: Props - {name} + {displayName ? displayName : name}
@@ -74,13 +82,10 @@ export function ControlPlaneCard({ controlPlane, workspace, projectName }: Props
- - ) : ( - - ))} - - - } - /> - } - data-testid="create-mcp-dialog" - onClose={resetFormAndClose} - > - - - - {t('buttons.close')} + ) : ( + + ))} + + + } /> - - -
- - + + + + + + + + + + + + + + {/* this condition is to remount the component from scratch to fix a bug with data loading */} + {selectedStep === 'componentSelection' && ( + -
- -
- - - - - - - - - -
- + )} + + + + + + {isEditMode ? ( + + ) : ( + + )} + + + + ); }; diff --git a/src/components/Wizards/CreateManagedControlPlane/EditManagedControlPlaneWizardDataLoader.module.css b/src/components/Wizards/CreateManagedControlPlane/EditManagedControlPlaneWizardDataLoader.module.css new file mode 100644 index 00000000..b01f6cc4 --- /dev/null +++ b/src/components/Wizards/CreateManagedControlPlane/EditManagedControlPlaneWizardDataLoader.module.css @@ -0,0 +1,7 @@ +.absolute { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 100; +} diff --git a/src/components/Wizards/CreateManagedControlPlane/EditManagedControlPlaneWizardDataLoader.tsx b/src/components/Wizards/CreateManagedControlPlane/EditManagedControlPlaneWizardDataLoader.tsx new file mode 100644 index 00000000..3c973f9e --- /dev/null +++ b/src/components/Wizards/CreateManagedControlPlane/EditManagedControlPlaneWizardDataLoader.tsx @@ -0,0 +1,60 @@ +import { FC } from 'react'; +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 { PROJECT_NAME_LABEL, WORKSPACE_LABEL } from '../../../lib/api/types/shared/keyNames.ts'; + +import { BusyIndicator } from '@ui5/webcomponents-react'; +import { ManagedControlPlaneInterface } from '../../../lib/api/types/mcpResource.ts'; + +export type EditManagedControlPlaneWizardDataLoaderProps = { + workspaceName?: string; + resourceName: string; + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; + isOnMcpPage?: boolean; +}; + +export const EditManagedControlPlaneWizardDataLoader: FC = ({ + workspaceName, + resourceName, + isOpen, + setIsOpen, + isOnMcpPage = false, +}) => { + const { isLoading, data, error } = useApiResource( + ResourceObject(workspaceName ?? '', 'managedcontrolplanes', resourceName), + undefined, + true, + !isOpen, + ); + + if (isLoading) { + return ( +
+ +
+ ); + } + if (error || !data) { + return null; + } + + return ( + <> + {isOpen ? ( + + ) : null} + + ); +}; diff --git a/src/components/Wizards/CreateManagedControlPlane/SummarizeStep.tsx b/src/components/Wizards/CreateManagedControlPlane/SummarizeStep.tsx index d9805f24..4bf6d4ae 100644 --- a/src/components/Wizards/CreateManagedControlPlane/SummarizeStep.tsx +++ b/src/components/Wizards/CreateManagedControlPlane/SummarizeStep.tsx @@ -10,16 +10,27 @@ import YamlViewer from '../../Yaml/YamlViewer.tsx'; import { idpPrefix } from '../../../utils/idpPrefix.ts'; import { UseFormWatch } from 'react-hook-form'; import { CreateDialogProps } from '../../Dialogs/CreateWorkspaceDialogContainer.tsx'; +import { YamlDiff } from '../../Yaml/YamlDiff.tsx'; interface SummarizeStepProps { watch: UseFormWatch; projectName: string; workspaceName: string; componentsList?: ComponentsListItem[]; + originalYamlString?: string; + isEditMode?: boolean; } -export const SummarizeStep: React.FC = ({ watch, projectName, workspaceName, componentsList }) => { +export const SummarizeStep: React.FC = ({ + originalYamlString, + watch, + projectName, + workspaceName, + componentsList, + isEditMode = false, +}) => { const { t } = useTranslation(); + return ( <> {t('common.summarize')} @@ -48,23 +59,44 @@ export const SummarizeStep: React.FC = ({ watch, projectName
- + {isEditMode ? ( + + ) : ( + + )}
diff --git a/src/components/Yaml/YamlDiff.module.css b/src/components/Yaml/YamlDiff.module.css new file mode 100644 index 00000000..be7300f6 --- /dev/null +++ b/src/components/Yaml/YamlDiff.module.css @@ -0,0 +1,11 @@ +.container { + width: 100%; +} + +.added { + background-color: rgba(56, 142, 60, 0.18); +} + +.removed { + background-color: rgba(211, 47, 47, 0.18); +} diff --git a/src/components/Yaml/YamlDiff.tsx b/src/components/Yaml/YamlDiff.tsx new file mode 100644 index 00000000..09cd40d3 --- /dev/null +++ b/src/components/Yaml/YamlDiff.tsx @@ -0,0 +1,56 @@ +import { FC, useMemo } from 'react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { materialLight, materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { diffLines } from 'diff'; +import styles from './YamlDiff.module.css'; +import { useTheme } from '../../hooks/useTheme.ts'; + +type YamlDiffProps = { + originalYaml: string; + modifiedYaml: string; +}; + +export const YamlDiff: FC = ({ originalYaml, modifiedYaml }) => { + const { isDarkTheme } = useTheme(); + + const hunks = useMemo(() => diffLines(originalYaml ?? '', modifiedYaml ?? ''), [originalYaml, modifiedYaml]); + + const { content, lineKinds } = useMemo(() => { + const lines: string[] = []; + const kinds: ('added' | 'removed' | 'context')[] = []; + hunks.forEach((part) => { + const prefix = part.added ? '+' : part.removed ? '-' : ' '; + const kind: 'added' | 'removed' | 'context' = part.added ? 'added' : part.removed ? 'removed' : 'context'; + const partLines = part.value.replace(/\n$/, '').split('\n'); + partLines.forEach((line) => { + lines.push(`${prefix}${line}`); + kinds.push(kind); + }); + }); + return { content: lines.join('\n'), lineKinds: kinds }; + }, [hunks]); + + const lineNumberStyle = useMemo(() => ({ paddingRight: '20px', minWidth: '40px', textAlign: 'right' as const }), []); + + return ( +
+ { + const kind = lineKinds[lineNumber - 1]; + if (kind === 'added') return { className: styles.added }; + if (kind === 'removed') return { className: styles.removed }; + return {}; + }} + customStyle={{ margin: 0, padding: '20px', borderRadius: '4px', fontSize: '1rem', background: 'transparent' }} + codeTagProps={{ style: { whiteSpace: 'pre-wrap' } }} + > + {content} + +
+ ); +}; diff --git a/src/lib/api/types/crate/controlPlanes.ts b/src/lib/api/types/crate/controlPlanes.ts index 0df87bcf..a00b9b9a 100644 --- a/src/lib/api/types/crate/controlPlanes.ts +++ b/src/lib/api/types/crate/controlPlanes.ts @@ -5,6 +5,9 @@ export type ListControlPlanesType = ControlPlaneType; export interface Metadata { name: string; namespace: string; + annotations: { + 'openmcp.cloud/display-name': string; + }; } export interface ControlPlaneType { @@ -68,7 +71,7 @@ export const ListControlPlanes = ( projectName === null ? null : `/apis/core.openmcp.cloud/v1alpha1/namespaces/project-${projectName}--ws-${workspaceName}/managedcontrolplanes`, - jq: '[.items[] |{spec: .spec | {authentication}, metadata: .metadata | {name, namespace}, status: { conditions: [.status.conditions[] | {type: .type, status: .status, message: .message, reason: .reason, lastTransitionTime: .lastTransitionTime}], access: .status.components.authentication.access, status: .status.status } }]', + jq: '[.items[] |{spec: .spec | {authentication}, metadata: .metadata | {name, namespace, annotations}, status: { conditions: [.status.conditions[] | {type: .type, status: .status, message: .message, reason: .reason, lastTransitionTime: .lastTransitionTime}], access: .status.components.authentication.access, status: .status.status } }]', }; }; @@ -79,6 +82,6 @@ export const ControlPlane = ( ): Resource => { return { path: `/apis/core.openmcp.cloud/v1alpha1/namespaces/project-${projectName}--ws-${workspaceName}/managedcontrolplanes/${controlPlaneName}`, - jq: '{ spec: .spec | {components}, status: { conditions: [.status.conditions[] | {type: .type, status: .status, message: .message, reason: .reason, lastTransitionTime: .lastTransitionTime}], access: .status.components.authentication.access, status: .status.status }}', + jq: '{ spec: .spec | {components}, metadata: .metadata | {name, namespace, annotations}, status: { conditions: [.status.conditions[] | {type: .type, status: .status, message: .message, reason: .reason, lastTransitionTime: .lastTransitionTime}], access: .status.components.authentication.access, status: .status.status }}', }; }; diff --git a/src/lib/api/types/crate/createManagedControlPlane.ts b/src/lib/api/types/crate/createManagedControlPlane.ts index b1a1dc06..315a5fa4 100644 --- a/src/lib/api/types/crate/createManagedControlPlane.ts +++ b/src/lib/api/types/crate/createManagedControlPlane.ts @@ -156,3 +156,16 @@ export const CreateManagedControlPlaneResource = (projectName: string, workspace body: undefined, }; }; + +export const UpdateManagedControlPlaneResource = ( + projectName: string, + workspaceName: string, + name: string, +): Resource => { + return { + path: `/apis/core.openmcp.cloud/v1alpha1/namespaces/${projectName}--ws-${workspaceName}/managedcontrolplanes/${name}`, + method: 'PATCH', + jq: undefined, + body: undefined, + }; +}; diff --git a/src/lib/api/types/crate/listManagedComponents.ts b/src/lib/api/types/crate/listManagedComponents.ts index 1f715ab0..7851946b 100644 --- a/src/lib/api/types/crate/listManagedComponents.ts +++ b/src/lib/api/types/crate/listManagedComponents.ts @@ -1,50 +1,44 @@ import { Resource } from '../resource'; -interface ManagedComponentList { - apiVersion: string; - kind: string; - items: ManagedComponent[]; - metadata: ListMetadata; -} - -interface ListMetadata { - continue: string; - resourceVersion: string; -} - -interface ManagedComponent { - apiVersion: string; - kind: string; - metadata: ManagedComponentMetadata; - spec: Record; - status: ManagedComponentStatus; +export interface ManagedComponentList { + apiVersion?: string; + kind?: string; + metadata?: { + continue?: string; + resourceVersion?: string; + [key: string]: unknown; + }; + items?: ManagedComponent[]; } -interface ManagedComponentMetadata { - creationTimestamp: string; - generation: number; - managedFields: ManagedField[]; - name: string; - resourceVersion: string; - uid: string; +export interface ManagedComponent { + apiVersion?: string; + kind?: string; + metadata?: { + creationTimestamp?: string; + generation?: number; + managedFields?: ManagedField[]; + name?: string; + resourceVersion?: string; + uid?: string; + [key: string]: unknown; + }; + spec?: Record; + status?: { + versions?: string[]; + [key: string]: unknown; + }; } -interface ManagedField { - apiVersion: string; - fieldsType: string; - fieldsV1: FieldsV1; - manager: string; - operation: string; - time: string; +export interface ManagedField { + apiVersion?: string; + fieldsType?: string; + fieldsV1?: Record; + manager?: string; + operation?: string; + time?: string; subresource?: string; -} - -interface FieldsV1 { - [key: string]: never; -} - -interface ManagedComponentStatus { - versions: string[]; + [key: string]: unknown; } export const ListManagedComponents = (): Resource => { diff --git a/src/lib/api/types/mcpResource.ts b/src/lib/api/types/mcpResource.ts new file mode 100644 index 00000000..fa92973e --- /dev/null +++ b/src/lib/api/types/mcpResource.ts @@ -0,0 +1,121 @@ +export interface ManagedControlPlaneInterface { + apiVersion: 'core.openmcp.cloud/v1alpha1' | string; + kind: 'ManagedControlPlane' | string; + metadata: KubernetesObjectMeta; + spec: ManagedControlPlaneSpec; + status?: ManagedControlPlaneStatus; +} + +export interface KubernetesObjectMeta { + name: string; + namespace?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + annotations?: Record; + labels?: Record; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface ManagedFieldsEntry { + apiVersion?: string; + fieldsType?: string; + fieldsV1?: unknown; + manager?: string; + operation?: string; + subresource?: string; + time?: string; +} + +export interface ManagedControlPlaneSpec { + authentication?: MCPAuthenticationSpec; + authorization?: MCPAuthorizationSpec; + components?: MCPComponentsSpec; +} + +export interface MCPAuthenticationSpec { + enableSystemIdentityProvider?: boolean; +} + +export interface MCPAuthorizationSpec { + roleBindings?: MCPRoleBinding[]; +} + +export interface MCPRoleBinding { + role: 'admin' | 'view' | string; + subjects: MCPSubject[]; +} + +export interface MCPSubject { + kind: 'User' | 'ServiceAccount' | string; + name: string; + namespace?: string; +} + +export interface MCPComponentsSpec { + apiServer?: MCPApiServerComponent; + crossplane?: MCPCrossplaneComponent; + externalSecretsOperator?: MCPVersionedComponent; + flux?: MCPVersionedComponent; +} + +export interface MCPApiServerComponent { + type?: 'GardenerDedicated' | string; +} + +export interface MCPCrossplaneComponent { + version?: string; + providers?: MCPCrossplaneProvider[]; +} + +export interface MCPCrossplaneProvider { + name: string; + version?: string; +} + +export interface MCPVersionedComponent { + version?: string; +} + +export interface ManagedControlPlaneStatus { + components?: MCPStatusComponents; + conditions?: MCPCondition[]; + observedGeneration?: number; + status?: string; +} + +export interface MCPStatusComponents { + apiServer?: { + endpoint?: string; + serviceAccountIssuer?: string; + }; + authentication?: { + access?: { + key?: string; + name?: string; + namespace?: string; + }; + }; + authorization?: Record; + cloudOrchestrator?: Record; +} + +export interface MCPCondition { + lastTransitionTime?: string; + managedBy?: string; + message?: string; + reason?: string; + status?: 'True' | 'False' | 'Unknown' | string; + type?: + | 'APIServerHealthy' + | 'AuthenticationHealthy' + | 'AuthorizationHealthy' + | 'CloudOrchestratorHealthy' + | 'CrossplaneReady' + | 'ExternalSecretsOperatorReady' + | 'Ready' + | 'MCPSuccessful' + | string; +} diff --git a/src/lib/api/types/shared/keyNames.ts b/src/lib/api/types/shared/keyNames.ts index 00c53573..db4a6531 100644 --- a/src/lib/api/types/shared/keyNames.ts +++ b/src/lib/api/types/shared/keyNames.ts @@ -1,3 +1,5 @@ export const DISPLAY_NAME_ANNOTATION: string = 'openmcp.cloud/display-name'; export const CHARGING_TARGET_LABEL: string = 'openmcp.cloud.sap/charging-target'; export const CHARGING_TARGET_TYPE_LABEL: string = 'openmcp.cloud.sap/charging-target-type'; +export const PROJECT_NAME_LABEL: string = 'openmcp.cloud/mcp-project'; +export const WORKSPACE_LABEL: string = 'openmcp.cloud/mcp-workspace'; diff --git a/src/lib/api/useApiResource.ts b/src/lib/api/useApiResource.ts index 3563ebed..63d846d4 100644 --- a/src/lib/api/useApiResource.ts +++ b/src/lib/api/useApiResource.ts @@ -158,6 +158,7 @@ export const useApiResourceMutation = ( resource: Resource, // eslint-disable-next-line @typescript-eslint/no-explicit-any config?: SWRMutationConfiguration, + excludeMcpConfig?: boolean, ) => { const apiConfig = useContext(ApiConfigContext); @@ -167,7 +168,13 @@ export const useApiResourceMutation = ( : [resource.path, apiConfig], // eslint-disable-next-line @typescript-eslint/no-explicit-any ([path, apiConfig]: [path: string, config: ApiConfig], arg: any) => - fetchApiServerJson(path, apiConfig, resource.jq, resource.method, JSON.stringify(arg.arg)), + fetchApiServerJson( + path, + excludeMcpConfig ? { ...apiConfig, mcpConfig: undefined } : apiConfig, + resource.jq, + resource.method, + JSON.stringify(arg.arg), + ), config, ); diff --git a/src/spaces/mcp/pages/McpPage.tsx b/src/spaces/mcp/pages/McpPage.tsx index de04cac0..58669c28 100644 --- a/src/spaces/mcp/pages/McpPage.tsx +++ b/src/spaces/mcp/pages/McpPage.tsx @@ -28,16 +28,21 @@ import { NotFoundBanner } from '../../../components/Ui/NotFoundBanner/NotFoundBa import Graph from '../../../components/Graphs/Graph.tsx'; import HintsCardsRow from '../../../components/HintsCardsRow/HintsCardsRow.tsx'; +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'; + export default function McpPage() { const { projectName, workspaceName, controlPlaneName } = useParams(); const { t } = useTranslation(); - + const [isEditManagedControlPlaneWizardOpen, setIsEditManagedControlPlaneWizardOpen] = useState(false); const { data: mcp, error, isLoading, } = useApiResource(ControlPlaneResource(projectName, workspaceName, controlPlaneName)); - + const displayName = mcp?.metadata?.annotations?.[DISPLAY_NAME_ANNOTATION]; if (isLoading) { return ; } @@ -64,7 +69,7 @@ export default function McpPage() { preserveHeaderStateOnClick={true} titleArea={ } //TODO: actionBar should use Toolbar and ToolbarButton for consistent design actionsBar={ @@ -88,6 +93,16 @@ export default function McpPage() { resourceName={controlPlaneName} /> + + } /> diff --git a/src/views/Login.tsx b/src/views/Login.tsx index ca935f9d..5317a3bc 100644 --- a/src/views/Login.tsx +++ b/src/views/Login.tsx @@ -1,5 +1,5 @@ import { useAuthOnboarding } from '../spaces/onboarding/auth/AuthContextOnboarding.tsx'; -import { Button, Card, FlexBox, Text } from '@ui5/webcomponents-react'; +import { Button, Card, FlexBox, Link, Text } from '@ui5/webcomponents-react'; import ButtonDesign from '@ui5/webcomponents/dist/types/ButtonDesign.js'; import './login.css'; import { ThemingParameters } from '@ui5/webcomponents-react-base'; @@ -31,12 +31,14 @@ export default function LoginView() {
Logo
{t('Login.welcomeMessage')}
- {t('Login.description')} + + {t('Login.description')} +

- + {t('Login.learnMore')} - +

diff --git a/src/views/login.css b/src/views/login.css index b510b4b2..11531f82 100644 --- a/src/views/login.css +++ b/src/views/login.css @@ -4,6 +4,12 @@ margin-bottom: 8px; } +.description { + max-width: 50ch; + text-align: center; + margin: 0 auto; +} + .logo { width: 400px; } @@ -17,4 +23,4 @@ .signinBtn { font-size: 1rem; padding: 10px 20px; -} \ No newline at end of file +}