Wizard: add support for content templates
This commit is contained in:
parent
aa545382e8
commit
0238c04dfe
21 changed files with 1160 additions and 225 deletions
|
|
@ -31,15 +31,21 @@ import {
|
|||
import RepositoriesStatus from './RepositoriesStatus';
|
||||
import RepositoryUnavailable from './RepositoryUnavailable';
|
||||
|
||||
import { ContentOrigin, PAGINATION_COUNT } from '../../../../constants';
|
||||
import {
|
||||
ContentOrigin,
|
||||
PAGINATION_COUNT,
|
||||
TEMPLATES_URL,
|
||||
} from '../../../../constants';
|
||||
import {
|
||||
ApiRepositoryResponseRead,
|
||||
useListRepositoriesQuery,
|
||||
useGetTemplateQuery,
|
||||
} from '../../../../store/contentSourcesApi';
|
||||
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
|
||||
import {
|
||||
changeCustomRepositories,
|
||||
changePayloadRepositories,
|
||||
changeRedHatRepositories,
|
||||
selectArchitecture,
|
||||
selectCustomRepositories,
|
||||
selectDistribution,
|
||||
|
|
@ -47,6 +53,7 @@ import {
|
|||
selectPackages,
|
||||
selectPayloadRepositories,
|
||||
selectRecommendedRepositories,
|
||||
selectTemplate,
|
||||
selectUseLatest,
|
||||
selectWizardMode,
|
||||
} from '../../../../store/wizardSlice';
|
||||
|
|
@ -66,6 +73,7 @@ const Repositories = () => {
|
|||
|
||||
const payloadRepositories = useAppSelector(selectPayloadRepositories);
|
||||
const recommendedRepos = useAppSelector(selectRecommendedRepositories);
|
||||
const templateUuid = useAppSelector(selectTemplate);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [reposToRemove, setReposToRemove] = useState<string[]>([]);
|
||||
|
|
@ -75,6 +83,7 @@ const Repositories = () => {
|
|||
const [toggleSelected, setToggleSelected] = useState<
|
||||
'toggle-group-all' | 'toggle-group-selected'
|
||||
>('toggle-group-all');
|
||||
const [isTemplateSelected, setIsTemplateSelected] = useState(false);
|
||||
|
||||
const debouncedFilterValue = useDebounce(filterValue);
|
||||
|
||||
|
|
@ -107,7 +116,7 @@ const Repositories = () => {
|
|||
offset: 0,
|
||||
uuid: [...initialSelectedState].join(','),
|
||||
},
|
||||
{ refetchOnMountOrArgChange: false }
|
||||
{ refetchOnMountOrArgChange: false, skip: isTemplateSelected }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -116,6 +125,10 @@ const Repositories = () => {
|
|||
}
|
||||
}, [selected, toggleSelected]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsTemplateSelected(templateUuid !== '');
|
||||
}, [templateUuid]);
|
||||
|
||||
const {
|
||||
data: { data: contentList = [], meta: { count } = { count: 0 } } = {},
|
||||
isError,
|
||||
|
|
@ -136,7 +149,7 @@ const Repositories = () => {
|
|||
? [...selected].join(',')
|
||||
: '',
|
||||
},
|
||||
{ refetchOnMountOrArgChange: 60 }
|
||||
{ refetchOnMountOrArgChange: 60, skip: isTemplateSelected }
|
||||
);
|
||||
|
||||
const refresh = () => {
|
||||
|
|
@ -389,207 +402,400 @@ const Repositories = () => {
|
|||
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) ||
|
||||
contentList.every(
|
||||
(repo) =>
|
||||
repo.uuid &&
|
||||
isRepoDisabled(repo, selected.has(repo.uuid))[0]
|
||||
)
|
||||
}
|
||||
/>
|
||||
</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 {
|
||||
uuid = '',
|
||||
url = '',
|
||||
name,
|
||||
status = '',
|
||||
origin = '',
|
||||
distribution_arch,
|
||||
distribution_versions,
|
||||
package_count,
|
||||
last_introspection_time,
|
||||
failed_introspections_count,
|
||||
} = repo;
|
||||
|
||||
const [isDisabled, disabledReason] = isRepoDisabled(
|
||||
repo,
|
||||
selected.has(uuid)
|
||||
);
|
||||
|
||||
return (
|
||||
<Tr
|
||||
key={`${uuid}-${rowIndex}`}
|
||||
data-testid="repositories-row"
|
||||
>
|
||||
<Td
|
||||
select={{
|
||||
isSelected: selected.has(uuid),
|
||||
rowIndex: rowIndex,
|
||||
onSelect: (_, isSelecting) =>
|
||||
handleAddRemove(repo, isSelecting),
|
||||
isDisabled: isDisabled,
|
||||
}}
|
||||
title={disabledReason}
|
||||
/>
|
||||
<Td dataLabel={'Name'}>
|
||||
{name}
|
||||
{origin === ContentOrigin.UPLOAD ? (
|
||||
<UploadRepositoryLabel />
|
||||
) : (
|
||||
<>
|
||||
<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 ?? PAGINATION_COUNT}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={(_, newPage) => setPage(newPage)}
|
||||
onPerPageSelect={handlePerPageSelect}
|
||||
variant={PaginationVariant.bottom}
|
||||
/>
|
||||
</Grid>
|
||||
const {
|
||||
data: selectedTemplateData,
|
||||
isError: isTemplateError,
|
||||
isLoading: isTemplateLoading,
|
||||
} = useGetTemplateQuery(
|
||||
{
|
||||
uuid: templateUuid,
|
||||
},
|
||||
{ refetchOnMountOrArgChange: true, skip: templateUuid === '' }
|
||||
);
|
||||
|
||||
const {
|
||||
data: {
|
||||
data: reposInTemplate = [],
|
||||
meta: { count: reposInTemplateCount } = { count: 0 },
|
||||
} = {},
|
||||
isError: isReposInTemplateError,
|
||||
isLoading: isReposInTemplateLoading,
|
||||
} = useListRepositoriesQuery(
|
||||
{
|
||||
contentType: 'rpm',
|
||||
limit: perPage,
|
||||
offset: perPage * (page - 1),
|
||||
uuid:
|
||||
selectedTemplateData && selectedTemplateData.repository_uuids
|
||||
? selectedTemplateData.repository_uuids?.join(',')
|
||||
: '',
|
||||
},
|
||||
{ refetchOnMountOrArgChange: true, skip: !isTemplateSelected }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTemplateSelected && reposInTemplate.length > 0) {
|
||||
const customReposInTemplate = reposInTemplate.filter(
|
||||
(repo) => repo.origin !== ContentOrigin.REDHAT
|
||||
);
|
||||
const redHatReposInTemplate = reposInTemplate.filter(
|
||||
(repo) => repo.origin === ContentOrigin.REDHAT
|
||||
);
|
||||
|
||||
dispatch(
|
||||
changeCustomRepositories(
|
||||
customReposInTemplate.map((repo) =>
|
||||
convertSchemaToIBCustomRepo(repo!)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
dispatch(
|
||||
changePayloadRepositories(
|
||||
customReposInTemplate.map((repo) =>
|
||||
convertSchemaToIBPayloadRepo(repo!)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
dispatch(
|
||||
changeRedHatRepositories(
|
||||
redHatReposInTemplate.map((repo) =>
|
||||
convertSchemaToIBPayloadRepo(repo!)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [templateUuid, reposInTemplate]);
|
||||
|
||||
if (isError || isTemplateError || isReposInTemplateError) return <Error />;
|
||||
if (isLoading || isTemplateLoading || isReposInTemplateLoading)
|
||||
return <Loading />;
|
||||
if (!isTemplateSelected) {
|
||||
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) ||
|
||||
contentList.every(
|
||||
(repo) =>
|
||||
repo.uuid &&
|
||||
isRepoDisabled(repo, selected.has(repo.uuid))[0]
|
||||
)
|
||||
}
|
||||
/>
|
||||
</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 {
|
||||
uuid = '',
|
||||
url = '',
|
||||
name,
|
||||
status = '',
|
||||
origin = '',
|
||||
distribution_arch,
|
||||
distribution_versions,
|
||||
package_count,
|
||||
last_introspection_time,
|
||||
failed_introspections_count,
|
||||
} = repo;
|
||||
|
||||
const [isDisabled, disabledReason] = isRepoDisabled(
|
||||
repo,
|
||||
selected.has(uuid)
|
||||
);
|
||||
|
||||
return (
|
||||
<Tr
|
||||
key={`${uuid}-${rowIndex}`}
|
||||
data-testid="repositories-row"
|
||||
>
|
||||
<Td
|
||||
select={{
|
||||
isSelected: selected.has(uuid),
|
||||
rowIndex: rowIndex,
|
||||
onSelect: (_, isSelecting) =>
|
||||
handleAddRemove(repo, isSelecting),
|
||||
isDisabled: isDisabled,
|
||||
}}
|
||||
title={disabledReason}
|
||||
/>
|
||||
<Td dataLabel={'Name'}>
|
||||
{name}
|
||||
{origin === ContentOrigin.UPLOAD ? (
|
||||
<UploadRepositoryLabel />
|
||||
) : (
|
||||
<>
|
||||
<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 ?? PAGINATION_COUNT}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={(_, newPage) => setPage(newPage)}
|
||||
onPerPageSelect={handlePerPageSelect}
|
||||
variant={PaginationVariant.bottom}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Alert
|
||||
variant="info"
|
||||
isInline
|
||||
title={
|
||||
<>
|
||||
The repositories seen below are from the selected content template
|
||||
and have been added automatically. If you do not want these
|
||||
repositories in your image, you can{' '}
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
isInline
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
href={`${TEMPLATES_URL}/${templateUuid}/edit`}
|
||||
>
|
||||
{' '}
|
||||
modify this content template
|
||||
</Button>{' '}
|
||||
or choose another snapshot option.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Grid>
|
||||
<Panel>
|
||||
<PanelMain>
|
||||
<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>
|
||||
{reposInTemplate.map((repo, rowIndex) => {
|
||||
const {
|
||||
uuid = '',
|
||||
url = '',
|
||||
name,
|
||||
status = '',
|
||||
origin = '',
|
||||
distribution_arch,
|
||||
distribution_versions,
|
||||
package_count,
|
||||
last_introspection_time,
|
||||
failed_introspections_count,
|
||||
} = repo;
|
||||
|
||||
return (
|
||||
<Tr
|
||||
key={`${uuid}-${rowIndex}`}
|
||||
data-testid="repositories-row"
|
||||
>
|
||||
<Td
|
||||
select={{
|
||||
isSelected: true,
|
||||
rowIndex: rowIndex,
|
||||
isDisabled: true,
|
||||
}}
|
||||
/>
|
||||
<Td dataLabel={'Name'}>
|
||||
{name}
|
||||
{origin === ContentOrigin.UPLOAD ? (
|
||||
<UploadRepositoryLabel />
|
||||
) : (
|
||||
<>
|
||||
<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={reposInTemplateCount ?? PAGINATION_COUNT}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={(_, newPage) => setPage(newPage)}
|
||||
onPerPageSelect={handlePerPageSelect}
|
||||
variant={PaginationVariant.bottom}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Repositories;
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ type EmptyProps = {
|
|||
hasFilterValue: boolean;
|
||||
};
|
||||
|
||||
export default function Empty({ hasFilterValue, refetch }: EmptyProps) {
|
||||
const Empty = ({ hasFilterValue, refetch }: EmptyProps) => {
|
||||
return (
|
||||
<EmptyState variant={EmptyStateVariant.lg} data-testid="empty-state">
|
||||
<EmptyStateHeader
|
||||
titleText={
|
||||
hasFilterValue
|
||||
? 'No matching repositories found'
|
||||
: 'No Custom Repositories'
|
||||
: 'No custom repositories'
|
||||
}
|
||||
icon={<EmptyStateIcon icon={RepositoryIcon} />}
|
||||
headingLevel="h4"
|
||||
|
|
@ -52,4 +52,6 @@ export default function Empty({ hasFilterValue, refetch }: EmptyProps) {
|
|||
</EmptyStateFooter>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Empty;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
|
|||
import { ContentOrigin } from '../../../../constants';
|
||||
import {
|
||||
ApiSnapshotForDate,
|
||||
useGetTemplateQuery,
|
||||
useListRepositoriesQuery,
|
||||
} from '../../../../store/contentSourcesApi';
|
||||
import { useAppSelector } from '../../../../store/hooks';
|
||||
|
|
@ -24,6 +25,7 @@ import {
|
|||
selectGroups,
|
||||
selectPartitions,
|
||||
selectRecommendedRepositories,
|
||||
selectTemplate,
|
||||
} from '../../../../store/wizardSlice';
|
||||
import PackageInfoNotAvailablePopover from '../Packages/components/PackageInfoNotAvailablePopover';
|
||||
|
||||
|
|
@ -37,7 +39,7 @@ const RepoName = ({ repoUuid }: repoPropType) => {
|
|||
// @ts-ignore if repoUrl is undefined the query is going to get skipped, so it's safe to ignore the linter here
|
||||
uuid: repoUuid ?? '',
|
||||
contentType: 'rpm',
|
||||
origin: ContentOrigin.CUSTOM,
|
||||
origin: ContentOrigin.ALL,
|
||||
},
|
||||
{ skip: !repoUuid }
|
||||
);
|
||||
|
|
@ -127,8 +129,22 @@ export const SnapshotTable = ({
|
|||
}: {
|
||||
snapshotForDate: ApiSnapshotForDate[];
|
||||
}) => {
|
||||
const template = useAppSelector(selectTemplate);
|
||||
|
||||
const { data: templateData } = useGetTemplateQuery(
|
||||
{
|
||||
uuid: template,
|
||||
},
|
||||
{ refetchOnMountOrArgChange: true, skip: template === '' }
|
||||
);
|
||||
|
||||
const { data, isSuccess, isLoading, isError } = useListRepositoriesQuery({
|
||||
uuid: snapshotForDate.map(({ repository_uuid }) => repository_uuid).join(),
|
||||
uuid:
|
||||
snapshotForDate.length > 0
|
||||
? snapshotForDate.map(({ repository_uuid }) => repository_uuid).join()
|
||||
: template && templateData && templateData.repository_uuids
|
||||
? templateData.repository_uuids.join(',')
|
||||
: '',
|
||||
origin: ContentOrigin.REDHAT + ',' + ContentOrigin.CUSTOM, // Make sure to show both redhat and external
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ import {
|
|||
targetOptions,
|
||||
UNIT_GIB,
|
||||
} from '../../../../constants';
|
||||
import { useListSnapshotsByDateMutation } from '../../../../store/contentSourcesApi';
|
||||
import {
|
||||
useListSnapshotsByDateMutation,
|
||||
useGetTemplateQuery,
|
||||
} from '../../../../store/contentSourcesApi';
|
||||
import { useAppSelector } from '../../../../store/hooks';
|
||||
import { useGetSourceListQuery } from '../../../../store/provisioningApi';
|
||||
import { useShowActivationKeyQuery } from '../../../../store/rhsmApi';
|
||||
|
|
@ -76,6 +79,8 @@ import {
|
|||
selectFirewall,
|
||||
selectServices,
|
||||
selectUsers,
|
||||
selectTemplate,
|
||||
selectRedHatRepositories,
|
||||
} from '../../../../store/wizardSlice';
|
||||
import { toMonthAndYear, yyyyMMddFormat } from '../../../../Utilities/time';
|
||||
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
|
||||
|
|
@ -444,6 +449,8 @@ export const ContentList = () => {
|
|||
const recommendedRepositories = useAppSelector(selectRecommendedRepositories);
|
||||
const snapshotDate = useAppSelector(selectSnapshotDate);
|
||||
const useLatest = useAppSelector(selectUseLatest);
|
||||
const template = useAppSelector(selectTemplate);
|
||||
const redHatRepositories = useAppSelector(selectRedHatRepositories);
|
||||
|
||||
const customAndRecommendedRepositoryUUIDS = useMemo(
|
||||
() =>
|
||||
|
|
@ -458,10 +465,14 @@ export const ContentList = () => {
|
|||
useListSnapshotsByDateMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!snapshotDate && !useLatest) return;
|
||||
|
||||
listSnapshotsByDate({
|
||||
apiListSnapshotByDateRequest: {
|
||||
repository_uuids: customAndRecommendedRepositoryUUIDS,
|
||||
date: useLatest ? yyyyMMddFormat(new Date()) : snapshotDate,
|
||||
date: useLatest
|
||||
? yyyyMMddFormat(new Date()) + 'T00:00:00Z'
|
||||
: snapshotDate,
|
||||
},
|
||||
});
|
||||
}, [
|
||||
|
|
@ -476,22 +487,33 @@ export const ContentList = () => {
|
|||
);
|
||||
|
||||
const noRepositoriesSelected =
|
||||
customAndRecommendedRepositoryUUIDS.length === 0;
|
||||
customAndRecommendedRepositoryUUIDS.length === 0 &&
|
||||
redHatRepositories.length === 0;
|
||||
|
||||
const hasSnapshotDateAfter = data?.data?.some(({ is_after }) => is_after);
|
||||
|
||||
const { data: templateData, isLoading: isTemplateLoading } =
|
||||
useGetTemplateQuery(
|
||||
{
|
||||
uuid: template,
|
||||
},
|
||||
{ refetchOnMountOrArgChange: true, skip: template === '' }
|
||||
);
|
||||
|
||||
const snapshottingText = useMemo(() => {
|
||||
switch (true) {
|
||||
case isLoading:
|
||||
case isLoading || isTemplateLoading:
|
||||
return '';
|
||||
case useLatest:
|
||||
return 'Use latest';
|
||||
case !!snapshotDate:
|
||||
return `State as of ${yyyyMMddFormat(new Date(snapshotDate))}`;
|
||||
case !!template:
|
||||
return `Use a content template: ${templateData?.name}`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}, [isLoading, useLatest, snapshotDate]);
|
||||
}, [isLoading, isTemplateLoading, useLatest, snapshotDate, template]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -513,6 +535,8 @@ export const ContentList = () => {
|
|||
headerContent={
|
||||
useLatest
|
||||
? 'Use the latest repository content'
|
||||
: template
|
||||
? 'Use content from the content template'
|
||||
: `Repositories as of ${yyyyMMddFormat(
|
||||
new Date(snapshotDate)
|
||||
)}`
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -11,14 +11,19 @@ import {
|
|||
Title,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import Templates from './components/Templates';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
|
||||
import {
|
||||
selectSnapshotDate,
|
||||
selectUseLatest,
|
||||
changeUseLatest,
|
||||
changeSnapshotDate,
|
||||
changeTemplate,
|
||||
selectUseLatest,
|
||||
selectTemplate,
|
||||
} from '../../../../store/wizardSlice';
|
||||
import { yyyyMMddFormat } from '../../../../Utilities/time';
|
||||
import { useFlag } from '../../../../Utilities/useGetEnvironment';
|
||||
import { isSnapshotDateValid } from '../../validators';
|
||||
|
||||
export default function Snapshot() {
|
||||
|
|
@ -26,6 +31,34 @@ export default function Snapshot() {
|
|||
const snapshotDate = useAppSelector(selectSnapshotDate);
|
||||
|
||||
const useLatest = useAppSelector(selectUseLatest);
|
||||
const templateUuid = useAppSelector(selectTemplate);
|
||||
const [selectedOption, setSelectedOption] = useState<
|
||||
'latest' | 'snapshotDate' | 'template'
|
||||
>(useLatest ? 'latest' : templateUuid ? 'template' : 'snapshotDate');
|
||||
|
||||
const isTemplatesEnabled = useFlag('image-builder.templates.enabled');
|
||||
|
||||
const handleOptionChange = (
|
||||
option: 'latest' | 'snapshotDate' | 'template'
|
||||
): void => {
|
||||
setSelectedOption(option);
|
||||
switch (option) {
|
||||
case 'latest':
|
||||
dispatch(changeUseLatest(true));
|
||||
dispatch(changeTemplate(''));
|
||||
dispatch(changeSnapshotDate(''));
|
||||
break;
|
||||
case 'snapshotDate':
|
||||
dispatch(changeUseLatest(false));
|
||||
dispatch(changeTemplate(''));
|
||||
break;
|
||||
case 'template':
|
||||
dispatch(changeUseLatest(false));
|
||||
dispatch(changeSnapshotDate(''));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormGroup>
|
||||
|
|
@ -35,8 +68,8 @@ export default function Snapshot() {
|
|||
name="use-latest-snapshot"
|
||||
label="Disable repeatable build"
|
||||
description="Use the newest repository content available when building this image"
|
||||
isChecked={useLatest}
|
||||
onChange={() => !useLatest && dispatch(changeUseLatest(true))}
|
||||
isChecked={selectedOption === 'latest'}
|
||||
onChange={() => handleOptionChange('latest')}
|
||||
/>
|
||||
<Radio
|
||||
id="use snapshot date radio"
|
||||
|
|
@ -44,11 +77,25 @@ export default function Snapshot() {
|
|||
name="use-snapshot-date"
|
||||
label="Enable repeatable build"
|
||||
description="Build this image with the repository content of a selected date"
|
||||
isChecked={!useLatest}
|
||||
onChange={() => useLatest && dispatch(changeUseLatest(false))}
|
||||
isChecked={selectedOption === 'snapshotDate'}
|
||||
onChange={() => handleOptionChange('snapshotDate')}
|
||||
/>
|
||||
{isTemplatesEnabled ? (
|
||||
<Radio
|
||||
id="use content template radio"
|
||||
ouiaId="use-content-template-radio"
|
||||
name="use-content-template"
|
||||
label="Use a content template"
|
||||
description="Select a content template and build this image with repository snapshots included in that template"
|
||||
isChecked={selectedOption === 'template'}
|
||||
onChange={() => handleOptionChange('template')}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</FormGroup>
|
||||
{useLatest ? (
|
||||
|
||||
{selectedOption === 'latest' ? (
|
||||
<>
|
||||
<Title headingLevel="h1" size="xl">
|
||||
Use latest content
|
||||
|
|
@ -60,7 +107,7 @@ export default function Snapshot() {
|
|||
</Text>
|
||||
</Grid>
|
||||
</>
|
||||
) : (
|
||||
) : selectedOption === 'snapshotDate' ? (
|
||||
<>
|
||||
<Title headingLevel="h1" size="xl">
|
||||
Use a snapshot
|
||||
|
|
@ -110,6 +157,15 @@ export default function Snapshot() {
|
|||
</Text>
|
||||
</Grid>
|
||||
</>
|
||||
) : isTemplatesEnabled && selectedOption === 'template' ? (
|
||||
<>
|
||||
<Title headingLevel="h1" size="xl">
|
||||
Use a content template
|
||||
</Title>
|
||||
<Templates />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,155 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Grid,
|
||||
Pagination,
|
||||
PaginationVariant,
|
||||
Panel,
|
||||
PanelMain,
|
||||
} from '@patternfly/react-core';
|
||||
import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
|
||||
|
||||
import TemplatesEmpty from './TemplatesEmpty';
|
||||
|
||||
import { PAGINATION_COUNT } from '../../../../../constants';
|
||||
import { useListTemplatesQuery } from '../../../../../store/contentSourcesApi';
|
||||
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
|
||||
import {
|
||||
selectArchitecture,
|
||||
selectDistribution,
|
||||
selectTemplate,
|
||||
changeTemplate,
|
||||
} from '../../../../../store/wizardSlice';
|
||||
import { releaseToVersion } from '../../../../../Utilities/releaseToVersion';
|
||||
import { Error } from '../../Repositories/components/Error';
|
||||
import { Loading } from '../../Repositories/components/Loading';
|
||||
|
||||
const Templates = () => {
|
||||
const arch = useAppSelector(selectArchitecture);
|
||||
const distribution = useAppSelector(selectDistribution);
|
||||
const version = releaseToVersion(distribution);
|
||||
const [perPage, setPerPage] = useState(10);
|
||||
const [page, setPage] = useState(1);
|
||||
const dispatch = useAppDispatch();
|
||||
const templateUuid = useAppSelector(selectTemplate);
|
||||
|
||||
const {
|
||||
data: {
|
||||
data: templateList = [],
|
||||
meta: { count: templateCount } = { count: 0 },
|
||||
} = {},
|
||||
isError,
|
||||
isFetching,
|
||||
isLoading,
|
||||
refetch: refetchTemplates,
|
||||
} = useListTemplatesQuery(
|
||||
{
|
||||
arch: arch,
|
||||
version: version,
|
||||
limit: perPage,
|
||||
offset: perPage * (page - 1),
|
||||
},
|
||||
{ refetchOnMountOrArgChange: 60 }
|
||||
);
|
||||
|
||||
const handleRowSelect = (templateUuid: string | undefined): void => {
|
||||
if (templateUuid) {
|
||||
dispatch(changeTemplate(templateUuid));
|
||||
}
|
||||
};
|
||||
|
||||
const handlePerPageSelect = (
|
||||
_: React.MouseEvent,
|
||||
newPerPage: number,
|
||||
newPage: number
|
||||
) => {
|
||||
setPerPage(newPerPage);
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
refetchTemplates();
|
||||
};
|
||||
|
||||
if (isError) return <Error />;
|
||||
if (isLoading) return <Loading />;
|
||||
return (
|
||||
<Grid>
|
||||
<Panel>
|
||||
{templateList.length > 0 ? (
|
||||
<Button
|
||||
variant="primary"
|
||||
isInline
|
||||
onClick={() => refresh()}
|
||||
isLoading={isFetching}
|
||||
>
|
||||
{isFetching ? 'Refreshing' : 'Refresh'}
|
||||
</Button>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<PanelMain>
|
||||
{templateList.length === 0 ? (
|
||||
<TemplatesEmpty refetch={refresh} />
|
||||
) : (
|
||||
<>
|
||||
<Pagination
|
||||
itemCount={templateCount ?? PAGINATION_COUNT}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={(_, newPage) => setPage(newPage)}
|
||||
onPerPageSelect={handlePerPageSelect}
|
||||
isCompact
|
||||
/>
|
||||
<Table variant="compact" data-testid="templates-table">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th aria-label="Selected" />
|
||||
<Th width={15}>Name</Th>
|
||||
<Th width={50}>Description</Th>
|
||||
<Th width={15}>Snapshot date</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{templateList.map((template, rowIndex) => {
|
||||
const { uuid, name, description, date, use_latest } =
|
||||
template;
|
||||
|
||||
return (
|
||||
<Tr key={uuid}>
|
||||
<Td
|
||||
select={{
|
||||
variant: 'radio',
|
||||
isSelected: uuid === templateUuid,
|
||||
rowIndex: rowIndex,
|
||||
onSelect: () => handleRowSelect(uuid),
|
||||
}}
|
||||
/>
|
||||
<Td dataLabel={'Name'}>{name}</Td>
|
||||
<Td dataLabel={'Description'}>{description}</Td>
|
||||
<Td dataLabel={'Snapshot date'}>
|
||||
{use_latest ? 'Use latest' : date?.split('T')[0]}
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<Pagination
|
||||
itemCount={templateCount ?? PAGINATION_COUNT}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={(_, newPage) => setPage(newPage)}
|
||||
onPerPageSelect={handlePerPageSelect}
|
||||
variant={PaginationVariant.bottom}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</PanelMain>
|
||||
</Panel>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default Templates;
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
EmptyState,
|
||||
EmptyStateVariant,
|
||||
EmptyStateHeader,
|
||||
EmptyStateBody,
|
||||
EmptyStateFooter,
|
||||
Button,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import { TEMPLATES_URL } from '../../../../../constants';
|
||||
|
||||
type TemplatesEmptyProps = {
|
||||
refetch: () => void;
|
||||
};
|
||||
|
||||
const TemplatesEmpty = ({ refetch }: TemplatesEmptyProps) => {
|
||||
const GoToTemplatesButton = () => {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
href={TEMPLATES_URL}
|
||||
>
|
||||
Go to content templates
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EmptyState variant={EmptyStateVariant.lg} data-testid="empty-state">
|
||||
<EmptyStateHeader titleText={'No content templates'} headingLevel="h4" />
|
||||
<EmptyStateBody>
|
||||
{`Content templates can be added in the "Templates" area of the
|
||||
console.`}
|
||||
</EmptyStateBody>
|
||||
<EmptyStateFooter>
|
||||
<GoToTemplatesButton />
|
||||
<Button variant="secondary" isInline onClick={() => refetch()}>
|
||||
Refresh
|
||||
</Button>
|
||||
</EmptyStateFooter>
|
||||
</EmptyState>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplatesEmpty;
|
||||
Loading…
Add table
Add a link
Reference in a new issue