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:
regexowl 2024-07-16 17:10:37 +02:00 committed by Ondřej Ezr
parent b1e5a8c7c6
commit 4fb37c187e
93 changed files with 20 additions and 22 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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>,
]}
/>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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;
};

View file

@ -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;