diff --git a/src/Components/CreateImageWizardV2/steps/Repositories/Repositories.tsx b/src/Components/CreateImageWizardV2/steps/Repositories/Repositories.tsx index 0d953bab..96e2160a 100644 --- a/src/Components/CreateImageWizardV2/steps/Repositories/Repositories.tsx +++ b/src/Components/CreateImageWizardV2/steps/Repositories/Repositories.tsx @@ -1,9 +1,5 @@ import React, { useMemo, useState } from 'react'; -import { - useFieldApi, - useFormApi, -} from '@data-driven-forms/react-form-renderer'; import { Alert, Button, @@ -33,14 +29,35 @@ import { 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'; +import { + ApiRepositoryResponseRead, + useListRepositoriesQuery, +} from '../../../../store/contentSourcesApi'; +import { useAppDispatch, useAppSelector } from '../../../../store/hooks'; +import { CustomRepository } from '../../../../store/imageBuilderApi'; +import { + changeCustomRepositories, + 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, @@ -51,7 +68,7 @@ const BulkSelect = ({ handleSelectPage, handleDeselectAll, isDisabled, -}) => { +}: BulkSelectProps) => { const [dropdownIsOpen, setDropdownIsOpen] = useState(false); const numSelected = selected.length; @@ -63,14 +80,17 @@ const BulkSelect = ({ const items = [ handleDeselectAll()} >{`Select none (0 items)`}, - {`Select page (${ - perPage > filteredCount ? filteredCount : perPage + handleSelectPage()} + >{`Select page (${ + perPage > filteredCount! ? filteredCount : perPage } items)`}, handleSelectAll()} >{`Select all (${count} items)`}, ]; @@ -107,28 +127,12 @@ const BulkSelect = ({ ); }; -// 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, - }; - 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, +const convertSchemaToIBCustomRepo = (repo: ApiRepositoryResponseRead) => { + const imageBuilderRepo: CustomRepository = { + id: repo.uuid!, name: repo.name, - baseurl: [repo.url], + baseurl: [repo.url!], check_gpg: false, }; if (repo.gpg_key) { @@ -140,74 +144,24 @@ const convertSchemaToIBCustomRepo = (repo) => { 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; - } +const Repositories = () => { + const dispatch = useAppDispatch(); - return contentSourcesRepo; -}; + const arch = useAppSelector((state) => selectArchitecture(state)); + const distribution = useAppSelector((state) => selectDistribution(state)); + const version = releaseToVersion(distribution); + const repositoriesList = useAppSelector((state) => + selectCustomRepositories(state) + ); -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) - : [] + repositoriesList ? repositoriesList.flatMap((repo) => repo.baseurl) : [] ); - const arch = getState().values?.arch; - const release = getState().values?.release; - const version = releaseToVersion(release); - const firstRequest = useListRepositoriesQuery( { availableForArch: arch, @@ -245,81 +199,89 @@ const Repositories = (props) => { const { data, isError, isFetching, isLoading, isSuccess, refetch } = useMemo(() => { - if (firstRequest?.data?.meta?.count > 100) { - return { ...followupRequest }; + if (firstRequest?.data?.meta?.count) { + 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 handleToggleClick = (event: React.MouseEvent) => { const id = event.currentTarget.id; setPage(1); setToggleSelected(id); }; - const isRepoSelected = (repoURL) => selected.includes(repoURL); + const isRepoSelected = (repoURL: string | undefined) => + selected.includes(repoURL); - const handlePerPageSelect = (event, newPerPage, newPage) => { + const handlePerPageSelect = ( + _: React.MouseEvent, + newPerPage: number, + newPage: number + ) => { setPerPage(newPerPage); setPage(newPage); }; - const handleSetPage = (event, newPage) => { + const handleSetPage = (_: React.MouseEvent, newPage: number) => { setPage(newPage); }; // filter displayed selected packages - const handleFilterRepositories = (_, value) => { + const handleFilterRepositories = ( + event: React.FormEvent, + value: string + ) => { setPage(1); setFilterValue(value); }; const filteredRepositoryURLs = useMemo(() => { - const repoUrls = Object.values(repositories).filter((repo) => - repo.name.toLowerCase().includes(filterValue.toLowerCase()) + 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) => repo.url); + return repoUrls.map((repo: ApiRepositoryResponseRead) => repo.url); } else if (toggleSelected === 'toggle-group-selected') { return repoUrls - .filter((repo) => isRepoSelected(repo.url)) - .map((repo) => repo.url); + .filter((repo: ApiRepositoryResponseRead) => isRepoSelected(repo.url!)) + .map((repo: ApiRepositoryResponseRead) => repo.url); } - }, [filterValue, repositories, toggleSelected]); + }, [filterValue, data, toggleSelected]); const handleClearFilter = () => { setFilterValue(''); }; - const updateFormState = (selectedRepoURLs) => { + const updateFormState = (selectedRepoURLs: (string | undefined)[]) => { // repositories is stored as an object with repoURLs as keys const selectedRepos = []; for (const repoURL of selectedRepoURLs) { - selectedRepos.push(repositories[repoURL]); + selectedRepos.push(data?.data?.find((repo) => repo.url === repoURL)); } - const payloadRepositories = selectedRepos.map((repo) => - convertSchemaToIBPayloadRepo(repo) - ); - const customRepositories = selectedRepos.map((repo) => - convertSchemaToIBCustomRepo(repo) + convertSchemaToIBCustomRepo(repo!) ); - input.onChange(payloadRepositories); - change('custom-repositories', customRepositories); + dispatch(changeCustomRepositories(customRepositories)); }; - const updateSelected = (selectedRepos) => { + const updateSelected = (selectedRepos: (string | undefined)[]) => { setSelected(selectedRepos); updateFormState(selectedRepos); }; - const handleSelect = (repoURL, rowIndex, isSelecting) => { + const handleSelect = ( + repoURL: string | undefined, + _: number, + isSelecting: boolean + ) => { if (isSelecting === true) { updateSelected([...selected, repoURL]); } else if (isSelecting === false) { @@ -330,36 +292,41 @@ const Repositories = (props) => { }; const handleSelectAll = () => { - updateSelected(Object.keys(repositories)); + if (data) { + updateSelected(data.data?.map((repo) => repo.url) || []); + } }; const computeStart = () => perPage * (page - 1); const computeEnd = () => perPage * page; const handleSelectPage = () => { - const pageRepos = filteredRepositoryURLs.slice( - computeStart(), - computeEnd() - ); + const pageRepos = + filteredRepositoryURLs && + filteredRepositoryURLs.slice(computeStart(), computeEnd()); // Filter to avoid adding duplicates - const newSelected = [ + const newSelected = pageRepos && [ ...pageRepos.filter((repoId) => !selected.includes(repoId)), ]; - updateSelected([...selected, ...newSelected]); + updateSelected([...selected, ...newSelected!]); }; const handleDeselectAll = () => { updateSelected([]); }; + const getRepoNameByUrl = (url: string) => { + return data!.data?.find((repo) => repo.url === url)?.name; + }; + return ( (isError && ) || (isLoading && ) || (isSuccess && ( <> - {Object.values(repositories).length === 0 ? ( + {data.data?.length === 0 ? ( ) : ( <> @@ -368,8 +335,8 @@ const Repositories = (props) => { { { - {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 ( - - - 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 : '-'} - - - { + 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 ( + + + 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 : '-'} + + + + + + ); + })} @@ -541,7 +524,12 @@ const Loading = () => { ); }; -const Empty = ({ isFetching, refetch }) => { +type EmptyProps = { + isFetching: boolean; + refetch: Function; +}; + +const Empty = ({ isFetching, refetch }: EmptyProps) => { const { isBeta } = useGetEnvironment(); return ( @@ -577,20 +565,4 @@ const Empty = ({ isFetching, refetch }) => { ); }; -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; diff --git a/src/Components/CreateImageWizardV2/steps/Repositories/RepositoriesStatus.tsx b/src/Components/CreateImageWizardV2/steps/Repositories/RepositoriesStatus.tsx index e1297eb5..510b55cc 100644 --- a/src/Components/CreateImageWizardV2/steps/Repositories/RepositoriesStatus.tsx +++ b/src/Components/CreateImageWizardV2/steps/Repositories/RepositoriesStatus.tsx @@ -17,12 +17,12 @@ import { InProgressIcon, } from '@patternfly/react-icons'; -import { ApiRepositoryResponse } from '../../../store/contentSourcesApi'; +import { ApiRepositoryResponse } from '../../../../store/contentSourcesApi'; import { convertStringToDate, timestampToDisplayString, -} from '../../../Utilities/time'; -import { useGetEnvironment } from '../../../Utilities/useGetEnvironment'; +} from '../../../../Utilities/time'; +import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment'; const getLastIntrospection = ( repoIntrospections: RepositoryStatusProps['repoIntrospections'] diff --git a/src/Components/CreateImageWizardV2/steps/Repositories/RepositoryUnavailable.tsx b/src/Components/CreateImageWizardV2/steps/Repositories/RepositoryUnavailable.tsx index 39f95ca4..385602a6 100644 --- a/src/Components/CreateImageWizardV2/steps/Repositories/RepositoryUnavailable.tsx +++ b/src/Components/CreateImageWizardV2/steps/Repositories/RepositoryUnavailable.tsx @@ -3,8 +3,8 @@ import React from 'react'; import { Alert, Button } from '@patternfly/react-core'; import { ExternalLinkAltIcon } from '@patternfly/react-icons'; -import { useCheckRepositoriesAvailability } from '../../../Utilities/checkRepositoriesAvailability'; -import { useGetEnvironment } from '../../../Utilities/useGetEnvironment'; +import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment'; +import { useCheckRepositoriesAvailability } from '../../utilities/checkRepositoriesAvailability'; const RepositoryUnavailable = () => { const { isBeta } = useGetEnvironment(); diff --git a/src/Components/CreateImageWizardV2/steps/Repositories/index.tsx b/src/Components/CreateImageWizardV2/steps/Repositories/index.tsx index 872145b0..8f20ad96 100644 --- a/src/Components/CreateImageWizardV2/steps/Repositories/index.tsx +++ b/src/Components/CreateImageWizardV2/steps/Repositories/index.tsx @@ -3,6 +3,8 @@ import React from 'react'; import { Button, Form, Text, Title } from '@patternfly/react-core'; import { ExternalLinkAltIcon } from '@patternfly/react-icons'; +import Repositories from './Repositories'; + import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment'; const ManageRepositoriesButton = () => { @@ -32,6 +34,7 @@ const RepositoriesStep = () => {
+ ); }; diff --git a/src/Components/CreateImageWizardV2/utilities/checkRepositoriesAvailability.ts b/src/Components/CreateImageWizardV2/utilities/checkRepositoriesAvailability.ts index 702b4ac9..4cd1d92f 100644 --- a/src/Components/CreateImageWizardV2/utilities/checkRepositoriesAvailability.ts +++ b/src/Components/CreateImageWizardV2/utilities/checkRepositoriesAvailability.ts @@ -1,24 +1,23 @@ import { useMemo } from 'react'; -import { useFormApi } from '@data-driven-forms/react-form-renderer'; - -import { releaseToVersion } from './releaseToVersion'; - -import { useListRepositoriesQuery } from '../store/contentSourcesApi'; +import { useListRepositoriesQuery } from '../../../store/contentSourcesApi'; +import { useAppSelector } from '../../../store/hooks'; +import { + selectArchitecture, + selectDistribution, + selectCustomRepositories, +} from '../../../store/wizardSlice'; +import { releaseToVersion } from '../../../Utilities/releaseToVersion.js'; /** - * This checks the list of the payload repositories against a list of repos freshly + * This checks the list of the custom repositories against a list of repos freshly * fetched from content source API and returns true whether there are some * repositories that are no longer available in the Repositories service. - * - * (The payload repositories are comming from the useFormApi hook). */ export const useCheckRepositoriesAvailability = () => { - const { getState } = useFormApi(); - - const arch = getState().values?.arch; - const release = getState().values?.release; - const version = releaseToVersion(release); + const arch = useAppSelector((state) => selectArchitecture(state)); + const distribution = useAppSelector((state) => selectDistribution(state)); + const version = releaseToVersion(distribution); // There needs to be two requests because the default limit for the // useListRepositoriesQuery is a 100 elements, and a first request is @@ -50,22 +49,26 @@ export const useCheckRepositoriesAvailability = () => { ); const { data: freshRepos, isSuccess } = useMemo(() => { - if (firstRequest?.data?.meta?.count > 100) { - return { ...followupRequest }; + if (firstRequest?.data?.meta?.count) { + if (firstRequest?.data?.meta?.count > 100) { + return { ...followupRequest }; + } } return { ...firstRequest }; }, [firstRequest, followupRequest]); - const payloadRepositories = getState()?.values?.['payload-repositories']; - // payloadRepositories existing === we came here from Recreate - if (isSuccess && payloadRepositories) { + const customRepositories = useAppSelector((state) => + selectCustomRepositories(state) + ); + // customRepositories existing === we came here from Recreate + if (isSuccess && customRepositories) { // Transform the fresh repos array into a Set to access its elements in O(1) // complexity later in the for loop. const freshReposUrls = new Set( - freshRepos.data.map((freshRepo) => freshRepo.url) + freshRepos.data?.map((freshRepo) => freshRepo.url) ); - for (const payloadRepo of payloadRepositories) { - if (!freshReposUrls.has(payloadRepo.baseurl)) { + for (const customRepo of customRepositories) { + if (customRepo.baseurl && !freshReposUrls.has(customRepo.baseurl[0])) { return true; } } diff --git a/src/store/wizardSlice.ts b/src/store/wizardSlice.ts index c5fa0dbf..558734ab 100644 --- a/src/store/wizardSlice.ts +++ b/src/store/wizardSlice.ts @@ -1,6 +1,7 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { + CustomRepository, DistributionProfileItem, Distributions, ImageRequest, @@ -45,6 +46,9 @@ type wizardState = { openScap: { profile: DistributionProfileItem | undefined; }; + repositories: { + customRepositories: CustomRepository[]; + }; details: { blueprintName: string; blueprintDescription: string; @@ -76,6 +80,9 @@ const initialState: wizardState = { openScap: { profile: undefined, }, + repositories: { + customRepositories: [], + }, details: { blueprintName: '', blueprintDescription: '', @@ -140,6 +147,10 @@ export const selectProfile = (state: RootState) => { return state.wizard.openScap.profile; }; +export const selectCustomRepositories = (state: RootState) => { + return state.wizard.repositories.customRepositories; +}; + export const selectBlueprintName = (state: RootState) => { return state.wizard.details.blueprintName; }; @@ -221,18 +232,21 @@ export const wizardSlice = createSlice({ ) => { state.registration.activationKey = action.payload; }, - changeOscapProfile: ( state, action: PayloadAction ) => { state.openScap.profile = action.payload; }, - + changeCustomRepositories: ( + state, + action: PayloadAction + ) => { + state.repositories.customRepositories = action.payload; + }, changeBlueprintName: (state, action: PayloadAction) => { state.details.blueprintName = action.payload; }, - changeBlueprintDescription: (state, action: PayloadAction) => { state.details.blueprintDescription = action.payload; }, @@ -257,6 +271,7 @@ export const { changeRegistrationType, changeActivationKey, changeOscapProfile, + changeCustomRepositories, changeBlueprintName, changeBlueprintDescription, } = wizardSlice.actions;