Skip to content

Commit 6cf174f

Browse files
authored
feat: Fix for the providers selection and MCP create form improvements (#170)
1 parent 99131bb commit 6cf174f

File tree

8 files changed

+367
-231
lines changed

8 files changed

+367
-231
lines changed

public/locales/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@
308308
"componentsSelection": {
309309
"selectComponents": "Select Components",
310310
"selectedComponents": "Selected Components",
311-
"pleaseSelectComponents": "Choose the components you want to add to your Managed Control Plane."
311+
"pleaseSelectComponents": "Choose the components you want to add to your Managed Control Plane.",
312+
"cannotLoad": "Cannot load components list"
312313
}
313314
}

src/components/ComponentsSelection/ComponentsSelection.tsx

Lines changed: 123 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useMemo, useCallback } from 'react';
22
import {
33
CheckBox,
44
Select,
@@ -20,58 +20,78 @@ import {
2020
import styles from './ComponentsSelection.module.css';
2121
import { Infobox } from '../Ui/Infobox/Infobox.tsx';
2222
import { useTranslation } from 'react-i18next';
23-
import { ComponentSelectionItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';
23+
import { ComponentsListItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';
24+
import { getSelectedComponents } from './ComponentsSelectionContainer.tsx';
2425

2526
export interface ComponentsSelectionProps {
26-
components: ComponentSelectionItem[];
27-
setSelectedComponents: React.Dispatch<
28-
React.SetStateAction<ComponentSelectionItem[]>
29-
>;
27+
componentsList: ComponentsListItem[];
28+
setComponentsList: (components: ComponentsListItem[]) => void;
3029
}
3130

3231
export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
33-
components,
34-
setSelectedComponents,
32+
componentsList,
33+
setComponentsList,
3534
}) => {
3635
const [searchTerm, setSearchTerm] = useState('');
3736
const { t } = useTranslation();
38-
const handleSelectionChange = (
39-
e: Ui5CustomEvent<CheckBoxDomRef, { checked: boolean }>,
40-
) => {
41-
const id = e.target?.id;
42-
setSelectedComponents((prev) =>
43-
prev.map((component) =>
44-
component.name === id
45-
? { ...component, isSelected: !component.isSelected }
46-
: component,
47-
),
48-
);
49-
};
5037

51-
const handleSearch = (e: Ui5CustomEvent<InputDomRef, never>) => {
52-
setSearchTerm(e.target.value.trim());
53-
};
38+
const selectedComponents = useMemo(
39+
() => getSelectedComponents(componentsList),
40+
[componentsList],
41+
);
5442

55-
const handleVersionChange = (
56-
e: Ui5CustomEvent<SelectDomRef, { selectedOption: HTMLElement }>,
57-
) => {
58-
const selectedOption = e.detail.selectedOption as HTMLElement;
59-
const name = selectedOption.dataset.name;
60-
const version = selectedOption.dataset.version;
61-
setSelectedComponents((prev) =>
62-
prev.map((component) =>
63-
component.name === name
64-
? { ...component, selectedVersion: version || '' }
65-
: component,
66-
),
43+
const searchResults = useMemo(() => {
44+
const lowerSearch = searchTerm.toLowerCase();
45+
return componentsList.filter(({ name }) =>
46+
name.toLowerCase().includes(lowerSearch),
6747
);
68-
};
48+
}, [componentsList, searchTerm]);
6949

70-
const filteredComponents = components.filter(({ name }) =>
71-
name.includes(searchTerm),
50+
const handleSelectionChange = useCallback(
51+
(e: Ui5CustomEvent<CheckBoxDomRef, { checked: boolean }>) => {
52+
const id = e.target?.id;
53+
if (!id) return;
54+
setComponentsList(
55+
componentsList.map((component) =>
56+
component.name === id
57+
? { ...component, isSelected: !component.isSelected }
58+
: component,
59+
),
60+
);
61+
},
62+
[componentsList, setComponentsList],
7263
);
73-
const selectedComponents = components.filter(
74-
(component) => component.isSelected,
64+
65+
const handleSearch = useCallback((e: Ui5CustomEvent<InputDomRef, never>) => {
66+
setSearchTerm(e.target.value.trim());
67+
}, []);
68+
69+
const handleVersionChange = useCallback(
70+
(e: Ui5CustomEvent<SelectDomRef, { selectedOption: HTMLElement }>) => {
71+
const selectedOption = e.detail.selectedOption as HTMLElement;
72+
const name = selectedOption.dataset.name;
73+
const version = selectedOption.dataset.version;
74+
if (!name) return;
75+
setComponentsList(
76+
componentsList.map((component) =>
77+
component.name === name
78+
? { ...component, selectedVersion: version || '' }
79+
: component,
80+
),
81+
);
82+
},
83+
[componentsList, setComponentsList],
84+
);
85+
86+
const isProviderDisabled = useCallback(
87+
(component: ComponentsListItem) => {
88+
if (!component.name?.includes('provider')) return false;
89+
const crossplane = componentsList.find(
90+
({ name }) => name === 'crossplane',
91+
);
92+
return crossplane?.isSelected === false;
93+
},
94+
[componentsList],
7595
);
7696

7797
return (
@@ -83,54 +103,75 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
83103
id="search"
84104
showClearIcon
85105
icon={<Icon name="search" />}
106+
value={searchTerm}
107+
aria-label={t('common.search')}
86108
onInput={handleSearch}
87109
/>
88110

89111
<Grid>
90112
<div data-layout-span="XL8 L8 M8 S8">
91-
{filteredComponents.map((component) => (
92-
<FlexBox
93-
key={component.name}
94-
className={styles.row}
95-
gap={10}
96-
justifyContent="SpaceBetween"
97-
>
98-
<CheckBox
99-
valueState="None"
100-
text={component.name}
101-
id={component.name}
102-
checked={component.isSelected}
103-
onChange={handleSelectionChange}
104-
/>
105-
<FlexBox
106-
gap={10}
107-
justifyContent="SpaceBetween"
108-
alignItems="Baseline"
109-
>
110-
{/*This button will be implemented later*/}
111-
{component.documentationUrl && (
112-
<Button design="Transparent">
113-
{t('common.documentation')}
114-
</Button>
115-
)}
116-
<Select
117-
value={component.selectedVersion}
118-
onChange={handleVersionChange}
113+
{searchResults.length > 0 ? (
114+
searchResults.map((component) => {
115+
const providerDisabled = isProviderDisabled(component);
116+
return (
117+
<FlexBox
118+
key={component.name}
119+
className={styles.row}
120+
gap={10}
121+
justifyContent="SpaceBetween"
122+
data-testid={`component-row-${component.name}`}
119123
>
120-
{component.versions.map((version) => (
121-
<Option
122-
key={version}
123-
data-version={version}
124-
data-name={component.name}
125-
selected={component.selectedVersion === version}
124+
<CheckBox
125+
valueState="None"
126+
text={component.name}
127+
id={component.name}
128+
checked={component.isSelected}
129+
disabled={providerDisabled}
130+
aria-label={component.name}
131+
onChange={handleSelectionChange}
132+
/>
133+
<FlexBox
134+
gap={10}
135+
justifyContent="SpaceBetween"
136+
alignItems="Baseline"
137+
>
138+
{/* TODO: Add documentation link */}
139+
{component.documentationUrl && (
140+
<Button
141+
design="Transparent"
142+
rel="noopener noreferrer"
143+
aria-label={t('common.documentation')}
144+
tabIndex={0}
145+
>
146+
{t('common.documentation')}
147+
</Button>
148+
)}
149+
<Select
150+
value={component.selectedVersion}
151+
disabled={!component.isSelected || providerDisabled}
152+
aria-label={`${component.name} version`}
153+
onChange={handleVersionChange}
126154
>
127-
{version}
128-
</Option>
129-
))}
130-
</Select>
131-
</FlexBox>
132-
</FlexBox>
133-
))}
155+
{component.versions.map((version) => (
156+
<Option
157+
key={version}
158+
data-version={version}
159+
data-name={component.name}
160+
selected={component.selectedVersion === version}
161+
>
162+
{version}
163+
</Option>
164+
))}
165+
</Select>
166+
</FlexBox>
167+
</FlexBox>
168+
);
169+
})
170+
) : (
171+
<Infobox fullWidth variant="success">
172+
<Text>{t('componentsSelection.pleaseSelectComponents')}</Text>
173+
</Infobox>
174+
)}
134175
</div>
135176
<div data-layout-span="XL4 L4 M4 S4">
136177
{selectedComponents.length > 0 ? (
@@ -144,7 +185,7 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
144185
))}
145186
</List>
146187
) : (
147-
<Infobox fullWidth variant={'success'}>
188+
<Infobox fullWidth variant="success">
148189
<Text>{t('componentsSelection.pleaseSelectComponents')}</Text>
149190
</Infobox>
150191
)}
Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from 'react';
1+
import React, { useEffect, useRef } from 'react';
22
import { ComponentsSelection } from './ComponentsSelection.tsx';
33

44
import IllustratedError from '../Shared/IllustratedError.tsx';
@@ -7,64 +7,85 @@ import { sortVersions } from '../../utils/componentsVersions.ts';
77
import { ListManagedComponents } from '../../lib/api/types/crate/listManagedComponents.ts';
88
import useApiResource from '../../lib/api/useApiResource.ts';
99
import Loading from '../Shared/Loading.tsx';
10-
import { ComponentSelectionItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';
11-
12-
export interface ComponentItem {
13-
name: string;
14-
versions: string[];
15-
}
10+
import { ComponentsListItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';
11+
import { useTranslation } from 'react-i18next';
1612

1713
export interface ComponentsSelectionProps {
18-
selectedComponents: ComponentSelectionItem[];
19-
setSelectedComponents: React.Dispatch<
20-
React.SetStateAction<ComponentSelectionItem[]>
21-
>;
14+
componentsList: ComponentsListItem[];
15+
setComponentsList: (components: ComponentsListItem[]) => void;
2216
}
17+
18+
/**
19+
* Returns the selected components. If Crossplane is not selected,
20+
* provider components are excluded.
21+
*/
22+
export const getSelectedComponents = (components: ComponentsListItem[]) => {
23+
const isCrossplaneSelected = components.some(
24+
({ name, isSelected }) => name === 'crossplane' && isSelected,
25+
);
26+
return components.filter((component) => {
27+
if (!component.isSelected) return false;
28+
if (component.name?.includes('provider') && !isCrossplaneSelected) {
29+
return false;
30+
}
31+
return true;
32+
});
33+
};
34+
2335
export const ComponentsSelectionContainer: React.FC<
2436
ComponentsSelectionProps
25-
> = ({ setSelectedComponents, selectedComponents }) => {
37+
> = ({ setComponentsList, componentsList }) => {
2638
const {
27-
data: allManagedComponents,
39+
data: availableManagedComponentsListData,
2840
error,
2941
isLoading,
3042
} = useApiResource(ListManagedComponents());
31-
const [isReady, setIsReady] = useState(false);
43+
const { t } = useTranslation();
44+
const initialized = useRef(false);
45+
3246
useEffect(() => {
3347
if (
34-
allManagedComponents?.items.length === 0 ||
35-
!allManagedComponents?.items ||
36-
isReady
37-
)
48+
initialized.current ||
49+
!availableManagedComponentsListData?.items ||
50+
availableManagedComponentsListData.items.length === 0
51+
) {
3852
return;
53+
}
3954

40-
setSelectedComponents(
41-
allManagedComponents?.items?.map((item) => {
55+
const newComponentsList = availableManagedComponentsListData.items.map(
56+
(item) => {
4257
const versions = sortVersions(item.status.versions);
4358
return {
4459
name: item.metadata.name,
45-
versions: versions,
46-
selectedVersion: versions[0],
60+
versions,
61+
selectedVersion: versions[0] ?? '',
4762
isSelected: false,
4863
documentationUrl: '',
4964
};
50-
}) ?? [],
65+
},
5166
);
52-
setIsReady(true);
53-
}, [allManagedComponents, isReady, setSelectedComponents]);
67+
68+
setComponentsList(newComponentsList);
69+
initialized.current = true;
70+
}, [availableManagedComponentsListData, setComponentsList]);
71+
5472
if (isLoading) {
5573
return <Loading />;
5674
}
57-
if (error) return <IllustratedError />;
75+
76+
if (error) {
77+
return <IllustratedError />;
78+
}
79+
80+
// Defensive: If the API returned no items, show error
81+
if (!componentsList || componentsList.length === 0) {
82+
return <IllustratedError title={t('componentsSelection.cannotLoad')} />;
83+
}
84+
5885
return (
59-
<>
60-
{selectedComponents.length > 0 ? (
61-
<ComponentsSelection
62-
components={selectedComponents}
63-
setSelectedComponents={setSelectedComponents}
64-
/>
65-
) : (
66-
<IllustratedError title={'Cannot load components list'} />
67-
)}
68-
</>
86+
<ComponentsSelection
87+
componentsList={componentsList}
88+
setComponentsList={setComponentsList}
89+
/>
6990
);
7091
};

src/components/ControlPlanes/List/ControlPlaneListWorkspaceGridTile.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { useLink } from '../../../lib/shared/useLink.ts';
3939
import IllustrationMessageType from '@ui5/webcomponents-fiori/dist/types/IllustrationMessageType.js';
4040
import styles from './WorkspacesList.module.css';
4141
import { ControlPlanesListMenu } from '../ControlPlanesListMenu.tsx';
42-
import { CreateManagedControlPlaneWizardContainer } from '../../Wizards/CreateManagedControlPlaneWizardContainer.tsx';
42+
import { CreateManagedControlPlaneWizardContainer } from '../../Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx';
4343

4444
interface Props {
4545
projectName: string;

0 commit comments

Comments
 (0)