Wizard V2: Packages refactor, recommendations fix.
This commit is contained in:
parent
8271e6d159
commit
f9aae48dd1
10 changed files with 408 additions and 236 deletions
|
|
@ -671,7 +671,6 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/RecommendationsResponse"
|
||||
components:
|
||||
|
|
@ -1064,7 +1063,7 @@ components:
|
|||
distribution. The snapshot that was made closest to, but before the specified date will
|
||||
be used. If no snapshots made before the specified date can be found, the snapshot
|
||||
closest to, but after the specified date will be used. If no snapshots can be found at
|
||||
all, the request will fail.
|
||||
all, the request will fail. The format must be YYYY-MM-DD (ISO 8601 extended).
|
||||
ImageTypes:
|
||||
type: string
|
||||
enum:
|
||||
|
|
|
|||
|
|
@ -82,3 +82,7 @@ div.pf-v5-c-alert.pf-m-inline.pf-m-plain.pf-m-warning {
|
|||
margin-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pf-v5-c-pagination.pf-m-bottom .pf-v5-c-menu-toggle {
|
||||
display: flex
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||
|
||||
import {
|
||||
Alert,
|
||||
Bullseye,
|
||||
Button,
|
||||
ExpandableSection,
|
||||
Icon,
|
||||
|
|
@ -19,47 +20,43 @@ import { useDispatch } from 'react-redux';
|
|||
import { useAppSelector } from '../../../../store/hooks';
|
||||
import { useRecommendPackageMutation } from '../../../../store/imageBuilderApi';
|
||||
import { addPackage, selectPackages } from '../../../../store/wizardSlice';
|
||||
import useDebounce from '../../../../Utilities/useDebounce';
|
||||
|
||||
const PackageRecommendations = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const packages = useAppSelector(selectPackages);
|
||||
const undebouncedPackages = useAppSelector(selectPackages);
|
||||
const packages = useDebounce(undebouncedPackages);
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const onToggle = (_event: React.MouseEvent, isExpanded: boolean) => {
|
||||
setIsExpanded(isExpanded);
|
||||
};
|
||||
|
||||
const [fetchRecommendedPackages, { data, isSuccess, isLoading, isError }] =
|
||||
useRecommendPackageMutation();
|
||||
|
||||
useEffect(() => {
|
||||
const getRecommendedPackages = async () => {
|
||||
await fetchRecommendedPackages({
|
||||
recommendPackageRequest: {
|
||||
packages: packages.map((pkg) => pkg.name),
|
||||
recommendedPackages: 5,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (packages.length > 0) {
|
||||
getRecommendedPackages();
|
||||
if (isExpanded && packages.length > 0) {
|
||||
(async () => {
|
||||
await fetchRecommendedPackages({
|
||||
recommendPackageRequest: {
|
||||
packages: packages.map((pkg) => pkg.name),
|
||||
recommendedPackages: 5,
|
||||
},
|
||||
});
|
||||
})();
|
||||
}
|
||||
}, [fetchRecommendedPackages, packages]);
|
||||
}, [fetchRecommendedPackages, packages, isExpanded]);
|
||||
|
||||
const addAllPackages = () => {
|
||||
if (data) {
|
||||
for (const pkg in data[0].packages) {
|
||||
if (data?.packages?.length) {
|
||||
data.packages.forEach((pkg) =>
|
||||
dispatch(
|
||||
addPackage({
|
||||
name: data[0].packages[pkg],
|
||||
name: pkg,
|
||||
summary: 'Added from recommended packages',
|
||||
repository: 'distro',
|
||||
})
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -74,7 +71,7 @@ const PackageRecommendations = () => {
|
|||
};
|
||||
|
||||
const isRecommendedPackageSelected = (recPkg: string) => {
|
||||
const foundInPackages = packages.some((pkg) => recPkg === pkg.name);
|
||||
const foundInPackages = packages?.some((pkg) => recPkg === pkg.name);
|
||||
return foundInPackages;
|
||||
};
|
||||
|
||||
|
|
@ -91,7 +88,7 @@ const PackageRecommendations = () => {
|
|||
Recommended Red Hat packages
|
||||
</>
|
||||
}
|
||||
onToggle={onToggle}
|
||||
onToggle={(_, bool) => setIsExpanded(bool)}
|
||||
isExpanded={isExpanded}
|
||||
>
|
||||
{packages.length === 0 && (
|
||||
|
|
@ -109,10 +106,10 @@ const PackageRecommendations = () => {
|
|||
again by changing your selected packages.
|
||||
</Alert>
|
||||
)}
|
||||
{isSuccess && !data && (
|
||||
{isSuccess && !data?.packages?.length && (
|
||||
<>No recommendations found for the set of selected packages</>
|
||||
)}
|
||||
{isSuccess && data && data[0].packages && (
|
||||
{isSuccess && data && data?.packages && (
|
||||
<>
|
||||
<TextContent>
|
||||
<Text>
|
||||
|
|
@ -139,7 +136,7 @@ const PackageRecommendations = () => {
|
|||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{data[0].packages.map((pkg) => (
|
||||
{data.packages.map((pkg) => (
|
||||
<Tr key={pkg}>
|
||||
<Td>{pkg}</Td>
|
||||
{/*<Td>TODO summary</Td>*/}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
Bullseye,
|
||||
|
|
@ -11,13 +11,17 @@ import {
|
|||
EmptyStateIcon,
|
||||
EmptyStateVariant,
|
||||
Icon,
|
||||
InputGroup,
|
||||
InputGroupItem,
|
||||
InputGroupText,
|
||||
Pagination,
|
||||
PaginationVariant,
|
||||
Popover,
|
||||
SearchInput,
|
||||
Spinner,
|
||||
Stack,
|
||||
Text,
|
||||
TextContent,
|
||||
TextInput,
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
Toolbar,
|
||||
|
|
@ -32,11 +36,11 @@ import {
|
|||
SearchIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
|
||||
import { debounce } from 'lodash';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import CustomHelperText from './components/CustomHelperText';
|
||||
|
||||
import {
|
||||
DEBOUNCED_SEARCH_WAIT_TIME,
|
||||
EPEL_8_REPO_DEFINITION,
|
||||
EPEL_9_REPO_DEFINITION,
|
||||
RH_ICON_SIZE,
|
||||
|
|
@ -62,6 +66,7 @@ import {
|
|||
removeRecommendedRepository,
|
||||
selectRecommendedRepositories,
|
||||
} from '../../../../store/wizardSlice';
|
||||
import useDebounce from '../../../../Utilities/useDebounce';
|
||||
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
|
||||
|
||||
type PackageRepository = 'distro' | 'custom' | 'recommended' | '';
|
||||
|
|
@ -72,6 +77,11 @@ export type IBPackageWithRepositoryInfo = {
|
|||
repository: PackageRepository;
|
||||
};
|
||||
|
||||
export enum RepoToggle {
|
||||
INCLUDED = 'toggle-included-repos',
|
||||
OTHER = 'toggle-other-repos',
|
||||
}
|
||||
|
||||
const Packages = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
|
@ -97,8 +107,9 @@ const Packages = () => {
|
|||
const [perPage, setPerPage] = useState(10);
|
||||
const [page, setPage] = useState(1);
|
||||
const [toggleSelected, setToggleSelected] = useState('toggle-available');
|
||||
const [toggleSourceRepos, setToggleSourceRepos] = useState(
|
||||
'toggle-included-repos'
|
||||
|
||||
const [toggleSourceRepos, setToggleSourceRepos] = useState<RepoToggle>(
|
||||
RepoToggle.INCLUDED
|
||||
);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
|
@ -110,6 +121,10 @@ const Packages = () => {
|
|||
isLoading: isLoadingCustomPackages,
|
||||
},
|
||||
] = useSearchRpmMutation();
|
||||
|
||||
const debouncedSearchTerm = useDebounce(searchTerm);
|
||||
const debouncedSearchTermLengthOf1 = debouncedSearchTerm.length === 1;
|
||||
|
||||
const [
|
||||
searchRecommendedRpms,
|
||||
{
|
||||
|
|
@ -127,49 +142,50 @@ const Packages = () => {
|
|||
{
|
||||
distribution: distribution,
|
||||
architecture: arch,
|
||||
search: searchTerm,
|
||||
search: debouncedSearchTerm,
|
||||
},
|
||||
{ skip: !searchTerm }
|
||||
{ skip: !debouncedSearchTerm || debouncedSearchTermLengthOf1 }
|
||||
);
|
||||
|
||||
const [createRepository] = useCreateRepositoryMutation();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCustomPackages = async () => {
|
||||
await searchRpms({
|
||||
apiContentUnitSearchRequest: {
|
||||
search: searchTerm,
|
||||
urls: customRepositories.flatMap((repo) => {
|
||||
if (!repo.baseurl) {
|
||||
throw new Error(
|
||||
`Repository (id: ${repo.id}, name: ${repo?.name}) is missing baseurl`
|
||||
);
|
||||
}
|
||||
return repo.baseurl;
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (searchTerm.length > 1) {
|
||||
fetchCustomPackages();
|
||||
if (debouncedSearchTerm.length > 2) {
|
||||
if (toggleSourceRepos === RepoToggle.INCLUDED) {
|
||||
(async () => {
|
||||
await searchRpms({
|
||||
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 {
|
||||
(async () => {
|
||||
await searchRecommendedRpms({
|
||||
apiContentUnitSearchRequest: {
|
||||
search: debouncedSearchTerm,
|
||||
urls: [epelRepoUrlByDistribution],
|
||||
},
|
||||
});
|
||||
})();
|
||||
}
|
||||
}
|
||||
}, [customRepositories, searchRpms, searchTerm]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRecommendedPackages = async () => {
|
||||
await searchRecommendedRpms({
|
||||
apiContentUnitSearchRequest: {
|
||||
search: searchTerm,
|
||||
urls: [epelRepoUrlByDistribution],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (searchTerm.length > 1) {
|
||||
fetchRecommendedPackages();
|
||||
}
|
||||
}, [distribution, searchRecommendedRpms, searchTerm]);
|
||||
}, [
|
||||
customRepositories,
|
||||
searchRpms,
|
||||
debouncedSearchTerm,
|
||||
toggleSourceRepos,
|
||||
searchRecommendedRpms,
|
||||
epelRepoUrlByDistribution,
|
||||
]);
|
||||
|
||||
const EmptySearch = () => {
|
||||
return (
|
||||
|
|
@ -206,7 +222,7 @@ const Packages = () => {
|
|||
<EmptyState variant={EmptyStateVariant.sm}>
|
||||
<EmptyStateHeader icon={<EmptyStateIcon icon={Spinner} />} />
|
||||
<EmptyStateBody>
|
||||
{toggleSourceRepos === 'toggle-other-repos'
|
||||
{toggleSourceRepos === RepoToggle.OTHER
|
||||
? 'Searching for recommendations'
|
||||
: 'Searching'}
|
||||
</EmptyStateBody>
|
||||
|
|
@ -238,6 +254,27 @@ const Packages = () => {
|
|||
);
|
||||
};
|
||||
|
||||
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>
|
||||
|
|
@ -273,7 +310,7 @@ const Packages = () => {
|
|||
Try looking under "
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => setToggleSourceRepos('toggle-included-repos')}
|
||||
onClick={() => setToggleSourceRepos(RepoToggle.INCLUDED)}
|
||||
isInline
|
||||
>
|
||||
Included repos
|
||||
|
|
@ -289,7 +326,7 @@ const Packages = () => {
|
|||
|
||||
const NoResultsFound = () => {
|
||||
const { isBeta } = useGetEnvironment();
|
||||
if (toggleSourceRepos === 'toggle-included-repos') {
|
||||
if (toggleSourceRepos === RepoToggle.INCLUDED) {
|
||||
return (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
|
|
@ -308,7 +345,7 @@ const Packages = () => {
|
|||
<EmptyStateActions>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => setToggleSourceRepos('toggle-other-repos')}
|
||||
onClick={() => setToggleSourceRepos(RepoToggle.OTHER)}
|
||||
>
|
||||
Search other repositories
|
||||
</Button>
|
||||
|
|
@ -439,7 +476,7 @@ const Packages = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const transformPackageData = () => {
|
||||
const transformedPackages = useMemo(() => {
|
||||
let transformedDistroData: IBPackageWithRepositoryInfo[] = [];
|
||||
let transformedCustomData: IBPackageWithRepositoryInfo[] = [];
|
||||
let transformedRecommendedData: IBPackageWithRepositoryInfo[] = [];
|
||||
|
|
@ -464,10 +501,10 @@ const Packages = () => {
|
|||
);
|
||||
|
||||
if (
|
||||
searchTerm !== '' &&
|
||||
debouncedSearchTerm !== '' &&
|
||||
combinedPackageData.length === 0 &&
|
||||
isSuccessRecommendedPackages &&
|
||||
toggleSourceRepos === 'toggle-other-repos'
|
||||
toggleSourceRepos === RepoToggle.OTHER
|
||||
) {
|
||||
transformedRecommendedData = dataRecommendedPackages!.map((values) => ({
|
||||
name: values.package_name!,
|
||||
|
|
@ -481,7 +518,7 @@ const Packages = () => {
|
|||
}
|
||||
|
||||
if (toggleSelected === 'toggle-available') {
|
||||
if (toggleSourceRepos === 'toggle-included-repos') {
|
||||
if (toggleSourceRepos === RepoToggle.INCLUDED) {
|
||||
return combinedPackageData.filter(
|
||||
(pkg) => pkg.repository !== 'recommended'
|
||||
);
|
||||
|
|
@ -492,30 +529,44 @@ const Packages = () => {
|
|||
}
|
||||
} else {
|
||||
const selectedPackages = [...packages];
|
||||
if (toggleSourceRepos === 'toggle-included-repos') {
|
||||
if (toggleSourceRepos === RepoToggle.INCLUDED) {
|
||||
return selectedPackages;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Get and sort the list of packages including repository info
|
||||
const transformedPackages = transformPackageData().sort((a, b) => {
|
||||
}, [
|
||||
dataCustomPackages,
|
||||
dataDistroPackages?.data,
|
||||
dataRecommendedPackages,
|
||||
debouncedSearchTerm,
|
||||
isSuccessCustomPackages,
|
||||
isSuccessDistroPackages,
|
||||
isSuccessRecommendedPackages,
|
||||
packages,
|
||||
toggleSelected,
|
||||
toggleSourceRepos,
|
||||
]).sort((a, b) => {
|
||||
const aPkg = a.name.toLowerCase();
|
||||
const bPkg = b.name.toLowerCase();
|
||||
// check exact match first
|
||||
if (aPkg === searchTerm) {
|
||||
if (aPkg === debouncedSearchTerm) {
|
||||
return -1;
|
||||
}
|
||||
if (bPkg === searchTerm) {
|
||||
if (bPkg === debouncedSearchTerm) {
|
||||
return 1;
|
||||
}
|
||||
// check for packages that start with the search term
|
||||
if (aPkg.startsWith(searchTerm) && !bPkg.startsWith(searchTerm)) {
|
||||
if (
|
||||
aPkg.startsWith(debouncedSearchTerm) &&
|
||||
!bPkg.startsWith(debouncedSearchTerm)
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
if (bPkg.startsWith(searchTerm) && !aPkg.startsWith(searchTerm)) {
|
||||
if (
|
||||
bPkg.startsWith(debouncedSearchTerm) &&
|
||||
!aPkg.startsWith(debouncedSearchTerm)
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
// if both (or neither) start with the search term
|
||||
|
|
@ -536,8 +587,6 @@ const Packages = () => {
|
|||
setSearchTerm(selection);
|
||||
};
|
||||
|
||||
const debounceOnChange = debounce(handleSearch, DEBOUNCED_SEARCH_WAIT_TIME);
|
||||
|
||||
const handleSelect = (
|
||||
pkg: IBPackageWithRepositoryInfo,
|
||||
_: number,
|
||||
|
|
@ -572,10 +621,11 @@ const Packages = () => {
|
|||
setToggleSelected(id);
|
||||
};
|
||||
|
||||
const handleRepoToggleClick = (event: React.MouseEvent) => {
|
||||
const id = event.currentTarget.id;
|
||||
setPage(1);
|
||||
setToggleSourceRepos(id);
|
||||
const handleRepoToggleClick = (type: RepoToggle) => {
|
||||
if (toggleSourceRepos !== type) {
|
||||
setPage(1);
|
||||
setToggleSourceRepos(type);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetPage = (_: React.MouseEvent, newPage: number) => {
|
||||
|
|
@ -596,7 +646,7 @@ const Packages = () => {
|
|||
|
||||
const handleExactMatch = () => {
|
||||
const exactMatch = transformedPackages.find(
|
||||
(pkg) => pkg.name === searchTerm
|
||||
(pkg) => pkg.name === debouncedSearchTerm
|
||||
);
|
||||
|
||||
if (exactMatch) {
|
||||
|
|
@ -729,116 +779,184 @@ const Packages = () => {
|
|||
));
|
||||
};
|
||||
|
||||
const bodyContent = useMemo(() => {
|
||||
switch (true) {
|
||||
case debouncedSearchTermLengthOf1 && transformedPackages.length === 0:
|
||||
return TooShort();
|
||||
case (toggleSelected === 'toggle-selected' && packages.length === 0) ||
|
||||
(!debouncedSearchTerm && toggleSelected === 'toggle-available'):
|
||||
return <EmptySearch />;
|
||||
case (debouncedSearchTerm &&
|
||||
isLoadingRecommendedPackages &&
|
||||
toggleSourceRepos === RepoToggle.OTHER) ||
|
||||
(debouncedSearchTerm &&
|
||||
(isLoadingDistroPackages || isLoadingCustomPackages) &&
|
||||
toggleSourceRepos === RepoToggle.INCLUDED):
|
||||
return <Searching />;
|
||||
case debouncedSearchTerm &&
|
||||
transformedPackages.length === 0 &&
|
||||
toggleSelected === 'toggle-available':
|
||||
return <NoResultsFound />;
|
||||
case debouncedSearchTerm &&
|
||||
toggleSelected === 'toggle-selected' &&
|
||||
toggleSourceRepos === RepoToggle.OTHER &&
|
||||
packages.length > 0:
|
||||
return <TryLookingUnderIncluded />;
|
||||
case debouncedSearchTerm && transformedPackages.length >= 100:
|
||||
return handleExactMatch();
|
||||
case (debouncedSearchTerm || toggleSelected === 'toggle-selected') &&
|
||||
transformedPackages.length < 100:
|
||||
return composePkgTable();
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
// Would need significant rewrite to fix this
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
debouncedSearchTerm,
|
||||
debouncedSearchTermLengthOf1,
|
||||
isLoadingCustomPackages,
|
||||
isLoadingDistroPackages,
|
||||
isLoadingRecommendedPackages,
|
||||
isSuccessRecommendedPackages,
|
||||
packages.length,
|
||||
toggleSelected,
|
||||
toggleSourceRepos,
|
||||
transformedPackages,
|
||||
transformedPackages.length,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<RepositoryModal />
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarItem variant="search-filter">
|
||||
<SearchInput
|
||||
aria-label="Search packages"
|
||||
data-testid="packages-search-input"
|
||||
value={searchTerm}
|
||||
onChange={debounceOnChange}
|
||||
/>
|
||||
</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 <= 100 ? packages.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
|
||||
<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 ? 'error' : 'default'
|
||||
}
|
||||
placeholder="Type to search"
|
||||
aria-label="Search packages"
|
||||
data-testid="packages-search-input"
|
||||
value={searchTerm}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
</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 <= 100 ? packages.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>
|
||||
}
|
||||
>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
}
|
||||
buttonId="toggle-included-repos"
|
||||
isSelected={toggleSourceRepos === 'toggle-included-repos'}
|
||||
onChange={handleRepoToggleClick}
|
||||
/>
|
||||
<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
|
||||
<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>
|
||||
}
|
||||
>
|
||||
<HelpIcon />
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
}
|
||||
buttonId="toggle-other-repos"
|
||||
isSelected={toggleSourceRepos === 'toggle-other-repos'}
|
||||
onChange={handleRepoToggleClick}
|
||||
<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
|
||||
itemCount={transformedPackages.length}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={handleSetPage}
|
||||
onPerPageSelect={handlePerPageSelect}
|
||||
isCompact
|
||||
/>
|
||||
</ToggleGroup>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem variant="pagination">
|
||||
<Pagination
|
||||
itemCount={transformedPackages.length}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={handleSetPage}
|
||||
onPerPageSelect={handlePerPageSelect}
|
||||
isCompact
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
<ToolbarContent>
|
||||
<CustomHelperText
|
||||
hide={!debouncedSearchTermLengthOf1}
|
||||
textValue="The search value must be greater than 1 character"
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
</ToolbarContent>
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
|
||||
<Table variant="compact" data-testid="packages-table">
|
||||
<Thead>
|
||||
<Tr>
|
||||
|
|
@ -878,32 +996,7 @@ const Packages = () => {
|
|||
<Th width={20}>Support</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{((toggleSelected === 'toggle-selected' && packages.length === 0) ||
|
||||
(!searchTerm && toggleSelected === 'toggle-available')) && (
|
||||
<EmptySearch />
|
||||
)}
|
||||
{((searchTerm &&
|
||||
isLoadingRecommendedPackages &&
|
||||
toggleSourceRepos === 'toggle-other-repos') ||
|
||||
(searchTerm &&
|
||||
(isLoadingDistroPackages || isLoadingCustomPackages) &&
|
||||
toggleSourceRepos === 'toggle-included-repos')) && <Searching />}
|
||||
{searchTerm &&
|
||||
isSuccessRecommendedPackages &&
|
||||
transformedPackages.length === 0 &&
|
||||
toggleSelected === 'toggle-available' && <NoResultsFound />}
|
||||
{searchTerm &&
|
||||
toggleSelected === 'toggle-selected' &&
|
||||
toggleSourceRepos === 'toggle-other-repos' &&
|
||||
packages.length > 0 && <TryLookingUnderIncluded />}
|
||||
{searchTerm &&
|
||||
transformedPackages.length >= 100 &&
|
||||
handleExactMatch()}
|
||||
{(searchTerm || toggleSelected === 'toggle-selected') &&
|
||||
transformedPackages.length < 100 &&
|
||||
composePkgTable()}
|
||||
</Tbody>
|
||||
<Tbody>{bodyContent}</Tbody>
|
||||
</Table>
|
||||
<Pagination
|
||||
itemCount={transformedPackages.length}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
HelperText,
|
||||
HelperTextItem,
|
||||
FormHelperText,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExclamationCircleIcon } from '@patternfly/react-icons';
|
||||
export type HelperTextVariant =
|
||||
| 'default'
|
||||
| 'indeterminate'
|
||||
| 'warning'
|
||||
| 'success'
|
||||
| 'error';
|
||||
|
||||
interface Props {
|
||||
variant?: HelperTextVariant;
|
||||
textValue?: string;
|
||||
defaultText?: string;
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
const CustomHelperText = ({
|
||||
hide = false,
|
||||
variant = 'error',
|
||||
textValue = '',
|
||||
defaultText = '',
|
||||
}: Props) =>
|
||||
(!!textValue || !!defaultText) && !hide ? (
|
||||
<FormHelperText>
|
||||
<HelperText>
|
||||
<HelperTextItem icon={<ExclamationCircleIcon />} variant={variant}>
|
||||
{textValue ? textValue : defaultText}
|
||||
</HelperTextItem>
|
||||
</HelperText>
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
export default CustomHelperText;
|
||||
32
src/Utilities/useDebounce.tsx
Normal file
32
src/Utilities/useDebounce.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { DEBOUNCED_SEARCH_WAIT_TIME } from '../constants';
|
||||
|
||||
function useDebounce<T>(
|
||||
value: T,
|
||||
delay: number = DEBOUNCED_SEARCH_WAIT_TIME
|
||||
): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
// We need to make sure that we compare-deep here as the default useEffect deps do not.
|
||||
if (!isEqual(value, debouncedValue)) {
|
||||
const timer = setTimeout(
|
||||
() => setDebouncedValue(value),
|
||||
value === '' ? 0 : delay !== undefined ? delay : 500 //If value is empty string, instantly return
|
||||
);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}
|
||||
// Eslint apparrently likes cyclic dependencies.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
|
||||
export default useDebounce;
|
||||
|
|
@ -194,4 +194,4 @@ export const EPEL_9_REPO_DEFINITION = {
|
|||
metadata_verification: false,
|
||||
};
|
||||
|
||||
export const DEBOUNCED_SEARCH_WAIT_TIME = 300;
|
||||
export const DEBOUNCED_SEARCH_WAIT_TIME = 500;
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ export type GetBlueprintComposesApiArg = {
|
|||
ignoreImageTypes?: ImageTypes[];
|
||||
};
|
||||
export type RecommendPackageApiResponse =
|
||||
/** status 200 Return the recommended packages. */ RecommendationsResponse[];
|
||||
/** status 200 Return the recommended packages. */ RecommendationsResponse;
|
||||
export type RecommendPackageApiArg = {
|
||||
recommendPackageRequest: RecommendPackageRequest;
|
||||
};
|
||||
|
|
@ -472,7 +472,7 @@ export type ImageRequest = {
|
|||
distribution. The snapshot that was made closest to, but before the specified date will
|
||||
be used. If no snapshots made before the specified date can be found, the snapshot
|
||||
closest to, but after the specified date will be used. If no snapshots can be found at
|
||||
all, the request will fail.
|
||||
all, the request will fail. The format must be YYYY-MM-DD (ISO 8601 extended).
|
||||
*/
|
||||
snapshot_date?: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -312,6 +312,14 @@ describe('Step Packages', () => {
|
|||
await screen.findByText('Too many results to display');
|
||||
});
|
||||
|
||||
test('should display too short', async () => {
|
||||
await setUp();
|
||||
|
||||
await typeIntoSearchBox('t');
|
||||
|
||||
await screen.findByText('The search value is too short');
|
||||
});
|
||||
|
||||
test('should display relevant results in selected first', async () => {
|
||||
await setUp();
|
||||
|
||||
|
|
|
|||
20
src/test/fixtures/packages.ts
vendored
20
src/test/fixtures/packages.ts
vendored
|
|
@ -141,14 +141,12 @@ export const mockPkgResultAll: PackagesResponse = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const mockPkgRecommendations: RecommendPackageApiResponse = [
|
||||
{
|
||||
packages: [
|
||||
'recommendedPackage1',
|
||||
'recommendedPackage2',
|
||||
'recommendedPackage3',
|
||||
'recommendedPackage4',
|
||||
'recommendedPackage5',
|
||||
],
|
||||
},
|
||||
];
|
||||
export const mockPkgRecommendations: RecommendPackageApiResponse = {
|
||||
packages: [
|
||||
'recommendedPackage1',
|
||||
'recommendedPackage2',
|
||||
'recommendedPackage3',
|
||||
'recommendedPackage4',
|
||||
'recommendedPackage5',
|
||||
],
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue