This adds support for EPEL 10 repository. Changes the way of getting the correct EPEL version for RHEL distribution to be more future proof.
1570 lines
46 KiB
TypeScript
1570 lines
46 KiB
TypeScript
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
|
|
|
|
import {
|
|
Bullseye,
|
|
Button,
|
|
DescriptionList,
|
|
DescriptionListDescription,
|
|
DescriptionListGroup,
|
|
DescriptionListTerm,
|
|
EmptyState,
|
|
EmptyStateActions,
|
|
EmptyStateBody,
|
|
EmptyStateFooter,
|
|
EmptyStateVariant,
|
|
Icon,
|
|
Pagination,
|
|
PaginationVariant,
|
|
Popover,
|
|
SearchInput,
|
|
Spinner,
|
|
Stack,
|
|
Tab,
|
|
Tabs,
|
|
TabTitleText,
|
|
Content,
|
|
ToggleGroup,
|
|
ToggleGroupItem,
|
|
Toolbar,
|
|
ToolbarContent,
|
|
ToolbarItem,
|
|
} from '@patternfly/react-core';
|
|
import { Modal } from '@patternfly/react-core/deprecated';
|
|
import {
|
|
CheckCircleIcon,
|
|
ExclamationCircleIcon,
|
|
ExclamationTriangleIcon,
|
|
ExternalLinkAltIcon,
|
|
HelpIcon,
|
|
OptimizeIcon,
|
|
SearchIcon,
|
|
} from '@patternfly/react-icons';
|
|
import {
|
|
ExpandableRowContent,
|
|
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 {
|
|
IncludedReposPopover,
|
|
OtherReposPopover,
|
|
} from './components/RepoPopovers';
|
|
|
|
import {
|
|
CONTENT_URL,
|
|
ContentOrigin,
|
|
EPEL_10_REPO_DEFINITION,
|
|
} from '../../../../constants';
|
|
import { useGetArchitecturesQuery } from '../../../../store/backendApi';
|
|
import {
|
|
ApiRepositoryResponseRead,
|
|
useCreateRepositoryMutation,
|
|
useListRepositoriesQuery,
|
|
useSearchRpmMutation,
|
|
useSearchPackageGroupMutation,
|
|
ApiSearchRpmResponse,
|
|
ApiPackageSourcesResponse,
|
|
} from '../../../../store/contentSourcesApi';
|
|
import { useAppSelector } from '../../../../store/hooks';
|
|
import { Package } from '../../../../store/imageBuilderApi';
|
|
import {
|
|
selectArchitecture,
|
|
selectPackages,
|
|
selectGroups,
|
|
selectCustomRepositories,
|
|
selectDistribution,
|
|
addPackage,
|
|
removePackage,
|
|
addGroup,
|
|
removeGroup,
|
|
addRecommendedRepository,
|
|
removeRecommendedRepository,
|
|
selectRecommendedRepositories,
|
|
addModule,
|
|
removeModule,
|
|
selectModules,
|
|
} from '../../../../store/wizardSlice';
|
|
import {
|
|
getEpelDefinitionForDistribution,
|
|
getEpelUrlForDistribution,
|
|
getEpelVersionForDistribution,
|
|
} from '../../../../Utilities/epel';
|
|
import useDebounce from '../../../../Utilities/useDebounce';
|
|
|
|
export type PackageRepository = 'distro' | 'custom' | 'recommended' | '';
|
|
|
|
export type ItemWithSources = {
|
|
name: Package['name'];
|
|
summary: Package['summary'];
|
|
repository: PackageRepository;
|
|
sources?: ApiSearchRpmResponse['package_sources'];
|
|
};
|
|
|
|
export type IBPackageWithRepositoryInfo = {
|
|
name: Package['name'];
|
|
summary: Package['summary'];
|
|
repository: PackageRepository;
|
|
type?: string;
|
|
module_name?: string;
|
|
stream?: string;
|
|
end_date?: string;
|
|
};
|
|
|
|
export type GroupWithRepositoryInfo = {
|
|
name: string;
|
|
description: string;
|
|
repository: PackageRepository;
|
|
package_list: string[];
|
|
};
|
|
|
|
export enum Repos {
|
|
INCLUDED = 'included-repos',
|
|
OTHER = 'other-repos',
|
|
}
|
|
|
|
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 modules = useAppSelector(selectModules);
|
|
|
|
const { data: distroRepositories, isSuccess: isSuccessDistroRepositories } =
|
|
useGetArchitecturesQuery({
|
|
distribution: distribution,
|
|
});
|
|
|
|
const epelRepoUrlByDistribution =
|
|
getEpelUrlForDistribution(distribution) ?? EPEL_10_REPO_DEFINITION.url;
|
|
|
|
const { data: epelRepo, isSuccess: isSuccessEpelRepo } =
|
|
useListRepositoriesQuery({
|
|
url: epelRepoUrlByDistribution,
|
|
origin: ContentOrigin.EXTERNAL,
|
|
});
|
|
|
|
const [currentlyRemovedPackages, setCurrentlyRemovedPackages] = useState<
|
|
IBPackageWithRepositoryInfo[]
|
|
>([]);
|
|
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 [activeTabKey, setActiveTabKey] = useState(Repos.INCLUDED);
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [activeStream, setActiveStream] = useState<string>('');
|
|
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();
|
|
|
|
useEffect(() => {
|
|
if (debouncedSearchTermIsGroup) {
|
|
return;
|
|
}
|
|
if (debouncedSearchTerm.length > 1 && isSuccessDistroRepositories) {
|
|
if (process.env.IS_ON_PREMISE) {
|
|
searchDistroRpms({
|
|
apiContentUnitSearchRequest: {
|
|
packages: [debouncedSearchTerm],
|
|
architecture: arch,
|
|
distribution,
|
|
},
|
|
});
|
|
} else {
|
|
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;
|
|
}),
|
|
limit: 500,
|
|
include_package_sources: true,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
if (debouncedSearchTerm.length > 2) {
|
|
if (activeTabKey === Repos.INCLUDED && customRepositories.length > 0) {
|
|
searchCustomRpms({
|
|
apiContentUnitSearchRequest: {
|
|
search: debouncedSearchTerm,
|
|
uuids: customRepositories.flatMap((repo) => {
|
|
return repo.id;
|
|
}),
|
|
limit: 500,
|
|
include_package_sources: true,
|
|
},
|
|
});
|
|
} else {
|
|
searchRecommendedRpms({
|
|
apiContentUnitSearchRequest: {
|
|
search: debouncedSearchTerm,
|
|
urls: [epelRepoUrlByDistribution],
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}, [
|
|
customRepositories,
|
|
searchCustomRpms,
|
|
searchDistroRpms,
|
|
debouncedSearchTerm,
|
|
activeTabKey,
|
|
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 (activeTabKey === Repos.INCLUDED && customRepositories.length > 0) {
|
|
searchCustomGroups({
|
|
apiContentUnitSearchRequest: {
|
|
search: debouncedSearchTerm.substr(1),
|
|
uuids: customRepositories.flatMap((repo) => {
|
|
return repo.id;
|
|
}),
|
|
},
|
|
});
|
|
} else if (activeTabKey === Repos.OTHER && isSuccessEpelRepo) {
|
|
searchRecommendedGroups({
|
|
apiContentUnitSearchRequest: {
|
|
search: debouncedSearchTerm.substr(1),
|
|
urls: [epelRepoUrlByDistribution],
|
|
},
|
|
});
|
|
}
|
|
}, [
|
|
customRepositories,
|
|
searchDistroGroups,
|
|
searchCustomGroups,
|
|
searchRecommendedGroups,
|
|
debouncedSearchTerm,
|
|
activeTabKey,
|
|
epelRepoUrlByDistribution,
|
|
]);
|
|
|
|
const EmptySearch = () => {
|
|
return (
|
|
<Tbody>
|
|
<Tr>
|
|
<Td colSpan={6}>
|
|
<Bullseye>
|
|
<EmptyState icon={SearchIcon} variant={EmptyStateVariant.sm}>
|
|
{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>
|
|
</Tbody>
|
|
);
|
|
};
|
|
|
|
const Searching = () => {
|
|
return (
|
|
<Tbody>
|
|
<Tr>
|
|
<Td colSpan={6}>
|
|
<Bullseye>
|
|
<EmptyState icon={Spinner} variant={EmptyStateVariant.sm}>
|
|
<EmptyStateBody>
|
|
{activeTabKey === Repos.OTHER
|
|
? 'Searching for recommendations'
|
|
: 'Searching'}
|
|
</EmptyStateBody>
|
|
</EmptyState>
|
|
</Bullseye>
|
|
</Td>
|
|
</Tr>
|
|
</Tbody>
|
|
);
|
|
};
|
|
|
|
const TooShort = () => {
|
|
return (
|
|
<Tbody>
|
|
<Tr>
|
|
<Td colSpan={6}>
|
|
<Bullseye>
|
|
<EmptyState
|
|
headingLevel="h4"
|
|
icon={SearchIcon}
|
|
titleText="The search value is too short"
|
|
variant={EmptyStateVariant.sm}
|
|
>
|
|
<EmptyStateBody>
|
|
Please make the search more specific and try again.
|
|
</EmptyStateBody>
|
|
</EmptyState>
|
|
</Bullseye>
|
|
</Td>
|
|
</Tr>
|
|
</Tbody>
|
|
);
|
|
};
|
|
|
|
const TryLookingUnderIncluded = () => {
|
|
return (
|
|
<Tbody>
|
|
<Tr>
|
|
<Td colSpan={6}>
|
|
<Bullseye>
|
|
<EmptyState
|
|
headingLevel="h4"
|
|
titleText="No selected packages in Other repos"
|
|
variant={EmptyStateVariant.sm}
|
|
>
|
|
<EmptyStateBody>
|
|
Try looking under "
|
|
<Button
|
|
variant="link"
|
|
onClick={() => setActiveTabKey(Repos.INCLUDED)}
|
|
isInline
|
|
>
|
|
Included repos
|
|
</Button>
|
|
".
|
|
</EmptyStateBody>
|
|
</EmptyState>
|
|
</Bullseye>
|
|
</Td>
|
|
</Tr>
|
|
</Tbody>
|
|
);
|
|
};
|
|
|
|
const NoResultsFound = () => {
|
|
if (activeTabKey === Repos.INCLUDED) {
|
|
return (
|
|
<Tbody>
|
|
<Tr>
|
|
<Td colSpan={6}>
|
|
<Bullseye>
|
|
<EmptyState
|
|
headingLevel="h4"
|
|
titleText="No results found"
|
|
icon={SearchIcon}
|
|
variant={EmptyStateVariant.sm}
|
|
>
|
|
<EmptyStateBody>
|
|
Adjust your search and try again, or search in other
|
|
repositories (your repositories and popular repositories).
|
|
</EmptyStateBody>
|
|
<EmptyStateFooter>
|
|
<EmptyStateActions>
|
|
{!process.env.IS_ON_PREMISE && (
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => setActiveTabKey(Repos.OTHER)}
|
|
>
|
|
Search other repositories
|
|
</Button>
|
|
)}
|
|
</EmptyStateActions>
|
|
<EmptyStateActions>
|
|
<Button
|
|
className="pf-v6-u-pt-md"
|
|
variant="link"
|
|
isInline
|
|
component="a"
|
|
target="_blank"
|
|
iconPosition="right"
|
|
icon={<ExternalLinkAltIcon />}
|
|
href={CONTENT_URL}
|
|
>
|
|
Manage your repositories and popular repositories
|
|
</Button>
|
|
</EmptyStateActions>
|
|
</EmptyStateFooter>
|
|
</EmptyState>
|
|
</Bullseye>
|
|
</Td>
|
|
</Tr>
|
|
</Tbody>
|
|
);
|
|
} else {
|
|
return (
|
|
<Tbody>
|
|
<Tr>
|
|
<Td colSpan={6}>
|
|
<Bullseye>
|
|
<EmptyState
|
|
headingLevel="h4"
|
|
titleText="No results found"
|
|
icon={SearchIcon}
|
|
variant={EmptyStateVariant.sm}
|
|
>
|
|
<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={CONTENT_URL}
|
|
>
|
|
your repositories
|
|
</Button>{' '}
|
|
and try searching for it again.
|
|
</EmptyStateBody>
|
|
</EmptyState>
|
|
</Bullseye>
|
|
</Td>
|
|
</Tr>
|
|
</Tbody>
|
|
);
|
|
}
|
|
};
|
|
|
|
const RepositoryModal = () => {
|
|
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}
|
|
>
|
|
Add listed repositories
|
|
</Button>,
|
|
<Button key="back" variant="link" onClick={handleCloseModalToggle}>
|
|
Back
|
|
</Button>,
|
|
]}
|
|
>
|
|
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={CONTENT_URL}
|
|
>
|
|
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 {getEpelVersionForDistribution(distribution)} 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: ItemWithSources[] = [];
|
|
let transformedCustomData: ItemWithSources[] = [];
|
|
let transformedRecommendedData: ItemWithSources[] = [];
|
|
|
|
if (isSuccessDistroPackages) {
|
|
transformedDistroData = dataDistroPackages.map((values) => ({
|
|
name: values.package_name!,
|
|
summary: values.summary!,
|
|
repository: 'distro',
|
|
sources: values.package_sources,
|
|
}));
|
|
}
|
|
|
|
if (isSuccessCustomPackages) {
|
|
transformedCustomData = dataCustomPackages.map((values) => ({
|
|
name: values.package_name!,
|
|
summary: values.summary!,
|
|
repository: 'custom',
|
|
sources: values.package_sources,
|
|
}));
|
|
}
|
|
|
|
let combinedPackageData = transformedDistroData.concat(
|
|
transformedCustomData
|
|
);
|
|
|
|
if (
|
|
debouncedSearchTerm !== '' &&
|
|
combinedPackageData.length === 0 &&
|
|
isSuccessRecommendedPackages &&
|
|
activeTabKey === Repos.OTHER
|
|
) {
|
|
transformedRecommendedData = dataRecommendedPackages!.map((values) => ({
|
|
name: values.package_name!,
|
|
summary: values.summary!,
|
|
repository: 'recommended',
|
|
sources: values.package_sources,
|
|
}));
|
|
|
|
combinedPackageData = combinedPackageData.concat(
|
|
transformedRecommendedData
|
|
);
|
|
}
|
|
|
|
const unpackedData: IBPackageWithRepositoryInfo[] =
|
|
combinedPackageData.flatMap((item) => {
|
|
// Spread modules into separate rows by application stream
|
|
if (item.sources) {
|
|
return item.sources.map((source) => ({
|
|
name: item.name,
|
|
summary: item.summary,
|
|
repository: item.repository,
|
|
type: source.type,
|
|
module_name: source.name,
|
|
stream: source.stream,
|
|
end_date: source.end_date,
|
|
}));
|
|
}
|
|
return [
|
|
{
|
|
name: item.name,
|
|
summary: item.summary,
|
|
repository: item.repository,
|
|
},
|
|
];
|
|
});
|
|
|
|
// group by name, but sort by application stream in descending order
|
|
unpackedData.sort((a, b) => {
|
|
if (a.name === b.name) {
|
|
return (b.stream ?? '').localeCompare(a.stream ?? '');
|
|
} else {
|
|
return a.name.localeCompare(b.name);
|
|
}
|
|
});
|
|
|
|
if (toggleSelected === 'toggle-available') {
|
|
if (activeTabKey === Repos.INCLUDED) {
|
|
return unpackedData.filter((pkg) => pkg.repository !== 'recommended');
|
|
}
|
|
return unpackedData.filter((pkg) => pkg.repository === 'recommended');
|
|
} else {
|
|
const selectedPackages = [...packages];
|
|
if (currentlyRemovedPackages.length > 0) {
|
|
selectedPackages.push(...currentlyRemovedPackages);
|
|
}
|
|
if (activeTabKey === Repos.INCLUDED) {
|
|
return selectedPackages;
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
}, [
|
|
currentlyRemovedPackages,
|
|
dataCustomPackages,
|
|
dataDistroPackages,
|
|
dataRecommendedPackages,
|
|
debouncedSearchTerm,
|
|
isSuccessCustomPackages,
|
|
isSuccessDistroPackages,
|
|
isSuccessRecommendedPackages,
|
|
packages,
|
|
toggleSelected,
|
|
activeTabKey,
|
|
]);
|
|
|
|
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 (activeTabKey === Repos.INCLUDED) {
|
|
return combinedGroupData.filter(
|
|
(pkg) => pkg.repository !== 'recommended'
|
|
);
|
|
} else {
|
|
return combinedGroupData.filter(
|
|
(pkg) => pkg.repository === 'recommended'
|
|
);
|
|
}
|
|
} else {
|
|
const selectedGroups = [...groups];
|
|
if (activeTabKey === Repos.INCLUDED) {
|
|
return selectedGroups;
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
return combinedGroupData;
|
|
}, [
|
|
dataDistroGroups,
|
|
dataCustomGroups,
|
|
dataRecommendedGroups,
|
|
debouncedSearchTerm,
|
|
isSuccessDistroGroups,
|
|
isSuccessCustomGroups,
|
|
isSuccessRecommendedGroups,
|
|
groups,
|
|
toggleSelected,
|
|
activeTabKey,
|
|
]);
|
|
|
|
const handleSearch = async (
|
|
event: React.FormEvent<HTMLInputElement>,
|
|
selection: string
|
|
) => {
|
|
setSearchTerm(selection);
|
|
setActiveTabKey(Repos.INCLUDED);
|
|
setToggleSelected('toggle-available');
|
|
setActiveStream('');
|
|
setActiveSortIndex(0);
|
|
setActiveSortDirection('asc');
|
|
};
|
|
|
|
const handleClear = async () => {
|
|
setSearchTerm('');
|
|
setActiveTabKey(Repos.INCLUDED);
|
|
setActiveStream('');
|
|
setActiveSortIndex(0);
|
|
setActiveSortDirection('asc');
|
|
};
|
|
|
|
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));
|
|
if (pkg.type === 'module') {
|
|
setActiveStream(pkg.stream || '');
|
|
setActiveSortIndex(2);
|
|
setPage(1);
|
|
dispatch(
|
|
addModule({
|
|
name: pkg.module_name || '',
|
|
stream: pkg.stream || '',
|
|
})
|
|
);
|
|
}
|
|
setCurrentlyRemovedPackages((prev) =>
|
|
prev.filter((curr) => curr.name !== pkg.name)
|
|
);
|
|
}
|
|
} else {
|
|
dispatch(removePackage(pkg.name));
|
|
if (pkg.type === 'module' && pkg.module_name) {
|
|
dispatch(removeModule(pkg.module_name));
|
|
}
|
|
setCurrentlyRemovedPackages((last) => [...last, pkg]);
|
|
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;
|
|
setCurrentlyRemovedPackages([]);
|
|
setPage(1);
|
|
setToggleSelected(id);
|
|
};
|
|
|
|
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 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:
|
|
getEpelDefinitionForDistribution(distribution) ??
|
|
EPEL_10_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 handleTabClick = (event: React.MouseEvent, tabIndex: Repos) => {
|
|
if (tabIndex !== activeTabKey) {
|
|
setCurrentlyRemovedPackages([]);
|
|
setPage(1);
|
|
setActiveTabKey(tabIndex);
|
|
}
|
|
};
|
|
|
|
const initialExpandedPkgs: IBPackageWithRepositoryInfo[] = [];
|
|
const [expandedPkgs, setExpandedPkgs] = useState(initialExpandedPkgs);
|
|
|
|
const setPkgExpanded = (
|
|
pkg: IBPackageWithRepositoryInfo,
|
|
isExpanding: boolean
|
|
) =>
|
|
setExpandedPkgs((prevExpanded) => {
|
|
const otherExpandedPkgs = prevExpanded.filter((p) => p.name !== pkg.name);
|
|
return isExpanding ? [...otherExpandedPkgs, pkg] : otherExpandedPkgs;
|
|
});
|
|
|
|
const isPkgExpanded = (pkg: IBPackageWithRepositoryInfo) =>
|
|
expandedPkgs.includes(pkg);
|
|
|
|
const initialExpandedGroups: GroupWithRepositoryInfo['name'][] = [];
|
|
const [expandedGroups, setExpandedGroups] = useState(initialExpandedGroups);
|
|
|
|
const setGroupsExpanded = (
|
|
group: GroupWithRepositoryInfo['name'],
|
|
isExpanding: boolean
|
|
) =>
|
|
setExpandedGroups((prevExpanded) => {
|
|
const otherExpandedGroups = prevExpanded.filter((g) => g !== group);
|
|
return isExpanding
|
|
? [...otherExpandedGroups, group]
|
|
: otherExpandedGroups;
|
|
});
|
|
|
|
const isGroupExpanded = (group: GroupWithRepositoryInfo['name']) =>
|
|
expandedGroups.includes(group);
|
|
|
|
const [activeSortIndex, setActiveSortIndex] = useState<number>(0);
|
|
const [activeSortDirection, setActiveSortDirection] = useState<
|
|
'asc' | 'desc'
|
|
>('asc');
|
|
|
|
const getSortableRowValues = (
|
|
pkg: IBPackageWithRepositoryInfo
|
|
): (string | number | ApiPackageSourcesResponse[] | undefined)[] => {
|
|
return [pkg.name, pkg.summary, pkg.stream, pkg.end_date, pkg.repository];
|
|
};
|
|
|
|
let sortedPackages = transformedPackages;
|
|
sortedPackages = transformedPackages.sort((a, b) => {
|
|
const aValue = getSortableRowValues(a)[activeSortIndex];
|
|
const bValue = getSortableRowValues(b)[activeSortIndex];
|
|
if (typeof aValue === 'number') {
|
|
// Numeric sort
|
|
if (activeSortDirection === 'asc') {
|
|
return (aValue as number) - (bValue as number);
|
|
}
|
|
return (bValue as number) - (aValue as number);
|
|
}
|
|
// String sort
|
|
// if active stream is set, sort it to the top
|
|
if (aValue === activeStream) {
|
|
return -1;
|
|
}
|
|
if (bValue === activeStream) {
|
|
return 1;
|
|
}
|
|
if (activeSortDirection === 'asc') {
|
|
// handle packages with undefined stream
|
|
if (!aValue) {
|
|
return -1;
|
|
}
|
|
if (!bValue) {
|
|
return 1;
|
|
}
|
|
return (aValue as string).localeCompare(bValue as string);
|
|
} else {
|
|
// handle packages with undefined stream
|
|
if (!aValue) {
|
|
return 1;
|
|
}
|
|
if (!bValue) {
|
|
return -1;
|
|
}
|
|
return (bValue as string).localeCompare(aValue as string);
|
|
}
|
|
});
|
|
|
|
const getSortParams = (columnIndex: number) => ({
|
|
sortBy: {
|
|
index: activeSortIndex,
|
|
direction: activeSortDirection,
|
|
},
|
|
onSort: (
|
|
_event: React.MouseEvent,
|
|
index: number,
|
|
direction: 'asc' | 'desc'
|
|
) => {
|
|
setActiveSortIndex(index);
|
|
setActiveSortDirection(direction);
|
|
},
|
|
columnIndex,
|
|
});
|
|
|
|
const isPackageSelected = (pkg: IBPackageWithRepositoryInfo) => {
|
|
let isSelected = false;
|
|
|
|
if (!pkg.type || pkg.type === 'package') {
|
|
const isModuleWithSameName = modules.some(
|
|
(module) => module.name === pkg.name
|
|
);
|
|
isSelected =
|
|
packages.some((p) => p.name === pkg.name) && !isModuleWithSameName;
|
|
}
|
|
|
|
if (pkg.type === 'module') {
|
|
// the package is selected if it's added to the packages state
|
|
// and its module stream matches one in enabled_modules
|
|
isSelected =
|
|
packages.some((p) => p.name === pkg.name) &&
|
|
modules.some(
|
|
(m) => m.name === pkg.module_name && m.stream === pkg.stream
|
|
);
|
|
}
|
|
|
|
return isSelected;
|
|
};
|
|
|
|
/**
|
|
* Determines if the package's (or group's) select is disabled.
|
|
*
|
|
* Select should be disabled:
|
|
* - if the item is a module
|
|
* - and the module is added to enabled_modules
|
|
* - but the stream doesn't match the stream in enabled_modules
|
|
*
|
|
* @param pkg Package
|
|
* @returns Package (or group) is / is not selected
|
|
*/
|
|
const isSelectDisabled = (pkg: IBPackageWithRepositoryInfo) => {
|
|
const isModuleDisabledByPackage =
|
|
pkg.type === 'module' &&
|
|
packages.some(
|
|
(p) => (!p.type || p.type === 'package') && p.name === pkg.module_name
|
|
);
|
|
|
|
const isPackageDisabledByModule =
|
|
(!pkg.type || pkg.type === 'package') &&
|
|
modules.some((module) => module.name === pkg.name);
|
|
|
|
const isModuleStreamConflict =
|
|
pkg.type === 'module' &&
|
|
modules.some((module) => module.name === pkg.module_name) &&
|
|
!modules.some((m) => m.stream === pkg.stream);
|
|
|
|
return (
|
|
isModuleDisabledByPackage ||
|
|
isPackageDisabledByModule ||
|
|
isModuleStreamConflict
|
|
);
|
|
};
|
|
|
|
const formatDate = (date: string | undefined) => {
|
|
if (!date) {
|
|
return <>N/A</>;
|
|
}
|
|
|
|
const retirementDate = new Date(date);
|
|
|
|
const currentDate = new Date();
|
|
const msPerDay = 1000 * 60 * 60 * 24;
|
|
const differenceInDays = Math.round(
|
|
(retirementDate.getTime() - currentDate.getTime()) / msPerDay
|
|
);
|
|
|
|
let icon;
|
|
|
|
switch (true) {
|
|
case differenceInDays < 0:
|
|
icon = (
|
|
<Icon status="danger" isInline>
|
|
<ExclamationCircleIcon />
|
|
</Icon>
|
|
);
|
|
break;
|
|
case differenceInDays <= 365:
|
|
icon = (
|
|
<Icon status="warning" isInline>
|
|
<ExclamationTriangleIcon />
|
|
</Icon>
|
|
);
|
|
break;
|
|
case differenceInDays > 365:
|
|
icon = (
|
|
<Icon status="success" isInline>
|
|
<CheckCircleIcon />
|
|
</Icon>
|
|
);
|
|
break;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{icon}{' '}
|
|
{retirementDate.toLocaleString('en-US', { month: 'short' }) +
|
|
' ' +
|
|
retirementDate.getFullYear()}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const composePkgTable = () => {
|
|
let rows: ReactElement[] = [];
|
|
|
|
if (showGroups) {
|
|
rows = rows.concat(
|
|
transformedGroups
|
|
.slice(computeStart(), computeEnd())
|
|
.map((grp, rowIndex) => (
|
|
<Tbody
|
|
key={`${grp.name}-${rowIndex}`}
|
|
isExpanded={isGroupExpanded(grp.name)}
|
|
>
|
|
<Tr data-testid="package-row">
|
|
<Td
|
|
expand={{
|
|
rowIndex: rowIndex,
|
|
isExpanded: isGroupExpanded(grp.name),
|
|
onToggle: () =>
|
|
setGroupsExpanded(grp.name, !isGroupExpanded(grp.name)),
|
|
expandId: `${grp.name}-expandable`,
|
|
}}
|
|
/>
|
|
<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>
|
|
) : (
|
|
<Content>This group has no packages</Content>
|
|
)}
|
|
</div>
|
|
}
|
|
>
|
|
<Button
|
|
icon={<HelpIcon className="pf-v6-u-ml-xs" />}
|
|
variant="plain"
|
|
aria-label="About included packages"
|
|
component="span"
|
|
className="pf-v6-u-p-0"
|
|
isInline
|
|
/>
|
|
</Popover>
|
|
</Td>
|
|
<Td>N/A</Td>
|
|
<Td>N/A</Td>
|
|
{grp.repository === 'distro' ? (
|
|
<>
|
|
<Td>Red Hat</Td>
|
|
</>
|
|
) : grp.repository === 'custom' ? (
|
|
<>
|
|
<Td>Third party repository</Td>
|
|
</>
|
|
) : grp.repository === 'recommended' ? (
|
|
<>
|
|
<Td>
|
|
<Icon status="warning">
|
|
<OptimizeIcon />
|
|
</Icon>{' '}
|
|
EPEL {getEpelVersionForDistribution(distribution)}{' '}
|
|
Everything x86_64
|
|
</Td>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Td className="not-available">Not available</Td>
|
|
</>
|
|
)}
|
|
</Tr>
|
|
<Tr isExpanded={isGroupExpanded(grp.name)}>
|
|
<Td colSpan={6}>
|
|
<ExpandableRowContent>
|
|
{
|
|
<DescriptionList>
|
|
<DescriptionListGroup>
|
|
<DescriptionListTerm>
|
|
Description
|
|
{toggleSelected === 'toggle-selected' && (
|
|
<PackageInfoNotAvailablePopover />
|
|
)}
|
|
</DescriptionListTerm>
|
|
<DescriptionListDescription>
|
|
{grp.description
|
|
? grp.description
|
|
: 'Not available'}
|
|
</DescriptionListDescription>
|
|
</DescriptionListGroup>
|
|
</DescriptionList>
|
|
}
|
|
</ExpandableRowContent>
|
|
</Td>
|
|
</Tr>
|
|
</Tbody>
|
|
))
|
|
);
|
|
}
|
|
|
|
if (showPackages) {
|
|
rows = rows.concat(
|
|
sortedPackages
|
|
.slice(computeStart(), computeEnd())
|
|
.map((pkg, rowIndex) => (
|
|
<Tbody
|
|
key={`${pkg.name}-${rowIndex}`}
|
|
isExpanded={isPkgExpanded(pkg)}
|
|
>
|
|
<Tr data-testid="package-row">
|
|
<Td
|
|
expand={{
|
|
rowIndex: rowIndex,
|
|
isExpanded: isPkgExpanded(pkg),
|
|
onToggle: () => setPkgExpanded(pkg, !isPkgExpanded(pkg)),
|
|
expandId: `${pkg.name}-expandable`,
|
|
}}
|
|
/>
|
|
<Td
|
|
select={{
|
|
isSelected: isPackageSelected(pkg),
|
|
rowIndex: rowIndex,
|
|
onSelect: (event, isSelecting) =>
|
|
handleSelect(pkg, rowIndex, isSelecting),
|
|
isDisabled: isSelectDisabled(pkg),
|
|
}}
|
|
title={
|
|
isSelectDisabled(pkg)
|
|
? 'Disabled due to the package(s) you selected. You cannot select packages from different application stream versions.'
|
|
: ''
|
|
}
|
|
/>
|
|
<Td>{pkg.name}</Td>
|
|
<Td>{pkg.stream ? pkg.stream : 'N/A'}</Td>
|
|
<Td>{pkg.end_date ? formatDate(pkg.end_date) : 'N/A'}</Td>
|
|
{pkg.repository === 'distro' ? (
|
|
<>
|
|
<Td>Red Hat</Td>
|
|
</>
|
|
) : pkg.repository === 'custom' ? (
|
|
<>
|
|
<Td>Third party repository</Td>
|
|
</>
|
|
) : pkg.repository === 'recommended' ? (
|
|
<>
|
|
<Td>
|
|
<Icon status="warning">
|
|
<OptimizeIcon />
|
|
</Icon>{' '}
|
|
EPEL {getEpelVersionForDistribution(distribution)}{' '}
|
|
Everything x86_64
|
|
</Td>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Td className="not-available">Not available</Td>
|
|
</>
|
|
)}
|
|
</Tr>
|
|
<Tr isExpanded={isPkgExpanded(pkg)}>
|
|
<Td colSpan={6}>
|
|
<ExpandableRowContent>
|
|
{
|
|
<DescriptionList>
|
|
<DescriptionListGroup>
|
|
<DescriptionListTerm>
|
|
Description
|
|
{toggleSelected === 'toggle-selected' && (
|
|
<PackageInfoNotAvailablePopover />
|
|
)}
|
|
</DescriptionListTerm>
|
|
<DescriptionListDescription>
|
|
{pkg.summary ? pkg.summary : 'Not available'}
|
|
</DescriptionListDescription>
|
|
</DescriptionListGroup>
|
|
</DescriptionList>
|
|
}
|
|
</ExpandableRowContent>
|
|
</Td>
|
|
</Tr>
|
|
</Tbody>
|
|
))
|
|
);
|
|
}
|
|
return rows;
|
|
};
|
|
|
|
const bodyContent = useMemo(() => {
|
|
switch (true) {
|
|
case debouncedSearchTermLengthOf1 && !debouncedSearchTermIsGroup:
|
|
return TooShort();
|
|
case (toggleSelected === 'toggle-selected' &&
|
|
packages.length === 0 &&
|
|
groups.length === 0) ||
|
|
(!debouncedSearchTerm && toggleSelected === 'toggle-available'):
|
|
return <EmptySearch />;
|
|
case (debouncedSearchTerm &&
|
|
(isLoadingRecommendedPackages || isLoadingRecommendedGroups) &&
|
|
activeTabKey === Repos.OTHER) ||
|
|
(debouncedSearchTerm &&
|
|
(isLoadingDistroPackages ||
|
|
isLoadingCustomPackages ||
|
|
isLoadingDistroGroups ||
|
|
isLoadingCustomGroups) &&
|
|
activeTabKey === Repos.INCLUDED):
|
|
return <Searching />;
|
|
case debouncedSearchTerm &&
|
|
transformedPackages.length === 0 &&
|
|
transformedGroups.length === 0 &&
|
|
toggleSelected === 'toggle-available':
|
|
return <NoResultsFound />;
|
|
case debouncedSearchTerm &&
|
|
toggleSelected === 'toggle-selected' &&
|
|
activeTabKey === Repos.OTHER &&
|
|
packages.length > 0 &&
|
|
groups.length > 0:
|
|
return <TryLookingUnderIncluded />;
|
|
default:
|
|
return composePkgTable();
|
|
}
|
|
// 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,
|
|
activeTabKey,
|
|
transformedPackages,
|
|
isSelectingPackage,
|
|
recommendedRepositories,
|
|
transformedPackages.length,
|
|
transformedGroups.length,
|
|
expandedPkgs,
|
|
expandedGroups,
|
|
sortedPackages,
|
|
activeSortDirection,
|
|
activeSortIndex,
|
|
]);
|
|
|
|
const PackagesTable = () => {
|
|
return (
|
|
<Table variant="compact" data-testid="packages-table">
|
|
<Thead>
|
|
<Tr>
|
|
<Th aria-label="Expanded" />
|
|
<Th aria-label="Selected" />
|
|
<Th sort={getSortParams(0)} width={20}>
|
|
Name
|
|
</Th>
|
|
<Th sort={getSortParams(2)} width={20}>
|
|
Application stream
|
|
</Th>
|
|
<Th sort={getSortParams(3)} width={20}>
|
|
Retirement date
|
|
</Th>
|
|
<Th width={20}>Package repository</Th>
|
|
</Tr>
|
|
</Thead>
|
|
{bodyContent}
|
|
</Table>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<RepositoryModal />
|
|
<Toolbar>
|
|
<Stack>
|
|
<ToolbarContent>
|
|
<ToolbarItem>
|
|
<SearchInput
|
|
type="text"
|
|
placeholder="Search packages"
|
|
aria-label="Search packages"
|
|
data-testid="packages-search-input"
|
|
value={searchTerm}
|
|
onChange={handleSearch}
|
|
onClear={() => handleClear()}
|
|
/>
|
|
</ToolbarItem>
|
|
<ToolbarItem>
|
|
<ToggleGroup>
|
|
<ToggleGroupItem
|
|
text="Available"
|
|
buttonId="toggle-available"
|
|
isSelected={toggleSelected === 'toggle-available'}
|
|
onChange={handleFilterToggleClick}
|
|
/>
|
|
<ToggleGroupItem
|
|
text={`Selected${
|
|
packages.length + groups.length === 0
|
|
? ''
|
|
: packages.length + groups.length <= 100
|
|
? ` (${packages.length + groups.length})`
|
|
: ' (100+)'
|
|
}`}
|
|
buttonId="toggle-selected"
|
|
isSelected={toggleSelected === 'toggle-selected'}
|
|
onChange={handleFilterToggleClick}
|
|
/>
|
|
</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>
|
|
|
|
<Tabs
|
|
activeKey={activeTabKey}
|
|
onSelect={handleTabClick}
|
|
aria-label="Repositories tabs on packages step"
|
|
>
|
|
<Tab
|
|
eventKey="included-repos"
|
|
title={<TabTitleText>Included repos</TabTitleText>}
|
|
actions={<IncludedReposPopover />}
|
|
aria-label="Included repositories"
|
|
/>
|
|
{!process.env.IS_ON_PREMISE && (
|
|
<Tab
|
|
eventKey="other-repos"
|
|
title={<TabTitleText>Other repos</TabTitleText>}
|
|
actions={<OtherReposPopover />}
|
|
aria-label="Other repositories"
|
|
/>
|
|
)}
|
|
</Tabs>
|
|
<PackagesTable />
|
|
<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;
|