From b69935946801e6f28f92cac5c609938152408606 Mon Sep 17 00:00:00 2001 From: regexowl Date: Mon, 30 Mar 2026 13:12:11 +0200 Subject: [PATCH 01/18] Wizard: Packages - update title and description This updates the step title and description as per revamp mocks. --- .../CreateImageWizard/steps/Packages/index.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Components/CreateImageWizard/steps/Packages/index.tsx b/src/Components/CreateImageWizard/steps/Packages/index.tsx index ad3d7dab8f..1933ad2f6f 100644 --- a/src/Components/CreateImageWizard/steps/Packages/index.tsx +++ b/src/Components/CreateImageWizard/steps/Packages/index.tsx @@ -19,23 +19,18 @@ const PackagesStep = () => {
- Additional packages + Packages - Blueprints created with Images include all required packages. + Search and add individual packages to include in your image. You can + select packages from the repositories included in the previous step. - {isOnPremise ? ( + {isOnPremise && ( <> Search for exact matches by specifying the whole package name, or glob using asterisk wildcards (*) before or after the package name. - ) : ( - <> - Search for package groups by starting your search with the - '@' character. A single '@' as search input - lists all available package groups. - )} From b4c55aafb0af7bf10880d0a98961be10c0a969ca Mon Sep 17 00:00:00 2001 From: regexowl Date: Mon, 30 Mar 2026 13:23:19 +0200 Subject: [PATCH 02/18] Wizard: Add package type dropdown This adds a new dropdown to the package table toolbar that allows to switch between individual package and package group search. --- .../steps/Packages/components/Packages.tsx | 74 +++++++++++++++---- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/src/Components/CreateImageWizard/steps/Packages/components/Packages.tsx b/src/Components/CreateImageWizard/steps/Packages/components/Packages.tsx index 3a9e2ed670..2303f7660e 100644 --- a/src/Components/CreateImageWizard/steps/Packages/components/Packages.tsx +++ b/src/Components/CreateImageWizard/steps/Packages/components/Packages.tsx @@ -1,9 +1,15 @@ import React, { useEffect, useMemo, useState } from 'react'; import { + FormGroup, + MenuToggle, + MenuToggleElement, Pagination, PaginationVariant, SearchInput, + Select, + SelectList, + SelectOption, Stack, Tab, Tabs, @@ -128,6 +134,11 @@ const Packages = () => { const [page, setPage] = useState(1); const [toggleSelected, setToggleSelected] = useState('toggle-available'); const [activeTabKey, setActiveTabKey] = useState(Repos.INCLUDED); + const [packageType, setPackageType] = useState<'packages' | 'groups'>( + 'packages', + ); + const [isPackageTypeDropdownOpen, setIsPackageTypeDropdownOpen] = + useState(false); const [searchTerm, setSearchTerm] = useState(''); const [activeStream, setActiveStream] = useState(''); @@ -146,15 +157,13 @@ const Packages = () => { 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'; + packageType === 'packages' || + (toggleSelected === 'toggle-selected' && debouncedSearchTerm === ''); const showGroups = - (debouncedSearchTerm && debouncedSearchTermIsGroup) || - toggleSelected === 'toggle-selected'; + packageType === 'groups' || + (toggleSelected === 'toggle-selected' && debouncedSearchTerm === ''); const [ searchRecommendedRpms, @@ -207,7 +216,7 @@ const Packages = () => { ] = useSearchRepositoryModuleStreamsMutation(); useEffect(() => { - if (debouncedSearchTermIsGroup) { + if (packageType === 'groups') { return; } if (debouncedSearchTerm.length > 1 && isSuccessDistroRepositories) { @@ -280,19 +289,19 @@ const Packages = () => { arch, template, distribution, - debouncedSearchTermIsGroup, + packageType, snapshotDate, distroUrls, ]); useEffect(() => { - if (!debouncedSearchTermIsGroup) { + if (packageType === 'packages') { return; } if (isSuccessDistroRepositories) { searchDistroGroups({ apiContentUnitSearchRequest: { - search: debouncedSearchTerm.substring(1), + search: debouncedSearchTerm, urls: distroUrls, date: snapshotDate ? new Date(convertStringToDate(snapshotDate)).toISOString() @@ -303,7 +312,7 @@ const Packages = () => { if (activeTabKey === Repos.INCLUDED && customRepositories.length > 0) { searchCustomGroups({ apiContentUnitSearchRequest: { - search: debouncedSearchTerm.substring(1), + search: debouncedSearchTerm, uuids: customRepositories.flatMap((repo) => { return repo.id; }), @@ -315,7 +324,7 @@ const Packages = () => { } else if (activeTabKey === Repos.OTHER && isSuccessEpelRepo) { searchRecommendedGroups({ apiContentUnitSearchRequest: { - search: debouncedSearchTerm.substring(1), + search: debouncedSearchTerm, urls: [epelRepoUrlByDistribution], date: snapshotDate ? new Date(convertStringToDate(snapshotDate)).toISOString() @@ -331,7 +340,7 @@ const Packages = () => { debouncedSearchTerm, activeTabKey, epelRepoUrlByDistribution, - debouncedSearchTermIsGroup, + packageType, arch, distroRepositories, isSuccessDistroRepositories, @@ -639,10 +648,45 @@ const Packages = () => { + + + + + { From 5fa3d6185b1a5ab723b421fc057e255e861d1580 Mon Sep 17 00:00:00 2001 From: regexowl Date: Mon, 30 Mar 2026 13:48:05 +0200 Subject: [PATCH 03/18] Wizard: Replace filter input with search with dropdown This replaces the original input that only filtered the table of packages with a input with dropdown. The packages can be searched in the input and the results are rendered in the dropdown below the input. --- .../Packages/components/PackageSearch.tsx | 294 ++++++++++++++++++ .../steps/Packages/components/Packages.tsx | 54 ++-- 2 files changed, 319 insertions(+), 29 deletions(-) create mode 100644 src/Components/CreateImageWizard/steps/Packages/components/PackageSearch.tsx diff --git a/src/Components/CreateImageWizard/steps/Packages/components/PackageSearch.tsx b/src/Components/CreateImageWizard/steps/Packages/components/PackageSearch.tsx new file mode 100644 index 0000000000..54d0440732 --- /dev/null +++ b/src/Components/CreateImageWizard/steps/Packages/components/PackageSearch.tsx @@ -0,0 +1,294 @@ +import React, { useMemo, useState } from 'react'; + +import { + Button, + MenuToggle, + MenuToggleElement, + Select, + SelectList, + SelectOption, + TextInputGroup, + TextInputGroupMain, + TextInputGroupUtilities, +} from '@patternfly/react-core'; +import { SearchIcon, TimesIcon } from '@patternfly/react-icons'; +import { useDispatch } from 'react-redux'; + +import { ApiRepositoryCollectionResponseRead } from '@/store/api/contentSources'; +import { useAppSelector } from '@/store/hooks'; +import { + addPackage, + addPackageGroup, + removePackage, + removePackageGroup, + removeRecommendedRepository, + selectGroups, + selectPackages, + selectRecommendedRepositories, +} from '@/store/slices/wizard'; + +import { + GroupWithRepositoryInfo, + IBPackageWithRepositoryInfo, + Repos, +} from '../packagesTypes'; + +type PackageSearchProps = { + packageType: 'packages' | 'groups'; + searchTerm: string; + setSearchTerm: (value: string) => void; + transformedPackages: IBPackageWithRepositoryInfo[]; + transformedGroups: GroupWithRepositoryInfo[]; + isLoadingDistroPackages: boolean; + isLoadingCustomPackages: boolean; + isLoadingRecommendedPackages: boolean; + isLoadingDistroGroups: boolean; + isLoadingCustomGroups: boolean; + isLoadingRecommendedGroups: boolean; + debouncedSearchTerm: string; + isSuccessEpelRepo: boolean; + epelRepo: ApiRepositoryCollectionResponseRead | undefined; + setIsRepoModalOpen: (value: boolean) => void; + setIsSelectingPackage: ( + value: IBPackageWithRepositoryInfo | undefined, + ) => void; + setIsSelectingGroup: (value: GroupWithRepositoryInfo | undefined) => void; + setActiveTabKey: (value: Repos) => void; + setToggleSelected: (value: string) => void; + setActiveStream: (value: string) => void; + setActiveSortIndex: (value: number) => void; + setActiveSortDirection: (value: 'asc' | 'desc') => void; + setPage: (value: number) => void; +}; + +const PackageSearch = ({ + packageType, + searchTerm, + setSearchTerm, + transformedPackages, + transformedGroups, + isLoadingDistroPackages, + isLoadingCustomPackages, + isLoadingRecommendedPackages, + isLoadingDistroGroups, + isLoadingCustomGroups, + isLoadingRecommendedGroups, + debouncedSearchTerm, + isSuccessEpelRepo, + epelRepo, + setIsRepoModalOpen, + setIsSelectingPackage, + setIsSelectingGroup, + setActiveTabKey, + setToggleSelected, + setActiveStream, + setActiveSortIndex, + setActiveSortDirection, + setPage, +}: PackageSearchProps) => { + const dispatch = useDispatch(); + const packages = useAppSelector(selectPackages); + const groups = useAppSelector(selectGroups); + const recommendedRepositories = useAppSelector(selectRecommendedRepositories); + + const [isOpen, setIsOpen] = useState(false); + + const packageTypeLabel = + packageType === 'packages' ? 'packages' : 'package groups'; + + const selectedPackageNames = useMemo( + () => packages.map((p) => p.name), + [packages], + ); + const selectedGroupNames = useMemo(() => groups.map((g) => g.name), [groups]); + + const onInputClick = () => { + if (!isOpen && searchTerm) { + setIsOpen(true); + } + }; + + const onTextInputChange = (_event: React.FormEvent, value: string) => { + setSearchTerm(value); + setIsOpen(true); + setActiveTabKey(Repos.INCLUDED); + setToggleSelected('toggle-available'); + setActiveStream(''); + setActiveSortIndex(0); + setActiveSortDirection('asc'); + setPage(1); + }; + + const onToggleClick = () => { + setIsOpen(!isOpen); + }; + + const onClearButtonClick = () => { + setSearchTerm(''); + setIsOpen(false); + setActiveTabKey(Repos.INCLUDED); + setActiveStream(''); + setActiveSortIndex(0); + setActiveSortDirection('asc'); + }; + + const onSelect = (_event?: React.MouseEvent, value?: string | number) => { + if (!value || typeof value !== 'string') return; + + if (packageType === 'packages') { + const pkg = transformedPackages.find((p) => p.name === value); + if (!pkg) return; + + const isSelected = packages.some((p) => p.name === pkg.name); + + if (!isSelected) { + if ( + isSuccessEpelRepo && + epelRepo && + epelRepo.data && + pkg.repository === 'recommended' && + !recommendedRepositories.some((repo) => repo.name?.startsWith('EPEL')) + ) { + setIsRepoModalOpen(true); + setIsSelectingPackage(pkg); + } else { + dispatch(addPackage(pkg)); + } + } else { + dispatch(removePackage(pkg.name)); + if ( + isSuccessEpelRepo && + epelRepo && + epelRepo.data && + packages.filter((pkg) => pkg.repository === 'recommended').length === + 1 && + groups.filter((grp) => grp.repository === 'recommended').length === 0 + ) { + dispatch(removeRecommendedRepository(epelRepo.data[0])); + } + } + } else { + const grp = transformedGroups.find((g) => g.name === value); + if (!grp) return; + + const isSelected = groups.some((g) => g.name === grp.name); + + if (!isSelected) { + if ( + isSuccessEpelRepo && + epelRepo && + epelRepo.data && + grp.repository === 'recommended' && + !recommendedRepositories.some((repo) => repo.name?.startsWith('EPEL')) + ) { + setIsRepoModalOpen(true); + setIsSelectingGroup(grp); + } else { + dispatch(addPackageGroup(grp)); + } + } else { + dispatch(removePackageGroup(grp.name)); + if ( + isSuccessEpelRepo && + epelRepo && + epelRepo.data && + groups.filter((grp) => grp.repository === 'recommended').length === + 1 && + packages.filter((pkg) => pkg.repository === 'recommended').length === + 0 + ) { + dispatch(removeRecommendedRepository(epelRepo.data[0])); + } + } + } + + setIsOpen(false); + }; + + const toggle = (toggleRef: React.Ref) => ( + + + } + aria-label={`Search ${packageTypeLabel}`} + data-testid='packages-search-input' + /> + +