V2Wizard: Add components to the Repositories step skeleton
This adds needed components to the Repositories step skeleton.
This commit is contained in:
parent
3c441425bb
commit
8a178f5b7d
6 changed files with 238 additions and 245 deletions
|
|
@ -1,9 +1,5 @@
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import {
|
|
||||||
useFieldApi,
|
|
||||||
useFormApi,
|
|
||||||
} from '@data-driven-forms/react-form-renderer';
|
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -33,14 +29,35 @@ import {
|
||||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||||
import { RepositoryIcon } from '@patternfly/react-icons';
|
import { RepositoryIcon } from '@patternfly/react-icons';
|
||||||
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
|
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import RepositoriesStatus from './RepositoriesStatus';
|
import RepositoriesStatus from './RepositoriesStatus';
|
||||||
import RepositoryUnavailable from './RepositoryUnavailable';
|
import RepositoryUnavailable from './RepositoryUnavailable';
|
||||||
|
|
||||||
import { useListRepositoriesQuery } from '../../../store/contentSourcesApi';
|
import {
|
||||||
import { releaseToVersion } from '../../../Utilities/releaseToVersion';
|
ApiRepositoryResponseRead,
|
||||||
import { useGetEnvironment } from '../../../Utilities/useGetEnvironment';
|
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 = ({
|
const BulkSelect = ({
|
||||||
selected,
|
selected,
|
||||||
|
|
@ -51,7 +68,7 @@ const BulkSelect = ({
|
||||||
handleSelectPage,
|
handleSelectPage,
|
||||||
handleDeselectAll,
|
handleDeselectAll,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
}) => {
|
}: BulkSelectProps) => {
|
||||||
const [dropdownIsOpen, setDropdownIsOpen] = useState(false);
|
const [dropdownIsOpen, setDropdownIsOpen] = useState(false);
|
||||||
|
|
||||||
const numSelected = selected.length;
|
const numSelected = selected.length;
|
||||||
|
|
@ -63,14 +80,17 @@ const BulkSelect = ({
|
||||||
const items = [
|
const items = [
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="none"
|
key="none"
|
||||||
onClick={handleDeselectAll}
|
onClick={() => handleDeselectAll()}
|
||||||
>{`Select none (0 items)`}</DropdownItem>,
|
>{`Select none (0 items)`}</DropdownItem>,
|
||||||
<DropdownItem key="page" onClick={handleSelectPage}>{`Select page (${
|
<DropdownItem
|
||||||
perPage > filteredCount ? filteredCount : perPage
|
key="page"
|
||||||
|
onClick={() => handleSelectPage()}
|
||||||
|
>{`Select page (${
|
||||||
|
perPage > filteredCount! ? filteredCount : perPage
|
||||||
} items)`}</DropdownItem>,
|
} items)`}</DropdownItem>,
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="all"
|
key="all"
|
||||||
onClick={handleSelectAll}
|
onClick={() => handleSelectAll()}
|
||||||
>{`Select all (${count} items)`}</DropdownItem>,
|
>{`Select all (${count} items)`}</DropdownItem>,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -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
|
// Utility function to convert from Content Sources to Image Builder custom repo API schema
|
||||||
const convertSchemaToIBCustomRepo = (repo) => {
|
const convertSchemaToIBCustomRepo = (repo: ApiRepositoryResponseRead) => {
|
||||||
const imageBuilderRepo = {
|
const imageBuilderRepo: CustomRepository = {
|
||||||
id: repo.uuid,
|
id: repo.uuid!,
|
||||||
name: repo.name,
|
name: repo.name,
|
||||||
baseurl: [repo.url],
|
baseurl: [repo.url!],
|
||||||
check_gpg: false,
|
check_gpg: false,
|
||||||
};
|
};
|
||||||
if (repo.gpg_key) {
|
if (repo.gpg_key) {
|
||||||
|
|
@ -140,74 +144,24 @@ const convertSchemaToIBCustomRepo = (repo) => {
|
||||||
return imageBuilderRepo;
|
return imageBuilderRepo;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Utility function to convert from Image Builder to Content Sources API schema
|
const Repositories = () => {
|
||||||
const convertSchemaToContentSources = (repo) => {
|
const dispatch = useAppDispatch();
|
||||||
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 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 [filterValue, setFilterValue] = useState('');
|
||||||
const [perPage, setPerPage] = useState(10);
|
const [perPage, setPerPage] = useState(10);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [toggleSelected, setToggleSelected] = useState('toggle-group-all');
|
const [toggleSelected, setToggleSelected] = useState('toggle-group-all');
|
||||||
const [selected, setSelected] = useState(
|
const [selected, setSelected] = useState(
|
||||||
getState()?.values?.['payload-repositories']
|
repositoriesList ? repositoriesList.flatMap((repo) => repo.baseurl) : []
|
||||||
? 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(
|
const firstRequest = useListRepositoriesQuery(
|
||||||
{
|
{
|
||||||
availableForArch: arch,
|
availableForArch: arch,
|
||||||
|
|
@ -245,81 +199,89 @@ const Repositories = (props) => {
|
||||||
|
|
||||||
const { data, isError, isFetching, isLoading, isSuccess, refetch } =
|
const { data, isError, isFetching, isLoading, isSuccess, refetch } =
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
if (firstRequest?.data?.meta?.count > 100) {
|
if (firstRequest?.data?.meta?.count) {
|
||||||
return { ...followupRequest };
|
if (firstRequest?.data?.meta?.count > 100) {
|
||||||
|
return { ...followupRequest };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { ...firstRequest };
|
return { ...firstRequest };
|
||||||
}, [firstRequest, followupRequest]);
|
}, [firstRequest, followupRequest]);
|
||||||
|
|
||||||
const repositories = useMemo(() => {
|
const handleToggleClick = (event: React.MouseEvent) => {
|
||||||
return data ? initializeRepositories(data.data) : {};
|
|
||||||
}, [firstRequest.data, followupRequest.data]);
|
|
||||||
|
|
||||||
const handleToggleClick = (event) => {
|
|
||||||
const id = event.currentTarget.id;
|
const id = event.currentTarget.id;
|
||||||
setPage(1);
|
setPage(1);
|
||||||
setToggleSelected(id);
|
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);
|
setPerPage(newPerPage);
|
||||||
setPage(newPage);
|
setPage(newPage);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetPage = (event, newPage) => {
|
const handleSetPage = (_: React.MouseEvent, newPage: number) => {
|
||||||
setPage(newPage);
|
setPage(newPage);
|
||||||
};
|
};
|
||||||
|
|
||||||
// filter displayed selected packages
|
// filter displayed selected packages
|
||||||
const handleFilterRepositories = (_, value) => {
|
const handleFilterRepositories = (
|
||||||
|
event: React.FormEvent<HTMLInputElement>,
|
||||||
|
value: string
|
||||||
|
) => {
|
||||||
setPage(1);
|
setPage(1);
|
||||||
setFilterValue(value);
|
setFilterValue(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredRepositoryURLs = useMemo(() => {
|
const filteredRepositoryURLs = useMemo(() => {
|
||||||
const repoUrls = Object.values(repositories).filter((repo) =>
|
if (!data || !data.data) {
|
||||||
repo.name.toLowerCase().includes(filterValue.toLowerCase())
|
return [];
|
||||||
|
}
|
||||||
|
const repoUrls = data.data.filter((repo) =>
|
||||||
|
repo.name?.toLowerCase().includes(filterValue.toLowerCase())
|
||||||
);
|
);
|
||||||
if (toggleSelected === 'toggle-group-all') {
|
if (toggleSelected === 'toggle-group-all') {
|
||||||
return repoUrls.map((repo) => repo.url);
|
return repoUrls.map((repo: ApiRepositoryResponseRead) => repo.url);
|
||||||
} else if (toggleSelected === 'toggle-group-selected') {
|
} else if (toggleSelected === 'toggle-group-selected') {
|
||||||
return repoUrls
|
return repoUrls
|
||||||
.filter((repo) => isRepoSelected(repo.url))
|
.filter((repo: ApiRepositoryResponseRead) => isRepoSelected(repo.url!))
|
||||||
.map((repo) => repo.url);
|
.map((repo: ApiRepositoryResponseRead) => repo.url);
|
||||||
}
|
}
|
||||||
}, [filterValue, repositories, toggleSelected]);
|
}, [filterValue, data, toggleSelected]);
|
||||||
|
|
||||||
const handleClearFilter = () => {
|
const handleClearFilter = () => {
|
||||||
setFilterValue('');
|
setFilterValue('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateFormState = (selectedRepoURLs) => {
|
const updateFormState = (selectedRepoURLs: (string | undefined)[]) => {
|
||||||
// repositories is stored as an object with repoURLs as keys
|
// repositories is stored as an object with repoURLs as keys
|
||||||
const selectedRepos = [];
|
const selectedRepos = [];
|
||||||
for (const repoURL of selectedRepoURLs) {
|
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) =>
|
const customRepositories = selectedRepos.map((repo) =>
|
||||||
convertSchemaToIBCustomRepo(repo)
|
convertSchemaToIBCustomRepo(repo!)
|
||||||
);
|
);
|
||||||
|
|
||||||
input.onChange(payloadRepositories);
|
dispatch(changeCustomRepositories(customRepositories));
|
||||||
change('custom-repositories', customRepositories);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSelected = (selectedRepos) => {
|
const updateSelected = (selectedRepos: (string | undefined)[]) => {
|
||||||
setSelected(selectedRepos);
|
setSelected(selectedRepos);
|
||||||
updateFormState(selectedRepos);
|
updateFormState(selectedRepos);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = (repoURL, rowIndex, isSelecting) => {
|
const handleSelect = (
|
||||||
|
repoURL: string | undefined,
|
||||||
|
_: number,
|
||||||
|
isSelecting: boolean
|
||||||
|
) => {
|
||||||
if (isSelecting === true) {
|
if (isSelecting === true) {
|
||||||
updateSelected([...selected, repoURL]);
|
updateSelected([...selected, repoURL]);
|
||||||
} else if (isSelecting === false) {
|
} else if (isSelecting === false) {
|
||||||
|
|
@ -330,36 +292,41 @@ const Repositories = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
updateSelected(Object.keys(repositories));
|
if (data) {
|
||||||
|
updateSelected(data.data?.map((repo) => repo.url) || []);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const computeStart = () => perPage * (page - 1);
|
const computeStart = () => perPage * (page - 1);
|
||||||
const computeEnd = () => perPage * page;
|
const computeEnd = () => perPage * page;
|
||||||
|
|
||||||
const handleSelectPage = () => {
|
const handleSelectPage = () => {
|
||||||
const pageRepos = filteredRepositoryURLs.slice(
|
const pageRepos =
|
||||||
computeStart(),
|
filteredRepositoryURLs &&
|
||||||
computeEnd()
|
filteredRepositoryURLs.slice(computeStart(), computeEnd());
|
||||||
);
|
|
||||||
|
|
||||||
// Filter to avoid adding duplicates
|
// Filter to avoid adding duplicates
|
||||||
const newSelected = [
|
const newSelected = pageRepos && [
|
||||||
...pageRepos.filter((repoId) => !selected.includes(repoId)),
|
...pageRepos.filter((repoId) => !selected.includes(repoId)),
|
||||||
];
|
];
|
||||||
|
|
||||||
updateSelected([...selected, ...newSelected]);
|
updateSelected([...selected, ...newSelected!]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeselectAll = () => {
|
const handleDeselectAll = () => {
|
||||||
updateSelected([]);
|
updateSelected([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getRepoNameByUrl = (url: string) => {
|
||||||
|
return data!.data?.find((repo) => repo.url === url)?.name;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(isError && <Error />) ||
|
(isError && <Error />) ||
|
||||||
(isLoading && <Loading />) ||
|
(isLoading && <Loading />) ||
|
||||||
(isSuccess && (
|
(isSuccess && (
|
||||||
<>
|
<>
|
||||||
{Object.values(repositories).length === 0 ? (
|
{data.data?.length === 0 ? (
|
||||||
<Empty refetch={refetch} isFetching={isFetching} />
|
<Empty refetch={refetch} isFetching={isFetching} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
@ -368,8 +335,8 @@ const Repositories = (props) => {
|
||||||
<ToolbarItem variant="bulk-select">
|
<ToolbarItem variant="bulk-select">
|
||||||
<BulkSelect
|
<BulkSelect
|
||||||
selected={selected}
|
selected={selected}
|
||||||
count={Object.values(repositories).length}
|
count={data.data?.length}
|
||||||
filteredCount={filteredRepositoryURLs.length}
|
filteredCount={filteredRepositoryURLs?.length}
|
||||||
perPage={perPage}
|
perPage={perPage}
|
||||||
handleSelectAll={handleSelectAll}
|
handleSelectAll={handleSelectAll}
|
||||||
handleSelectPage={handleSelectPage}
|
handleSelectPage={handleSelectPage}
|
||||||
|
|
@ -415,7 +382,9 @@ const Repositories = (props) => {
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem variant="pagination">
|
<ToolbarItem variant="pagination">
|
||||||
<Pagination
|
<Pagination
|
||||||
itemCount={filteredRepositoryURLs.length}
|
itemCount={
|
||||||
|
filteredRepositoryURLs && filteredRepositoryURLs.length
|
||||||
|
}
|
||||||
perPage={perPage}
|
perPage={perPage}
|
||||||
page={page}
|
page={page}
|
||||||
onSetPage={handleSetPage}
|
onSetPage={handleSetPage}
|
||||||
|
|
@ -441,75 +410,89 @@ const Repositories = (props) => {
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{filteredRepositoryURLs
|
{filteredRepositoryURLs &&
|
||||||
.sort((a, b) => {
|
filteredRepositoryURLs
|
||||||
if (repositories[a].name < repositories[b].name) {
|
.sort((a, b) => {
|
||||||
return -1;
|
if (getRepoNameByUrl(a!)! < getRepoNameByUrl(b!)!) {
|
||||||
} else if (
|
return -1;
|
||||||
repositories[b].name < repositories[a].name
|
} else if (
|
||||||
) {
|
getRepoNameByUrl(b!)! < getRepoNameByUrl(a!)!
|
||||||
return 1;
|
) {
|
||||||
} else {
|
return 1;
|
||||||
return 0;
|
} else {
|
||||||
}
|
return 0;
|
||||||
})
|
}
|
||||||
.slice(computeStart(), computeEnd())
|
})
|
||||||
.map((repoURL, rowIndex) => {
|
.slice(computeStart(), computeEnd())
|
||||||
const repo = repositories[repoURL];
|
.map((repoURL, rowIndex) => {
|
||||||
const repoExists = repo.name ? true : false;
|
const repo = data?.data?.find(
|
||||||
return (
|
(repo) => repo.url === repoURL
|
||||||
<Tr key={repo.url}>
|
);
|
||||||
<Td
|
|
||||||
select={{
|
if (!repo) {
|
||||||
isSelected: isRepoSelected(repo.url),
|
return <></>;
|
||||||
rowIndex: rowIndex,
|
}
|
||||||
onSelect: (event, isSelecting) =>
|
|
||||||
handleSelect(repo.url, rowIndex, isSelecting),
|
const repoExists = repo.name ? true : false;
|
||||||
isDisabled:
|
return (
|
||||||
isFetching || repo.status !== 'Valid',
|
<Tr key={repo.url}>
|
||||||
}}
|
<Td
|
||||||
/>
|
select={{
|
||||||
<Td dataLabel={'Name'}>
|
isSelected: isRepoSelected(repo.url),
|
||||||
{repoExists
|
rowIndex: rowIndex,
|
||||||
? repo.name
|
onSelect: (event, isSelecting) =>
|
||||||
: 'Repository with the following url is no longer available:'}
|
handleSelect(
|
||||||
<br />
|
repo.url,
|
||||||
<Button
|
rowIndex,
|
||||||
component="a"
|
isSelecting
|
||||||
target="_blank"
|
),
|
||||||
variant="link"
|
isDisabled:
|
||||||
icon={<ExternalLinkAltIcon />}
|
isFetching || repo.status !== 'Valid',
|
||||||
iconPosition="right"
|
}}
|
||||||
isInline
|
|
||||||
href={repo.url}
|
|
||||||
>
|
|
||||||
{repo.url}
|
|
||||||
</Button>
|
|
||||||
</Td>
|
|
||||||
<Td dataLabel={'Architecture'}>
|
|
||||||
{repoExists ? repo.distribution_arch : '-'}
|
|
||||||
</Td>
|
|
||||||
<Td dataLabel={'Version'}>
|
|
||||||
{repoExists ? repo.distribution_versions : '-'}
|
|
||||||
</Td>
|
|
||||||
<Td dataLabel={'Packages'}>
|
|
||||||
{repoExists ? repo.package_count : '-'}
|
|
||||||
</Td>
|
|
||||||
<Td dataLabel={'Status'}>
|
|
||||||
<RepositoriesStatus
|
|
||||||
repoStatus={
|
|
||||||
repoExists ? repo.status : 'Unavailable'
|
|
||||||
}
|
|
||||||
repoUrl={repo.url}
|
|
||||||
repoIntrospections={
|
|
||||||
repo.last_introspection_time
|
|
||||||
}
|
|
||||||
repoFailCount={repo.failed_introspections_count}
|
|
||||||
/>
|
/>
|
||||||
</Td>
|
<Td dataLabel={'Name'}>
|
||||||
</Tr>
|
{repoExists
|
||||||
);
|
? repo.name
|
||||||
})}
|
: 'Repository with the following url is no longer available:'}
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
target="_blank"
|
||||||
|
variant="link"
|
||||||
|
icon={<ExternalLinkAltIcon />}
|
||||||
|
iconPosition="right"
|
||||||
|
isInline
|
||||||
|
href={repo.url}
|
||||||
|
>
|
||||||
|
{repo.url}
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
<Td dataLabel={'Architecture'}>
|
||||||
|
{repoExists ? repo.distribution_arch : '-'}
|
||||||
|
</Td>
|
||||||
|
<Td dataLabel={'Version'}>
|
||||||
|
{repoExists ? repo.distribution_versions : '-'}
|
||||||
|
</Td>
|
||||||
|
<Td dataLabel={'Packages'}>
|
||||||
|
{repoExists ? repo.package_count : '-'}
|
||||||
|
</Td>
|
||||||
|
<Td dataLabel={'Status'}>
|
||||||
|
<RepositoriesStatus
|
||||||
|
repoStatus={
|
||||||
|
repoExists ? repo.status : 'Unavailable'
|
||||||
|
}
|
||||||
|
repoUrl={repo.url}
|
||||||
|
repoIntrospections={
|
||||||
|
repo.last_introspection_time
|
||||||
|
}
|
||||||
|
repoFailCount={
|
||||||
|
repo.failed_introspections_count
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</PanelMain>
|
</PanelMain>
|
||||||
|
|
@ -541,7 +524,12 @@ const Loading = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Empty = ({ isFetching, refetch }) => {
|
type EmptyProps = {
|
||||||
|
isFetching: boolean;
|
||||||
|
refetch: Function;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Empty = ({ isFetching, refetch }: EmptyProps) => {
|
||||||
const { isBeta } = useGetEnvironment();
|
const { isBeta } = useGetEnvironment();
|
||||||
return (
|
return (
|
||||||
<EmptyState variant={EmptyStateVariant.lg} data-testid="empty-state">
|
<EmptyState variant={EmptyStateVariant.lg} data-testid="empty-state">
|
||||||
|
|
@ -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;
|
export default Repositories;
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,12 @@ import {
|
||||||
InProgressIcon,
|
InProgressIcon,
|
||||||
} from '@patternfly/react-icons';
|
} from '@patternfly/react-icons';
|
||||||
|
|
||||||
import { ApiRepositoryResponse } from '../../../store/contentSourcesApi';
|
import { ApiRepositoryResponse } from '../../../../store/contentSourcesApi';
|
||||||
import {
|
import {
|
||||||
convertStringToDate,
|
convertStringToDate,
|
||||||
timestampToDisplayString,
|
timestampToDisplayString,
|
||||||
} from '../../../Utilities/time';
|
} from '../../../../Utilities/time';
|
||||||
import { useGetEnvironment } from '../../../Utilities/useGetEnvironment';
|
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
|
||||||
|
|
||||||
const getLastIntrospection = (
|
const getLastIntrospection = (
|
||||||
repoIntrospections: RepositoryStatusProps['repoIntrospections']
|
repoIntrospections: RepositoryStatusProps['repoIntrospections']
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import React from 'react';
|
||||||
import { Alert, Button } from '@patternfly/react-core';
|
import { Alert, Button } from '@patternfly/react-core';
|
||||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
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 RepositoryUnavailable = () => {
|
||||||
const { isBeta } = useGetEnvironment();
|
const { isBeta } = useGetEnvironment();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import React from 'react';
|
||||||
import { Button, Form, Text, Title } from '@patternfly/react-core';
|
import { Button, Form, Text, Title } from '@patternfly/react-core';
|
||||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
import Repositories from './Repositories';
|
||||||
|
|
||||||
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
|
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
|
||||||
|
|
||||||
const ManageRepositoriesButton = () => {
|
const ManageRepositoriesButton = () => {
|
||||||
|
|
@ -32,6 +34,7 @@ const RepositoriesStep = () => {
|
||||||
<br />
|
<br />
|
||||||
<ManageRepositoriesButton />
|
<ManageRepositoriesButton />
|
||||||
</Text>
|
</Text>
|
||||||
|
<Repositories />
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,23 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { useFormApi } from '@data-driven-forms/react-form-renderer';
|
import { useListRepositoriesQuery } from '../../../store/contentSourcesApi';
|
||||||
|
import { useAppSelector } from '../../../store/hooks';
|
||||||
import { releaseToVersion } from './releaseToVersion';
|
import {
|
||||||
|
selectArchitecture,
|
||||||
import { useListRepositoriesQuery } from '../store/contentSourcesApi';
|
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
|
* fetched from content source API and returns true whether there are some
|
||||||
* repositories that are no longer available in the Repositories service.
|
* repositories that are no longer available in the Repositories service.
|
||||||
*
|
|
||||||
* (The payload repositories are comming from the useFormApi hook).
|
|
||||||
*/
|
*/
|
||||||
export const useCheckRepositoriesAvailability = () => {
|
export const useCheckRepositoriesAvailability = () => {
|
||||||
const { getState } = useFormApi();
|
const arch = useAppSelector((state) => selectArchitecture(state));
|
||||||
|
const distribution = useAppSelector((state) => selectDistribution(state));
|
||||||
const arch = getState().values?.arch;
|
const version = releaseToVersion(distribution);
|
||||||
const release = getState().values?.release;
|
|
||||||
const version = releaseToVersion(release);
|
|
||||||
|
|
||||||
// There needs to be two requests because the default limit for the
|
// There needs to be two requests because the default limit for the
|
||||||
// useListRepositoriesQuery is a 100 elements, and a first request is
|
// useListRepositoriesQuery is a 100 elements, and a first request is
|
||||||
|
|
@ -50,22 +49,26 @@ export const useCheckRepositoriesAvailability = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: freshRepos, isSuccess } = useMemo(() => {
|
const { data: freshRepos, isSuccess } = useMemo(() => {
|
||||||
if (firstRequest?.data?.meta?.count > 100) {
|
if (firstRequest?.data?.meta?.count) {
|
||||||
return { ...followupRequest };
|
if (firstRequest?.data?.meta?.count > 100) {
|
||||||
|
return { ...followupRequest };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { ...firstRequest };
|
return { ...firstRequest };
|
||||||
}, [firstRequest, followupRequest]);
|
}, [firstRequest, followupRequest]);
|
||||||
|
|
||||||
const payloadRepositories = getState()?.values?.['payload-repositories'];
|
const customRepositories = useAppSelector((state) =>
|
||||||
// payloadRepositories existing === we came here from Recreate
|
selectCustomRepositories(state)
|
||||||
if (isSuccess && payloadRepositories) {
|
);
|
||||||
|
// 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)
|
// Transform the fresh repos array into a Set to access its elements in O(1)
|
||||||
// complexity later in the for loop.
|
// complexity later in the for loop.
|
||||||
const freshReposUrls = new Set(
|
const freshReposUrls = new Set(
|
||||||
freshRepos.data.map((freshRepo) => freshRepo.url)
|
freshRepos.data?.map((freshRepo) => freshRepo.url)
|
||||||
);
|
);
|
||||||
for (const payloadRepo of payloadRepositories) {
|
for (const customRepo of customRepositories) {
|
||||||
if (!freshReposUrls.has(payloadRepo.baseurl)) {
|
if (customRepo.baseurl && !freshReposUrls.has(customRepo.baseurl[0])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CustomRepository,
|
||||||
DistributionProfileItem,
|
DistributionProfileItem,
|
||||||
Distributions,
|
Distributions,
|
||||||
ImageRequest,
|
ImageRequest,
|
||||||
|
|
@ -45,6 +46,9 @@ type wizardState = {
|
||||||
openScap: {
|
openScap: {
|
||||||
profile: DistributionProfileItem | undefined;
|
profile: DistributionProfileItem | undefined;
|
||||||
};
|
};
|
||||||
|
repositories: {
|
||||||
|
customRepositories: CustomRepository[];
|
||||||
|
};
|
||||||
details: {
|
details: {
|
||||||
blueprintName: string;
|
blueprintName: string;
|
||||||
blueprintDescription: string;
|
blueprintDescription: string;
|
||||||
|
|
@ -76,6 +80,9 @@ const initialState: wizardState = {
|
||||||
openScap: {
|
openScap: {
|
||||||
profile: undefined,
|
profile: undefined,
|
||||||
},
|
},
|
||||||
|
repositories: {
|
||||||
|
customRepositories: [],
|
||||||
|
},
|
||||||
details: {
|
details: {
|
||||||
blueprintName: '',
|
blueprintName: '',
|
||||||
blueprintDescription: '',
|
blueprintDescription: '',
|
||||||
|
|
@ -140,6 +147,10 @@ export const selectProfile = (state: RootState) => {
|
||||||
return state.wizard.openScap.profile;
|
return state.wizard.openScap.profile;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const selectCustomRepositories = (state: RootState) => {
|
||||||
|
return state.wizard.repositories.customRepositories;
|
||||||
|
};
|
||||||
|
|
||||||
export const selectBlueprintName = (state: RootState) => {
|
export const selectBlueprintName = (state: RootState) => {
|
||||||
return state.wizard.details.blueprintName;
|
return state.wizard.details.blueprintName;
|
||||||
};
|
};
|
||||||
|
|
@ -221,18 +232,21 @@ export const wizardSlice = createSlice({
|
||||||
) => {
|
) => {
|
||||||
state.registration.activationKey = action.payload;
|
state.registration.activationKey = action.payload;
|
||||||
},
|
},
|
||||||
|
|
||||||
changeOscapProfile: (
|
changeOscapProfile: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<DistributionProfileItem | undefined>
|
action: PayloadAction<DistributionProfileItem | undefined>
|
||||||
) => {
|
) => {
|
||||||
state.openScap.profile = action.payload;
|
state.openScap.profile = action.payload;
|
||||||
},
|
},
|
||||||
|
changeCustomRepositories: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<CustomRepository[]>
|
||||||
|
) => {
|
||||||
|
state.repositories.customRepositories = action.payload;
|
||||||
|
},
|
||||||
changeBlueprintName: (state, action: PayloadAction<string>) => {
|
changeBlueprintName: (state, action: PayloadAction<string>) => {
|
||||||
state.details.blueprintName = action.payload;
|
state.details.blueprintName = action.payload;
|
||||||
},
|
},
|
||||||
|
|
||||||
changeBlueprintDescription: (state, action: PayloadAction<string>) => {
|
changeBlueprintDescription: (state, action: PayloadAction<string>) => {
|
||||||
state.details.blueprintDescription = action.payload;
|
state.details.blueprintDescription = action.payload;
|
||||||
},
|
},
|
||||||
|
|
@ -257,6 +271,7 @@ export const {
|
||||||
changeRegistrationType,
|
changeRegistrationType,
|
||||||
changeActivationKey,
|
changeActivationKey,
|
||||||
changeOscapProfile,
|
changeOscapProfile,
|
||||||
|
changeCustomRepositories,
|
||||||
changeBlueprintName,
|
changeBlueprintName,
|
||||||
changeBlueprintDescription,
|
changeBlueprintDescription,
|
||||||
} = wizardSlice.actions;
|
} = wizardSlice.actions;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue