Wizard V2: Packages refactor, recommendations fix.

This commit is contained in:
Andrew Dewar 2024-04-30 22:26:57 +01:00 committed by Michal Gold
parent 8271e6d159
commit f9aae48dd1
10 changed files with 408 additions and 236 deletions

View file

@ -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:

View file

@ -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
}

View file

@ -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>*/}

View file

@ -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 &quot;
<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&apos;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&apos;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}

View file

@ -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;

View 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;

View file

@ -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;

View file

@ -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;
};

View file

@ -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();

View file

@ -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',
],
};