debian-image-builder-frontend/src/Components/CreateImageWizardV2/steps/Packages/Packages.tsx
regexowl b198768ca6 src: Move content urls to const
This moves both preview and stable links to content to the `constants.ts` file.
2024-07-01 13:54:15 +02:00

1419 lines
42 KiB
TypeScript

import React, { ReactElement, useEffect, useMemo, useState } from 'react';
import {
Bullseye,
Button,
EmptyState,
EmptyStateActions,
EmptyStateBody,
EmptyStateFooter,
EmptyStateHeader,
EmptyStateIcon,
EmptyStateVariant,
Icon,
InputGroup,
InputGroupItem,
InputGroupText,
Pagination,
PaginationVariant,
Popover,
Spinner,
Stack,
Text,
TextContent,
TextInput,
ToggleGroup,
ToggleGroupItem,
Toolbar,
ToolbarContent,
ToolbarItem,
} from '@patternfly/react-core';
import { Modal } from '@patternfly/react-core';
import {
ExternalLinkAltIcon,
HelpIcon,
OptimizeIcon,
SearchIcon,
TimesIcon,
} from '@patternfly/react-icons';
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import { useDispatch } from 'react-redux';
import CustomHelperText from './components/CustomHelperText';
import PackageInfoNotAvailablePopover from './components/PackageInfoNotAvailablePopover';
import {
CONTENT_BETA,
CONTENT_STABLE,
EPEL_8_REPO_DEFINITION,
EPEL_9_REPO_DEFINITION,
RH_ICON_SIZE,
} from '../../../../constants';
import {
ApiRepositoryResponseRead,
useCreateRepositoryMutation,
useListRepositoriesQuery,
useSearchRpmMutation,
useSearchPackageGroupMutation,
} from '../../../../store/contentSourcesApi';
import { useAppSelector } from '../../../../store/hooks';
import {
Package,
useGetArchitecturesQuery,
} from '../../../../store/imageBuilderApi';
import {
selectArchitecture,
selectPackages,
selectGroups,
selectCustomRepositories,
selectDistribution,
addPackage,
removePackage,
addGroup,
removeGroup,
addRecommendedRepository,
removeRecommendedRepository,
selectRecommendedRepositories,
} from '../../../../store/wizardSlice';
import useDebounce from '../../../../Utilities/useDebounce';
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
type PackageRepository = 'distro' | 'custom' | 'recommended' | '';
export type IBPackageWithRepositoryInfo = {
name: Package['name'];
summary: Package['summary'];
repository: PackageRepository;
};
export type GroupWithRepositoryInfo = {
name: string;
description: string;
repository: PackageRepository;
package_list: string[];
};
export enum RepoToggle {
INCLUDED = 'toggle-included-repos',
OTHER = 'toggle-other-repos',
}
export const RedHatRepository = () => {
return (
<>
{' '}
<img
src={'/apps/frontend-assets/red-hat-logos/logo_hat-only.svg'}
alt="Red Hat logo"
height={RH_ICON_SIZE}
width={RH_ICON_SIZE}
/>{' '}
Red Hat repository
</>
);
};
const Packages = () => {
const dispatch = useDispatch();
const arch = useAppSelector(selectArchitecture);
const distribution = useAppSelector(selectDistribution);
const customRepositories = useAppSelector(selectCustomRepositories);
const recommendedRepositories = useAppSelector(selectRecommendedRepositories);
const packages = useAppSelector(selectPackages);
const groups = useAppSelector(selectGroups);
const { data: distroRepositories, isSuccess: isSuccessDistroRepositories } =
useGetArchitecturesQuery({
distribution: distribution,
});
// select the correct version of EPEL repository
// the urls are copied over from the content service
const epelRepoUrlByDistribution = distribution.startsWith('rhel-8')
? EPEL_8_REPO_DEFINITION.url
: EPEL_9_REPO_DEFINITION.url;
const { data: epelRepo, isSuccess: isSuccessEpelRepo } =
useListRepositoriesQuery({
url: epelRepoUrlByDistribution,
});
const [isRepoModalOpen, setIsRepoModalOpen] = useState(false);
const [isSelectingPackage, setIsSelectingPackage] = useState<
IBPackageWithRepositoryInfo | undefined
>();
const [isSelectingGroup, setIsSelectingGroup] = useState<
GroupWithRepositoryInfo | undefined
>();
const [perPage, setPerPage] = useState(10);
const [page, setPage] = useState(1);
const [toggleSelected, setToggleSelected] = useState('toggle-available');
const [toggleSourceRepos, setToggleSourceRepos] = useState<RepoToggle>(
RepoToggle.INCLUDED
);
const [searchTerm, setSearchTerm] = useState('');
const [
searchCustomRpms,
{
data: dataCustomPackages,
isSuccess: isSuccessCustomPackages,
isLoading: isLoadingCustomPackages,
},
] = useSearchRpmMutation();
const debouncedSearchTerm = useDebounce(searchTerm.trim());
const debouncedSearchTermLengthOf1 = debouncedSearchTerm.length === 1;
const debouncedSearchTermIsGroup = debouncedSearchTerm.startsWith('@');
// While it's searching for packages or groups, only show either packages or groups, without mixing the two.
const showPackages =
(debouncedSearchTerm && !debouncedSearchTermIsGroup) ||
toggleSelected === 'toggle-selected';
const showGroups =
(debouncedSearchTerm && debouncedSearchTermIsGroup) ||
toggleSelected === 'toggle-selected';
const [
searchRecommendedRpms,
{
data: dataRecommendedPackages,
isSuccess: isSuccessRecommendedPackages,
isLoading: isLoadingRecommendedPackages,
},
] = useSearchRpmMutation();
const [
searchDistroRpms,
{
data: dataDistroPackages,
isSuccess: isSuccessDistroPackages,
isLoading: isLoadingDistroPackages,
},
] = useSearchRpmMutation();
const [
searchDistroGroups,
{
data: dataDistroGroups,
isSuccess: isSuccessDistroGroups,
isLoading: isLoadingDistroGroups,
},
] = useSearchPackageGroupMutation();
const [
searchCustomGroups,
{
data: dataCustomGroups,
isSuccess: isSuccessCustomGroups,
isLoading: isLoadingCustomGroups,
},
] = useSearchPackageGroupMutation();
const [
searchRecommendedGroups,
{
data: dataRecommendedGroups,
isSuccess: isSuccessRecommendedGroups,
isLoading: isLoadingRecommendedGroups,
},
] = useSearchPackageGroupMutation();
const [createRepository, { isLoading: createLoading }] =
useCreateRepositoryMutation();
const sortfn = (a: string, b: string) => {
const aPkg = a.toLowerCase();
const bPkg = b.toLowerCase();
// check exact match first
if (aPkg === debouncedSearchTerm) {
return -1;
}
if (bPkg === debouncedSearchTerm) {
return 1;
}
// check for packages that start with the search term
if (
aPkg.startsWith(debouncedSearchTerm) &&
!bPkg.startsWith(debouncedSearchTerm)
) {
return -1;
}
if (
bPkg.startsWith(debouncedSearchTerm) &&
!aPkg.startsWith(debouncedSearchTerm)
) {
return 1;
}
// if both (or neither) start with the search term
// sort alphabetically
if (aPkg < bPkg) {
return -1;
}
if (bPkg < aPkg) {
return 1;
}
return 0;
};
useEffect(() => {
if (debouncedSearchTermIsGroup) {
return;
}
if (debouncedSearchTerm.length > 1 && isSuccessDistroRepositories) {
searchDistroRpms({
apiContentUnitSearchRequest: {
search: debouncedSearchTerm,
urls: distroRepositories
.filter((archItem) => {
return archItem.arch === arch;
})[0]
.repositories.flatMap((repo) => {
if (!repo.baseurl) {
throw new Error(`Repository ${repo} missing baseurl`);
}
return repo.baseurl;
}),
},
});
}
if (debouncedSearchTerm.length > 2) {
if (toggleSourceRepos === RepoToggle.INCLUDED) {
searchCustomRpms({
apiContentUnitSearchRequest: {
search: debouncedSearchTerm,
urls: customRepositories.flatMap((repo) => {
if (!repo.baseurl) {
throw new Error(
`Repository (id: ${repo.id}, name: ${repo?.name}) is missing baseurl`
);
}
return repo.baseurl;
}),
},
});
} else {
searchRecommendedRpms({
apiContentUnitSearchRequest: {
search: debouncedSearchTerm,
urls: [epelRepoUrlByDistribution],
},
});
}
}
}, [
customRepositories,
searchCustomRpms,
searchDistroRpms,
debouncedSearchTerm,
toggleSourceRepos,
searchRecommendedRpms,
epelRepoUrlByDistribution,
isSuccessDistroRepositories,
searchDistroRpms,
distroRepositories,
arch,
]);
useEffect(() => {
if (!debouncedSearchTermIsGroup) {
return;
}
if (isSuccessDistroRepositories) {
searchDistroGroups({
apiContentUnitSearchRequest: {
search: debouncedSearchTerm.substr(1),
urls: distroRepositories
.filter((archItem) => {
return archItem.arch === arch;
})[0]
.repositories.flatMap((repo) => {
if (!repo.baseurl) {
throw new Error(`Repository ${repo} missing baseurl`);
}
return repo.baseurl;
}),
},
});
}
if (
toggleSourceRepos === RepoToggle.INCLUDED &&
customRepositories.length > 0
) {
searchCustomGroups({
apiContentUnitSearchRequest: {
search: debouncedSearchTerm.substr(1),
urls: customRepositories.flatMap((repo) => {
if (!repo.baseurl) {
throw new Error(
`Repository (id: ${repo.id}, name: ${repo?.name}) is missing baseurl`
);
}
return repo.baseurl;
}),
},
});
} else if (toggleSourceRepos === RepoToggle.OTHER && isSuccessEpelRepo) {
searchRecommendedGroups({
apiContentUnitSearchRequest: {
search: debouncedSearchTerm.substr(1),
urls: [epelRepoUrlByDistribution],
},
});
}
}, [
customRepositories,
searchDistroGroups,
searchCustomGroups,
searchRecommendedGroups,
debouncedSearchTerm,
toggleSourceRepos,
epelRepoUrlByDistribution,
]);
const EmptySearch = () => {
return (
<Tr>
<Td colSpan={5}>
<Bullseye>
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader icon={<EmptyStateIcon icon={SearchIcon} />} />
{toggleSelected === 'toggle-available' ? (
<EmptyStateBody>
Search above to add additional
<br />
packages to your image.
</EmptyStateBody>
) : (
<EmptyStateBody>
No packages selected.
<br />
Search above to see available packages.
</EmptyStateBody>
)}
</EmptyState>
</Bullseye>
</Td>
</Tr>
);
};
const Searching = () => {
return (
<Tr>
<Td colSpan={5}>
<Bullseye>
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader icon={<EmptyStateIcon icon={Spinner} />} />
<EmptyStateBody>
{toggleSourceRepos === RepoToggle.OTHER
? 'Searching for recommendations'
: 'Searching'}
</EmptyStateBody>
</EmptyState>
</Bullseye>
</Td>
</Tr>
);
};
const TooManyResults = () => {
return (
<Tr>
<Td colSpan={5}>
<Bullseye>
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader
icon={<EmptyStateIcon icon={SearchIcon} />}
titleText="Too many results to display"
headingLevel="h4"
/>
<EmptyStateBody>
Please make the search more specific and try again.
</EmptyStateBody>
</EmptyState>
</Bullseye>
</Td>
</Tr>
);
};
const TooShort = () => {
return (
<Tr>
<Td colSpan={5}>
<Bullseye>
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader
icon={<EmptyStateIcon icon={SearchIcon} />}
titleText="The search value is too short"
headingLevel="h4"
/>
<EmptyStateBody>
Please make the search more specific and try again.
</EmptyStateBody>
</EmptyState>
</Bullseye>
</Td>
</Tr>
);
};
const TooManyResultsWithExactMatch = () => {
return (
<Tr>
<Td colSpan={5}>
<Bullseye>
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader
titleText="Too many results to display"
headingLevel="h4"
/>
<EmptyStateBody>
To see more results, please make the search more specific and
try again.
</EmptyStateBody>
</EmptyState>
</Bullseye>
</Td>
</Tr>
);
};
const TryLookingUnderIncluded = () => {
return (
<Tr>
<Td colSpan={5}>
<Bullseye>
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader
titleText="No selected packages in Other repos"
headingLevel="h4"
/>
<EmptyStateBody>
Try looking under &quot;
<Button
variant="link"
onClick={() => setToggleSourceRepos(RepoToggle.INCLUDED)}
isInline
>
Included repos
</Button>
&quot;.
</EmptyStateBody>
</EmptyState>
</Bullseye>
</Td>
</Tr>
);
};
const NoResultsFound = () => {
const { isBeta } = useGetEnvironment();
if (toggleSourceRepos === RepoToggle.INCLUDED) {
return (
<Tr>
<Td colSpan={5}>
<Bullseye>
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader icon={<EmptyStateIcon icon={SearchIcon} />} />
<EmptyStateHeader
titleText="No results found"
headingLevel="h4"
/>
<EmptyStateBody>
Adjust your search and try again, or search in other
repositories (your repositories and popular repositories).
</EmptyStateBody>
<EmptyStateFooter>
<EmptyStateActions>
<Button
variant="primary"
ouiaId="search-other-repositories"
onClick={() => setToggleSourceRepos(RepoToggle.OTHER)}
>
Search other repositories
</Button>
</EmptyStateActions>
<EmptyStateActions>
<Button
className="pf-u-pt-md"
variant="link"
isInline
component="a"
target="_blank"
iconPosition="right"
icon={<ExternalLinkAltIcon />}
href={isBeta() ? CONTENT_BETA : CONTENT_STABLE}
>
Manage your repositories and popular repositories
</Button>
</EmptyStateActions>
</EmptyStateFooter>
</EmptyState>
</Bullseye>
</Td>
</Tr>
);
} else {
return (
<Tr>
<Td colSpan={5}>
<Bullseye>
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader icon={<EmptyStateIcon icon={SearchIcon} />} />
<EmptyStateHeader
titleText="No results found"
headingLevel="h4"
/>
<EmptyStateBody>
No packages found in known repositories. If you know of a
repository containing this packages, add it to{' '}
<Button
variant="link"
isInline
component="a"
target="_blank"
href={isBeta() ? CONTENT_BETA : CONTENT_STABLE}
>
your repositories
</Button>{' '}
and try searching for it again.
</EmptyStateBody>
</EmptyState>
</Bullseye>
</Td>
</Tr>
);
}
};
const RepositoryModal = () => {
const { isBeta } = useGetEnvironment();
return (
<Modal
titleIconVariant="warning"
title="Custom repositories will be added to your image"
isOpen={isRepoModalOpen}
onClose={handleCloseModalToggle}
width="50%"
actions={[
<Button
key="add"
variant="primary"
isLoading={createLoading}
isDisabled={createLoading}
onClick={handleConfirmModalToggle}
ouiaId="Add-listed-repos"
>
Add listed repositories
</Button>,
<Button key="back" variant="link" onClick={handleCloseModalToggle}>
Back
</Button>,
]}
ouiaId="Custom-repos-warning-modal"
>
You have selected packages that belong to custom repositories. By
continuing, you are acknowledging and consenting to adding the following
custom repositories to your image.
<br />
<br />
The repositories will also get enabled in{' '}
<Button
component="a"
target="_blank"
variant="link"
iconPosition="right"
isInline
icon={<ExternalLinkAltIcon />}
href={isBeta() ? CONTENT_BETA : CONTENT_STABLE}
>
content services
</Button>{' '}
if they were not enabled yet:
<br />
<Table variant="compact">
<Thead>
<Tr>
{isSelectingPackage ? <Th>Packages</Th> : <Th>Package groups</Th>}
<Th>Repositories</Th>
</Tr>
</Thead>
<Tbody>
<Tr>
{isSelectingPackage ? (
<Td>{isSelectingPackage?.name}</Td>
) : (
<Td>{isSelectingGroup?.name}</Td>
)}
<Td>
EPEL {distribution === 'rhel-8' ? '8' : '9'} Everything x86_64
</Td>
</Tr>
</Tbody>
</Table>
<br />
To move forward, either add the repos to your image, or go back to
review your package selections.
</Modal>
);
};
const transformedPackages = useMemo(() => {
let transformedDistroData: IBPackageWithRepositoryInfo[] = [];
let transformedCustomData: IBPackageWithRepositoryInfo[] = [];
let transformedRecommendedData: IBPackageWithRepositoryInfo[] = [];
if (isSuccessDistroPackages) {
transformedDistroData = dataDistroPackages!.map((values) => ({
name: values.package_name!,
summary: values.summary!,
repository: 'distro',
}));
}
if (isSuccessCustomPackages) {
transformedCustomData = dataCustomPackages!.map((values) => ({
name: values.package_name!,
summary: values.summary!,
repository: 'custom',
}));
}
let combinedPackageData = transformedDistroData.concat(
transformedCustomData
);
if (
debouncedSearchTerm !== '' &&
combinedPackageData.length === 0 &&
isSuccessRecommendedPackages &&
toggleSourceRepos === RepoToggle.OTHER
) {
transformedRecommendedData = dataRecommendedPackages!.map((values) => ({
name: values.package_name!,
summary: values.summary!,
repository: 'recommended',
}));
combinedPackageData = combinedPackageData.concat(
transformedRecommendedData
);
}
if (toggleSelected === 'toggle-available') {
if (toggleSourceRepos === RepoToggle.INCLUDED) {
return combinedPackageData.filter(
(pkg) => pkg.repository !== 'recommended'
);
} else {
return combinedPackageData.filter(
(pkg) => pkg.repository === 'recommended'
);
}
} else {
const selectedPackages = [...packages];
if (toggleSourceRepos === RepoToggle.INCLUDED) {
return selectedPackages;
} else {
return [];
}
}
}, [
dataCustomPackages,
dataDistroPackages,
dataRecommendedPackages,
debouncedSearchTerm,
isSuccessCustomPackages,
isSuccessDistroPackages,
isSuccessRecommendedPackages,
packages,
toggleSelected,
toggleSourceRepos,
]).sort((a, b) => sortfn(a.name, b.name));
const transformedGroups = useMemo(() => {
let combinedGroupData: GroupWithRepositoryInfo[] = [];
if (isSuccessDistroGroups) {
combinedGroupData = combinedGroupData.concat(
dataDistroGroups!.map((values) => ({
name: values.id!,
description: values.description!,
repository: 'distro',
package_list: values.package_list!,
}))
);
}
if (isSuccessCustomGroups) {
combinedGroupData = combinedGroupData.concat(
dataCustomGroups!.map((values) => ({
name: values.id!,
description: values.description!,
repository: 'custom',
package_list: values.package_list!,
}))
);
}
if (isSuccessRecommendedGroups) {
combinedGroupData = combinedGroupData.concat(
dataRecommendedGroups!.map((values) => ({
name: values.id!,
description: values.description!,
repository: 'recommended',
package_list: values.package_list!,
}))
);
}
if (toggleSelected === 'toggle-available') {
if (toggleSourceRepos === RepoToggle.INCLUDED) {
return combinedGroupData.filter(
(pkg) => pkg.repository !== 'recommended'
);
} else {
return combinedGroupData.filter(
(pkg) => pkg.repository === 'recommended'
);
}
} else {
const selectedGroups = [...groups];
if (toggleSourceRepos === RepoToggle.INCLUDED) {
return selectedGroups;
} else {
return [];
}
}
return combinedGroupData;
}, [
dataDistroGroups,
dataCustomGroups,
dataRecommendedGroups,
debouncedSearchTerm,
isSuccessDistroGroups,
isSuccessCustomGroups,
isSuccessRecommendedGroups,
groups,
toggleSelected,
toggleSourceRepos,
]).sort((a, b) => sortfn(a.name, b.name));
const handleSearch = async (
event: React.FormEvent<HTMLInputElement>,
selection: string
) => {
setSearchTerm(selection);
};
const handleClear = async () => {
setSearchTerm('');
};
const handleSelect = (
pkg: IBPackageWithRepositoryInfo,
_: number,
isSelecting: boolean
) => {
if (isSelecting) {
if (
isSuccessEpelRepo &&
epelRepo.data &&
pkg.repository === 'recommended' &&
!recommendedRepositories.some((repo) => repo.name?.startsWith('EPEL'))
) {
setIsRepoModalOpen(true);
setIsSelectingPackage(pkg);
} else {
dispatch(addPackage(pkg));
}
} else {
dispatch(removePackage(pkg.name));
if (
isSuccessEpelRepo &&
epelRepo.data &&
packages.filter((pkg) => pkg.repository === 'recommended').length ===
1 &&
groups.filter((grp) => grp.repository === 'recommended').length === 0
) {
dispatch(removeRecommendedRepository(epelRepo.data[0]));
}
}
};
const handleGroupSelect = (
grp: GroupWithRepositoryInfo,
_: number,
isSelecting: boolean
) => {
if (isSelecting) {
if (
isSuccessEpelRepo &&
epelRepo.data &&
grp.repository === 'recommended' &&
!recommendedRepositories.some((repo) => repo.name?.startsWith('EPEL'))
) {
setIsRepoModalOpen(true);
setIsSelectingGroup(grp);
} else {
dispatch(addGroup(grp));
}
} else {
dispatch(removeGroup(grp.name));
if (
isSuccessEpelRepo &&
epelRepo.data &&
groups.filter((grp) => grp.repository === 'recommended').length === 1 &&
packages.filter((pkg) => pkg.repository === 'recommended').length === 0
) {
dispatch(removeRecommendedRepository(epelRepo.data[0]));
}
}
};
const handleFilterToggleClick = (event: React.MouseEvent) => {
const id = event.currentTarget.id;
setPage(1);
setToggleSelected(id);
};
const handleRepoToggleClick = (type: RepoToggle) => {
if (toggleSourceRepos !== type) {
setPage(1);
setToggleSourceRepos(type);
}
};
const handleSetPage = (_: React.MouseEvent, newPage: number) => {
setPage(newPage);
};
const handlePerPageSelect = (
_: React.MouseEvent,
newPerPage: number,
newPage: number
) => {
setPerPage(newPerPage);
setPage(newPage);
};
const computeStart = () => perPage * (page - 1);
const computeEnd = () => perPage * page;
const handleExactMatch = () => {
const exactMatch = transformedPackages.find(
(pkg) => pkg.name === debouncedSearchTerm
);
if (exactMatch) {
return (
<>
<Tr key={`${exactMatch.name}`} data-testid="exact-match-row">
<Td
select={{
isSelected: packages.some((p) => p.name === exactMatch.name),
rowIndex: 0,
onSelect: (event, isSelecting) =>
handleSelect(exactMatch, 0, isSelecting),
}}
/>
<Td>{exactMatch.name}</Td>
<Td>{exactMatch.summary}</Td>
{exactMatch.repository === 'distro' ? (
<>
<Td>
<RedHatRepository />
</Td>
<Td>Supported</Td>
</>
) : (
<>
<Td>Third party repository</Td>
<Td>Not supported</Td>
</>
)}
</Tr>
<TooManyResultsWithExactMatch />
</>
);
} else {
return <TooManyResults />;
}
};
const handleCloseModalToggle = () => {
setIsRepoModalOpen(!isRepoModalOpen);
setIsSelectingPackage(undefined);
};
const handleConfirmModalToggle = async () => {
if (!epelRepo || !epelRepo.data) {
throw new Error(
`There was an error while adding the recommended repository.`
);
}
if (epelRepo.data.length === 0) {
const result = await createRepository({
apiRepositoryRequest: distribution.startsWith('rhel-8')
? EPEL_8_REPO_DEFINITION
: EPEL_9_REPO_DEFINITION,
});
dispatch(
addRecommendedRepository(
(result as { data: ApiRepositoryResponseRead }).data
)
);
} else {
dispatch(addRecommendedRepository(epelRepo.data[0]));
}
if (isSelectingPackage) {
dispatch(addPackage(isSelectingPackage!));
}
if (isSelectingGroup) {
dispatch(addGroup(isSelectingGroup!));
}
setIsRepoModalOpen(!isRepoModalOpen);
};
const composePkgTable = () => {
let rows: ReactElement[] = [];
if (showGroups) {
rows = rows.concat(
transformedGroups
.slice(computeStart(), computeEnd())
.map((grp, rowIndex) => (
<Tr key={`${grp.name}-${rowIndex}`} data-testid="package-row">
<Td
select={{
isSelected: groups.some((g) => g.name === grp.name),
rowIndex: rowIndex,
onSelect: (event, isSelecting) =>
handleGroupSelect(grp, rowIndex, isSelecting),
}}
/>
<Td>
@{grp.name}
<Popover
minWidth="25rem"
headerContent="Included packages"
bodyContent={
<div
style={
grp.package_list.length > 0
? { height: '40em', overflow: 'scroll' }
: {}
}
>
{grp.package_list.length > 0 ? (
<Table
variant="compact"
data-testid="group-included-packages-table"
>
<Tbody>
{grp.package_list.map((pkg) => (
<Tr key={`details-${pkg}`}>
<Td>{pkg}</Td>
</Tr>
))}
</Tbody>
</Table>
) : (
<Text>This group has no packages</Text>
)}
</div>
}
>
<Button
variant="plain"
aria-label="About included packages"
component="span"
className="pf-u-p-0"
isInline
>
<HelpIcon className="pf-u-ml-xs" />
</Button>
</Popover>
</Td>
<Td>
{grp.description ? (
grp.description
) : (
<span className="not-available">Not available</span>
)}
</Td>
{grp.repository === 'distro' ? (
<>
<Td>
<img
src={
'/apps/frontend-assets/red-hat-logos/logo_hat-only.svg'
}
alt="Red Hat logo"
height={RH_ICON_SIZE}
width={RH_ICON_SIZE}
/>{' '}
Red Hat repository
</Td>
<Td>Supported</Td>
</>
) : grp.repository === 'custom' ? (
<>
<Td>Third party repository</Td>
<Td>Not supported</Td>
</>
) : grp.repository === 'recommended' ? (
<>
<Td>
<Icon status="warning">
<OptimizeIcon />
</Icon>{' '}
EPEL {distribution.startsWith('rhel-8') ? '8' : '9'}{' '}
Everything x86_64
</Td>
<Td>Not supported</Td>
</>
) : (
<>
<Td className="not-available">Not available</Td>
<Td className="not-available">Not available</Td>
</>
)}
</Tr>
))
);
}
if (showPackages) {
rows = rows.concat(
transformedPackages
.slice(computeStart(), computeEnd())
.map((pkg, rowIndex) => (
<Tr key={`${pkg.name}-${rowIndex}`} data-testid="package-row">
<Td
select={{
isSelected: packages.some((p) => p.name === pkg.name),
rowIndex: rowIndex,
onSelect: (event, isSelecting) =>
handleSelect(pkg, rowIndex, isSelecting),
}}
/>
<Td>{pkg.name}</Td>
<Td>
{pkg.summary ? (
pkg.summary
) : (
<span className="not-available">Not available</span>
)}
</Td>
{pkg.repository === 'distro' ? (
<>
<Td>
<img
src={
'/apps/frontend-assets/red-hat-logos/logo_hat-only.svg'
}
alt="Red Hat logo"
height={RH_ICON_SIZE}
width={RH_ICON_SIZE}
/>{' '}
Red Hat repository
</Td>
<Td>Supported</Td>
</>
) : pkg.repository === 'custom' ? (
<>
<Td>Third party repository</Td>
<Td>Not supported</Td>
</>
) : pkg.repository === 'recommended' ? (
<>
<Td>
<Icon status="warning">
<OptimizeIcon />
</Icon>{' '}
EPEL {distribution.startsWith('rhel-8') ? '8' : '9'}{' '}
Everything x86_64
</Td>
<Td>Not supported</Td>
</>
) : (
<>
<Td className="not-available">Not available</Td>
<Td className="not-available">Not available</Td>
</>
)}
</Tr>
))
);
}
return rows;
};
const bodyContent = useMemo(() => {
switch (true) {
case debouncedSearchTermLengthOf1 &&
!debouncedSearchTermIsGroup &&
transformedPackages.length === 0 &&
transformedGroups.length === 0:
return TooShort();
case (toggleSelected === 'toggle-selected' &&
packages.length === 0 &&
groups.length === 0) ||
(!debouncedSearchTerm && toggleSelected === 'toggle-available'):
return <EmptySearch />;
case (debouncedSearchTerm &&
(isLoadingRecommendedPackages || isLoadingRecommendedGroups) &&
toggleSourceRepos === RepoToggle.OTHER) ||
(debouncedSearchTerm &&
(isLoadingDistroPackages ||
isLoadingCustomPackages ||
isLoadingDistroGroups ||
isLoadingCustomGroups) &&
toggleSourceRepos === RepoToggle.INCLUDED):
return <Searching />;
case debouncedSearchTerm &&
transformedPackages.length === 0 &&
transformedGroups.length === 0 &&
toggleSelected === 'toggle-available':
return <NoResultsFound />;
case debouncedSearchTerm &&
toggleSelected === 'toggle-selected' &&
toggleSourceRepos === RepoToggle.OTHER &&
packages.length > 0 &&
groups.length > 0:
return <TryLookingUnderIncluded />;
case debouncedSearchTerm && transformedPackages.length >= 100:
return handleExactMatch();
case (debouncedSearchTerm || toggleSelected === 'toggle-selected') &&
transformedPackages.length < 100 &&
transformedGroups.length < 100:
return composePkgTable();
default:
return <></>;
}
// Would need significant rewrite to fix this
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
page,
perPage,
debouncedSearchTerm,
debouncedSearchTermLengthOf1,
debouncedSearchTermIsGroup,
isLoadingCustomPackages,
isLoadingDistroPackages,
isLoadingRecommendedPackages,
isSuccessRecommendedPackages,
isLoadingDistroGroups,
isLoadingCustomGroups,
isLoadingRecommendedGroups,
packages.length,
groups.length,
toggleSelected,
toggleSourceRepos,
transformedPackages,
isSelectingPackage,
recommendedRepositories,
transformedPackages.length,
transformedGroups.length,
]);
return (
<>
<RepositoryModal />
<Toolbar>
<Stack>
<ToolbarContent>
<ToolbarItem variant="search-filter">
<InputGroup>
<InputGroupItem isFill>
<InputGroupText id="search-icon">
<SearchIcon />
</InputGroupText>
<TextInput
data-ouia-component-id="packages-search-input"
type="text"
validated={
debouncedSearchTermLengthOf1 &&
!debouncedSearchTermIsGroup
? 'error'
: 'default'
}
placeholder="Type to search"
aria-label="Search packages"
data-testid="packages-search-input"
value={searchTerm}
onChange={handleSearch}
/>
</InputGroupItem>
{searchTerm && (
<InputGroupItem>
<Button
variant="control"
aria-label="clear-package-search"
onClick={handleClear}
icon={<TimesIcon />}
ouiaId="clear-package-search-button"
/>
</InputGroupItem>
)}
</InputGroup>
</ToolbarItem>
<ToolbarItem>
<ToggleGroup>
<ToggleGroupItem
text="Available"
buttonId="toggle-available"
data-testid="packages-available-toggle"
isSelected={toggleSelected === 'toggle-available'}
onChange={handleFilterToggleClick}
/>
<ToggleGroupItem
text={`Selected (${
packages.length + groups.length <= 100
? packages.length + groups.length
: '100+'
})`}
buttonId="toggle-selected"
data-testid="packages-selected-toggle"
isSelected={toggleSelected === 'toggle-selected'}
onChange={handleFilterToggleClick}
/>
</ToggleGroup>
</ToolbarItem>
<ToolbarItem>
<ToggleGroup>
<ToggleGroupItem
text={
<>
Included repos{' '}
<Popover
bodyContent={
<TextContent>
<Text>
View packages from the Red Hat repository and
repositories you&apos;ve selected.
</Text>
</TextContent>
}
>
<Button
variant="plain"
aria-label="About included repositories"
component="span"
className="pf-u-p-0"
isInline
>
<HelpIcon />
</Button>
</Popover>
</>
}
buttonId={RepoToggle.INCLUDED}
isSelected={toggleSourceRepos === RepoToggle.INCLUDED}
onChange={() => handleRepoToggleClick(RepoToggle.INCLUDED)}
/>
<ToggleGroupItem
text={
<>
Other repos{' '}
<Popover
bodyContent={
<TextContent>
<Text>
View packages from popular repositories and your
other repositories not included in the image.
</Text>
</TextContent>
}
>
<Button
variant="plain"
aria-label="About other repositories"
component="span"
className="pf-u-p-0"
isInline
>
<HelpIcon />
</Button>
</Popover>
</>
}
buttonId="toggle-other-repos"
isSelected={toggleSourceRepos === RepoToggle.OTHER}
onChange={() => handleRepoToggleClick(RepoToggle.OTHER)}
/>
</ToggleGroup>
</ToolbarItem>
<ToolbarItem variant="pagination">
<Pagination
data-testid="packages-pagination-top"
itemCount={
searchTerm === '' && toggleSelected === 'toggle-available'
? 0
: showPackages && showGroups
? transformedPackages.length + transformedGroups.length
: showPackages
? transformedPackages.length
: transformedGroups.length
}
perPage={perPage}
page={page}
onSetPage={handleSetPage}
onPerPageSelect={handlePerPageSelect}
isCompact
/>
</ToolbarItem>
</ToolbarContent>
<ToolbarContent>
<CustomHelperText
hide={!debouncedSearchTermLengthOf1 || debouncedSearchTermIsGroup}
textValue="The search value must be greater than 1 character"
/>
</ToolbarContent>
</Stack>
</Toolbar>
<Table variant="compact" data-testid="packages-table">
<Thead>
<Tr>
<Th aria-label="Selected" />
<Th width={20}>Package name</Th>
<Th width={35}>
Description
{toggleSelected === 'toggle-selected' && (
<PackageInfoNotAvailablePopover />
)}
</Th>
<Th width={25}>Package repository</Th>
<Th width={20}>Support</Th>
</Tr>
</Thead>
<Tbody>{bodyContent}</Tbody>
</Table>
<Pagination
data-testid="packages-pagination-bottom"
itemCount={
searchTerm === '' && toggleSelected === 'toggle-available'
? 0
: showPackages && showGroups
? transformedPackages.length + transformedGroups.length
: showPackages
? transformedPackages.length
: transformedGroups.length
}
perPage={perPage}
page={page}
onSetPage={handleSetPage}
onPerPageSelect={handlePerPageSelect}
variant={PaginationVariant.bottom}
/>
</>
);
};
export default Packages;