src: Rename "V2" folders to just Wizard
This replaces all occurences of "CreateImageWizardV2" with just "CreateImageWizard" as it is the only version now.
This commit is contained in:
parent
b1e5a8c7c6
commit
4fb37c187e
93 changed files with 20 additions and 22 deletions
|
|
@ -0,0 +1,569 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Pagination,
|
||||
Panel,
|
||||
PanelMain,
|
||||
SearchInput,
|
||||
Toolbar,
|
||||
ToolbarContent,
|
||||
ToolbarItem,
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
PaginationVariant,
|
||||
Grid,
|
||||
Modal,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
|
||||
|
||||
import { BulkSelect } from './components/BulkSelect';
|
||||
import Empty from './components/Empty';
|
||||
import { Error } from './components/Error';
|
||||
import { Loading } from './components/Loading';
|
||||
import {
|
||||
convertSchemaToIBCustomRepo,
|
||||
convertSchemaToIBPayloadRepo,
|
||||
} from './components/Utilities';
|
||||
import RepositoriesStatus from './RepositoriesStatus';
|
||||
import RepositoryUnavailable from './RepositoryUnavailable';
|
||||
|
||||
import {
|
||||
ApiRepositoryResponseRead,
|
||||
useListRepositoriesQuery,
|
||||
} from '../../../../store/contentSourcesApi';
|
||||
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
|
||||
import {
|
||||
changeCustomRepositories,
|
||||
changePayloadRepositories,
|
||||
selectArchitecture,
|
||||
selectCustomRepositories,
|
||||
selectDistribution,
|
||||
selectGroups,
|
||||
selectPackages,
|
||||
selectPayloadRepositories,
|
||||
selectRecommendedRepositories,
|
||||
selectWizardMode,
|
||||
} from '../../../../store/wizardSlice';
|
||||
import { releaseToVersion } from '../../../../Utilities/releaseToVersion';
|
||||
import useDebounce from '../../../../Utilities/useDebounce';
|
||||
|
||||
const Repositories = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const wizardMode = useAppSelector(selectWizardMode);
|
||||
const arch = useAppSelector(selectArchitecture);
|
||||
const distribution = useAppSelector(selectDistribution);
|
||||
const version = releaseToVersion(distribution);
|
||||
const customRepositories = useAppSelector(selectCustomRepositories);
|
||||
const packages = useAppSelector(selectPackages);
|
||||
const groups = useAppSelector(selectGroups);
|
||||
const payloadRepositories = useAppSelector(selectPayloadRepositories);
|
||||
const recommendedRepos = useAppSelector(selectRecommendedRepositories);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [reposToRemove, setReposToRemove] = useState<string[]>([]);
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
const [perPage, setPerPage] = useState(10);
|
||||
const [page, setPage] = useState(1);
|
||||
const [toggleSelected, setToggleSelected] = useState<
|
||||
'toggle-group-all' | 'toggle-group-selected'
|
||||
>('toggle-group-all');
|
||||
|
||||
const debouncedFilterValue = useDebounce(filterValue);
|
||||
|
||||
const selected = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
[
|
||||
...customRepositories.map(({ baseurl }) => baseurl || []).flat(1),
|
||||
...(payloadRepositories.map(({ baseurl }) => baseurl) || []),
|
||||
...(recommendedRepos.map(({ url }) => url) || []),
|
||||
].filter((url) => !!url) as string[]
|
||||
),
|
||||
[customRepositories, payloadRepositories, recommendedRepos]
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const initialSelectedState = useMemo(() => new Set([...selected]), []);
|
||||
|
||||
const {
|
||||
data: { data: previousReposData = [] } = {},
|
||||
isLoading: previousLoading,
|
||||
isSuccess: previousSuccess,
|
||||
refetch: refetchIntial,
|
||||
} = useListRepositoriesQuery(
|
||||
{
|
||||
availableForArch: arch,
|
||||
availableForVersion: version,
|
||||
origin: 'external',
|
||||
limit: 999,
|
||||
offset: 0,
|
||||
url: [...initialSelectedState].join(','),
|
||||
},
|
||||
{ refetchOnMountOrArgChange: false }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (toggleSelected === 'toggle-group-selected' && !selected.size) {
|
||||
setToggleSelected('toggle-group-all');
|
||||
}
|
||||
}, [selected, toggleSelected]);
|
||||
|
||||
const {
|
||||
data: { data: contentList = [], meta: { count } = { count: 0 } } = {},
|
||||
isError,
|
||||
isFetching,
|
||||
isLoading,
|
||||
refetch: refetchMain,
|
||||
} = useListRepositoriesQuery(
|
||||
{
|
||||
availableForArch: arch,
|
||||
availableForVersion: version,
|
||||
contentType: 'rpm',
|
||||
origin: 'external',
|
||||
limit: perPage,
|
||||
offset: perPage * (page - 1),
|
||||
search: debouncedFilterValue,
|
||||
url:
|
||||
toggleSelected === 'toggle-group-selected'
|
||||
? [...selected].join(',')
|
||||
: undefined,
|
||||
},
|
||||
{ refetchOnMountOrArgChange: 60 }
|
||||
);
|
||||
|
||||
const refresh = () => {
|
||||
// In case the user deletes an intially selected repository.
|
||||
// Refetching will react to both added and removed repositories.
|
||||
refetchMain();
|
||||
refetchIntial();
|
||||
};
|
||||
|
||||
const addSelected = (
|
||||
repo: ApiRepositoryResponseRead | ApiRepositoryResponseRead[]
|
||||
) => {
|
||||
let reposToAdd: ApiRepositoryResponseRead[] = [];
|
||||
// Check if array of items
|
||||
if ((repo as ApiRepositoryResponseRead[])?.length) {
|
||||
reposToAdd = (repo as ApiRepositoryResponseRead[]).filter(
|
||||
({ url }) => url && !selected.has(url)
|
||||
);
|
||||
} else {
|
||||
// Then it should be a single item
|
||||
const singleRepo = repo as ApiRepositoryResponseRead;
|
||||
if (singleRepo?.url && !selected.has(singleRepo.url)) {
|
||||
reposToAdd.push(singleRepo);
|
||||
}
|
||||
}
|
||||
|
||||
const customToAdd = reposToAdd.map((repo) =>
|
||||
convertSchemaToIBCustomRepo(repo!)
|
||||
);
|
||||
|
||||
const payloadToAdd = reposToAdd.map((repo) =>
|
||||
convertSchemaToIBPayloadRepo(repo!)
|
||||
);
|
||||
|
||||
dispatch(changeCustomRepositories([...customRepositories, ...customToAdd]));
|
||||
dispatch(
|
||||
changePayloadRepositories([...payloadRepositories, ...payloadToAdd])
|
||||
);
|
||||
};
|
||||
|
||||
const clearSelected = () => {
|
||||
const recommendedReposSet = new Set(recommendedRepos.map(({ url }) => url));
|
||||
const initiallySelected = [...selected].some(
|
||||
(url) => url && initialSelectedState.has(url)
|
||||
);
|
||||
|
||||
if (initiallySelected) {
|
||||
setModalOpen(true);
|
||||
setReposToRemove([...selected]);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
changeCustomRepositories(
|
||||
customRepositories.filter(({ baseurl }) =>
|
||||
baseurl?.some((url) => recommendedReposSet.has(url))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
dispatch(
|
||||
changePayloadRepositories(
|
||||
payloadRepositories.filter(({ baseurl }) =>
|
||||
recommendedReposSet.has(baseurl)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const removeSelected = (
|
||||
repo: ApiRepositoryResponseRead | ApiRepositoryResponseRead[]
|
||||
) => {
|
||||
if ((repo as ApiRepositoryResponseRead[])?.length) {
|
||||
const itemsToRemove = new Set(
|
||||
(repo as ApiRepositoryResponseRead[]).map(({ url }) => url)
|
||||
);
|
||||
|
||||
dispatch(
|
||||
changeCustomRepositories(
|
||||
customRepositories.filter(
|
||||
({ baseurl }) => !baseurl?.some((url) => itemsToRemove.has(url))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
dispatch(
|
||||
changePayloadRepositories(
|
||||
payloadRepositories.filter(
|
||||
({ baseurl }) => !itemsToRemove.has(baseurl)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const urlToRemove = (repo as ApiRepositoryResponseRead)?.url;
|
||||
if (urlToRemove) {
|
||||
dispatch(
|
||||
changeCustomRepositories(
|
||||
customRepositories.filter(
|
||||
({ baseurl }) => !baseurl?.some((url) => urlToRemove === url)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
dispatch(
|
||||
changePayloadRepositories(
|
||||
payloadRepositories.filter(({ baseurl }) => urlToRemove !== baseurl)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddRemove = (
|
||||
repo: ApiRepositoryResponseRead | ApiRepositoryResponseRead[],
|
||||
selected: boolean
|
||||
) => {
|
||||
if (selected) return addSelected(repo);
|
||||
if ((repo as ApiRepositoryResponseRead[])?.length) {
|
||||
const initiallySelectedItems = (repo as ApiRepositoryResponseRead[]).map(
|
||||
({ url }) => url
|
||||
);
|
||||
|
||||
const hasSome = initiallySelectedItems.some(
|
||||
(url) => url && initialSelectedState.has(url)
|
||||
);
|
||||
|
||||
if (hasSome) {
|
||||
setModalOpen(true);
|
||||
setReposToRemove(initiallySelectedItems as string[]);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const isInitiallySelected =
|
||||
(repo as ApiRepositoryResponseRead).url &&
|
||||
initialSelectedState.has((repo as ApiRepositoryResponseRead).url || '');
|
||||
if (isInitiallySelected) {
|
||||
setModalOpen(true);
|
||||
setReposToRemove([(repo as ApiRepositoryResponseRead).url as string]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return removeSelected(repo);
|
||||
};
|
||||
|
||||
const previousReposNowUnavailable: number = useMemo(() => {
|
||||
if (
|
||||
!previousLoading &&
|
||||
previousSuccess &&
|
||||
previousReposData.length !== initialSelectedState.size &&
|
||||
previousReposData.length < initialSelectedState.size
|
||||
) {
|
||||
const prevSet = new Set(previousReposData.map(({ url }) => url));
|
||||
const itemsToRemove = [...initialSelectedState]
|
||||
.filter((url) => !prevSet.has(url))
|
||||
.map((url) => ({ url })) as ApiRepositoryResponseRead[];
|
||||
removeSelected(itemsToRemove);
|
||||
return initialSelectedState.size - previousReposData.length;
|
||||
}
|
||||
return 0;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
previousLoading,
|
||||
previousSuccess,
|
||||
previousReposData,
|
||||
initialSelectedState,
|
||||
]);
|
||||
|
||||
const handleToggleClick = (
|
||||
toggleType: 'toggle-group-all' | 'toggle-group-selected'
|
||||
) => {
|
||||
setPage(1);
|
||||
setToggleSelected(toggleType);
|
||||
};
|
||||
|
||||
const isRepoDisabled = (
|
||||
repo: ApiRepositoryResponseRead,
|
||||
isSelected: boolean
|
||||
): [boolean, string] => {
|
||||
if (isFetching) {
|
||||
return [true, 'Repository data is still fetching, please wait.'];
|
||||
}
|
||||
|
||||
if (
|
||||
recommendedRepos.length > 0 &&
|
||||
repo.url?.includes('epel') &&
|
||||
isSelected &&
|
||||
(packages.length || groups.length)
|
||||
) {
|
||||
return [
|
||||
true,
|
||||
'This repository was added because of previously recommended packages added to the image.\n' +
|
||||
'To remove the repository, its related packages must be removed first.',
|
||||
];
|
||||
}
|
||||
|
||||
if (repo.status !== 'Valid') {
|
||||
return [
|
||||
true,
|
||||
`Repository can't be selected. The status is still '${repo.status}'.`,
|
||||
];
|
||||
}
|
||||
|
||||
return [false, '']; // Repository is enabled
|
||||
};
|
||||
|
||||
const handlePerPageSelect = (
|
||||
_: React.MouseEvent,
|
||||
newPerPage: number,
|
||||
newPage: number
|
||||
) => {
|
||||
setPerPage(newPerPage);
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleFilterRepositories = (
|
||||
e: React.FormEvent<HTMLInputElement>,
|
||||
value: string
|
||||
) => {
|
||||
e.preventDefault();
|
||||
setPage(1);
|
||||
setFilterValue(value);
|
||||
};
|
||||
|
||||
const onClose = () => setModalOpen(false);
|
||||
|
||||
const handleRemoveAnyway = () => {
|
||||
const itemsToRemove = new Set(reposToRemove);
|
||||
|
||||
dispatch(
|
||||
changeCustomRepositories(
|
||||
customRepositories.filter(
|
||||
({ baseurl }) => !baseurl?.some((url) => itemsToRemove.has(url))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
dispatch(
|
||||
changePayloadRepositories(
|
||||
payloadRepositories.filter(
|
||||
({ baseurl }) => !itemsToRemove.has(baseurl || '')
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
setReposToRemove([]);
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (isError) return <Error />;
|
||||
if (isLoading) return <Loading />;
|
||||
return (
|
||||
<Grid>
|
||||
<Modal
|
||||
titleIconVariant="warning"
|
||||
title="Are you sure?"
|
||||
isOpen={modalOpen}
|
||||
onClose={onClose}
|
||||
variant="small"
|
||||
actions={[
|
||||
<Button key="remove" variant="primary" onClick={handleRemoveAnyway}>
|
||||
Remove anyway
|
||||
</Button>,
|
||||
<Button key="back" variant="link" onClick={onClose}>
|
||||
Back
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
You are removing a previously added repository.
|
||||
<br />
|
||||
We do not recommend removing repositories if you have added packages
|
||||
from them.
|
||||
</Modal>
|
||||
{wizardMode === 'edit' && (
|
||||
<Alert
|
||||
title="Removing previously added repositories may lead to issues with selected packages"
|
||||
variant="warning"
|
||||
isPlain
|
||||
isInline
|
||||
/>
|
||||
)}
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarItem variant="bulk-select">
|
||||
<BulkSelect
|
||||
selected={selected}
|
||||
contentList={contentList}
|
||||
deselectAll={clearSelected}
|
||||
perPage={perPage}
|
||||
handleAddRemove={handleAddRemove}
|
||||
isDisabled={isFetching || (!selected.size && !contentList.length)}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem variant="search-filter">
|
||||
<SearchInput
|
||||
aria-label="Search repositories"
|
||||
onChange={handleFilterRepositories}
|
||||
value={filterValue}
|
||||
onClear={() => setFilterValue('')}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
variant="primary"
|
||||
isInline
|
||||
onClick={() => refresh()}
|
||||
isLoading={isFetching}
|
||||
>
|
||||
{isFetching ? 'Refreshing' : 'Refresh'}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<ToggleGroup aria-label="Filter repositories list">
|
||||
<ToggleGroupItem
|
||||
text="All"
|
||||
aria-label="All repositories"
|
||||
buttonId="toggle-group-all"
|
||||
isSelected={toggleSelected === 'toggle-group-all'}
|
||||
onChange={() => handleToggleClick('toggle-group-all')}
|
||||
/>
|
||||
<ToggleGroupItem
|
||||
text="Selected"
|
||||
isDisabled={!selected.size}
|
||||
aria-label="Selected repositories"
|
||||
buttonId="toggle-group-selected"
|
||||
isSelected={toggleSelected === 'toggle-group-selected'}
|
||||
onChange={() => handleToggleClick('toggle-group-selected')}
|
||||
/>
|
||||
</ToggleGroup>
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
<Panel>
|
||||
<PanelMain>
|
||||
{previousReposNowUnavailable ? (
|
||||
<RepositoryUnavailable quantity={previousReposNowUnavailable} />
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{contentList.length === 0 ? (
|
||||
<Empty hasFilterValue={!!debouncedFilterValue} refetch={refresh} />
|
||||
) : (
|
||||
<Table variant="compact" data-testid="repositories-table">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th aria-label="Selected" />
|
||||
<Th width={45}>Name</Th>
|
||||
<Th width={15}>Architecture</Th>
|
||||
<Th>Version</Th>
|
||||
<Th width={10}>Packages</Th>
|
||||
<Th>Status</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{contentList.map((repo, rowIndex) => {
|
||||
const {
|
||||
url = '',
|
||||
name,
|
||||
status = '',
|
||||
distribution_arch,
|
||||
distribution_versions,
|
||||
package_count,
|
||||
last_introspection_time,
|
||||
failed_introspections_count,
|
||||
} = repo;
|
||||
|
||||
const [isDisabled, disabledReason] = isRepoDisabled(
|
||||
repo,
|
||||
selected.has(url)
|
||||
);
|
||||
|
||||
return (
|
||||
<Tr key={url}>
|
||||
<Td
|
||||
select={{
|
||||
isSelected: selected.has(url),
|
||||
rowIndex: rowIndex,
|
||||
onSelect: (_, isSelecting) =>
|
||||
handleAddRemove(repo, isSelecting),
|
||||
isDisabled: isDisabled,
|
||||
}}
|
||||
title={disabledReason}
|
||||
/>
|
||||
<Td dataLabel={'Name'}>
|
||||
{name}
|
||||
<br />
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={url}
|
||||
>
|
||||
{url}
|
||||
</Button>
|
||||
</Td>
|
||||
<Td dataLabel={'Architecture'}>
|
||||
{distribution_arch || '-'}
|
||||
</Td>
|
||||
<Td dataLabel={'Version'}>
|
||||
{distribution_versions || '-'}
|
||||
</Td>
|
||||
<Td dataLabel={'Packages'}>{package_count || '-'}</Td>
|
||||
<Td dataLabel={'Status'}>
|
||||
<RepositoriesStatus
|
||||
repoStatus={status || 'Unavailable'}
|
||||
repoUrl={url}
|
||||
repoIntrospections={last_introspection_time}
|
||||
repoFailCount={failed_introspections_count}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</PanelMain>
|
||||
</Panel>
|
||||
<Pagination
|
||||
itemCount={count}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={(_, newPage) => setPage(newPage)}
|
||||
onPerPageSelect={handlePerPageSelect}
|
||||
variant={PaginationVariant.bottom}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default Repositories;
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
DescriptionList,
|
||||
DescriptionListDescription,
|
||||
DescriptionListGroup,
|
||||
DescriptionListTerm,
|
||||
Popover,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ExclamationCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
ExternalLinkAltIcon,
|
||||
InProgressIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
|
||||
import { CONTENT_URL } from '../../../../constants';
|
||||
import { ApiRepositoryResponse } from '../../../../store/contentSourcesApi';
|
||||
import {
|
||||
convertStringToDate,
|
||||
timestampToDisplayString,
|
||||
} from '../../../../Utilities/time';
|
||||
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
|
||||
import { betaPath } from '../../utilities/betaPath';
|
||||
|
||||
const getLastIntrospection = (
|
||||
repoIntrospections: RepositoryStatusProps['repoIntrospections']
|
||||
) => {
|
||||
const currentDate = Date.now();
|
||||
const lastIntrospectionDate = convertStringToDate(repoIntrospections);
|
||||
const timeDeltaInSeconds = Math.floor(
|
||||
(currentDate - lastIntrospectionDate) / 1000
|
||||
);
|
||||
|
||||
if (timeDeltaInSeconds <= 60) {
|
||||
return 'A few seconds ago';
|
||||
} else if (timeDeltaInSeconds <= 60 * 60) {
|
||||
return 'A few minutes ago';
|
||||
} else if (timeDeltaInSeconds <= 60 * 60 * 24) {
|
||||
return 'A few hours ago';
|
||||
} else {
|
||||
return timestampToDisplayString(repoIntrospections);
|
||||
}
|
||||
};
|
||||
|
||||
type RepositoryStatusProps = {
|
||||
repoStatus: ApiRepositoryResponse['status'];
|
||||
repoUrl: ApiRepositoryResponse['url'];
|
||||
repoIntrospections: ApiRepositoryResponse['last_introspection_time'];
|
||||
repoFailCount: ApiRepositoryResponse['failed_introspections_count'];
|
||||
};
|
||||
|
||||
const RepositoriesStatus = ({
|
||||
repoStatus,
|
||||
repoUrl,
|
||||
repoIntrospections,
|
||||
repoFailCount,
|
||||
}: RepositoryStatusProps) => {
|
||||
const { isBeta } = useGetEnvironment();
|
||||
if (repoStatus === 'Valid') {
|
||||
return (
|
||||
<>
|
||||
<CheckCircleIcon className="success" /> {repoStatus}
|
||||
</>
|
||||
);
|
||||
} else if (repoStatus === 'Invalid' || repoStatus === 'Unavailable') {
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
position="bottom"
|
||||
minWidth="30rem"
|
||||
bodyContent={
|
||||
<>
|
||||
<Alert
|
||||
variant={repoStatus === 'Invalid' ? 'danger' : 'warning'}
|
||||
title={repoStatus}
|
||||
className="pf-u-pb-sm"
|
||||
isInline
|
||||
isPlain
|
||||
/>
|
||||
<p className="pf-u-pb-md">Cannot fetch {repoUrl}</p>
|
||||
{(repoIntrospections || repoFailCount) && (
|
||||
<>
|
||||
<DescriptionList
|
||||
columnModifier={{
|
||||
default: '2Col',
|
||||
}}
|
||||
>
|
||||
{repoIntrospections && (
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
Last introspection
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{getLastIntrospection(repoIntrospections)}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
)}
|
||||
{repoFailCount && (
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
Failed attempts
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{repoFailCount}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
)}
|
||||
</DescriptionList>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
iconPosition="right"
|
||||
isInline
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
href={betaPath(CONTENT_URL, isBeta())}
|
||||
>
|
||||
Go to Repositories
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button variant="link" className="pf-u-p-0 pf-u-font-size-sm">
|
||||
{repoStatus === 'Invalid' && (
|
||||
<ExclamationCircleIcon className="error" />
|
||||
)}
|
||||
{repoStatus === 'Unavailable' && (
|
||||
<ExclamationTriangleIcon className="expiring" />
|
||||
)}{' '}
|
||||
<span className="failure-button">{repoStatus}</span>
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
} else if (repoStatus === 'Pending') {
|
||||
return (
|
||||
<>
|
||||
<InProgressIcon className="pending" /> {repoStatus}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default RepositoriesStatus;
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Alert, Button } from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import { CONTENT_URL } from '../../../../constants';
|
||||
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
|
||||
import { betaPath } from '../../utilities/betaPath';
|
||||
|
||||
const RepositoryUnavailable = ({ quantity }: { quantity: number }) => {
|
||||
const { isBeta } = useGetEnvironment();
|
||||
return (
|
||||
<Alert
|
||||
variant="warning"
|
||||
title="Previously added custom repository unavailable"
|
||||
isInline
|
||||
>
|
||||
{quantity > 1
|
||||
? `${quantity} repositories that were used to build this image previously are not available.`
|
||||
: 'One repository that was used to build this image previously is not available. '}
|
||||
Address the error found in the last introspection and validate that the
|
||||
repository is still accessible.
|
||||
<br />
|
||||
<br />
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
iconPosition="right"
|
||||
isInline
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
href={betaPath(CONTENT_URL, isBeta())}
|
||||
>
|
||||
Go to Repositories
|
||||
</Button>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default RepositoryUnavailable;
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownToggle,
|
||||
DropdownToggleCheckbox,
|
||||
} from '@patternfly/react-core/deprecated';
|
||||
|
||||
import { ApiRepositoryResponseRead } from '../../../../../store/contentSourcesApi';
|
||||
|
||||
interface BulkSelectProps {
|
||||
selected: Set<string>;
|
||||
contentList: ApiRepositoryResponseRead[];
|
||||
deselectAll: () => void;
|
||||
perPage: number;
|
||||
handleAddRemove: (
|
||||
repo: ApiRepositoryResponseRead | ApiRepositoryResponseRead[],
|
||||
selected: boolean
|
||||
) => void;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
export function BulkSelect({
|
||||
selected,
|
||||
contentList,
|
||||
deselectAll,
|
||||
perPage,
|
||||
handleAddRemove,
|
||||
isDisabled,
|
||||
}: BulkSelectProps) {
|
||||
const [dropdownIsOpen, setDropdownIsOpen] = useState(false);
|
||||
|
||||
const allChecked = !contentList.some(({ url }) => !selected.has(url!));
|
||||
|
||||
const someChecked =
|
||||
allChecked || contentList.some(({ url }) => selected.has(url!));
|
||||
|
||||
const toggleDropdown = () => setDropdownIsOpen(!dropdownIsOpen);
|
||||
|
||||
const handleSelectPage = () => handleAddRemove(contentList, !allChecked);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
id="stacked-example-toggle"
|
||||
isDisabled={isDisabled}
|
||||
splitButtonItems={[
|
||||
<DropdownToggleCheckbox
|
||||
id="example-checkbox-1"
|
||||
key="split-checkbox"
|
||||
aria-label="Select all"
|
||||
isChecked={allChecked || someChecked ? null : false}
|
||||
onClick={handleSelectPage}
|
||||
/>,
|
||||
]}
|
||||
onToggle={toggleDropdown}
|
||||
>
|
||||
{someChecked ? `${selected.size} selected` : null}
|
||||
</DropdownToggle>
|
||||
}
|
||||
isOpen={dropdownIsOpen}
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
key="none"
|
||||
isDisabled={!selected.size}
|
||||
onClick={() => {
|
||||
deselectAll();
|
||||
toggleDropdown();
|
||||
}}
|
||||
>{`Clear all (${selected.size} items)`}</DropdownItem>,
|
||||
<DropdownItem
|
||||
key="page"
|
||||
isDisabled={!contentList.length}
|
||||
onClick={() => {
|
||||
handleSelectPage();
|
||||
toggleDropdown();
|
||||
}}
|
||||
>{`${allChecked ? 'Remove' : 'Select'} page (${
|
||||
perPage > contentList.length ? contentList.length : perPage
|
||||
} items)`}</DropdownItem>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateVariant,
|
||||
EmptyStateHeader,
|
||||
EmptyStateIcon,
|
||||
EmptyStateBody,
|
||||
EmptyStateFooter,
|
||||
Button,
|
||||
} from '@patternfly/react-core';
|
||||
import { RepositoryIcon } from '@patternfly/react-icons';
|
||||
|
||||
import { CONTENT_URL } from '../../../../../constants';
|
||||
import { useGetEnvironment } from '../../../../../Utilities/useGetEnvironment';
|
||||
import { betaPath } from '../../../utilities/betaPath';
|
||||
|
||||
type EmptyProps = {
|
||||
refetch: () => void;
|
||||
hasFilterValue: boolean;
|
||||
};
|
||||
|
||||
export default function Empty({ hasFilterValue, refetch }: EmptyProps) {
|
||||
const { isBeta } = useGetEnvironment();
|
||||
return (
|
||||
<EmptyState variant={EmptyStateVariant.lg} data-testid="empty-state">
|
||||
<EmptyStateHeader
|
||||
titleText={
|
||||
hasFilterValue
|
||||
? 'No matching repositories found'
|
||||
: 'No Custom Repositories'
|
||||
}
|
||||
icon={<EmptyStateIcon icon={RepositoryIcon} />}
|
||||
headingLevel="h4"
|
||||
/>
|
||||
<EmptyStateBody>
|
||||
{hasFilterValue
|
||||
? 'Try another search query or clear the current search value'
|
||||
: `Repositories can be added in the "Repositories" area of the
|
||||
console. Once added, refresh this page to see them.`}
|
||||
</EmptyStateBody>
|
||||
<EmptyStateFooter>
|
||||
<Button
|
||||
variant="primary"
|
||||
component="a"
|
||||
target="_blank"
|
||||
href={betaPath(CONTENT_URL, isBeta())}
|
||||
className="pf-u-mr-sm"
|
||||
>
|
||||
Go to repositories
|
||||
</Button>
|
||||
<Button variant="secondary" isInline onClick={() => refetch()}>
|
||||
Refresh
|
||||
</Button>
|
||||
</EmptyStateFooter>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Alert } from '@patternfly/react-core';
|
||||
|
||||
export const Error = () => {
|
||||
return (
|
||||
<Alert title="Repositories unavailable" variant="danger" isPlain isInline>
|
||||
Repositories cannot be reached, try again later.
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateIcon,
|
||||
Spinner,
|
||||
EmptyStateHeader,
|
||||
Bullseye,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
export const Loading = () => {
|
||||
return (
|
||||
<Bullseye>
|
||||
<EmptyState>
|
||||
<EmptyStateHeader
|
||||
titleText="Loading"
|
||||
icon={<EmptyStateIcon icon={Spinner} />}
|
||||
headingLevel="h4"
|
||||
/>
|
||||
</EmptyState>
|
||||
</Bullseye>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { ApiRepositoryResponseRead } from '../../../../../store/contentSourcesApi';
|
||||
import {
|
||||
CustomRepository,
|
||||
Repository,
|
||||
} from '../../../../../store/imageBuilderApi';
|
||||
|
||||
// Utility function to convert from Content Sources to Image Builder custom repo API schema
|
||||
export const convertSchemaToIBCustomRepo = (
|
||||
repo: ApiRepositoryResponseRead
|
||||
) => {
|
||||
const imageBuilderRepo: CustomRepository = {
|
||||
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 Content Sources to Image Builder payload repo API schema
|
||||
export const convertSchemaToIBPayloadRepo = (
|
||||
repo: ApiRepositoryResponseRead
|
||||
) => {
|
||||
const imageBuilderRepo: Repository = {
|
||||
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;
|
||||
};
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Alert, Button, Form, Text, Title } from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import Repositories from './Repositories';
|
||||
|
||||
import { CONTENT_URL } from '../../../../constants';
|
||||
import { useAppSelector } from '../../../../store/hooks';
|
||||
import {
|
||||
selectPackages,
|
||||
selectRecommendedRepositories,
|
||||
} from '../../../../store/wizardSlice';
|
||||
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
|
||||
import { betaPath } from '../../utilities/betaPath';
|
||||
|
||||
const ManageRepositoriesButton = () => {
|
||||
const { isBeta } = useGetEnvironment();
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
iconPosition="right"
|
||||
isInline
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
href={betaPath(CONTENT_URL, isBeta())}
|
||||
>
|
||||
Create and manage repositories here
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const RepositoriesStep = () => {
|
||||
const packages = useAppSelector(selectPackages);
|
||||
const recommendedRepos = useAppSelector(selectRecommendedRepositories);
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<Title headingLevel="h1" size="xl">
|
||||
Custom repositories
|
||||
</Title>
|
||||
<Text>
|
||||
Select the linked custom repositories from which you can add packages to
|
||||
the image.
|
||||
<br />
|
||||
<ManageRepositoriesButton />
|
||||
</Text>
|
||||
{packages.length && recommendedRepos.length ? (
|
||||
<Alert
|
||||
title="Why can't I remove a selected repository?"
|
||||
variant="info"
|
||||
isInline
|
||||
>
|
||||
EPEL repository cannot be removed, because packages from it were
|
||||
selected. If you wish to remove the repository, please remove
|
||||
following packages on the Packages step:{' '}
|
||||
{packages.map((pkg) => pkg.name).join(', ')}
|
||||
</Alert>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<Repositories />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default RepositoriesStep;
|
||||
Loading…
Add table
Add a link
Reference in a new issue