Skip to content

Commit 7da9380

Browse files
feat: mcp templates
1 parent 6baab28 commit 7da9380

File tree

13 files changed

+724
-55
lines changed

13 files changed

+724
-55
lines changed

public/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@
9090
"menuDownload": "Download",
9191
"menuCopy": "Copy to clipboard"
9292
},
93+
"ComponentsSelection": {
94+
"chooseVersion": "Please select version"
95+
},
9396
"IllustratedBanner": {
9497
"titleMessage": "No Managed Control Planes found",
9598
"subtitleMessage": "Get started by creating your first Managed Control Plane.",

src/components/ComponentsSelection/ComponentsSelection.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,19 @@ import { Infobox } from '../Ui/Infobox/Infobox.tsx';
2222
import { useTranslation } from 'react-i18next';
2323
import { ComponentsListItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';
2424
import { getSelectedComponents } from './ComponentsSelectionContainer.tsx';
25+
import IllustratedError from '../Shared/IllustratedError.tsx';
2526

2627
export interface ComponentsSelectionProps {
2728
componentsList: ComponentsListItem[];
2829
setComponentsList: (components: ComponentsListItem[]) => void;
30+
templateDefaultsError?: string;
2931
}
3032

31-
export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({ componentsList, setComponentsList }) => {
33+
export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
34+
componentsList,
35+
setComponentsList,
36+
templateDefaultsError,
37+
}) => {
3238
const [searchTerm, setSearchTerm] = useState('');
3339
const { t } = useTranslation();
3440

@@ -132,8 +138,19 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({ compon
132138
value={component.selectedVersion}
133139
disabled={!component.isSelected || providerDisabled}
134140
aria-label={`${component.name} version`}
141+
valueState={component.isSelected && !component.selectedVersion ? 'Negative' : 'None'}
142+
valueStateMessage={
143+
component.isSelected && !component.selectedVersion ? (
144+
<span>{t('ComponentsSelection.chooseVersion')}</span>
145+
) : undefined
146+
}
135147
onChange={handleVersionChange}
136148
>
149+
{!component.selectedVersion && (
150+
<Option key="__placeholder" data-version="" data-name={component.name} selected>
151+
{t('ComponentsSelection.chooseVersion')}
152+
</Option>
153+
)}
137154
{component.versions.map((version) => (
138155
<Option
139156
key={version}
@@ -155,7 +172,14 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({ compon
155172
</Infobox>
156173
)}
157174
</div>
175+
158176
<div data-layout-span="XL4 L4 M4 S4">
177+
{templateDefaultsError ? (
178+
<div style={{ marginBottom: 8 }}>
179+
<IllustratedError title={templateDefaultsError} compact />
180+
</div>
181+
) : null}
182+
159183
{selectedComponents.length > 0 ? (
160184
<List headerText={t('componentsSelection.selectedComponents')}>
161185
{selectedComponents.map((component) => (
Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useRef } from 'react';
1+
import React, { useEffect, useMemo, useRef, useState } from 'react';
22
import { ComponentsSelection } from './ComponentsSelection.tsx';
33

44
import IllustratedError from '../Shared/IllustratedError.tsx';
@@ -9,10 +9,12 @@ import { useApiResource } from '../../lib/api/useApiResource.ts';
99
import Loading from '../Shared/Loading.tsx';
1010
import { ComponentsListItem, removeComponents } from '../../lib/api/types/crate/createManagedControlPlane.ts';
1111
import { useTranslation } from 'react-i18next';
12+
import { ManagedControlPlaneTemplate } from '../../lib/api/types/templates/mcpTemplate.ts';
1213

1314
export interface ComponentsSelectionProps {
1415
componentsList: ComponentsListItem[];
1516
setComponentsList: (components: ComponentsListItem[]) => void;
17+
managedControlPlaneTemplate?: ManagedControlPlaneTemplate;
1618
}
1719

1820
/**
@@ -30,39 +32,82 @@ export const getSelectedComponents = (components: ComponentsListItem[]) => {
3032
});
3133
};
3234

35+
type TemplateDefaultComponent = {
36+
name: string;
37+
version: string;
38+
removable?: boolean;
39+
versionChangeable?: boolean;
40+
};
41+
3342
export const ComponentsSelectionContainer: React.FC<ComponentsSelectionProps> = ({
3443
setComponentsList,
3544
componentsList,
45+
managedControlPlaneTemplate,
3646
}) => {
3747
const { data: availableManagedComponentsListData, error, isLoading } = useApiResource(ListManagedComponents());
3848
const { t } = useTranslation();
3949
const initialized = useRef(false);
50+
const [templateDefaultsError, setTemplateDefaultsError] = useState<string | null>(null);
51+
const defaultComponents = useMemo<TemplateDefaultComponent[]>(
52+
() => managedControlPlaneTemplate?.spec?.spec?.components?.defaultComponents ?? [],
53+
[managedControlPlaneTemplate],
54+
);
4055

4156
useEffect(() => {
42-
if (
43-
initialized.current ||
44-
!availableManagedComponentsListData?.items ||
45-
availableManagedComponentsListData.items.length === 0
46-
) {
57+
const items = availableManagedComponentsListData?.items ?? [];
58+
59+
if (!items.length) {
60+
if (!initialized.current) return;
61+
setTemplateDefaultsError(null);
62+
return;
63+
}
64+
65+
if (!initialized.current) {
66+
const newComponentsList = items
67+
.map((item) => {
68+
const versions = sortVersions(item.status.versions);
69+
const template = defaultComponents.find((dc) => dc.name === item.metadata.name);
70+
const templateVersion = template?.version;
71+
const selectedVersion = template
72+
? templateVersion && versions.includes(templateVersion)
73+
? templateVersion
74+
: ''
75+
: (versions[0] ?? '');
76+
return {
77+
name: item.metadata.name,
78+
versions,
79+
selectedVersion,
80+
isSelected: !!template,
81+
documentationUrl: '',
82+
};
83+
})
84+
.filter((component) => !removeComponents.find((item) => item === component.name));
85+
86+
setComponentsList(newComponentsList);
87+
initialized.current = true;
88+
}
89+
90+
if (!defaultComponents.length) {
91+
setTemplateDefaultsError(null);
4792
return;
4893
}
4994

50-
const newComponentsList = availableManagedComponentsListData.items
51-
.map((item) => {
52-
const versions = sortVersions(item.status.versions);
53-
return {
54-
name: item.metadata.name,
55-
versions,
56-
selectedVersion: versions[0] ?? '',
57-
isSelected: false,
58-
documentationUrl: '',
59-
};
60-
})
61-
.filter((component) => !removeComponents.find((item) => item === component.name));
62-
63-
setComponentsList(newComponentsList);
64-
initialized.current = true;
65-
}, [availableManagedComponentsListData, setComponentsList]);
95+
const errors: string[] = [];
96+
defaultComponents.forEach((dc: TemplateDefaultComponent) => {
97+
if (!dc?.name) return;
98+
const item = items.find((it) => it.metadata.name === dc.name);
99+
if (!item) {
100+
errors.push(`Component "${dc.name}" from template is not available.`);
101+
return;
102+
}
103+
const versions: string[] = Array.isArray(item.status?.versions) ? item.status.versions : [];
104+
if (dc.version && !versions.includes(dc.version)) {
105+
errors.push(`Component "${dc.name}" version "${dc.version}" from template is not available.`);
106+
}
107+
});
108+
109+
setTemplateDefaultsError(errors.length ? errors.join('\n') : null);
110+
}, [availableManagedComponentsListData, defaultComponents, setComponentsList]);
66111

67112
if (isLoading) {
68113
return <Loading />;
@@ -77,5 +122,11 @@ export const ComponentsSelectionContainer: React.FC<ComponentsSelectionProps> =
77122
return <IllustratedError title={t('componentsSelection.cannotLoad')} compact={true} />;
78123
}
79124

80-
return <ComponentsSelection componentsList={componentsList} setComponentsList={setComponentsList} />;
125+
return (
126+
<ComponentsSelection
127+
componentsList={componentsList}
128+
setComponentsList={setComponentsList}
129+
templateDefaultsError={templateDefaultsError || undefined}
130+
/>
131+
);
81132
};

src/components/ControlPlanes/ControlPlanesListMenu.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,27 @@ import '@ui5/webcomponents-icons/dist/copy';
55
import '@ui5/webcomponents-icons/dist/accept';
66

77
import { useTranslation } from 'react-i18next';
8+
import { ManagedControlPlaneTemplate } from '../../lib/api/types/templates/mcpTemplate.ts';
89

910
type ControlPlanesListMenuProps = {
1011
setDialogDeleteWsIsOpen: Dispatch<SetStateAction<boolean>>;
1112
setIsCreateManagedControlPlaneWizardOpen: Dispatch<SetStateAction<boolean>>;
13+
setInitialTemplateName: Dispatch<SetStateAction<string | undefined>>;
1214
};
1315

1416
export const ControlPlanesListMenu: FC<ControlPlanesListMenuProps> = ({
1517
setDialogDeleteWsIsOpen,
1618
setIsCreateManagedControlPlaneWizardOpen,
19+
setInitialTemplateName,
1720
}) => {
1821
const popoverRef = useRef<MenuDomRef>(null);
1922
const [open, setOpen] = useState(false);
2023

2124
const { t } = useTranslation();
2225

26+
// Here we will pass template list from OnboardingAPI
27+
const allTemplates: ManagedControlPlaneTemplate[] = [];
28+
2329
const handleOpenerClick = (e: Ui5CustomEvent<ButtonDomRef, ButtonClickEventDetail>) => {
2430
if (popoverRef.current && e.currentTarget) {
2531
popoverRef.current.opener = e.currentTarget as HTMLElement;
@@ -34,14 +40,20 @@ export const ControlPlanesListMenu: FC<ControlPlanesListMenuProps> = ({
3440
ref={popoverRef}
3541
open={open}
3642
onItemClick={(event) => {
37-
const action = (event.detail.item as HTMLElement).dataset.action;
43+
const item = event.detail.item as HTMLElement;
44+
const action = item.dataset.action;
3845
if (action === 'newManagedControlPlane') {
46+
setInitialTemplateName(undefined);
47+
setIsCreateManagedControlPlaneWizardOpen(true);
48+
}
49+
if (action === 'newManagedControlPlaneWithTemplate') {
50+
const tplName = item.dataset.templateName || undefined;
51+
setInitialTemplateName(tplName);
3952
setIsCreateManagedControlPlaneWizardOpen(true);
4053
}
4154
if (action === 'deleteWorkspace') {
4255
setDialogDeleteWsIsOpen(true);
4356
}
44-
4557
setOpen(false);
4658
}}
4759
>
@@ -51,6 +63,17 @@ export const ControlPlanesListMenu: FC<ControlPlanesListMenuProps> = ({
5163
data-action="newManagedControlPlane"
5264
icon="add"
5365
/>
66+
{allTemplates.map((tpl) => (
67+
<MenuItem
68+
key={`tpl-${tpl.metadata.name}`}
69+
text={tpl.metadata.name}
70+
title={tpl.metadata.descriptionText || ''}
71+
data-action="newManagedControlPlaneWithTemplate"
72+
data-template-name={tpl.metadata.name}
73+
icon="document-text"
74+
/>
75+
))}
76+
5477
<MenuItem
5578
key={'delete'}
5679
text={t('ControlPlaneListToolbar.deleteWorkspace')}

src/components/ControlPlanes/List/ControlPlaneListWorkspaceGridTile.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import '@ui5/webcomponents-icons/dist/delete';
55
import { CopyButton } from '../../Shared/CopyButton.tsx';
66
import { ControlPlaneCard } from '../ControlPlaneCard/ControlPlaneCard.tsx';
77
import { ListWorkspacesType, isWorkspaceReady } from '../../../lib/api/types/crate/listWorkspaces.ts';
8-
import { useState } from 'react';
8+
import { useMemo, useState } from 'react';
99
import { MembersAvatarView } from './MembersAvatarView.tsx';
1010
import { DeleteWorkspaceResource, DeleteWorkspaceType } from '../../../lib/api/types/crate/deleteWorkspace.ts';
1111
import { useApiResourceMutation, useApiResource } from '../../../lib/api/useApiResource.ts';
@@ -32,6 +32,7 @@ interface Props {
3232

3333
export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Props) {
3434
const [isCreateManagedControlPlaneWizardOpen, setIsCreateManagedControlPlaneWizardOpen] = useState(false);
35+
const [initialTemplateName, setInitialTemplateName] = useState<string | undefined>(undefined);
3536
const workspaceName = workspace.metadata.name;
3637
const workspaceDisplayName = workspace.metadata.annotations?.[DISPLAY_NAME_ANNOTATION] || '';
3738
const showDisplayName = workspaceDisplayName.length > 0;
@@ -68,6 +69,24 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr
6869
return null;
6970
}
7071

72+
const uniqueMembers = useMemo(() => {
73+
const seenKeys = new Set<string>();
74+
const fallbackNamespace = workspace.status?.namespace ?? '';
75+
76+
return (workspace.spec.members ?? []).filter((member: { name?: string; namespace?: string }) => {
77+
const memberNamespace = member?.namespace ?? fallbackNamespace;
78+
const memberName = String(member?.name ?? '')
79+
.trim()
80+
.toLowerCase();
81+
if (!memberName) return false;
82+
83+
const dedupeKey = `${memberNamespace}::${memberName}`;
84+
if (seenKeys.has(dedupeKey)) return false;
85+
seenKeys.add(dedupeKey);
86+
return true;
87+
});
88+
}, [workspace.spec.members, workspace.status?.namespace]);
89+
7190
return (
7291
<>
7392
<ObjectPageSection
@@ -98,7 +117,7 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr
98117

99118
<CopyButton text={workspace.status?.namespace || '-'} style={{ justifyContent: 'start' }} />
100119

101-
<MembersAvatarView members={workspace.spec.members} project={projectName} workspace={workspaceName} />
120+
<MembersAvatarView members={uniqueMembers} project={projectName} workspace={workspaceName} />
102121
<FlexBox justifyContent={'SpaceBetween'} gap={10}>
103122
<YamlViewButtonWithLoader
104123
workspaceName={workspace.metadata.namespace}
@@ -108,6 +127,7 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr
108127
<ControlPlanesListMenu
109128
setDialogDeleteWsIsOpen={setDialogDeleteWsIsOpen}
110129
setIsCreateManagedControlPlaneWizardOpen={setIsCreateManagedControlPlaneWizardOpen}
130+
setInitialTemplateName={setInitialTemplateName}
111131
/>
112132
</FlexBox>
113133
</div>
@@ -168,6 +188,7 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr
168188
setIsOpen={setIsCreateManagedControlPlaneWizardOpen}
169189
projectName={projectNamespace}
170190
workspaceName={workspaceName}
191+
initialTemplateName={initialTemplateName}
171192
/>
172193
</>
173194
);

src/components/Dialogs/CreateProjectWorkspaceDialog.module.css renamed to src/components/Dialogs/MetadataForm.module.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
.affixRow {
2+
display: flex;
3+
gap: 8px;
4+
align-items: center;
5+
}
6+
17
.input {
28
width: 100%;
39
margin-bottom: 2rem;

0 commit comments

Comments
 (0)