V2Wizard: Add repository recommendations

This PR is a part of Proactive Assistance - Repository recommendations.

New toggle group was added which allows user to see results from non-added repositories in a case that searched term didn't return any results from added custom and distribution repos.
This commit is contained in:
regexowl 2024-03-07 16:37:41 +01:00 committed by Lucas Garfield
parent 39cb4336b4
commit bfd7420925
2 changed files with 311 additions and 95 deletions

View file

@ -1,27 +1,37 @@
import React, { useEffect, useState } from 'react';
import {
Alert,
Bullseye,
Button,
EmptyState,
EmptyStateBody,
EmptyStateFooter,
EmptyStateHeader,
EmptyStateIcon,
EmptyStateVariant,
Icon,
Pagination,
PaginationVariant,
Popover,
SearchInput,
Text,
TextContent,
ToggleGroup,
ToggleGroupItem,
Toolbar,
ToolbarContent,
ToolbarItem,
} from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons';
import { HelpIcon, OptimizeIcon, SearchIcon } from '@patternfly/react-icons';
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import { useDispatch } from 'react-redux';
import { RH_ICON_SIZE } from '../../../../constants';
import { useSearchRpmMutation } from '../../../../store/contentSourcesApi';
import {
useListRepositoriesQuery,
useSearchRpmMutation,
} from '../../../../store/contentSourcesApi';
import { useAppSelector } from '../../../../store/hooks';
import {
Package,
@ -34,9 +44,12 @@ import {
selectCustomRepositories,
selectDistribution,
addPackage,
addRecommendedRepository,
removeRecommendedRepository,
} from '../../../../store/wizardSlice';
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
type PackageRepository = 'distro' | 'custom' | '';
type PackageRepository = 'distro' | 'custom' | 'recommended' | '';
export type IBPackageWithRepositoryInfo = {
name: Package['name'];
@ -45,82 +58,6 @@ export type IBPackageWithRepositoryInfo = {
isRequiredByOpenScap: boolean;
};
const EmptySearch = () => {
return (
<Tr>
<Td colSpan={5}>
<Bullseye>
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader icon={<EmptyStateIcon icon={SearchIcon} />} />
<EmptyStateBody>
Search above to add additional
<br />
packages to your image.
</EmptyStateBody>
</EmptyState>
</Bullseye>
</Td>
</Tr>
);
};
const NoResultsFound = () => {
return (
<Tr>
<Td colSpan={5}>
<Bullseye>
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader titleText="No results found" headingLevel="h4" />
<EmptyStateBody>Adjust your search and try again.</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 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 Packages = () => {
const dispatch = useDispatch();
@ -129,21 +66,33 @@ const Packages = () => {
const customRepositories = useAppSelector(selectCustomRepositories);
const packages = useAppSelector(selectPackages);
// select the correct version of EPEL repository
// the urls are copied over from the content service
const epelRepoUrlByDistribution = distribution.startsWith('rhel-8')
? 'https://dl.fedoraproject.org/pub/epel/8/Everything/x86_64/'
: 'https://dl.fedoraproject.org/pub/epel/9/Everything/x86_64/';
const { data: epelRepo, isSuccess: isSuccessEpelRepo } =
useListRepositoriesQuery({
url: epelRepoUrlByDistribution,
});
const [perPage, setPerPage] = useState(10);
const [page, setPage] = useState(1);
const [toggleSelected, setToggleSelected] = useState('toggle-available');
/*FOLLOW UP
const [toggleSourceRepos, setToggleSourceRepos] = useState(
'toggle-included-repos'
);
*/
const [searchTerm, setSearchTerm] = useState<string>('');
const [searchTerm, setSearchTerm] = useState('');
const [
searchRpms,
{ data: dataCustomPackages, isSuccess: isSuccessCustomPackages },
] = useSearchRpmMutation();
const [
searchRecommendedRpms,
{ data: dataRecommendedPackages, isSuccess: isSuccessRecommendedPackages },
] = useSearchRpmMutation();
const { data: dataDistroPackages, isSuccess: isSuccessDistroPackages } =
useGetPackagesQuery(
@ -172,12 +121,146 @@ const Packages = () => {
});
};
fetchCustomPackages();
if (searchTerm.length > 1) {
fetchCustomPackages();
}
}, [customRepositories, searchRpms, searchTerm]);
useEffect(() => {
const fetchRecommendedPackages = async () => {
await searchRecommendedRpms({
apiContentUnitSearchRequest: {
search: searchTerm,
urls: [epelRepoUrlByDistribution],
},
});
};
if (searchTerm.length > 1) {
fetchRecommendedPackages();
}
}, [distribution, searchRecommendedRpms, searchTerm]);
const EmptySearch = () => {
return (
<Tr>
<Td colSpan={5}>
<Bullseye>
<EmptyState variant={EmptyStateVariant.sm}>
<EmptyStateHeader icon={<EmptyStateIcon icon={SearchIcon} />} />
<EmptyStateBody>
Search above to add additional
<br />
packages to your image.
</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 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 NoResultsFound = () => {
const { isBeta } = useGetEnvironment();
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 from{' '}
<Button
variant="link"
isInline
component="a"
target="_blank"
href={
isBeta() ? '/preview/settings/content' : '/settings/content'
}
>
your repositories
</Button>{' '}
and{' '}
<Button
variant="link"
isInline
component="a"
target="_blank"
href={
isBeta()
? '/preview/settings/content/popular-repositories'
: '/settings/content/popular-repositories'
}
>
popular repositories
</Button>
</EmptyStateBody>
<EmptyStateFooter>
<Button
variant="primary"
onClick={() => setToggleSourceRepos('toggle-other-repos')}
>
Search other repositories
</Button>
</EmptyStateFooter>
</EmptyState>
</Bullseye>
</Td>
</Tr>
);
};
const transformPackageData = () => {
let transformedDistroData: IBPackageWithRepositoryInfo[] = [];
let transformedCustomData: IBPackageWithRepositoryInfo[] = [];
let transformedRecommendedData: IBPackageWithRepositoryInfo[] = [];
if (isSuccessDistroPackages) {
transformedDistroData = dataDistroPackages.data.map((values) => ({
@ -196,10 +279,28 @@ const Packages = () => {
}));
}
const combinedPackageData = transformedDistroData.concat(
let combinedPackageData = transformedDistroData.concat(
transformedCustomData
);
if (
searchTerm !== '' &&
combinedPackageData.length === 0 &&
isSuccessRecommendedPackages &&
toggleSourceRepos === 'toggle-other-repos'
) {
transformedRecommendedData = dataRecommendedPackages!.map((values) => ({
name: values.package_name!,
summary: values.summary!,
repository: 'recommended',
isRequiredByOpenScap: false,
}));
combinedPackageData = combinedPackageData.concat(
transformedRecommendedData
);
}
if (toggleSelected === 'toggle-available') {
return combinedPackageData;
} else {
@ -251,8 +352,22 @@ const Packages = () => {
) => {
if (isSelecting) {
dispatch(addPackage(pkg));
if (
isSuccessEpelRepo &&
epelRepo.data &&
pkg.repository === 'recommended'
) {
dispatch(addRecommendedRepository(epelRepo.data[0]));
}
} else {
dispatch(removePackage(pkg.name));
if (
isSuccessEpelRepo &&
epelRepo.data &&
packages.filter((pkg) => pkg.repository === 'recommended').length === 1
) {
dispatch(removeRecommendedRepository(epelRepo.data[0]));
}
}
};
@ -262,13 +377,11 @@ const Packages = () => {
setToggleSelected(id);
};
/*FOLLOW UP
const handleRepoToggleClick = (event: React.MouseEvent) => {
const id = event.currentTarget.id;
setPage(1);
setToggleSourceRepos(id);
};
*/
const handleSetPage = (_: React.MouseEvent, newPage: number) => {
setPage(newPage);
@ -365,25 +478,73 @@ const Packages = () => {
/>
</ToggleGroup>
</ToolbarItem>
{/*FOLLOW UP
<ToolbarItem>
{' '}
<ToggleGroup>
<ToggleGroupItem
text="Included repos"
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"
size="sm"
isInline
>
<HelpIcon />
</Button>
</Popover>
</>
}
buttonId="toggle-included-repos"
isSelected={toggleSourceRepos === 'toggle-included-repos'}
onChange={handleRepoToggleClick}
/>
<ToggleGroupItem
text="All repos"
buttonId="toggle-all-repos"
isSelected={toggleSourceRepos === 'toggle-all-repos'}
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"
size="sm"
isInline
>
<HelpIcon />
</Button>
</Popover>
</>
}
buttonId="toggle-other-repos"
isSelected={toggleSourceRepos === 'toggle-other-repos'}
onChange={handleRepoToggleClick}
/>
</ToggleGroup>
</ToolbarItem>
*/}
<ToolbarItem variant="pagination">
<Pagination
itemCount={transformedPackages.length}
@ -410,7 +571,9 @@ const Packages = () => {
{!searchTerm && toggleSelected === 'toggle-available' && (
<EmptySearch />
)}
{searchTerm && transformedPackages.length === 0 && <NoResultsFound />}
{searchTerm &&
transformedPackages.length === 0 &&
toggleSelected === 'toggle-available' && <NoResultsFound />}
{searchTerm &&
transformedPackages.length >= 100 &&
handleExactMatch()}
@ -444,11 +607,22 @@ const Packages = () => {
</Td>
<Td>Supported</Td>
</>
) : (
) : pkg.repository === 'custom' ? (
<>
<Td>Third party repository</Td>
<Td>Not supported</Td>
</>
) : (
<>
<Td>
<Icon status="warning">
<OptimizeIcon />
</Icon>{' '}
EPEL {distribution === 'rhel-8' ? '8' : '9'} Everything
x86_64
</Td>
<Td>Not supported</Td>
</>
)}
</Tr>
))}
@ -462,6 +636,18 @@ const Packages = () => {
onPerPageSelect={handlePerPageSelect}
variant={PaginationVariant.bottom}
/>
{packages.some((pkg) => pkg.repository === 'recommended') && (
<Alert
variant="warning"
title="Custom repositories will be added to your image"
isInline
>
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: EPEL{' '}
{distribution === 'rhel-8' ? '8' : '9'} Everything x86_64
</Alert>
)}
</>
);
};

View file

@ -1,5 +1,6 @@
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { ApiRepositoryResponseRead } from './contentSourcesApi';
import {
CustomRepository,
DistributionProfileItem,
@ -81,6 +82,7 @@ export type wizardState = {
repositories: {
customRepositories: CustomRepository[];
payloadRepositories: Repository[];
recommendedRepositories: ApiRepositoryResponseRead[];
};
packages: IBPackageWithRepositoryInfo[];
details: {
@ -136,6 +138,7 @@ const initialState: wizardState = {
repositories: {
customRepositories: [],
payloadRepositories: [],
recommendedRepositories: [],
},
packages: [],
details: {
@ -251,6 +254,10 @@ export const selectPayloadRepositories = (state: RootState) => {
return state.wizard.repositories.payloadRepositories;
};
export const selectRecommendedRepositories = (state: RootState) => {
return state.wizard.repositories.recommendedRepositories;
};
export const selectPackages = (state: RootState) => {
return state.wizard.packages;
};
@ -450,6 +457,27 @@ export const wizardSlice = createSlice({
changePayloadRepositories: (state, action: PayloadAction<Repository[]>) => {
state.repositories.payloadRepositories = action.payload;
},
addRecommendedRepository: (
state,
action: PayloadAction<ApiRepositoryResponseRead>
) => {
if (
!state.repositories.recommendedRepositories.some(
(repo) => repo.url === action.payload.url
)
) {
state.repositories.recommendedRepositories.push(action.payload);
}
},
removeRecommendedRepository: (
state,
action: PayloadAction<ApiRepositoryResponseRead>
) => {
state.repositories.recommendedRepositories =
state.repositories.recommendedRepositories.filter(
(repo) => repo.url !== action.payload.url
);
},
addPackage: (state, action: PayloadAction<IBPackageWithRepositoryInfo>) => {
state.packages.push(action.payload);
},
@ -512,6 +540,8 @@ export const {
changePartitionMinSize,
changeCustomRepositories,
changePayloadRepositories,
addRecommendedRepository,
removeRecommendedRepository,
addPackage,
removePackage,
clearOscapPackages,