Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@
"componentsSelection": {
"selectComponents": "Select Components",
"selectedComponents": "Selected Components",
"pleaseSelectComponents": "Choose the components you want to add to your Managed Control Plane."
"pleaseSelectComponents": "Choose the components you want to add to your Managed Control Plane.",
"cannotLoad": "Cannot load components list"
}
}
205 changes: 123 additions & 82 deletions src/components/ComponentsSelection/ComponentsSelection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useMemo, useCallback } from 'react';
import {
CheckBox,
Select,
Expand All @@ -20,58 +20,78 @@ import {
import styles from './ComponentsSelection.module.css';
import { Infobox } from '../Ui/Infobox/Infobox.tsx';
import { useTranslation } from 'react-i18next';
import { ComponentSelectionItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';
import { ComponentsListItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';
import { getSelectedComponents } from './ComponentsSelectionContainer.tsx';

export interface ComponentsSelectionProps {
components: ComponentSelectionItem[];
setSelectedComponents: React.Dispatch<
React.SetStateAction<ComponentSelectionItem[]>
>;
componentsList: ComponentsListItem[];
setComponentsList: (components: ComponentsListItem[]) => void;
}

export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
components,
setSelectedComponents,
componentsList,
setComponentsList,
}) => {
const [searchTerm, setSearchTerm] = useState('');
const { t } = useTranslation();
const handleSelectionChange = (
e: Ui5CustomEvent<CheckBoxDomRef, { checked: boolean }>,
) => {
const id = e.target?.id;
setSelectedComponents((prev) =>
prev.map((component) =>
component.name === id
? { ...component, isSelected: !component.isSelected }
: component,
),
);
};

const handleSearch = (e: Ui5CustomEvent<InputDomRef, never>) => {
setSearchTerm(e.target.value.trim());
};
const selectedComponents = useMemo(
() => getSelectedComponents(componentsList),
[componentsList],
);

const handleVersionChange = (
e: Ui5CustomEvent<SelectDomRef, { selectedOption: HTMLElement }>,
) => {
const selectedOption = e.detail.selectedOption as HTMLElement;
const name = selectedOption.dataset.name;
const version = selectedOption.dataset.version;
setSelectedComponents((prev) =>
prev.map((component) =>
component.name === name
? { ...component, selectedVersion: version || '' }
: component,
),
const searchResults = useMemo(() => {
const lowerSearch = searchTerm.toLowerCase();
return componentsList.filter(({ name }) =>
name.toLowerCase().includes(lowerSearch),
);
};
}, [componentsList, searchTerm]);

const filteredComponents = components.filter(({ name }) =>
name.includes(searchTerm),
const handleSelectionChange = useCallback(
(e: Ui5CustomEvent<CheckBoxDomRef, { checked: boolean }>) => {
const id = e.target?.id;
if (!id) return;
setComponentsList(
componentsList.map((component) =>
component.name === id
? { ...component, isSelected: !component.isSelected }
: component,
),
);
},
[componentsList, setComponentsList],
);
const selectedComponents = components.filter(
(component) => component.isSelected,

const handleSearch = useCallback((e: Ui5CustomEvent<InputDomRef, never>) => {
setSearchTerm(e.target.value.trim());
}, []);

const handleVersionChange = useCallback(
(e: Ui5CustomEvent<SelectDomRef, { selectedOption: HTMLElement }>) => {
const selectedOption = e.detail.selectedOption as HTMLElement;
const name = selectedOption.dataset.name;
const version = selectedOption.dataset.version;
if (!name) return;
setComponentsList(
componentsList.map((component) =>
component.name === name
? { ...component, selectedVersion: version || '' }
: component,
),
);
},
[componentsList, setComponentsList],
);

const isProviderDisabled = useCallback(
(component: ComponentsListItem) => {
if (!component.name?.includes('provider')) return false;
const crossplane = componentsList.find(
({ name }) => name === 'crossplane',
);
return crossplane?.isSelected === false;
},
[componentsList],
);

return (
Expand All @@ -83,54 +103,75 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
id="search"
showClearIcon
icon={<Icon name="search" />}
value={searchTerm}
aria-label={t('common.search')}
onInput={handleSearch}
/>

<Grid>
<div data-layout-span="XL8 L8 M8 S8">
{filteredComponents.map((component) => (
<FlexBox
key={component.name}
className={styles.row}
gap={10}
justifyContent="SpaceBetween"
>
<CheckBox
valueState="None"
text={component.name}
id={component.name}
checked={component.isSelected}
onChange={handleSelectionChange}
/>
<FlexBox
gap={10}
justifyContent="SpaceBetween"
alignItems="Baseline"
>
{/*This button will be implemented later*/}
{component.documentationUrl && (
<Button design="Transparent">
{t('common.documentation')}
</Button>
)}
<Select
value={component.selectedVersion}
onChange={handleVersionChange}
{searchResults.length > 0 ? (
searchResults.map((component) => {
const providerDisabled = isProviderDisabled(component);
return (
<FlexBox
key={component.name}
className={styles.row}
gap={10}
justifyContent="SpaceBetween"
data-testid={`component-row-${component.name}`}
>
{component.versions.map((version) => (
<Option
key={version}
data-version={version}
data-name={component.name}
selected={component.selectedVersion === version}
<CheckBox
valueState="None"
text={component.name}
id={component.name}
checked={component.isSelected}
disabled={providerDisabled}
aria-label={component.name}
onChange={handleSelectionChange}
/>
<FlexBox
gap={10}
justifyContent="SpaceBetween"
alignItems="Baseline"
>
{/* TODO: Add documentation link */}
{component.documentationUrl && (
<Button
design="Transparent"
rel="noopener noreferrer"
aria-label={t('common.documentation')}
tabIndex={0}
>
{t('common.documentation')}
</Button>
)}
<Select
value={component.selectedVersion}
disabled={!component.isSelected || providerDisabled}
aria-label={`${component.name} version`}
onChange={handleVersionChange}
>
{version}
</Option>
))}
</Select>
</FlexBox>
</FlexBox>
))}
{component.versions.map((version) => (
<Option
key={version}
data-version={version}
data-name={component.name}
selected={component.selectedVersion === version}
>
{version}
</Option>
))}
</Select>
</FlexBox>
</FlexBox>
);
})
) : (
<Infobox fullWidth variant="success">
<Text>{t('componentsSelection.pleaseSelectComponents')}</Text>
</Infobox>
)}
</div>
<div data-layout-span="XL4 L4 M4 S4">
{selectedComponents.length > 0 ? (
Expand All @@ -144,7 +185,7 @@ export const ComponentsSelection: React.FC<ComponentsSelectionProps> = ({
))}
</List>
) : (
<Infobox fullWidth variant={'success'}>
<Infobox fullWidth variant="success">
<Text>{t('componentsSelection.pleaseSelectComponents')}</Text>
</Infobox>
)}
Expand Down
93 changes: 57 additions & 36 deletions src/components/ComponentsSelection/ComponentsSelectionContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef } from 'react';
import { ComponentsSelection } from './ComponentsSelection.tsx';

import IllustratedError from '../Shared/IllustratedError.tsx';
Expand All @@ -7,64 +7,85 @@ import { sortVersions } from '../../utils/componentsVersions.ts';
import { ListManagedComponents } from '../../lib/api/types/crate/listManagedComponents.ts';
import useApiResource from '../../lib/api/useApiResource.ts';
import Loading from '../Shared/Loading.tsx';
import { ComponentSelectionItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';

export interface ComponentItem {
name: string;
versions: string[];
}
import { ComponentsListItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';
import { useTranslation } from 'react-i18next';

export interface ComponentsSelectionProps {
selectedComponents: ComponentSelectionItem[];
setSelectedComponents: React.Dispatch<
React.SetStateAction<ComponentSelectionItem[]>
>;
componentsList: ComponentsListItem[];
setComponentsList: (components: ComponentsListItem[]) => void;
}

/**
* Returns the selected components. If Crossplane is not selected,
* provider components are excluded.
*/
export const getSelectedComponents = (components: ComponentsListItem[]) => {
const isCrossplaneSelected = components.some(
({ name, isSelected }) => name === 'crossplane' && isSelected,
);
return components.filter((component) => {
if (!component.isSelected) return false;
if (component.name?.includes('provider') && !isCrossplaneSelected) {
return false;
}
return true;
});
};

export const ComponentsSelectionContainer: React.FC<
ComponentsSelectionProps
> = ({ setSelectedComponents, selectedComponents }) => {
> = ({ setComponentsList, componentsList }) => {
const {
data: allManagedComponents,
data: availableManagedComponentsListData,
error,
isLoading,
} = useApiResource(ListManagedComponents());
const [isReady, setIsReady] = useState(false);
const { t } = useTranslation();
const initialized = useRef(false);

useEffect(() => {
if (
allManagedComponents?.items.length === 0 ||
!allManagedComponents?.items ||
isReady
)
initialized.current ||
!availableManagedComponentsListData?.items ||
availableManagedComponentsListData.items.length === 0
) {
return;
}

setSelectedComponents(
allManagedComponents?.items?.map((item) => {
const newComponentsList = availableManagedComponentsListData.items.map(
(item) => {
const versions = sortVersions(item.status.versions);
return {
name: item.metadata.name,
versions: versions,
selectedVersion: versions[0],
versions,
selectedVersion: versions[0] ?? '',
isSelected: false,
documentationUrl: '',
};
}) ?? [],
},
);
setIsReady(true);
}, [allManagedComponents, isReady, setSelectedComponents]);

setComponentsList(newComponentsList);
initialized.current = true;
}, [availableManagedComponentsListData, setComponentsList]);

if (isLoading) {
return <Loading />;
}
if (error) return <IllustratedError />;

if (error) {
return <IllustratedError />;
}

// Defensive: If the API returned no items, show error
if (!componentsList || componentsList.length === 0) {
return <IllustratedError title={t('componentsSelection.cannotLoad')} />;
}

return (
<>
{selectedComponents.length > 0 ? (
<ComponentsSelection
components={selectedComponents}
setSelectedComponents={setSelectedComponents}
/>
) : (
<IllustratedError title={'Cannot load components list'} />
)}
</>
<ComponentsSelection
componentsList={componentsList}
setComponentsList={setComponentsList}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { useLink } from '../../../lib/shared/useLink.ts';
import IllustrationMessageType from '@ui5/webcomponents-fiori/dist/types/IllustrationMessageType.js';
import styles from './WorkspacesList.module.css';
import { ControlPlanesListMenu } from '../ControlPlanesListMenu.tsx';
import { CreateManagedControlPlaneWizardContainer } from '../../Wizards/CreateManagedControlPlaneWizardContainer.tsx';
import { CreateManagedControlPlaneWizardContainer } from '../../Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx';

interface Props {
projectName: string;
Expand Down
Loading
Loading