diff --git a/public/locales/en.json b/public/locales/en.json index bf51c33f..5b4b23d9 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -203,6 +203,11 @@ "landscapersTitle": "Landscapers", "graphTitle": "Graph" }, + "McpHeader": { + "nameLabel": "Name", + "createdByLabel": "Created By", + "createdOnLabel": "Created On" + }, "ToastContext": { "errorMessage": "useToast must be used within a ToastProvider" }, diff --git a/src/lib/api/types/crate/controlPlanes.ts b/src/lib/api/types/crate/controlPlanes.ts index a00b9b9a..7392fa47 100644 --- a/src/lib/api/types/crate/controlPlanes.ts +++ b/src/lib/api/types/crate/controlPlanes.ts @@ -5,8 +5,10 @@ export type ListControlPlanesType = ControlPlaneType; export interface Metadata { name: string; namespace: string; - annotations: { - 'openmcp.cloud/display-name': string; + creationTimestamp: string; + annotations?: { + 'openmcp.cloud/display-name'?: string; + 'openmcp.cloud/created-by'?: string; }; } @@ -82,6 +84,6 @@ export const ControlPlane = ( ): Resource => { return { path: `/apis/core.openmcp.cloud/v1alpha1/namespaces/project-${projectName}--ws-${workspaceName}/managedcontrolplanes/${controlPlaneName}`, - 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 }}', + jq: '{ spec: .spec | {components}, metadata: .metadata | {name, namespace, creationTimestamp, 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/spaces/mcp/components/McpHeader.cy.tsx b/src/spaces/mcp/components/McpHeader.cy.tsx new file mode 100644 index 00000000..9e7fc611 --- /dev/null +++ b/src/spaces/mcp/components/McpHeader.cy.tsx @@ -0,0 +1,50 @@ +import { McpHeader } from './McpHeader'; +import { ControlPlaneType } from '../../../lib/api/types/crate/controlPlanes.ts'; + +describe('McpHeader', () => { + it('renders MCP metadata', () => { + const mcp = { + metadata: { + name: 'my-control-plane', + creationTimestamp: '2024-04-15T10:30:00.000Z', + annotations: { + 'openmcp.cloud/created-by': 'alice@example.com', + }, + }, + } as ControlPlaneType; + + cy.clock(new Date('2024-04-17T10:30:00.000Z').getTime()); // 2 days after MCP creation date + const creationDateAsString = new Date('2024-04-15T10:30:00.000Z').toLocaleDateString(undefined, { + day: 'numeric', + month: 'long', + year: 'numeric', + }); + + cy.mount(); + + cy.contains('span', 'my-control-plane').should('be.visible'); + cy.contains('span', 'alice@example.com').should('be.visible'); + cy.contains('span', `${creationDateAsString} (2 days ago)`).should('be.visible'); + }); + + it('renders with missing MCP metadata', () => { + const mcp = { + metadata: { + name: 'my-control-plane', + creationTimestamp: '2024-04-15T10:30:00.000Z', + }, + } as ControlPlaneType; // missing annotations + + cy.clock(new Date('2024-04-17T10:30:00.000Z').getTime()); // 2 days after MCP creation date + const creationDateAsString = new Date('2024-04-15T10:30:00.000Z').toLocaleDateString(undefined, { + day: 'numeric', + month: 'long', + year: 'numeric', + }); + + cy.mount(); + + cy.contains('span', 'my-control-plane').should('be.visible'); + cy.contains('span', `${creationDateAsString} (2 days ago)`).should('be.visible'); + }); +}); diff --git a/src/spaces/mcp/components/McpHeader.module.css b/src/spaces/mcp/components/McpHeader.module.css new file mode 100644 index 00000000..d55d4bfb --- /dev/null +++ b/src/spaces/mcp/components/McpHeader.module.css @@ -0,0 +1,15 @@ +.container { + display: flex; +} + +.grid { + display: grid; + grid-template-columns: auto auto; + gap: 0.375rem 0.75rem; + align-items: center; + font-size: var(--sapFontSize); +} + +.label { + color: var(--sapContent_LabelColor); +} diff --git a/src/spaces/mcp/components/McpHeader.tsx b/src/spaces/mcp/components/McpHeader.tsx new file mode 100644 index 00000000..dcedba6f --- /dev/null +++ b/src/spaces/mcp/components/McpHeader.tsx @@ -0,0 +1,42 @@ +import { ControlPlaneType } from '../../../lib/api/types/crate/controlPlanes.ts'; + +import styles from './McpHeader.module.css'; +import { formatDateAsTimeAgo } from '../../../utils/i18n/timeAgo.ts'; +import { useTranslation } from 'react-i18next'; + +export interface McpHeaderProps { + mcp: ControlPlaneType; +} + +export function McpHeader({ mcp }: McpHeaderProps) { + const { t } = useTranslation(); + + const created = new Date(mcp.metadata.creationTimestamp).toLocaleDateString(undefined, { + day: 'numeric', + month: 'long', + year: 'numeric', + }); + + const createdBy = mcp.metadata.annotations?.['openmcp.cloud/created-by']; + + return ( +
+
+ {t('McpHeader.nameLabel')} + {mcp.metadata.name} + + {t('McpHeader.createdOnLabel')} + + {created} ({formatDateAsTimeAgo(mcp.metadata.creationTimestamp)}) + + + {createdBy ? ( + <> + {t('McpHeader.createdByLabel')} + {createdBy} + + ) : null} +
+
+ ); +} diff --git a/src/spaces/mcp/pages/McpPage.tsx b/src/spaces/mcp/pages/McpPage.tsx index d6b0fa29..0661cb55 100644 --- a/src/spaces/mcp/pages/McpPage.tsx +++ b/src/spaces/mcp/pages/McpPage.tsx @@ -3,6 +3,7 @@ import { Button, FlexBox, ObjectPage, + ObjectPageHeader, ObjectPageSection, ObjectPageTitle, Panel, @@ -42,6 +43,7 @@ import { EditManagedControlPlaneWizardDataLoader } from '../../../components/Wiz 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'; +import { McpHeader } from '../components/McpHeader.tsx'; export default function McpPage() { const { projectName, workspaceName, controlPlaneName } = useParams(); @@ -127,6 +129,11 @@ export default function McpPage() { } /> } + headerArea={ + + + + } >