From 0238c04dfecf36b69b574f2f98766e39fa5ea4c5 Mon Sep 17 00:00:00 2001 From: Bryttanie House Date: Wed, 26 Feb 2025 15:02:50 -0500 Subject: [PATCH] Wizard: add support for content templates --- api/config/contentSources.ts | 2 + api/schema/imageBuilder.yaml | 5 + .../steps/Repositories/Repositories.tsx | 612 ++++++++++++------ .../steps/Repositories/components/Empty.tsx | 8 +- .../steps/Review/ReviewStepTables.tsx | 20 +- .../steps/Review/ReviewStepTextLists.tsx | 34 +- .../steps/Snapshot/Snapshot.tsx | 72 ++- .../steps/Snapshot/components/Templates.tsx | 155 +++++ .../Snapshot/components/TemplatesEmpty.tsx | 51 ++ .../utilities/requestMapper.ts | 9 +- .../utilities/useValidation.tsx | 4 +- src/Utilities/useGetEnvironment.ts | 1 + src/constants.ts | 1 + src/store/contentSourcesApi.ts | 2 + src/store/service/contentSourcesApi.ts | 131 ++++ src/store/service/imageBuilderApi.ts | 4 + src/store/wizardSlice.ts | 21 + .../steps/Snapshot/Snapshot.test.tsx | 39 +- src/test/fixtures/templates.ts | 195 ++++++ src/test/mocks/handlers.js | 17 + src/test/setup.ts | 2 + 21 files changed, 1160 insertions(+), 225 deletions(-) create mode 100644 src/Components/CreateImageWizard/steps/Snapshot/components/Templates.tsx create mode 100644 src/Components/CreateImageWizard/steps/Snapshot/components/TemplatesEmpty.tsx create mode 100644 src/test/fixtures/templates.ts diff --git a/api/config/contentSources.ts b/api/config/contentSources.ts index 87707d14..f0e48671 100644 --- a/api/config/contentSources.ts +++ b/api/config/contentSources.ts @@ -17,6 +17,8 @@ const config: ConfigFile = { 'listFeatures', 'listSnapshotsByDate', 'bulkImportRepositories', + 'listTemplates', + 'getTemplate', ], }; diff --git a/api/schema/imageBuilder.yaml b/api/schema/imageBuilder.yaml index 9386dd26..6483e324 100644 --- a/api/schema/imageBuilder.yaml +++ b/api/schema/imageBuilder.yaml @@ -1218,6 +1218,11 @@ components: be used. If no snapshots made before the specified date can be found, the snapshot closest to, but after the specified date will be used. If no snapshots can be found at all, the request will fail. The format must be YYYY-MM-DD (ISO 8601 extended). + content_template: + type: string + description: | + ID of the content template. A content template and snapshot date cannot both be specified. + If a content template is specified, the snapshot date used will be the one from the content template. ImageTypes: type: string enum: diff --git a/src/Components/CreateImageWizard/steps/Repositories/Repositories.tsx b/src/Components/CreateImageWizard/steps/Repositories/Repositories.tsx index 8ceef334..f47a7a6b 100644 --- a/src/Components/CreateImageWizard/steps/Repositories/Repositories.tsx +++ b/src/Components/CreateImageWizard/steps/Repositories/Repositories.tsx @@ -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([]); @@ -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 ; - if (isLoading) return ; - return ( - - - Remove anyway - , - , - ]} - > - You are removing a previously added repository. -
- We do not recommend removing repositories if you have added packages - from them. -
- {wizardMode === 'edit' && ( - - )} - - - - - repo.uuid && - isRepoDisabled(repo, selected.has(repo.uuid))[0] - ) - } - /> - - - setFilterValue('')} - /> - - - - - - - handleToggleClick('toggle-group-all')} - /> - handleToggleClick('toggle-group-selected')} - /> - - - - - - - {previousReposNowUnavailable ? ( - - ) : ( - '' - )} - {contentList.length === 0 ? ( - - ) : ( - - - - - - - - - - - - {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 ( - - - - - - - - ); - })} - -
- NameArchitectureVersionPackagesStatus
- handleAddRemove(repo, isSelecting), - isDisabled: isDisabled, - }} - title={disabledReason} - /> - - {name} - {origin === ContentOrigin.UPLOAD ? ( - - ) : ( - <> -
- - - )} -
- {distribution_arch || '-'} - - {distribution_versions || '-'} - {package_count || '-'} - -
- )} -
-
- setPage(newPage)} - onPerPageSelect={handlePerPageSelect} - variant={PaginationVariant.bottom} - /> -
+ 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 ; + if (isLoading || isTemplateLoading || isReposInTemplateLoading) + return ; + if (!isTemplateSelected) { + return ( + + + Remove anyway + , + , + ]} + > + You are removing a previously added repository. +
+ We do not recommend removing repositories if you have added packages + from them. +
+ {wizardMode === 'edit' && ( + + )} + + + + + repo.uuid && + isRepoDisabled(repo, selected.has(repo.uuid))[0] + ) + } + /> + + + setFilterValue('')} + /> + + + + + + + handleToggleClick('toggle-group-all')} + /> + handleToggleClick('toggle-group-selected')} + /> + + + + + + + {previousReposNowUnavailable ? ( + + ) : ( + '' + )} + {contentList.length === 0 ? ( + + ) : ( + + + + + + + + + + + + {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 ( + + + + + + + + ); + })} + +
+ NameArchitectureVersionPackagesStatus
+ handleAddRemove(repo, isSelecting), + isDisabled: isDisabled, + }} + title={disabledReason} + /> + + {name} + {origin === ContentOrigin.UPLOAD ? ( + + ) : ( + <> +
+ + + )} +
+ {distribution_arch || '-'} + + {distribution_versions || '-'} + {package_count || '-'} + +
+ )} +
+
+ setPage(newPage)} + onPerPageSelect={handlePerPageSelect} + variant={PaginationVariant.bottom} + /> +
+ ); + } else { + return ( + <> + + 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{' '} + {' '} + or choose another snapshot option. + + } + /> + + + + + + + + + + + + + + + {reposInTemplate.map((repo, rowIndex) => { + const { + uuid = '', + url = '', + name, + status = '', + origin = '', + distribution_arch, + distribution_versions, + package_count, + last_introspection_time, + failed_introspections_count, + } = repo; + + return ( + + + + + + + + ); + })} + +
+ NameArchitectureVersionPackagesStatus
+ + {name} + {origin === ContentOrigin.UPLOAD ? ( + + ) : ( + <> +
+ + + )} +
+ {distribution_arch || '-'} + + {distribution_versions || '-'} + {package_count || '-'} + +
+
+
+ setPage(newPage)} + onPerPageSelect={handlePerPageSelect} + variant={PaginationVariant.bottom} + /> +
+ + ); + } }; export default Repositories; diff --git a/src/Components/CreateImageWizard/steps/Repositories/components/Empty.tsx b/src/Components/CreateImageWizard/steps/Repositories/components/Empty.tsx index 03ca4c8b..e228475f 100644 --- a/src/Components/CreateImageWizard/steps/Repositories/components/Empty.tsx +++ b/src/Components/CreateImageWizard/steps/Repositories/components/Empty.tsx @@ -18,14 +18,14 @@ type EmptyProps = { hasFilterValue: boolean; }; -export default function Empty({ hasFilterValue, refetch }: EmptyProps) { +const Empty = ({ hasFilterValue, refetch }: EmptyProps) => { return ( } headingLevel="h4" @@ -52,4 +52,6 @@ export default function Empty({ hasFilterValue, refetch }: EmptyProps) { ); -} +}; + +export default Empty; diff --git a/src/Components/CreateImageWizard/steps/Review/ReviewStepTables.tsx b/src/Components/CreateImageWizard/steps/Review/ReviewStepTables.tsx index 8dcf7882..7b5a5af9 100644 --- a/src/Components/CreateImageWizard/steps/Review/ReviewStepTables.tsx +++ b/src/Components/CreateImageWizard/steps/Review/ReviewStepTables.tsx @@ -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 }); diff --git a/src/Components/CreateImageWizard/steps/Review/ReviewStepTextLists.tsx b/src/Components/CreateImageWizard/steps/Review/ReviewStepTextLists.tsx index d8e38251..52824d57 100644 --- a/src/Components/CreateImageWizard/steps/Review/ReviewStepTextLists.tsx +++ b/src/Components/CreateImageWizard/steps/Review/ReviewStepTextLists.tsx @@ -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) )}` diff --git a/src/Components/CreateImageWizard/steps/Snapshot/Snapshot.tsx b/src/Components/CreateImageWizard/steps/Snapshot/Snapshot.tsx index 3d2b1754..1234ed89 100644 --- a/src/Components/CreateImageWizard/steps/Snapshot/Snapshot.tsx +++ b/src/Components/CreateImageWizard/steps/Snapshot/Snapshot.tsx @@ -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 ( <> @@ -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')} /> useLatest && dispatch(changeUseLatest(false))} + isChecked={selectedOption === 'snapshotDate'} + onChange={() => handleOptionChange('snapshotDate')} /> + {isTemplatesEnabled ? ( + handleOptionChange('template')} + /> + ) : ( + <> + )} - {useLatest ? ( + + {selectedOption === 'latest' ? ( <> 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 + + + + ) : ( + <> )} ); diff --git a/src/Components/CreateImageWizard/steps/Snapshot/components/Templates.tsx b/src/Components/CreateImageWizard/steps/Snapshot/components/Templates.tsx new file mode 100644 index 00000000..6c61184b --- /dev/null +++ b/src/Components/CreateImageWizard/steps/Snapshot/components/Templates.tsx @@ -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 ; + if (isLoading) return ; + return ( + + + {templateList.length > 0 ? ( + + ) : ( + <> + )} + + {templateList.length === 0 ? ( + + ) : ( + <> + setPage(newPage)} + onPerPageSelect={handlePerPageSelect} + isCompact + /> + + + + + + + + + + {templateList.map((template, rowIndex) => { + const { uuid, name, description, date, use_latest } = + template; + + return ( + + + + + + ); + })} + +
+ NameDescriptionSnapshot date
handleRowSelect(uuid), + }} + /> + {name}{description} + {use_latest ? 'Use latest' : date?.split('T')[0]} +
+ setPage(newPage)} + onPerPageSelect={handlePerPageSelect} + variant={PaginationVariant.bottom} + /> + + )} +
+
+
+ ); +}; + +export default Templates; diff --git a/src/Components/CreateImageWizard/steps/Snapshot/components/TemplatesEmpty.tsx b/src/Components/CreateImageWizard/steps/Snapshot/components/TemplatesEmpty.tsx new file mode 100644 index 00000000..d57f3e73 --- /dev/null +++ b/src/Components/CreateImageWizard/steps/Snapshot/components/TemplatesEmpty.tsx @@ -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 ( + + ); + }; + + return ( + + + + {`Content templates can be added in the "Templates" area of the + console.`} + + + + + + + ); +}; + +export default TemplatesEmpty; diff --git a/src/Components/CreateImageWizard/utilities/requestMapper.ts b/src/Components/CreateImageWizard/utilities/requestMapper.ts index 9e7f79f9..1043698c 100644 --- a/src/Components/CreateImageWizard/utilities/requestMapper.ts +++ b/src/Components/CreateImageWizard/utilities/requestMapper.ts @@ -85,6 +85,7 @@ import { selectUsers, selectMetadata, selectFirewall, + selectTemplate, selectSatelliteCaCertificate, selectSatelliteRegistrationCommand, } from '../../../store/wizardSlice'; @@ -301,13 +302,15 @@ function commonRequestToState( sourceId: awsUploadOptions?.share_with_sources?.[0], }, snapshotting: { - useLatest: !snapshot_date, + useLatest: !snapshot_date && !request.image_requests[0]?.content_template, snapshotDate: snapshot_date, + template: request.image_requests[0]?.content_template || '', }, repositories: { customRepositories: request.customizations?.custom_repositories || [], payloadRepositories: request.customizations?.payload_repositories || [], recommendedRepositories: [], + redHatRepositories: [], }, packages: request.customizations?.packages @@ -432,6 +435,7 @@ const getImageRequests = (state: RootState): ImageRequest[] => { const imageTypes = selectImageTypes(state); const snapshotDate = selectSnapshotDate(state); const useLatest = selectUseLatest(state); + const template = selectTemplate(state); return imageTypes.map((type) => ({ architecture: selectArchitecture(state), image_type: type, @@ -439,7 +443,8 @@ const getImageRequests = (state: RootState): ImageRequest[] => { type: uploadTypeByTargetEnv(type), options: getImageOptions(type, state), }, - snapshot_date: useLatest ? undefined : snapshotDate, + snapshot_date: !useLatest && !template ? snapshotDate : undefined, + content_template: template || undefined, })); }; diff --git a/src/Components/CreateImageWizard/utilities/useValidation.tsx b/src/Components/CreateImageWizard/utilities/useValidation.tsx index 2a618e21..dbe4cc35 100644 --- a/src/Components/CreateImageWizard/utilities/useValidation.tsx +++ b/src/Components/CreateImageWizard/utilities/useValidation.tsx @@ -32,6 +32,7 @@ import { selectSatelliteRegistrationCommand, selectImageTypes, UserWithAdditionalInfo, + selectTemplate, } from '../../../store/wizardSlice'; import { keyboardsList } from '../steps/Locale/keyboardsList'; import { languagesList } from '../steps/Locale/languagesList'; @@ -226,8 +227,9 @@ export function useFilesystemValidation(): StepValidation { export function useSnapshotValidation(): StepValidation { const snapshotDate = useAppSelector(selectSnapshotDate); const useLatest = useAppSelector(selectUseLatest); + const template = useAppSelector(selectTemplate); - if (!useLatest && !isSnapshotValid(snapshotDate)) { + if (!useLatest && !isSnapshotValid(snapshotDate) && template === '') { return { errors: { snapshotDate: 'Invalid snapshot date' }, disabledNext: true, diff --git a/src/Utilities/useGetEnvironment.ts b/src/Utilities/useGetEnvironment.ts index 88617423..443e6dd0 100644 --- a/src/Utilities/useGetEnvironment.ts +++ b/src/Utilities/useGetEnvironment.ts @@ -44,6 +44,7 @@ const onPremFlag = (flag: string): boolean => { case 'image-builder.kernel.enabled': case 'image-builder.firewall.enabled': case 'image-builder.services.enabled': + case 'image-builder.templates.enabled': return true; default: return false; diff --git a/src/constants.ts b/src/constants.ts index 3b418d41..04a303c0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,6 +12,7 @@ export const EDIT_BLUEPRINT = `${IMAGE_BUILDER_API}/blueprints`; export const CDN_PROD_URL = 'https://cdn.redhat.com/'; export const CDN_STAGE_URL = 'https://cdn.stage.redhat.com/'; export const CONTENT_URL = '/insights/content/repositories'; +export const TEMPLATES_URL = '/insights/content/templates'; export const DEVELOPERS_URL = 'https://developers.redhat.com/about'; export const FILE_SYSTEM_CUSTOMIZATION_URL = 'https://docs.redhat.com/en/documentation/red_hat_insights/1-latest/html/deploying_and_managing_rhel_systems_in_hybrid_clouds/index#creating-a-blueprint_creating-blueprints-and-blueprint-images'; diff --git a/src/store/contentSourcesApi.ts b/src/store/contentSourcesApi.ts index 510e8ef3..fec39c46 100644 --- a/src/store/contentSourcesApi.ts +++ b/src/store/contentSourcesApi.ts @@ -16,6 +16,8 @@ export const { useCreateRepositoryMutation, useBulkImportRepositoriesMutation, useListRepositoriesRpmsQuery, + useListTemplatesQuery, + useGetTemplateQuery, contentSourcesApi, } = serviceQueries; diff --git a/src/store/service/contentSourcesApi.ts b/src/store/service/contentSourcesApi.ts index 426d2c15..a9e4c55c 100644 --- a/src/store/service/contentSourcesApi.ts +++ b/src/store/service/contentSourcesApi.ts @@ -89,6 +89,24 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.apiListSnapshotByDateRequest, }), }), + listTemplates: build.query({ + query: (queryArg) => ({ + url: `/templates/`, + params: { + offset: queryArg.offset, + limit: queryArg.limit, + version: queryArg.version, + arch: queryArg.arch, + name: queryArg.name, + repository_uuids: queryArg.repositoryUuids, + snapshot_uuids: queryArg.snapshotUuids, + sort_by: queryArg.sortBy, + }, + }), + }), + getTemplate: build.query({ + query: (queryArg) => ({ url: `/templates/${queryArg.uuid}` }), + }), }), overrideExisting: false, }); @@ -170,6 +188,32 @@ export type ListSnapshotsByDateApiArg = { /** request body */ apiListSnapshotByDateRequest: ApiListSnapshotByDateRequest; }; +export type ListTemplatesApiResponse = + /** status 200 OK */ ApiTemplateCollectionResponseRead; +export type ListTemplatesApiArg = { + /** Starting point for retrieving a subset of results. Determines how many items to skip from the beginning of the result set. Default value:`0`. */ + offset?: number; + /** Number of items to include in response. Use it to control the number of items, particularly when dealing with large datasets. Default value: `100`. */ + limit?: number; + /** Filter templates by version. */ + version?: string; + /** Filter templates by architecture. */ + arch?: string; + /** Filter templates by name. */ + name?: string; + /** Filter templates by associated repositories using a comma separated list of repository UUIDs */ + repositoryUuids?: string; + /** Filter templates by associated snapshots using a comma separated list of snapshot UUIDs */ + snapshotUuids?: string; + /** Sort the response data based on specific parameters. Sort criteria can include `name`, `arch`, and `version`. */ + sortBy?: string; +}; +export type GetTemplateApiResponse = + /** status 200 OK */ ApiTemplateResponseRead; +export type GetTemplateApiArg = { + /** Template ID. */ + uuid: string; +}; export type ApiFeature = { /** Whether the current user can access the feature */ accessible?: boolean | undefined; @@ -636,6 +680,91 @@ export type ApiListSnapshotByDateRequest = { /** Repository UUIDs to find snapshots for */ repository_uuids: string[]; }; +export type ApiTemplateResponse = { + /** Architecture of the template */ + arch?: string | undefined; + /** Datetime template was created */ + created_at?: string | undefined; + /** User that created the template */ + created_by?: string | undefined; + /** Latest date to include snapshots for */ + date?: string | undefined; + /** Description of the template */ + description?: string | undefined; + /** Error of last update_latest_snapshot task that updated the template */ + last_update_snapshot_error?: string | undefined; + last_update_task?: ApiTaskInfoResponse | undefined; + /** UUID of the last update_template_content task that updated the template */ + last_update_task_uuid?: string | undefined; + /** User that most recently updated the template */ + last_updated_by?: string | undefined; + /** Name of the template */ + name?: string | undefined; + /** Organization ID of the owner */ + org_id?: string | undefined; + /** Repositories added to the template */ + repository_uuids?: string[] | undefined; + /** Environment ID used by subscription-manager and candlepin */ + rhsm_environment_id?: string | undefined; + /** Datetime template was last updated */ + updated_at?: string | undefined; + /** Use latest snapshot for all repositories in the template */ + use_latest?: boolean | undefined; + /** Version of the template */ + version?: string | undefined; +}; +export type ApiTemplateResponseRead = { + /** Architecture of the template */ + arch?: string | undefined; + /** Datetime template was created */ + created_at?: string | undefined; + /** User that created the template */ + created_by?: string | undefined; + /** Latest date to include snapshots for */ + date?: string | undefined; + /** Description of the template */ + description?: string | undefined; + /** Error of last update_latest_snapshot task that updated the template */ + last_update_snapshot_error?: string | undefined; + last_update_task?: ApiTaskInfoResponse | undefined; + /** UUID of the last update_template_content task that updated the template */ + last_update_task_uuid?: string | undefined; + /** User that most recently updated the template */ + last_updated_by?: string | undefined; + /** Name of the template */ + name?: string | undefined; + /** Organization ID of the owner */ + org_id?: string | undefined; + /** Repositories added to the template */ + repository_uuids?: string[] | undefined; + /** Whether the candlepin environment is created and systems can be added */ + rhsm_environment_created?: boolean | undefined; + /** Environment ID used by subscription-manager and candlepin */ + rhsm_environment_id?: string | undefined; + /** The list of snapshots in use by the template */ + snapshots?: ApiSnapshotResponse[] | undefined; + /** List of snapshots used by this template which are going to be deleted soon */ + to_be_deleted_snapshots?: ApiSnapshotResponse[] | undefined; + /** Datetime template was last updated */ + updated_at?: string | undefined; + /** Use latest snapshot for all repositories in the template */ + use_latest?: boolean | undefined; + uuid?: string | undefined; + /** Version of the template */ + version?: string | undefined; +}; +export type ApiTemplateCollectionResponse = { + /** Requested Data */ + data?: ApiTemplateResponse[] | undefined; + links?: ApiLinks | undefined; + meta?: ApiResponseMetadata | undefined; +}; +export type ApiTemplateCollectionResponseRead = { + /** Requested Data */ + data?: ApiTemplateResponseRead[] | undefined; + links?: ApiLinks | undefined; + meta?: ApiResponseMetadata | undefined; +}; export const { useListFeaturesQuery, useSearchPackageGroupMutation, @@ -645,4 +774,6 @@ export const { useListRepositoriesRpmsQuery, useSearchRpmMutation, useListSnapshotsByDateMutation, + useListTemplatesQuery, + useGetTemplateQuery, } = injectedRtkApi; diff --git a/src/store/service/imageBuilderApi.ts b/src/store/service/imageBuilderApi.ts index 93a099e8..1da99e5b 100644 --- a/src/store/service/imageBuilderApi.ts +++ b/src/store/service/imageBuilderApi.ts @@ -549,6 +549,10 @@ export type ImageRequest = { all, the request will fail. The format must be YYYY-MM-DD (ISO 8601 extended). */ snapshot_date?: string | undefined; + /** ID of the content template. A content template and snapshot date cannot both be specified. + If a content template is specified, the snapshot date used will be the one from the content template. + */ + content_template?: string | undefined; }; export type Container = { /** Reference to the container to embed */ diff --git a/src/store/wizardSlice.ts b/src/store/wizardSlice.ts index a3ae5d1a..08909ecc 100644 --- a/src/store/wizardSlice.ts +++ b/src/store/wizardSlice.ts @@ -128,6 +128,7 @@ export type wizardState = { snapshotting: { useLatest: boolean; snapshotDate: string; + template: string; }; users: UserWithAdditionalInfo[]; firstBoot: { @@ -137,6 +138,7 @@ export type wizardState = { customRepositories: CustomRepository[]; payloadRepositories: Repository[]; recommendedRepositories: ApiRepositoryResponseRead[]; + redHatRepositories: Repository[]; }; packages: IBPackageWithRepositoryInfo[]; groups: GroupWithRepositoryInfo[]; @@ -221,11 +223,13 @@ export const initialState: wizardState = { snapshotting: { useLatest: true, snapshotDate: '', + template: '', }, repositories: { customRepositories: [], payloadRepositories: [], recommendedRepositories: [], + redHatRepositories: [], }, packages: [], groups: [], @@ -382,10 +386,15 @@ export const selectPartitions = (state: RootState) => { export const selectUseLatest = (state: RootState) => { return state.wizard.snapshotting.useLatest; }; + export const selectSnapshotDate = (state: RootState) => { return state.wizard.snapshotting.snapshotDate; }; +export const selectTemplate = (state: RootState) => { + return state.wizard.snapshotting.template; +}; + export const selectCustomRepositories = (state: RootState) => { return state.wizard.repositories.customRepositories; }; @@ -398,6 +407,10 @@ export const selectRecommendedRepositories = (state: RootState) => { return state.wizard.repositories.recommendedRepositories; }; +export const selectRedHatRepositories = (state: RootState) => { + return state.wizard.repositories.redHatRepositories; +}; + export const selectPackages = (state: RootState) => { return state.wizard.packages; }; @@ -728,6 +741,9 @@ export const wizardSlice = createSlice({ state.snapshotting.snapshotDate = date.toISOString(); } }, + changeTemplate: (state, action: PayloadAction) => { + state.snapshotting.template = action.payload; + }, importCustomRepositories: ( state, action: PayloadAction @@ -746,6 +762,9 @@ export const wizardSlice = createSlice({ changePayloadRepositories: (state, action: PayloadAction) => { state.repositories.payloadRepositories = action.payload; }, + changeRedHatRepositories: (state, action: PayloadAction) => { + state.repositories.redHatRepositories = action.payload; + }, addRecommendedRepository: ( state, action: PayloadAction @@ -1102,6 +1121,7 @@ export const { changePartitionOrder, changeUseLatest, changeSnapshotDate, + changeTemplate, changeCustomRepositories, importCustomRepositories, changePayloadRepositories, @@ -1153,5 +1173,6 @@ export const { setUserAdministratorByIndex, addUserGroupByIndex, removeUserGroupByIndex, + changeRedHatRepositories, } = wizardSlice.actions; export default wizardSlice.reducer; diff --git a/src/test/Components/CreateImageWizard/steps/Snapshot/Snapshot.test.tsx b/src/test/Components/CreateImageWizard/steps/Snapshot/Snapshot.test.tsx index 0a670729..77b19d0f 100644 --- a/src/test/Components/CreateImageWizard/steps/Snapshot/Snapshot.test.tsx +++ b/src/test/Components/CreateImageWizard/steps/Snapshot/Snapshot.test.tsx @@ -10,7 +10,11 @@ import { expectedPayloadRepositories, snapshotCreateBlueprintRequest, } from '../../../../fixtures/editMode'; -import { clickNext, clickReviewAndFinish } from '../../wizardTestUtils'; +import { + clickNext, + clickReviewAndFinish, + getNextButton, +} from '../../wizardTestUtils'; import { blueprintRequest, clickRegisterLater, @@ -123,6 +127,22 @@ const clickReset = async () => { await waitFor(async () => user.click(resetButton)); }; +const selectUseTemplate = async () => { + const user = userEvent.setup(); + const templateRadio = await screen.findByRole('radio', { + name: /Use a content template/i, + }); + await waitFor(async () => user.click(templateRadio)); +}; + +const selectFirstTemplate = async () => { + const user = userEvent.setup(); + const row0Radio = await screen.findByRole('radio', { + name: /select row 0/i, + }); + await waitFor(async () => user.click(row0Radio)); +}; + describe('repository snapshot tab - ', () => { beforeEach(() => { vi.clearAllMocks(); @@ -286,6 +306,23 @@ describe('repository snapshot tab - ', () => { await clickRevisitButton(); await screen.findByRole('heading', { name: /Custom repositories/i }); }); + + test('select use a content template', async () => { + await renderCreateMode(); + await goToSnapshotStep(); + await selectUseTemplate(); + const nextBtn = await getNextButton(); + await waitFor(() => { + expect(nextBtn).toHaveAttribute('aria-disabled', 'true'); + }); + await selectFirstTemplate(); + await waitFor(() => { + expect(nextBtn).toHaveAttribute('aria-disabled', 'false'); + }); + await clickNext(); + await goToReviewStep(); + await screen.findByText(/Use a content template/); + }); }); describe('Snapshot edit mode', () => { diff --git a/src/test/fixtures/templates.ts b/src/test/fixtures/templates.ts new file mode 100644 index 00000000..b8d5dc68 --- /dev/null +++ b/src/test/fixtures/templates.ts @@ -0,0 +1,195 @@ +import { + ApiLinks, + ApiResponseMetadata, + ApiTemplateResponse, + ApiTemplateResponseRead, + ListTemplatesApiArg, +} from '../../store/contentSourcesApi'; + +type templateArgs = { + arch: ListTemplatesApiArg['arch']; + version: ListTemplatesApiArg['version']; + limit: ListTemplatesApiArg['limit']; + offset: ListTemplatesApiArg['offset']; +}; + +export const mockTemplateResults = (request: templateArgs) => { + const templates = filterTemplates(request); + const limit = request.limit ? request.limit : 100; + const data = templates.slice(request.offset, limit); + const meta = generateMeta(request.limit, request.offset, templates.length); + const links = generateLinks(request.limit, request.offset); + const response = { + data: data, + meta: meta, + links: links, + }; + return response; +}; + +const filterTemplates = (args: templateArgs): ApiTemplateResponse[] => { + let templates = testingTemplates; + + if (args.arch) { + templates = templates.filter((template) => template.arch === args.arch); + } + + if (args.version) { + templates = templates.filter( + (template) => template.version === args.version + ); + } + + return templates; +}; + +const testingTemplates: ApiTemplateResponseRead[] = [ + { + uuid: 'c40e221b-93d6-4f7e-a704-f3041b8d75c3', + name: 'template-abc', + org_id: '13476545', + description: 'description-abc', + arch: 'x86_64', + version: '9', + date: '0001-01-01T00:00:00Z', + repository_uuids: [ + '828e7db8-c0d4-48fc-a887-9070e0e75c45', + 'ae39f556-6986-478a-95d1-f9c7e33d066c', + ], + snapshots: [ + { + uuid: '90302927-848a-4fa9-ba44-c58bb162a009', + created_at: '2025-02-27T16:23:59.148649Z', + repository_path: 'test/snapshot1', + content_counts: { + 'rpm.advisory': 5, + 'rpm.package': 5, + 'rpm.repo_metadata_file': 1, + }, + added_counts: { + 'rpm.advisory': 5, + 'rpm.package': 5, + 'rpm.repo_metadata_file': 1, + }, + removed_counts: {}, + url: 'http://test.com/test/snapshot1/', + repository_name: '2zmya', + repository_uuid: '828e7db8-c0d4-48fc-a887-9070e0e75c45', + }, + { + uuid: '80303926-948a-4fa8-ba44-c59bb162a008', + created_at: '2025-02-27T16:23:59.148649Z', + repository_path: 'test/snapshot2', + content_counts: { + 'rpm.advisory': 5, + 'rpm.package': 5, + 'rpm.repo_metadata_file': 1, + }, + added_counts: { + 'rpm.advisory': 5, + 'rpm.package': 5, + 'rpm.repo_metadata_file': 1, + }, + removed_counts: {}, + url: 'http://test.com/test/snapshot2/', + repository_name: '01-test-valid-repo', + repository_uuid: 'ae39f556-6986-478a-95d1-f9c7e33d066c', + }, + ], + rhsm_environment_id: '4202ed8d725e46079cc7454a64b69093', + created_by: 'test', + last_updated_by: 'test', + created_at: '2025-02-28T17:34:33.598161Z', + updated_at: '2025-02-28T17:34:33.598161Z', + use_latest: true, + last_update_snapshot_error: '', + last_update_task_uuid: '9aa99713-65d1-4057-908e-96150573a22f', + last_update_task: { + uuid: '9aa99713-65d1-4057-908e-96150573a22f', + status: 'completed', + created_at: '2025-02-28T17:34:33Z', + ended_at: '2025-02-28T17:34:34Z', + error: '', + org_id: '13476545', + type: 'update-template-content', + object_type: 'template', + object_name: 'template-abc', + object_uuid: 'c40e221b-93d6-4f7e-a704-f3041b8d75c3', + }, + rhsm_environment_created: true, + }, + { + uuid: '4202ed8d-725e-4607-9cc7-454a64b69093', + name: 'template-xyz', + org_id: '13476545', + description: 'description-xyz', + arch: 'x86_64', + version: '9', + date: '2025-02-28T05:00:00Z', + repository_uuids: ['828e7db8-c0d4-48fc-a887-9070e0e75c45'], + snapshots: [ + { + uuid: '90302927-848a-4fa9-ba44-c58bb162a009', + created_at: '2025-02-27T16:23:59.148649Z', + repository_path: 'test/snapshot1', + content_counts: { + 'rpm.advisory': 5, + 'rpm.package': 5, + 'rpm.repo_metadata_file': 1, + }, + added_counts: { + 'rpm.advisory': 5, + 'rpm.package': 5, + 'rpm.repo_metadata_file': 1, + }, + removed_counts: {}, + url: 'http://test.com/test/snapshot1/', + repository_name: '2zmya', + repository_uuid: '828e7db8-c0d4-48fc-a887-9070e0e75c45', + }, + ], + rhsm_environment_id: '4202ed8d725e46079cc7454a64b69093', + created_by: 'test', + last_updated_by: 'test', + created_at: '2025-02-28T18:35:34.792223Z', + updated_at: '2025-02-28T18:35:34.792223Z', + use_latest: false, + last_update_snapshot_error: '', + last_update_task_uuid: '8bn99713-65d1-4057-908e-96150573a22f', + last_update_task: { + uuid: '8bn99713-65d1-4057-908e-96150573a22f', + status: 'completed', + created_at: '2025-02-28T17:34:33Z', + ended_at: '2025-02-28T17:34:34Z', + error: '', + org_id: '13476545', + type: 'update-template-content', + object_type: 'template', + object_name: 'template-xyz', + object_uuid: '4202ed8d-725e-4607-9cc7-454a64b69093', + }, + rhsm_environment_created: true, + }, +]; + +const generateMeta = ( + limit: ApiResponseMetadata['limit'], + offset: ApiResponseMetadata['offset'], + count: ApiResponseMetadata['count'] +): ApiResponseMetadata => { + return { + limit: limit, + offset: offset, + count: count, + }; +}; + +const generateLinks = ( + limit: ApiResponseMetadata['limit'], + offset: ApiResponseMetadata['offset'] +): ApiLinks => { + return { + first: `/api/content-sources/v1/templates/?limit=${limit}&offset=${offset}`, + last: `/api/content-sources/v1/templates/?limit=${limit}&offset=${offset}`, + }; +}; diff --git a/src/test/mocks/handlers.js b/src/test/mocks/handlers.js index c1fcdab4..a8e1e416 100644 --- a/src/test/mocks/handlers.js +++ b/src/test/mocks/handlers.js @@ -43,6 +43,7 @@ import { mockRepositoryResults, } from '../fixtures/repositories'; import { mockSourcesByProvider, mockUploadInfo } from '../fixtures/sources'; +import { mockTemplateResults } from '../fixtures/templates'; export const handlers = [ http.get(`${PROVISIONING_API}/sources`, ({ request }) => { @@ -101,15 +102,31 @@ export const handlers = [ const limit = url.searchParams.get('limit'); const offset = url.searchParams.get('offset'); const search = url.searchParams.get('search'); + const uuid = url.searchParams.get('uuid'); const args = { available_for_arch, available_for_version, limit, offset, search, + uuid, }; return HttpResponse.json(mockRepositoryResults(args)); }), + http.get(`${CONTENT_SOURCES_API}/templates/`, ({ request }) => { + const url = new URL(request.url); + const arch = url.searchParams.get('arch'); + const version = url.searchParams.get('version'); + const limit = url.searchParams.get('limit'); + const offset = url.searchParams.get('offset'); + const args = { + arch, + version, + limit, + offset, + }; + return HttpResponse.json(mockTemplateResults(args)); + }), http.get(`${CONTENT_SOURCES_API}/repositories/:repo_id`, ({ params }) => { const { repo_id } = params; return HttpResponse.json(mockPopularRepo(repo_id)); diff --git a/src/test/setup.ts b/src/test/setup.ts index 994a41c1..2b7e58fc 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -79,6 +79,8 @@ vi.mock('@unleash/proxy-client-react', () => ({ return true; case 'image-builder.satellite.enabled': return true; + case 'image-builder.templates.enabled': + return true; default: return false; }