1419 lines
42 KiB
TypeScript
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 "
|
|
<Button
|
|
variant="link"
|
|
onClick={() => setToggleSourceRepos(RepoToggle.INCLUDED)}
|
|
isInline
|
|
>
|
|
Included repos
|
|
</Button>
|
|
".
|
|
</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'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;
|