import React, { useMemo, useState } from 'react'; import { useFieldApi, useFormApi, } from '@data-driven-forms/react-form-renderer'; import { Alert, Button, EmptyState, EmptyStateBody, EmptyStateIcon, EmptyStateVariant, Pagination, Panel, PanelMain, SearchInput, Spinner, Toolbar, ToolbarContent, ToolbarItem, EmptyStateHeader, EmptyStateFooter, ToggleGroup, ToggleGroupItem, PaginationVariant, } from '@patternfly/react-core'; import { Dropdown, DropdownItem, DropdownToggle, DropdownToggleCheckbox, } from '@patternfly/react-core/deprecated'; import { ExternalLinkAltIcon } from '@patternfly/react-icons'; import { RepositoryIcon } from '@patternfly/react-icons'; import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; import PropTypes from 'prop-types'; import RepositoriesStatus from './RepositoriesStatus'; import RepositoryUnavailable from './RepositoryUnavailable'; import { useListRepositoriesQuery } from '../../../store/contentSourcesApi'; import { releaseToVersion } from '../../../Utilities/releaseToVersion'; import { useGetEnvironment } from '../../../Utilities/useGetEnvironment'; const BulkSelect = ({ selected, count, filteredCount, perPage, handleSelectAll, handleSelectPage, handleDeselectAll, isDisabled, }) => { const [dropdownIsOpen, setDropdownIsOpen] = useState(false); const numSelected = selected.length; const allSelected = count !== 0 ? numSelected === count : undefined; const anySelected = numSelected > 0; const someChecked = anySelected ? null : false; const isChecked = allSelected ? true : someChecked; const items = [ {`Select none (0 items)`}, {`Select page (${ perPage > filteredCount ? filteredCount : perPage } items)`}, {`Select all (${count} items)`}, ]; const handleDropdownSelect = () => {}; const toggleDropdown = () => setDropdownIsOpen(!dropdownIsOpen); return ( { anySelected ? handleDeselectAll() : handleSelectAll(); }} />, ]} onToggle={toggleDropdown} > {numSelected !== 0 ? `${numSelected} selected` : null} } isOpen={dropdownIsOpen} dropdownItems={items} /> ); }; // Utility function to convert from Content Sources to Image Builder payload repo API schema const convertSchemaToIBPayloadRepo = (repo) => { const imageBuilderRepo = { baseurl: repo.url, rhsm: false, check_gpg: false, }; // only include the flag if enabled if (repo.module_hotfixes) { imageBuilderRepo.module_hotfixes = repo.module_hotfixes; } if (repo.gpg_key) { imageBuilderRepo.gpgkey = repo.gpg_key; imageBuilderRepo.check_gpg = true; imageBuilderRepo.check_repo_gpg = repo.metadata_verification; } return imageBuilderRepo; }; // Utility function to convert from Content Sources to Image Builder custom repo API schema const convertSchemaToIBCustomRepo = (repo) => { const imageBuilderRepo = { id: repo.uuid, name: repo.name, baseurl: [repo.url], check_gpg: false, }; // only include the flag if enabled if (repo.module_hotfixes) { imageBuilderRepo.module_hotfixes = repo.module_hotfixes; } if (repo.gpg_key) { imageBuilderRepo.gpgkey = [repo.gpg_key]; imageBuilderRepo.check_gpg = true; imageBuilderRepo.check_repo_gpg = repo.metadata_verification; } return imageBuilderRepo; }; // Utility function to convert from Image Builder to Content Sources API schema const convertSchemaToContentSources = (repo) => { const contentSourcesRepo = { url: repo.baseurl, rhsm: false, }; if (repo.gpgkey) { contentSourcesRepo.gpg_key = repo.gpgkey; contentSourcesRepo.metadata_verification = repo.check_repo_gpg; } return contentSourcesRepo; }; const Repositories = (props) => { const initializeRepositories = (contentSourcesReposList) => { // Convert list of repositories into an object where key is repo URL const contentSourcesRepos = contentSourcesReposList.reduce( (accumulator, currentValue) => { accumulator[currentValue.url] = currentValue; return accumulator; }, {} ); // Repositories in the form state can be present when 'Recreate image' is used // to open the wizard that are not necessarily in content sources. const formStateReposList = getState()?.values?.['original-payload-repositories']; const mergeRepositories = (contentSourcesRepos, formStateReposList) => { const formStateRepos = {}; for (const repo of formStateReposList) { formStateRepos[repo.baseurl] = convertSchemaToContentSources(repo); formStateRepos[repo.baseurl].name = ''; } // In case of duplicate repo urls, the repo from Content Sources overwrites the // repo from the form state. const mergedRepos = { ...formStateRepos, ...contentSourcesRepos }; return mergedRepos; }; const repositories = formStateReposList ? mergeRepositories(contentSourcesRepos, formStateReposList) : contentSourcesRepos; return repositories; }; const { getState, change } = useFormApi(); const { input } = useFieldApi(props); const [filterValue, setFilterValue] = useState(''); const [perPage, setPerPage] = useState(10); const [page, setPage] = useState(1); const [toggleSelected, setToggleSelected] = useState('toggle-group-all'); const [selected, setSelected] = useState( getState()?.values?.['payload-repositories'] ? getState().values['payload-repositories'].map((repo) => repo.baseurl) : [] ); const arch = getState().values?.arch; const release = getState().values?.release; const version = releaseToVersion(release); const firstRequest = useListRepositoriesQuery( { availableForArch: arch, availableForVersion: version, contentType: 'rpm', origin: 'external', limit: 100, offset: 0, }, // The cached repos may be incorrect, for now refetch on mount to ensure that // they are accurate when this step loads. Future PR will implement prefetching // and this can be removed. { refetchOnMountOrArgChange: true } ); const skip = firstRequest?.data?.meta?.count === undefined || firstRequest?.data?.meta?.count <= 100; // Fetch *all* repositories if there are more than 100 so that typeahead filter works const followupRequest = useListRepositoriesQuery( { availableForArch: arch, availableForVersion: version, contentType: 'rpm', origin: 'external', limit: firstRequest?.data?.meta?.count, offset: 0, }, { refetchOnMountOrArgChange: true, skip: skip, } ); const { data, isError, isFetching, isLoading, isSuccess, refetch } = useMemo(() => { if (firstRequest?.data?.meta?.count > 100) { return { ...followupRequest }; } return { ...firstRequest }; }, [firstRequest, followupRequest]); const repositories = useMemo(() => { return data ? initializeRepositories(data.data) : {}; }, [firstRequest.data, followupRequest.data]); const handleToggleClick = (event) => { const id = event.currentTarget.id; setPage(1); setToggleSelected(id); }; const isRepoSelected = (repoURL) => selected.includes(repoURL); const handlePerPageSelect = (event, newPerPage, newPage) => { setPerPage(newPerPage); setPage(newPage); }; const handleSetPage = (event, newPage) => { setPage(newPage); }; // filter displayed selected packages const handleFilterRepositories = (_, value) => { setPage(1); setFilterValue(value); }; const filteredRepositoryURLs = useMemo(() => { const repoUrls = Object.values(repositories).filter((repo) => repo.name.toLowerCase().includes(filterValue.toLowerCase()) ); if (toggleSelected === 'toggle-group-all') { return repoUrls.map((repo) => repo.url); } else if (toggleSelected === 'toggle-group-selected') { return repoUrls .filter((repo) => isRepoSelected(repo.url)) .map((repo) => repo.url); } }, [filterValue, repositories, toggleSelected]); const handleClearFilter = () => { setFilterValue(''); }; const updateFormState = (selectedRepoURLs) => { // repositories is stored as an object with repoURLs as keys const selectedRepos = []; for (const repoURL of selectedRepoURLs) { selectedRepos.push(repositories[repoURL]); } const payloadRepositories = selectedRepos.map((repo) => convertSchemaToIBPayloadRepo(repo) ); const customRepositories = selectedRepos.map((repo) => convertSchemaToIBCustomRepo(repo) ); input.onChange(payloadRepositories); change('custom-repositories', customRepositories); }; const updateSelected = (selectedRepos) => { setSelected(selectedRepos); updateFormState(selectedRepos); }; const handleSelect = (repoURL, rowIndex, isSelecting) => { if (isSelecting === true) { updateSelected([...selected, repoURL]); } else if (isSelecting === false) { updateSelected( selected.filter((selectedRepoId) => selectedRepoId !== repoURL) ); } }; const handleSelectAll = () => { updateSelected(Object.keys(repositories)); }; const computeStart = () => perPage * (page - 1); const computeEnd = () => perPage * page; const handleSelectPage = () => { const pageRepos = filteredRepositoryURLs.slice( computeStart(), computeEnd() ); // Filter to avoid adding duplicates const newSelected = [ ...pageRepos.filter((repoId) => !selected.includes(repoId)), ]; updateSelected([...selected, ...newSelected]); }; const handleDeselectAll = () => { updateSelected([]); }; return ( (isError && ) || (isLoading && ) || (isSuccess && ( <> {Object.values(repositories).length === 0 ? ( ) : ( <> {filteredRepositoryURLs .sort((a, b) => { if (repositories[a].name < repositories[b].name) { return -1; } else if ( repositories[b].name < repositories[a].name ) { return 1; } else { return 0; } }) .slice(computeStart(), computeEnd()) .map((repoURL, rowIndex) => { const repo = repositories[repoURL]; const repoExists = repo.name ? true : false; return ( ); })}
Name Architecture Version Packages Status
handleSelect(repo.url, rowIndex, isSelecting), isDisabled: isFetching || repo.status !== 'Valid', }} /> {repoExists ? repo.name : 'Repository with the following url is no longer available:'}
{repoExists ? repo.distribution_arch : '-'} {repoExists ? repo.distribution_versions : '-'} {repoExists ? repo.package_count : '-'}
)} )) ); }; const Error = () => { return ( Repositories cannot be reached, try again later. ); }; const Loading = () => { return ( } headingLevel="h4" /> ); }; const Empty = ({ isFetching, refetch }) => { const { isBeta } = useGetEnvironment(); return ( } headingLevel="h4" /> Repositories can be added in the "Repositories" area of the console. Once added, refresh this page to see them. ); }; BulkSelect.propTypes = { selected: PropTypes.array, count: PropTypes.number, filteredCount: PropTypes.number, perPage: PropTypes.number, handleSelectAll: PropTypes.func, handleSelectPage: PropTypes.func, handleDeselectAll: PropTypes.func, isDisabled: PropTypes.bool, }; Empty.propTypes = { isFetching: PropTypes.bool, refetch: PropTypes.func, }; export default Repositories;