Skip to content

Commit a7b2a81

Browse files
feat: show MCP metadata in the header (#305)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 7ebda58 commit a7b2a81

File tree

6 files changed

+124
-3
lines changed

6 files changed

+124
-3
lines changed

public/locales/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,11 @@
203203
"landscapersTitle": "Landscapers",
204204
"graphTitle": "Graph"
205205
},
206+
"McpHeader": {
207+
"nameLabel": "Name",
208+
"createdByLabel": "Created By",
209+
"createdOnLabel": "Created On"
210+
},
206211
"ToastContext": {
207212
"errorMessage": "useToast must be used within a ToastProvider"
208213
},

src/lib/api/types/crate/controlPlanes.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ export type ListControlPlanesType = ControlPlaneType;
55
export interface Metadata {
66
name: string;
77
namespace: string;
8-
annotations: {
9-
'openmcp.cloud/display-name': string;
8+
creationTimestamp: string;
9+
annotations?: {
10+
'openmcp.cloud/display-name'?: string;
11+
'openmcp.cloud/created-by'?: string;
1012
};
1113
}
1214

@@ -82,6 +84,6 @@ export const ControlPlane = (
8284
): Resource<ControlPlaneType> => {
8385
return {
8486
path: `/apis/core.openmcp.cloud/v1alpha1/namespaces/project-${projectName}--ws-${workspaceName}/managedcontrolplanes/${controlPlaneName}`,
85-
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 }}',
87+
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 }}',
8688
};
8789
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { McpHeader } from './McpHeader';
2+
import { ControlPlaneType } from '../../../lib/api/types/crate/controlPlanes.ts';
3+
4+
describe('McpHeader', () => {
5+
it('renders MCP metadata', () => {
6+
const mcp = {
7+
metadata: {
8+
name: 'my-control-plane',
9+
creationTimestamp: '2024-04-15T10:30:00.000Z',
10+
annotations: {
11+
'openmcp.cloud/created-by': 'alice@example.com',
12+
},
13+
},
14+
} as ControlPlaneType;
15+
16+
cy.clock(new Date('2024-04-17T10:30:00.000Z').getTime()); // 2 days after MCP creation date
17+
const creationDateAsString = new Date('2024-04-15T10:30:00.000Z').toLocaleDateString(undefined, {
18+
day: 'numeric',
19+
month: 'long',
20+
year: 'numeric',
21+
});
22+
23+
cy.mount(<McpHeader mcp={mcp} />);
24+
25+
cy.contains('span', 'my-control-plane').should('be.visible');
26+
cy.contains('span', 'alice@example.com').should('be.visible');
27+
cy.contains('span', `${creationDateAsString} (2 days ago)`).should('be.visible');
28+
});
29+
30+
it('renders with missing MCP metadata', () => {
31+
const mcp = {
32+
metadata: {
33+
name: 'my-control-plane',
34+
creationTimestamp: '2024-04-15T10:30:00.000Z',
35+
},
36+
} as ControlPlaneType; // missing annotations
37+
38+
cy.clock(new Date('2024-04-17T10:30:00.000Z').getTime()); // 2 days after MCP creation date
39+
const creationDateAsString = new Date('2024-04-15T10:30:00.000Z').toLocaleDateString(undefined, {
40+
day: 'numeric',
41+
month: 'long',
42+
year: 'numeric',
43+
});
44+
45+
cy.mount(<McpHeader mcp={mcp} />);
46+
47+
cy.contains('span', 'my-control-plane').should('be.visible');
48+
cy.contains('span', `${creationDateAsString} (2 days ago)`).should('be.visible');
49+
});
50+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.container {
2+
display: flex;
3+
}
4+
5+
.grid {
6+
display: grid;
7+
grid-template-columns: auto auto;
8+
gap: 0.375rem 0.75rem;
9+
align-items: center;
10+
font-size: var(--sapFontSize);
11+
}
12+
13+
.label {
14+
color: var(--sapContent_LabelColor);
15+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { ControlPlaneType } from '../../../lib/api/types/crate/controlPlanes.ts';
2+
3+
import styles from './McpHeader.module.css';
4+
import { formatDateAsTimeAgo } from '../../../utils/i18n/timeAgo.ts';
5+
import { useTranslation } from 'react-i18next';
6+
7+
export interface McpHeaderProps {
8+
mcp: ControlPlaneType;
9+
}
10+
11+
export function McpHeader({ mcp }: McpHeaderProps) {
12+
const { t } = useTranslation();
13+
14+
const created = new Date(mcp.metadata.creationTimestamp).toLocaleDateString(undefined, {
15+
day: 'numeric',
16+
month: 'long',
17+
year: 'numeric',
18+
});
19+
20+
const createdBy = mcp.metadata.annotations?.['openmcp.cloud/created-by'];
21+
22+
return (
23+
<div className={styles.container}>
24+
<div className={styles.grid}>
25+
<span className={styles.label}>{t('McpHeader.nameLabel')}</span>
26+
<span>{mcp.metadata.name}</span>
27+
28+
<span className={styles.label}>{t('McpHeader.createdOnLabel')}</span>
29+
<span>
30+
{created} ({formatDateAsTimeAgo(mcp.metadata.creationTimestamp)})
31+
</span>
32+
33+
{createdBy ? (
34+
<>
35+
<span className={styles.label}>{t('McpHeader.createdByLabel')}</span>
36+
<span>{createdBy}</span>
37+
</>
38+
) : null}
39+
</div>
40+
</div>
41+
);
42+
}

src/spaces/mcp/pages/McpPage.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Button,
44
FlexBox,
55
ObjectPage,
6+
ObjectPageHeader,
67
ObjectPageSection,
78
ObjectPageTitle,
89
Panel,
@@ -42,6 +43,7 @@ import { EditManagedControlPlaneWizardDataLoader } from '../../../components/Wiz
4243
import { ControlPlanePageMenu } from '../../../components/ControlPlanes/ControlPlanePageMenu.tsx';
4344
import { DISPLAY_NAME_ANNOTATION } from '../../../lib/api/types/shared/keyNames.ts';
4445
import { WizardStepType } from '../../../components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx';
46+
import { McpHeader } from '../components/McpHeader.tsx';
4547

4648
export default function McpPage() {
4749
const { projectName, workspaceName, controlPlaneName } = useParams();
@@ -127,6 +129,11 @@ export default function McpPage() {
127129
}
128130
/>
129131
}
132+
headerArea={
133+
<ObjectPageHeader>
134+
<McpHeader mcp={mcp} />
135+
</ObjectPageHeader>
136+
}
130137
>
131138
<ObjectPageSection
132139
className="cp-page-section-overview"

0 commit comments

Comments
 (0)