import React, { useMemo, useState } from 'react'; import { Alert, Button, EmptyState, EmptyStateBody, EmptyStateIcon, EmptyStateVariant, Pagination, Panel, PanelMain, SearchInput, Spinner, Toolbar, ToolbarContent, ToolbarItem, EmptyStateHeader, EmptyStateFooter, ToggleGroup, ToggleGroupItem, } 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 RepositoriesStatus from './RepositoriesStatus'; import RepositoryUnavailable from './RepositoryUnavailable'; import { ApiRepositoryResponseRead, useListRepositoriesQuery, } from '../../../../store/contentSourcesApi'; import { useAppDispatch, useAppSelector } from '../../../../store/hooks'; import { CustomRepository, Repository, } from '../../../../store/imageBuilderApi'; import { changeCustomRepositories, changePayloadRepositories, selectArchitecture, selectCustomRepositories, selectDistribution, } from '../../../../store/wizardSlice'; import { releaseToVersion } from '../../../../Utilities/releaseToVersion'; import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment'; type BulkSelectProps = { selected: (string | undefined)[]; count: number | undefined; filteredCount: number | undefined; perPage: number; handleSelectAll: Function; handleSelectPage: Function; handleDeselectAll: Function; isDisabled: boolean; }; const BulkSelect = ({ selected, count, filteredCount, perPage, handleSelectAll, handleSelectPage, handleDeselectAll, isDisabled, }: BulkSelectProps) => { 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 = [ handleDeselectAll()} >{`Select none (0 items)`}, handleSelectPage()} >{`Select page (${ perPage > filteredCount! ? filteredCount : perPage } items)`}, handleSelectAll()} >{`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 custom repo API schema const convertSchemaToIBCustomRepo = (repo: ApiRepositoryResponseRead) => { const imageBuilderRepo: CustomRepository = { id: repo.uuid!, name: repo.name, baseurl: [repo.url!], check_gpg: false, }; 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 payload repo API schema const convertSchemaToIBPayloadRepo = (repo: ApiRepositoryResponseRead) => { const imageBuilderRepo: Repository = { baseurl: repo.url, rhsm: false, check_gpg: false, }; if (repo.gpg_key) { imageBuilderRepo.gpgkey = repo.gpg_key; imageBuilderRepo.check_gpg = true; imageBuilderRepo.check_repo_gpg = repo.metadata_verification; } return imageBuilderRepo; }; const Repositories = () => { const dispatch = useAppDispatch(); const arch = useAppSelector((state) => selectArchitecture(state)); const distribution = useAppSelector((state) => selectDistribution(state)); const version = releaseToVersion(distribution); const repositoriesList = useAppSelector((state) => selectCustomRepositories(state) ); 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( repositoriesList ? repositoriesList.flatMap((repo) => repo.baseurl) : [] ); 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) { if (firstRequest?.data?.meta?.count > 100) { return { ...followupRequest }; } } return { ...firstRequest }; }, [firstRequest, followupRequest]); const handleToggleClick = (event: React.MouseEvent) => { const id = event.currentTarget.id; setPage(1); setToggleSelected(id); }; const isRepoSelected = (repoURL: string | undefined) => selected.includes(repoURL); const handlePerPageSelect = ( _: React.MouseEvent, newPerPage: number, newPage: number ) => { setPerPage(newPerPage); setPage(newPage); }; const handleSetPage = (_: React.MouseEvent, newPage: number) => { setPage(newPage); }; // filter displayed selected packages const handleFilterRepositories = ( event: React.FormEvent, value: string ) => { setPage(1); setFilterValue(value); }; const filteredRepositoryURLs = useMemo(() => { if (!data || !data.data) { return []; } const repoUrls = data.data.filter((repo) => repo.name?.toLowerCase().includes(filterValue.toLowerCase()) ); if (toggleSelected === 'toggle-group-all') { return repoUrls.map((repo: ApiRepositoryResponseRead) => repo.url); } else if (toggleSelected === 'toggle-group-selected') { return repoUrls .filter((repo: ApiRepositoryResponseRead) => isRepoSelected(repo.url!)) .map((repo: ApiRepositoryResponseRead) => repo.url); } }, [filterValue, data, toggleSelected]); const handleClearFilter = () => { setFilterValue(''); }; const updateFormState = (selectedRepoURLs: (string | undefined)[]) => { // repositories is stored as an object with repoURLs as keys const selectedRepos = []; for (const repoURL of selectedRepoURLs) { selectedRepos.push(data?.data?.find((repo) => repo.url === repoURL)); } const customRepositories = selectedRepos.map((repo) => convertSchemaToIBCustomRepo(repo!) ); const payloadRepositories = selectedRepos.map((repo) => convertSchemaToIBPayloadRepo(repo!) ); dispatch(changeCustomRepositories(customRepositories)); dispatch(changePayloadRepositories(payloadRepositories)); }; const updateSelected = (selectedRepos: (string | undefined)[]) => { setSelected(selectedRepos); updateFormState(selectedRepos); }; const handleSelect = ( repoURL: string | undefined, _: number, isSelecting: boolean ) => { if (isSelecting === true) { updateSelected([...selected, repoURL]); } else if (isSelecting === false) { updateSelected( selected.filter((selectedRepoId) => selectedRepoId !== repoURL) ); } }; const handleSelectAll = () => { if (data) { updateSelected(data.data?.map((repo) => repo.url) || []); } }; const computeStart = () => perPage * (page - 1); const computeEnd = () => perPage * page; const handleSelectPage = () => { const pageRepos = filteredRepositoryURLs && filteredRepositoryURLs.slice(computeStart(), computeEnd()); // Filter to avoid adding duplicates const newSelected = pageRepos && [ ...pageRepos.filter((repoId) => !selected.includes(repoId)), ]; updateSelected([...selected, ...newSelected!]); }; const handleDeselectAll = () => { updateSelected([]); }; const getRepoNameByUrl = (url: string) => { return data!.data?.find((repo) => repo.url === url)?.name; }; return ( (isError && ) || (isLoading && ) || (isSuccess && ( <> {data.data?.length === 0 ? ( ) : ( <> {filteredRepositoryURLs && filteredRepositoryURLs .sort((a, b) => { if (getRepoNameByUrl(a!)! < getRepoNameByUrl(b!)!) { return -1; } else if ( getRepoNameByUrl(b!)! < getRepoNameByUrl(a!)! ) { return 1; } else { return 0; } }) .slice(computeStart(), computeEnd()) .map((repoURL, rowIndex) => { const repo = data?.data?.find( (repo) => repo.url === repoURL ); if (!repo) { return <>; } 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" /> ); }; type EmptyProps = { isFetching: boolean; refetch: Function; }; const Empty = ({ isFetching, refetch }: EmptyProps) => { 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. ); }; export default Repositories;