V2Wizard: Create a folder for Packages step
This creates a new folder for the Packages step and copies over needed file: - Packages.tsx
This commit is contained in:
parent
7183d8d88b
commit
607bd6ed45
1 changed files with 545 additions and 0 deletions
545
src/Components/CreateImageWizardV2/steps/Packages/Packages.tsx
Normal file
545
src/Components/CreateImageWizardV2/steps/Packages/Packages.tsx
Normal file
|
|
@ -0,0 +1,545 @@
|
||||||
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||||
|
import WizardContext from '@data-driven-forms/react-form-renderer/wizard-context';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Divider,
|
||||||
|
DualListSelector,
|
||||||
|
DualListSelectorControl,
|
||||||
|
DualListSelectorControlsWrapper,
|
||||||
|
DualListSelectorList,
|
||||||
|
DualListSelectorListItem,
|
||||||
|
DualListSelectorPane,
|
||||||
|
SearchInput,
|
||||||
|
TextContent,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import {
|
||||||
|
AngleDoubleLeftIcon,
|
||||||
|
AngleDoubleRightIcon,
|
||||||
|
AngleLeftIcon,
|
||||||
|
AngleRightIcon,
|
||||||
|
} from '@patternfly/react-icons';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import api from '../../../api';
|
||||||
|
import {
|
||||||
|
useGetArchitecturesQuery,
|
||||||
|
useGetOscapCustomizationsQuery,
|
||||||
|
} from '../../../store/imageBuilderApi';
|
||||||
|
|
||||||
|
const ExactMatch = ({
|
||||||
|
pkgList,
|
||||||
|
search,
|
||||||
|
chosenPackages,
|
||||||
|
selectedAvailablePackages,
|
||||||
|
handleSelectAvailableFunc,
|
||||||
|
}) => {
|
||||||
|
const match = pkgList.find((pkg) => pkg.name === search);
|
||||||
|
return (
|
||||||
|
<DualListSelectorListItem
|
||||||
|
data-testid={`exact-match-${match.name}`}
|
||||||
|
isDisabled={chosenPackages[match.name] ? true : false}
|
||||||
|
isSelected={selectedAvailablePackages.has(match.name)}
|
||||||
|
onOptionSelect={(e) => handleSelectAvailableFunc(e, match.name)}
|
||||||
|
>
|
||||||
|
<TextContent key={`${match.name}`}>
|
||||||
|
<small className="pf-u-mb-sm">Exact match</small>
|
||||||
|
<span className="pf-c-dual-list-selector__item-text">{match.name}</span>
|
||||||
|
<small>{match.summary}</small>
|
||||||
|
<Divider />
|
||||||
|
</TextContent>
|
||||||
|
</DualListSelectorListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RedHatPackages = ({ defaultArch }) => {
|
||||||
|
const { getState } = useFormApi();
|
||||||
|
const distribution = getState()?.values?.release;
|
||||||
|
const arch = getState()?.values?.arch;
|
||||||
|
const { data: distributionInformation, isSuccess: isSuccessDistroInfo } =
|
||||||
|
useGetArchitecturesQuery({ distribution });
|
||||||
|
|
||||||
|
const getAllPackages = async (packagesSearchName) => {
|
||||||
|
// if the env is stage beta then use content-sources api
|
||||||
|
// else use image-builder api
|
||||||
|
if (getState()?.values?.contentSourcesEnabled) {
|
||||||
|
const filteredByArch = distributionInformation.find(
|
||||||
|
(info) => info.arch === arch
|
||||||
|
);
|
||||||
|
const repoUrls = filteredByArch.repositories.map((repo) => repo.baseurl);
|
||||||
|
return await api.getPackagesContentSources(repoUrls, packagesSearchName);
|
||||||
|
} else {
|
||||||
|
const args = [
|
||||||
|
getState()?.values?.release,
|
||||||
|
getState()?.values?.architecture || defaultArch,
|
||||||
|
packagesSearchName,
|
||||||
|
];
|
||||||
|
const response = await api.getPackages(...args);
|
||||||
|
let { data } = response;
|
||||||
|
const { meta } = response;
|
||||||
|
if (data?.length === meta.count) {
|
||||||
|
return data;
|
||||||
|
} else if (data) {
|
||||||
|
({ data } = await api.getPackages(...args, meta.count));
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Packages getAllPackages={getAllPackages} isSuccess={isSuccessDistroInfo} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ContentSourcesPackages = () => {
|
||||||
|
const { getState } = useFormApi();
|
||||||
|
|
||||||
|
const getAllPackages = async (packagesSearchName) => {
|
||||||
|
const repos = getState()?.values?.['payload-repositories'];
|
||||||
|
const repoUrls = repos?.map((repo) => repo.baseurl);
|
||||||
|
return await api.getPackagesContentSources(repoUrls, packagesSearchName);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Packages getAllPackages={getAllPackages} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Packages = ({ getAllPackages, isSuccess }) => {
|
||||||
|
const { currentStep } = useContext(WizardContext);
|
||||||
|
const { change, getState } = useFormApi();
|
||||||
|
const [packagesSearchName, setPackagesSearchName] = useState(undefined);
|
||||||
|
const [filterChosen, setFilterChosen] = useState('');
|
||||||
|
const [chosenPackages, setChosenPackages] = useState({});
|
||||||
|
const [focus, setFocus] = useState('');
|
||||||
|
const selectedPackages = getState()?.values?.['selected-packages'];
|
||||||
|
const [availablePackages, setAvailablePackages] = useState(undefined);
|
||||||
|
const [selectedAvailablePackages, setSelectedAvailablePackages] = useState(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
const [selectedChosenPackages, setSelectedChosenPackages] = useState(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
const firstInputElement = useRef(null);
|
||||||
|
|
||||||
|
const oscapProfile = getState()?.values?.['oscap-profile'];
|
||||||
|
|
||||||
|
const { data: customizations, isSuccess: isSuccessCustomizations } =
|
||||||
|
useGetOscapCustomizationsQuery(
|
||||||
|
{
|
||||||
|
distribution: getState()?.values?.['release'],
|
||||||
|
profile: oscapProfile,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skip: !oscapProfile,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (customizations && customizations.packages && isSuccessCustomizations) {
|
||||||
|
const oscapPackages = {};
|
||||||
|
for (const pkg of customizations.packages) {
|
||||||
|
oscapPackages[pkg] = { name: pkg };
|
||||||
|
}
|
||||||
|
updateState(oscapPackages);
|
||||||
|
}
|
||||||
|
}, [customizations, isSuccessCustomizations, updateState]);
|
||||||
|
|
||||||
|
// this effect only triggers on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedPackages) {
|
||||||
|
const newChosenPackages = {};
|
||||||
|
for (const pkg of selectedPackages) {
|
||||||
|
newChosenPackages[pkg.name] = pkg;
|
||||||
|
}
|
||||||
|
setChosenPackages(newChosenPackages);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSuccess) {
|
||||||
|
firstInputElement.current?.focus();
|
||||||
|
}
|
||||||
|
}, [isSuccess]);
|
||||||
|
|
||||||
|
const searchResultsComparator = useCallback((searchTerm) => {
|
||||||
|
return (a, b) => {
|
||||||
|
a = a.name.toLowerCase();
|
||||||
|
b = b.name.toLowerCase();
|
||||||
|
|
||||||
|
// check exact match first
|
||||||
|
if (a === searchTerm) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b === searchTerm) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for packages that start with the search term
|
||||||
|
if (a.startsWith(searchTerm) && !b.startsWith(searchTerm)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.startsWith(searchTerm) && !a.startsWith(searchTerm)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if both (or neither) start with the search term
|
||||||
|
// sort alphabetically
|
||||||
|
if (a < b) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b < a) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const availablePackagesDisplayList = useMemo(() => {
|
||||||
|
if (availablePackages === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const availablePackagesList = Object.values(availablePackages).sort(
|
||||||
|
searchResultsComparator(packagesSearchName)
|
||||||
|
);
|
||||||
|
return availablePackagesList;
|
||||||
|
}, [availablePackages, packagesSearchName, searchResultsComparator]);
|
||||||
|
|
||||||
|
const chosenPackagesDisplayList = useMemo(() => {
|
||||||
|
const chosenPackagesList = Object.values(chosenPackages)
|
||||||
|
.filter((pkg) => (pkg.name.includes(filterChosen) ? true : false))
|
||||||
|
.sort(searchResultsComparator(filterChosen));
|
||||||
|
return chosenPackagesList;
|
||||||
|
}, [chosenPackages, filterChosen, searchResultsComparator]);
|
||||||
|
|
||||||
|
// call api to list available packages
|
||||||
|
const handleAvailablePackagesSearch = async () => {
|
||||||
|
const packageList = await getAllPackages(packagesSearchName);
|
||||||
|
// If no packages are found, Image Builder returns null, while
|
||||||
|
// Content Sources returns an empty array [].
|
||||||
|
if (packageList) {
|
||||||
|
const newAvailablePackages = {};
|
||||||
|
for (const pkg of packageList) {
|
||||||
|
newAvailablePackages[pkg.name] = pkg;
|
||||||
|
}
|
||||||
|
setAvailablePackages(newAvailablePackages);
|
||||||
|
} else {
|
||||||
|
setAvailablePackages([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const keydownHandler = (event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
if (focus === 'available') {
|
||||||
|
event.stopPropagation();
|
||||||
|
handleAvailablePackagesSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('keydown', keydownHandler, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', keydownHandler, true);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateState = useCallback(
|
||||||
|
(newChosenPackages) => {
|
||||||
|
setSelectedAvailablePackages(new Set());
|
||||||
|
setSelectedChosenPackages(new Set());
|
||||||
|
setChosenPackages(newChosenPackages);
|
||||||
|
change('selected-packages', Object.values(newChosenPackages));
|
||||||
|
},
|
||||||
|
[change]
|
||||||
|
);
|
||||||
|
|
||||||
|
const moveSelectedToChosen = () => {
|
||||||
|
const newChosenPackages = { ...chosenPackages };
|
||||||
|
for (const pkgName of selectedAvailablePackages) {
|
||||||
|
newChosenPackages[pkgName] = { ...availablePackages[pkgName] };
|
||||||
|
}
|
||||||
|
updateState(newChosenPackages);
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveAllToChosen = () => {
|
||||||
|
const newChosenPackages = { ...chosenPackages, ...availablePackages };
|
||||||
|
updateState(newChosenPackages);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSelectedFromChosen = () => {
|
||||||
|
const newChosenPackages = {};
|
||||||
|
for (const pkgName in chosenPackages) {
|
||||||
|
if (!selectedChosenPackages.has(pkgName)) {
|
||||||
|
newChosenPackages[pkgName] = { ...chosenPackages[pkgName] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateState(newChosenPackages);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAllFromChosen = () => {
|
||||||
|
const newChosenPackages = {};
|
||||||
|
updateState(newChosenPackages);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectAvailable = (event, pkgName) => {
|
||||||
|
const newSelected = new Set(selectedAvailablePackages);
|
||||||
|
newSelected.has(pkgName)
|
||||||
|
? newSelected.delete(pkgName)
|
||||||
|
: newSelected.add(pkgName);
|
||||||
|
setSelectedAvailablePackages(newSelected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectChosen = (event, pkgName) => {
|
||||||
|
const newSelected = new Set(selectedChosenPackages);
|
||||||
|
newSelected.has(pkgName)
|
||||||
|
? newSelected.delete(pkgName)
|
||||||
|
: newSelected.add(pkgName);
|
||||||
|
setSelectedChosenPackages(newSelected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearAvailableSearch = () => {
|
||||||
|
setPackagesSearchName('');
|
||||||
|
setAvailablePackages(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearChosenSearch = () => {
|
||||||
|
setFilterChosen('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
// vv REMOVE WHEN CONTENT REACHABLE AGAIN
|
||||||
|
currentStep.name === 'packages-content-sources' && (
|
||||||
|
<Alert title="Repositories unavailable" variant="warning" isInline>
|
||||||
|
The Content service cannot be reached, please check back later.
|
||||||
|
</Alert>
|
||||||
|
// ^^ REMOVE WHEN CONTENT REACHABLE AGAIN
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<DualListSelector>
|
||||||
|
<DualListSelectorPane
|
||||||
|
title="Available packages"
|
||||||
|
searchInput={
|
||||||
|
<>
|
||||||
|
<SearchInput
|
||||||
|
placeholder="Search for a package"
|
||||||
|
data-testid="search-available-pkgs-input"
|
||||||
|
value={packagesSearchName}
|
||||||
|
ref={firstInputElement}
|
||||||
|
onFocus={() => setFocus('available')}
|
||||||
|
onBlur={() => setFocus('')}
|
||||||
|
onChange={(_, val) => setPackagesSearchName(val)}
|
||||||
|
submitSearchButtonLabel="Search button for available packages"
|
||||||
|
onSearch={handleAvailablePackagesSearch}
|
||||||
|
resetButtonLabel="Clear available packages search"
|
||||||
|
onClear={handleClearAvailableSearch}
|
||||||
|
// Temporarily disable search input for custom packages
|
||||||
|
isDisabled={currentStep.name === 'packages' ? !isSuccess : true}
|
||||||
|
/>
|
||||||
|
{availablePackagesDisplayList.length >= 100 && (
|
||||||
|
<Alert
|
||||||
|
title="Over 100 results found. Refine your search."
|
||||||
|
variant="warning"
|
||||||
|
isPlain
|
||||||
|
isInline
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
status={
|
||||||
|
selectedAvailablePackages.size > 0
|
||||||
|
? `${selectedAvailablePackages.size}
|
||||||
|
of ${availablePackagesDisplayList.length} items`
|
||||||
|
: `${availablePackagesDisplayList.length} items`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DualListSelectorList data-testid="available-pkgs-list">
|
||||||
|
{availablePackages === undefined ? (
|
||||||
|
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||||
|
Search above to add additional
|
||||||
|
<br />
|
||||||
|
packages to your image
|
||||||
|
</p>
|
||||||
|
) : availablePackagesDisplayList.length === 0 ? (
|
||||||
|
<>
|
||||||
|
<p className="pf-u-text-align-center pf-u-mt-md pf-u-font-size-lg pf-u-font-weight-bold">
|
||||||
|
No results found
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||||
|
Adjust your search and try again
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : availablePackagesDisplayList.length >= 100 ? (
|
||||||
|
<>
|
||||||
|
{availablePackagesDisplayList.some(
|
||||||
|
(pkg) => pkg.name === packagesSearchName
|
||||||
|
) && (
|
||||||
|
<ExactMatch
|
||||||
|
pkgList={availablePackagesDisplayList}
|
||||||
|
search={packagesSearchName}
|
||||||
|
chosenPackages={chosenPackages}
|
||||||
|
selectedAvailablePackages={selectedAvailablePackages}
|
||||||
|
handleSelectAvailableFunc={handleSelectAvailable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<p className="pf-u-text-align-center pf-u-mt-md pf-u-font-size-lg pf-u-font-weight-bold">
|
||||||
|
Too many results to display
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||||
|
Please make the search more specific
|
||||||
|
<br />
|
||||||
|
and try again
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
availablePackagesDisplayList.map((pkg) => {
|
||||||
|
return (
|
||||||
|
<DualListSelectorListItem
|
||||||
|
data-testid={`available-pkgs-${pkg.name}`}
|
||||||
|
key={pkg.name}
|
||||||
|
isDisabled={chosenPackages[pkg.name] ? true : false}
|
||||||
|
isSelected={selectedAvailablePackages.has(pkg.name)}
|
||||||
|
onOptionSelect={(e) => handleSelectAvailable(e, pkg.name)}
|
||||||
|
>
|
||||||
|
<TextContent key={`${pkg.name}`}>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
chosenPackages[pkg.name] && 'pf-v5-u-color-400'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{pkg.name}
|
||||||
|
</span>
|
||||||
|
<small>{pkg.summary}</small>
|
||||||
|
</TextContent>
|
||||||
|
</DualListSelectorListItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</DualListSelectorList>
|
||||||
|
</DualListSelectorPane>
|
||||||
|
<DualListSelectorControlsWrapper aria-label="Selector controls">
|
||||||
|
<DualListSelectorControl
|
||||||
|
isDisabled={selectedAvailablePackages.size === 0}
|
||||||
|
onClick={() => moveSelectedToChosen()}
|
||||||
|
aria-label="Add selected"
|
||||||
|
tooltipContent="Add selected"
|
||||||
|
>
|
||||||
|
<AngleRightIcon />
|
||||||
|
</DualListSelectorControl>
|
||||||
|
<DualListSelectorControl
|
||||||
|
isDisabled={
|
||||||
|
availablePackagesDisplayList.length === 0 ||
|
||||||
|
// also disable the "Add all" button if there are too many matches
|
||||||
|
// (even if there's an exact match)
|
||||||
|
availablePackagesDisplayList.length >= 100
|
||||||
|
}
|
||||||
|
onClick={() => moveAllToChosen()}
|
||||||
|
aria-label="Add all"
|
||||||
|
tooltipContent="Add all"
|
||||||
|
>
|
||||||
|
<AngleDoubleRightIcon />
|
||||||
|
</DualListSelectorControl>
|
||||||
|
<DualListSelectorControl
|
||||||
|
isDisabled={Object.values(chosenPackages).length === 0}
|
||||||
|
onClick={() => removeAllFromChosen()}
|
||||||
|
aria-label="Remove all"
|
||||||
|
tooltipContent="Remove all"
|
||||||
|
>
|
||||||
|
<AngleDoubleLeftIcon />
|
||||||
|
</DualListSelectorControl>
|
||||||
|
<DualListSelectorControl
|
||||||
|
onClick={() => removeSelectedFromChosen()}
|
||||||
|
isDisabled={selectedChosenPackages.size === 0}
|
||||||
|
aria-label="Remove selected"
|
||||||
|
tooltipContent="Remove selected"
|
||||||
|
>
|
||||||
|
<AngleLeftIcon />
|
||||||
|
</DualListSelectorControl>
|
||||||
|
</DualListSelectorControlsWrapper>
|
||||||
|
<DualListSelectorPane
|
||||||
|
title="Chosen packages"
|
||||||
|
searchInput={
|
||||||
|
<SearchInput
|
||||||
|
placeholder="Search for a package"
|
||||||
|
data-testid="search-chosen-pkgs-input"
|
||||||
|
value={filterChosen}
|
||||||
|
onFocus={() => setFocus('chosen')}
|
||||||
|
onBlur={() => setFocus('')}
|
||||||
|
onChange={(_, val) => setFilterChosen(val)}
|
||||||
|
resetButtonLabel="Clear chosen packages search"
|
||||||
|
onClear={handleClearChosenSearch}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
status={
|
||||||
|
selectedChosenPackages.size > 0
|
||||||
|
? `${selectedChosenPackages.size}
|
||||||
|
of ${chosenPackagesDisplayList.length} items`
|
||||||
|
: `${chosenPackagesDisplayList.length} items`
|
||||||
|
}
|
||||||
|
isChosen
|
||||||
|
>
|
||||||
|
<DualListSelectorList data-testid="chosen-pkgs-list">
|
||||||
|
{Object.values(chosenPackages).length === 0 ? (
|
||||||
|
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||||
|
No packages added
|
||||||
|
</p>
|
||||||
|
) : chosenPackagesDisplayList.length === 0 ? (
|
||||||
|
<p className="pf-u-text-align-center pf-u-mt-md">
|
||||||
|
No packages found
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
chosenPackagesDisplayList.map((pkg) => {
|
||||||
|
return (
|
||||||
|
<DualListSelectorListItem
|
||||||
|
data-testid={`selected-pkgs-${pkg.name}`}
|
||||||
|
key={pkg.name}
|
||||||
|
isSelected={selectedChosenPackages.has(pkg.name)}
|
||||||
|
onOptionSelect={(e) => handleSelectChosen(e, pkg.name)}
|
||||||
|
>
|
||||||
|
<TextContent key={`${pkg.name}`}>
|
||||||
|
<span className="pf-c-dual-list-selector__item-text">
|
||||||
|
{pkg.name}
|
||||||
|
</span>
|
||||||
|
<small>{pkg.summary}</small>
|
||||||
|
</TextContent>
|
||||||
|
</DualListSelectorListItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</DualListSelectorList>
|
||||||
|
</DualListSelectorPane>
|
||||||
|
</DualListSelector>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ExactMatch.propTypes = {
|
||||||
|
pkgList: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
search: PropTypes.string,
|
||||||
|
chosenPackages: PropTypes.object,
|
||||||
|
selectedAvailablePackages: PropTypes.object,
|
||||||
|
handleSelectAvailableFunc: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
RedHatPackages.propTypes = {
|
||||||
|
defaultArch: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
Packages.propTypes = {
|
||||||
|
getAllPackages: PropTypes.func,
|
||||||
|
isSuccess: PropTypes.bool,
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue