Skip to content

Commit d272e62

Browse files
authored
feat: MCP wizard improvements (#293)
1 parent e3eba14 commit d272e62

File tree

14 files changed

+358
-271
lines changed

14 files changed

+358
-271
lines changed

public/locales/en.json

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@
5959
},
6060
"ControlPlaneCard": {
6161
"deleteConfirmationDialog": "MCP deletion triggered. The list will refresh automatically once completed.",
62-
"editMCP": "Edit Managed Control Plane",
63-
"deleteMCP": "Delete Managed Control Plane"
62+
"editMCP": "Edit ManagedControlPlane",
63+
"duplicateMCP": "Duplicate ManagedControlPlane",
64+
"deleteMCP": "Delete ManagedControlPlane"
6465

6566
},
6667
"ControlPlaneListAllWorkspaces": {
@@ -375,18 +376,23 @@
375376
"createMCP": {
376377
"dialogTitle": "Create Managed Control Plane",
377378
"titleText": "Managed Control Plane Created Successfully!",
378-
"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."
379+
"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.",
380+
"copySuffix": "-copy"
379381
},
380382
"editMCP": {
381383
"dialogTitle": "Edit Managed Control Plane",
382384
"titleText": "Managed Control Plane Updated Successfully!",
383-
"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."
385+
"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.",
386+
"editComponents": "Edit components",
387+
"duplicatingMCPInfo1": "Duplicating a <span>ManagedControlPlane</span> will only create a <span>ManagedControlPlane</span> with the same configuration. ",
388+
"duplicatingMCPInfo2": "<b>It will NOT copy the managed resources inside</b>"
384389
},
385390
"componentsSelection": {
386391
"selectComponents": "Select Components",
387392
"selectedComponents": "Selected Components",
388393
"pleaseSelectComponents": "Choose the components you want to add to your Managed Control Plane.",
389-
"cannotLoad": "Cannot load components list"
394+
"cannotLoad": "Cannot load components list",
395+
"noComponentsFound": "No components found matching your search."
390396
},
391397
"Hints": {
392398
"CrossplaneHint": {

src/components/ComponentsSelection/ComponentsSelection.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
101101
/>
102102

103103
<Grid>
104-
<div data-layout-span="XL8 L8 M8 S8">
104+
<div data-layout-span="XL7 L7 M7 S7">
105105
{searchResults.length > 0 ? (
106106
searchResults.map((component) => {
107107
const providerDisabled = isProviderDisabled(component);
@@ -167,13 +167,13 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
167167
);
168168
})
169169
) : (
170-
<Infobox fullWidth variant="success">
171-
<Text>{t('componentsSelection.pleaseSelectComponents')}</Text>
170+
<Infobox fullWidth variant="normal" icon="search">
171+
<Text>{t('componentsSelection.noComponentsFound')}</Text>
172172
</Infobox>
173173
)}
174174
</div>
175175

176-
<div data-layout-span="XL4 L4 M4 S4">
176+
<div data-layout-span="XL5 L5 M5 S5">
177177
{templateDefaultsError ? (
178178
<div style={{ marginBottom: 8 }}>
179179
<IllustratedError title={templateDefaultsError} compact />
@@ -191,7 +191,7 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
191191
))}
192192
</List>
193193
) : (
194-
<Infobox fullWidth variant="success">
194+
<Infobox variant="success" icon="add">
195195
<Text>{t('componentsSelection.pleaseSelectComponents')}</Text>
196196
</Infobox>
197197
)}

src/components/ComponentsSelection/ComponentsSelectionContainer.tsx

Lines changed: 8 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
1-
import React, { useEffect, useMemo, useState } from 'react';
1+
import React from 'react';
22
import { ComponentsSelection } from './ComponentsSelection.tsx';
3-
43
import IllustratedError from '../Shared/IllustratedError.tsx';
5-
import { sortVersions } from '../../utils/componentsVersions.ts';
6-
7-
import { ListManagedComponents } from '../../lib/api/types/crate/listManagedComponents.ts';
8-
import { useApiResource } from '../../lib/api/useApiResource.ts';
94
import Loading from '../Shared/Loading.tsx';
10-
import { ComponentsListItem, removeComponents } from '../../lib/api/types/crate/createManagedControlPlane.ts';
5+
import { ComponentsListItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';
116
import { useTranslation } from 'react-i18next';
12-
import { ManagedControlPlaneTemplate } from '../../lib/api/types/templates/mcpTemplate.ts';
137

148
export interface ComponentsSelectionProps {
159
componentsList: ComponentsListItem[];
1610
setComponentsList: (components: ComponentsListItem[]) => void;
17-
setInitialComponentsList: (components: ComponentsListItem[]) => void;
18-
managedControlPlaneTemplate?: ManagedControlPlaneTemplate;
19-
initialSelection?: Record<string, { isSelected: boolean; version: string }>;
20-
isOnMcpPage?: boolean;
21-
initializedComponents: React.RefObject<boolean>;
11+
isLoading: boolean;
12+
error: unknown;
13+
templateDefaultsError?: string;
2214
}
2315

2416
/**
@@ -34,124 +26,15 @@ export const getSelectedComponents = (components: ComponentsListItem[]) => {
3426
});
3527
};
3628

37-
type TemplateDefaultComponent = {
38-
name: string;
39-
version: string;
40-
removable?: boolean;
41-
versionChangeable?: boolean;
42-
};
43-
4429
export const ComponentsSelectionContainer: React.FC<ComponentsSelectionProps> = ({
4530
setComponentsList,
4631
componentsList,
47-
managedControlPlaneTemplate,
48-
initialSelection,
49-
isOnMcpPage,
50-
setInitialComponentsList,
51-
initializedComponents,
32+
isLoading,
33+
error,
34+
templateDefaultsError,
5235
}) => {
53-
const {
54-
data: availableManagedComponentsListData,
55-
error,
56-
isLoading,
57-
} = useApiResource(ListManagedComponents(), undefined, !!isOnMcpPage);
5836
const { t } = useTranslation();
5937

60-
const [templateDefaultsError, setTemplateDefaultsError] = useState<string | null>(null);
61-
const defaultComponents = useMemo<TemplateDefaultComponent[]>(
62-
() => managedControlPlaneTemplate?.spec?.spec?.components?.defaultComponents ?? [],
63-
[managedControlPlaneTemplate],
64-
);
65-
66-
useEffect(() => {
67-
if (
68-
initializedComponents.current ||
69-
!availableManagedComponentsListData?.items ||
70-
availableManagedComponentsListData.items.length === 0
71-
) {
72-
return;
73-
}
74-
75-
const newComponentsList = availableManagedComponentsListData.items
76-
.map((item) => {
77-
const versions = sortVersions(item.status?.versions ?? []);
78-
const template = defaultComponents.find((dc) => dc.name === (item.metadata?.name ?? ''));
79-
const templateVersion = template?.version;
80-
let selectedVersion = template
81-
? templateVersion && versions.includes(templateVersion)
82-
? templateVersion
83-
: ''
84-
: (versions[0] ?? '');
85-
let isSelected = !!template;
86-
87-
const initSel = initialSelection?.[item.metadata?.name ?? ''];
88-
if (initSel) {
89-
// Override selection and version from initial selection if provided
90-
isSelected = Boolean(initSel.isSelected);
91-
selectedVersion = initSel.version && versions.includes(initSel.version) ? initSel.version : '';
92-
}
93-
return {
94-
name: item.metadata?.name ?? '',
95-
versions,
96-
selectedVersion,
97-
isSelected,
98-
documentationUrl: '',
99-
};
100-
})
101-
.filter((component) => !removeComponents.find((item) => item === component.name));
102-
setInitialComponentsList(newComponentsList);
103-
setComponentsList(newComponentsList);
104-
initializedComponents.current = true;
105-
// eslint-disable-next-line react-hooks/exhaustive-deps
106-
}, [setComponentsList, defaultComponents, initialSelection, availableManagedComponentsListData?.items]);
107-
108-
useEffect(() => {
109-
const items = availableManagedComponentsListData?.items ?? [];
110-
if (items.length === 0 || !defaultComponents.length) {
111-
setTemplateDefaultsError(null);
112-
return;
113-
}
114-
115-
const errors: string[] = [];
116-
defaultComponents.forEach((dc: TemplateDefaultComponent) => {
117-
if (!dc?.name) return;
118-
const item = items.find((it) => it.metadata?.name === dc.name);
119-
if (!item) {
120-
errors.push(`Component "${dc.name}" from template is not available.`);
121-
return;
122-
}
123-
const versions: string[] = Array.isArray(item.status?.versions) ? (item.status?.versions as string[]) : [];
124-
if (dc.version && !versions.includes(dc.version)) {
125-
errors.push(`Component "${dc.name}" version "${dc.version}" from template is not available.`);
126-
}
127-
});
128-
129-
setTemplateDefaultsError(errors.length ? errors.join('\n') : null);
130-
}, [availableManagedComponentsListData, defaultComponents]);
131-
132-
useEffect(() => {
133-
if (!initializedComponents.current) return;
134-
if (!defaultComponents?.length) return;
135-
if (!componentsList?.length) return;
136-
// If initialSelection is provided, do not auto-apply template defaults
137-
if (initialSelection && Object.keys(initialSelection).length > 0) return;
138-
139-
const anySelected = componentsList.some((c) => c.isSelected);
140-
if (anySelected) return;
141-
142-
const updated = componentsList.map((c) => {
143-
const template = defaultComponents.find((dc) => dc.name === c.name);
144-
if (!template) return c;
145-
const templateVersion = template.version;
146-
const selectedVersion =
147-
templateVersion && Array.isArray(c.versions) && c.versions.includes(templateVersion) ? templateVersion : '';
148-
return { ...c, isSelected: true, selectedVersion };
149-
});
150-
151-
setComponentsList(updated);
152-
// eslint-disable-next-line react-hooks/exhaustive-deps
153-
}, [defaultComponents, componentsList, setComponentsList, initialSelection]);
154-
15538
if (isLoading) {
15639
return <Loading />;
15740
}

src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCard.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,22 @@ interface Props {
3838
projectName: string;
3939
}
4040

41-
export function ControlPlaneCard({ controlPlane, workspace, projectName }: Props) {
41+
type MCPWizardState = {
42+
isOpen: boolean;
43+
mode?: 'edit' | 'duplicate';
44+
};
45+
export const ControlPlaneCard = ({ controlPlane, workspace, projectName }: Props) => {
4246
const [dialogDeleteMcpIsOpen, setDialogDeleteMcpIsOpen] = useState(false);
4347
const toast = useToast();
4448
const { t } = useTranslation();
45-
const [isEditManagedControlPlaneWizardOpen, setIsEditManagedControlPlaneWizardOpen] = useState(false);
49+
const [managedControlPlaneWizardState, setManagedControlPlaneWizardState] = useState<MCPWizardState>({
50+
isOpen: false,
51+
mode: undefined,
52+
});
53+
54+
const handleIsManagedControlPlaneWizardOpen = (isOpen: boolean, mode?: 'edit' | 'duplicate') => {
55+
setManagedControlPlaneWizardState({ isOpen, mode });
56+
};
4657
const { trigger: patchTrigger } = useApiResourceMutation<DeleteMCPType>(
4758
PatchMCPResourceForDeletion(controlPlane.metadata.namespace, controlPlane.metadata.name),
4859
);
@@ -85,7 +96,7 @@ export function ControlPlaneCard({ controlPlane, workspace, projectName }: Props
8596
<ControlPlaneCardMenu
8697
setDialogDeleteMcpIsOpen={setDialogDeleteMcpIsOpen}
8798
isDeleteMcpButtonDisabled={controlPlane.status?.status === ReadyStatus.InDeletion}
88-
setIsEditManagedControlPlaneWizardOpen={setIsEditManagedControlPlaneWizardOpen}
99+
setIsEditManagedControlPlaneWizardOpen={handleIsManagedControlPlaneWizardOpen}
89100
/>
90101
<FlexBox direction="Row" justifyContent="SpaceBetween" alignItems="Center" gap={10}>
91102
<YamlViewButtonWithLoader
@@ -130,11 +141,12 @@ export function ControlPlaneCard({ controlPlane, workspace, projectName }: Props
130141
}}
131142
/>
132143
<EditManagedControlPlaneWizardDataLoader
133-
isOpen={isEditManagedControlPlaneWizardOpen}
134-
setIsOpen={setIsEditManagedControlPlaneWizardOpen}
144+
isOpen={managedControlPlaneWizardState.isOpen}
145+
setIsOpen={(isOpen) => handleIsManagedControlPlaneWizardOpen(isOpen)}
135146
workspaceName={namespace}
136147
resourceName={name}
148+
mode={managedControlPlaneWizardState.mode}
137149
/>
138150
</>
139151
);
140-
}
152+
};

src/components/ControlPlanes/ControlPlaneCard/ControlPlaneCardMenu.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
99
type ControlPlanesListMenuProps = {
1010
setDialogDeleteMcpIsOpen: Dispatch<SetStateAction<boolean>>;
1111
isDeleteMcpButtonDisabled: boolean;
12-
setIsEditManagedControlPlaneWizardOpen: Dispatch<SetStateAction<boolean>>;
12+
setIsEditManagedControlPlaneWizardOpen: (isOpen: boolean, mode?: 'edit' | 'duplicate') => void;
1313
};
1414

1515
export const ControlPlaneCardMenu: FC<ControlPlanesListMenuProps> = ({
@@ -34,7 +34,10 @@ export const ControlPlaneCardMenu: FC<ControlPlanesListMenuProps> = ({
3434
onItemClick={(event) => {
3535
const action = (event.detail.item as HTMLElement).dataset.action;
3636
if (action === 'editMcp') {
37-
setIsEditManagedControlPlaneWizardOpen(true);
37+
setIsEditManagedControlPlaneWizardOpen(true, 'edit');
38+
}
39+
if (action === 'duplicateMcp') {
40+
setIsEditManagedControlPlaneWizardOpen(true, 'duplicate');
3841
}
3942
if (action === 'deleteMcp') {
4043
setDialogDeleteMcpIsOpen(true);
@@ -53,6 +56,7 @@ export const ControlPlaneCardMenu: FC<ControlPlanesListMenuProps> = ({
5356
icon="delete"
5457
disabled={isDeleteMcpButtonDisabled}
5558
/>
59+
<MenuItem key={'duplicate'} text={t('ControlPlaneCard.duplicateMCP')} data-action="duplicateMcp" icon="copy" />
5660
<MenuItem
5761
key={'edit'}
5862
text={t('ControlPlaneCard.editMCP')}
Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,35 @@
11
.infobox {
2-
display: inline-block;
3-
border: 1px solid;
4-
margin: 1rem 0;
2+
display: flex;
3+
align-items: center;
4+
border-radius: 1rem;
5+
padding: 1rem;
6+
margin: 0;
7+
overflow: hidden;
8+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
9+
margin-bottom: 2rem;
10+
}
11+
12+
.icon {
13+
flex-shrink: 0;
14+
width: 1.5rem;
15+
height: 1.5rem;
16+
color: var(--sapBackgroundColor);
17+
margin-right: 1rem;
18+
}
19+
20+
.content {
21+
flex-grow: 1;
22+
padding-right: 0.5rem;
23+
color: var(--sapBackgroundColor);
524
}
625

726
.full-width {
8-
display: block;
9-
margin: 0;
27+
width: 100%;
28+
margin: 1rem 0;
1029
}
1130

1231
.size-sm {
13-
padding: 0.5rem 0.75rem;
32+
padding: 0.75rem 1rem;
1433
font-size: 0.875rem;
1534
}
1635

@@ -21,29 +40,30 @@
2140

2241
.size-lg {
2342
padding: 1.5rem 2rem;
24-
font-size: 1.25rem;
43+
font-size: 1.125rem;
2544
}
2645

2746
.variant-normal {
28-
border-color: var(--sapNeutralTextColor);
29-
background-color: var(--sapNeutralBackground);
30-
color: var(--sapNeutralTextColor);
47+
background: var(--sapNeutralTextColor);
3148
}
3249

3350
.variant-success {
34-
border-color: var(--sapPositiveTextColor);
35-
background-color: var(--sapPositiveBackground);
36-
color: var(--sapPositiveTextColor);
51+
background: var(--sapPositiveTextColor);
3752
}
3853

3954
.variant-warning {
40-
border-color: var(--sapCriticalTextColor);
41-
background-color: var(--sapCriticalBackground);
42-
color: var(--sapCriticalTextColor);
55+
background: var(--sapCriticalTextColor);
4356
}
4457

4558
.variant-danger {
46-
border-color: var(--sapNegativeTextColor);
47-
background-color: var(--sapErrorBackground);
48-
color: var(--sapNegativeTextColor);
59+
background: var(--sapErrorTextColor);
60+
}
61+
62+
.content > *:not(:last-child) {
63+
margin-bottom: 1rem;
64+
}
65+
66+
.content > * {
67+
color: var(--sapBackgroundColor);
68+
line-height: 1.2rem;
4969
}

0 commit comments

Comments
 (0)