diff --git a/src/AppCockpit.tsx b/src/AppCockpit.tsx index 4c825be28c..be31af6c53 100644 --- a/src/AppCockpit.tsx +++ b/src/AppCockpit.tsx @@ -12,6 +12,8 @@ import { HashRouter } from 'react-router-dom'; import './AppCockpit.scss'; import { NotReady, RequireAdmin } from './Components/Cockpit'; +import { PlatformProvider } from './context/platform'; +import { onPremPlatform } from './context/platform/onprem'; import { Router } from './Router'; import { onPremStore as store } from './store'; import { useGetComposerSocketStatus } from './Utilities/useComposerStatus'; @@ -41,11 +43,13 @@ const Application = () => { }; const ImageBuilder = () => ( - - - - - + + + + + + + ); diff --git a/src/AppEntry.tsx b/src/AppEntry.tsx index 514c33828d..5303d3f36c 100644 --- a/src/AppEntry.tsx +++ b/src/AppEntry.tsx @@ -3,11 +3,15 @@ import React from 'react'; import { Provider } from 'react-redux'; import App from './App'; +import { PlatformProvider } from './context/platform'; +import { hostedPlatform } from './context/platform/hosted'; import { serviceStore as store } from './store'; const ImageBuilder = () => ( - + + + ); diff --git a/src/Components/Blueprints/BlueprintDiffModal.tsx b/src/Components/Blueprints/BlueprintDiffModal.tsx index b37b4c7517..96d499c3d9 100644 --- a/src/Components/Blueprints/BlueprintDiffModal.tsx +++ b/src/Components/Blueprints/BlueprintDiffModal.tsx @@ -10,7 +10,7 @@ import { ModalVariant, } from '@patternfly/react-core'; -import { useGetBlueprintQuery } from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; import { selectSelectedBlueprintId } from '@/store/slices/blueprint'; import { BuildImagesButton } from './BuildImagesButton'; @@ -31,6 +31,9 @@ const BlueprintDiffModal = ({ isOpen, onClose, }: blueprintDiffProps) => { + const { + queries: { useGetBlueprintQuery }, + } = usePlatform(); const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId); const { data: baseBlueprint } = useGetBlueprintQuery( diff --git a/src/Components/Blueprints/BlueprintsPagination.tsx b/src/Components/Blueprints/BlueprintsPagination.tsx index c8061b8d8c..1b000f20f0 100644 --- a/src/Components/Blueprints/BlueprintsPagination.tsx +++ b/src/Components/Blueprints/BlueprintsPagination.tsx @@ -3,10 +3,8 @@ import React from 'react'; import { Pagination, PaginationVariant } from '@patternfly/react-core'; import { OnSetPage } from '@patternfly/react-core/dist/esm/components/Pagination/Pagination'; -import { - GetBlueprintsApiArg, - useGetBlueprintsQuery, -} from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; +import { GetBlueprintsApiArg } from '@/store/api/backend'; import { selectBlueprintSearchInput, selectLimit, @@ -18,6 +16,9 @@ import { import { useAppDispatch, useAppSelector } from '../../store/hooks'; const BlueprintsPagination = () => { + const { + queries: { useGetBlueprintsQuery }, + } = usePlatform(); const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput); const blueprintsOffset = useAppSelector(selectOffset) || 0; const blueprintsLimit = useAppSelector(selectLimit) || 10; diff --git a/src/Components/Blueprints/BlueprintsSideBar.tsx b/src/Components/Blueprints/BlueprintsSideBar.tsx index 1600978d8e..95a07f7064 100644 --- a/src/Components/Blueprints/BlueprintsSideBar.tsx +++ b/src/Components/Blueprints/BlueprintsSideBar.tsx @@ -18,12 +18,8 @@ import { PlusCircleIcon, SearchIcon } from '@patternfly/react-icons'; import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon'; import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; -import { - BlueprintItem, - GetBlueprintsApiArg, - imageBuilderApi, - useGetBlueprintsQuery, -} from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; +import { BlueprintItem, GetBlueprintsApiArg } from '@/store/api/backend'; import { selectBlueprintSearchInput, selectLimit, @@ -57,6 +53,9 @@ type emptyBlueprintStateProps = { }; const BlueprintsSidebar = () => { + const { + queries: { useGetBlueprintsQuery }, + } = usePlatform(); const { analytics, auth } = useChrome(); const { userData } = useGetUser(auth); const isOnPremise = useAppSelector(selectIsOnPremise); @@ -181,6 +180,9 @@ const BlueprintsSidebar = () => { }; const BlueprintSearch = ({ blueprintsTotal }: blueprintSearchProps) => { + const { + api: { backendApi }, + } = usePlatform(); const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput); const dispatch = useAppDispatch(); const [localSearchValue, setLocalSearchValue] = useState( @@ -190,7 +192,7 @@ const BlueprintSearch = ({ blueprintsTotal }: blueprintSearchProps) => { useEffect(() => { dispatch(setBlueprintsOffset(0)); - dispatch(imageBuilderApi.util.invalidateTags([{ type: 'Blueprints' }])); + dispatch(backendApi.util.invalidateTags([{ type: 'Blueprints' }])); dispatch( setBlueprintSearchInput( debouncedSearchValue.length > 0 ? debouncedSearchValue : undefined, diff --git a/src/Components/Blueprints/BuildImagesButton.tsx b/src/Components/Blueprints/BuildImagesButton.tsx index ed77ce48c7..e3833abb57 100644 --- a/src/Components/Blueprints/BuildImagesButton.tsx +++ b/src/Components/Blueprints/BuildImagesButton.tsx @@ -18,7 +18,8 @@ import { MenuToggleElement } from '@patternfly/react-core/dist/esm/components/Me import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; import { skipToken } from '@reduxjs/toolkit/query'; -import { ImageTypes, useGetBlueprintQuery } from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; +import { ImageTypes } from '@/store/api/backend'; import { selectSelectedBlueprintId } from '@/store/slices/blueprint'; import { selectIsOnPremise } from '@/store/slices/env'; @@ -35,6 +36,9 @@ type BuildImagesButtonPropTypes = { }; export const BuildImagesButton = ({ children }: BuildImagesButtonPropTypes) => { + const { + queries: { useGetBlueprintQuery }, + } = usePlatform(); const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId); const [deselectedTargets, setDeselectedTargets] = useState([]); const { trigger: buildBlueprint, isLoading: imageBuildLoading } = diff --git a/src/Components/Blueprints/DeleteBlueprintModal.tsx b/src/Components/Blueprints/DeleteBlueprintModal.tsx index 60ff59ee33..179cc9f221 100644 --- a/src/Components/Blueprints/DeleteBlueprintModal.tsx +++ b/src/Components/Blueprints/DeleteBlueprintModal.tsx @@ -10,11 +10,8 @@ import { } from '@patternfly/react-core'; import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; -import { - backendApi, - GetBlueprintsApiArg, - useGetBlueprintsQuery, -} from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; +import { GetBlueprintsApiArg } from '@/store/api/backend'; import { selectBlueprintSearchInput, selectLimit, @@ -43,6 +40,10 @@ interface DeleteBlueprintModalProps { export const DeleteBlueprintModal: React.FunctionComponent< DeleteBlueprintModalProps > = ({ setShowDeleteModal, isOpen }: DeleteBlueprintModalProps) => { + const { + queries: { useGetBlueprintsQuery }, + api: { backendApi }, + } = usePlatform(); const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId); const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput); const blueprintsOffset = useAppSelector(selectOffset) || PAGINATION_OFFSET; diff --git a/src/Components/CreateImageWizard/steps/ImageOutput/components/ImageSourceSelect.tsx b/src/Components/CreateImageWizard/steps/ImageOutput/components/ImageSourceSelect.tsx index a4a7f95a62..30194c0864 100644 --- a/src/Components/CreateImageWizard/steps/ImageOutput/components/ImageSourceSelect.tsx +++ b/src/Components/CreateImageWizard/steps/ImageOutput/components/ImageSourceSelect.tsx @@ -19,10 +19,8 @@ import { import { SyncAltIcon } from '@patternfly/react-icons'; import { RHEL_10_IMAGE_MODE_IMAGE } from '@/constants'; -import { - BootcDistributionItem, - useGetDistributionsQuery, -} from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; +import { BootcDistributionItem } from '@/store/api/backend'; import { Distributions } from '@/store/api/backend/hosted'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { selectIsOnPremise } from '@/store/slices/env'; @@ -63,6 +61,9 @@ const InfoMessageContent = ({ source }: { source: string }) => { }; const ImageSourceSelect = () => { + const { + queries: { useGetDistributionsQuery }, + } = usePlatform(); const dispatch = useAppDispatch(); const isOnPremise = useAppSelector(selectIsOnPremise); const arch = useAppSelector(selectArchitecture); diff --git a/src/Components/CreateImageWizard/steps/ImageOutput/components/TargetEnvironment.tsx b/src/Components/CreateImageWizard/steps/ImageOutput/components/TargetEnvironment.tsx index ac57068963..e98a18f61b 100644 --- a/src/Components/CreateImageWizard/steps/ImageOutput/components/TargetEnvironment.tsx +++ b/src/Components/CreateImageWizard/steps/ImageOutput/components/TargetEnvironment.tsx @@ -11,14 +11,10 @@ import { Tooltip, } from '@patternfly/react-core'; +import { usePlatform } from '@/context/platform'; import { useTargetEnvironmentCategories } from '@/Hooks'; import { rhsmApi } from '@/store/api'; -import { - BootcDistributionItem, - ImageTypes, - useGetArchitecturesQuery, - useGetDistributionsQuery, -} from '@/store/api/backend'; +import { BootcDistributionItem, ImageTypes } from '@/store/api/backend'; import { useCustomizationRestrictions } from '@/store/api/distributions'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { @@ -67,6 +63,9 @@ const createLabelWithTooltip = ( }; const TargetEnvironment = () => { + const { + queries: { useGetArchitecturesQuery, useGetDistributionsQuery }, + } = usePlatform(); const arch = useAppSelector(selectArchitecture); const environments = useAppSelector(selectImageTypes); const distribution = useAppSelector(selectDistribution); diff --git a/src/Components/CreateImageWizard/steps/ImageOutput/tests/ImageSourceSelect.test.tsx b/src/Components/CreateImageWizard/steps/ImageOutput/tests/ImageSourceSelect.test.tsx index ffd9109a78..ec19770d5e 100644 --- a/src/Components/CreateImageWizard/steps/ImageOutput/tests/ImageSourceSelect.test.tsx +++ b/src/Components/CreateImageWizard/steps/ImageOutput/tests/ImageSourceSelect.test.tsx @@ -27,12 +27,19 @@ import ImageSourceSelect from '../components/ImageSourceSelect'; const mockRefetch = vi.fn(); const mockUseGetDistributionsQuery = vi.fn(); -vi.mock('@/store/api/backend', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@/context/platform', async (importOriginal) => { + const actual = await importOriginal(); + const { mockPlatform } = await import('@/context/platform/tests/mocks'); return { ...actual, - useGetDistributionsQuery: (...args: unknown[]) => - mockUseGetDistributionsQuery(...args), + usePlatform: () => ({ + ...mockPlatform, + queries: { + ...mockPlatform.queries, + useGetDistributionsQuery: (...args: unknown[]) => + mockUseGetDistributionsQuery(...args), + }, + }), }; }); diff --git a/src/Components/CreateImageWizard/steps/Kernel/components/KernelArguments.tsx b/src/Components/CreateImageWizard/steps/Kernel/components/KernelArguments.tsx index 9bedf583dc..e1a1377d08 100644 --- a/src/Components/CreateImageWizard/steps/Kernel/components/KernelArguments.tsx +++ b/src/Components/CreateImageWizard/steps/Kernel/components/KernelArguments.tsx @@ -5,7 +5,7 @@ import { FormGroup, HelperText, HelperTextItem } from '@patternfly/react-core'; import LabelInput from '@/Components/CreateImageWizard/LabelInput'; import { useKernelValidation } from '@/Components/CreateImageWizard/utilities/useValidation'; import { isKernelArgumentValid } from '@/Components/CreateImageWizard/validators'; -import { useGetOscapCustomizationsQuery } from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; import { useAppSelector } from '@/store/hooks'; import { addKernelArg, @@ -16,6 +16,9 @@ import { } from '@/store/slices/wizard'; const KernelArguments = () => { + const { + queries: { useGetOscapCustomizationsQuery }, + } = usePlatform(); const kernelAppend = useAppSelector(selectKernel).append; const stepValidation = useKernelValidation(); diff --git a/src/Components/CreateImageWizard/steps/Locale/index.tsx b/src/Components/CreateImageWizard/steps/Locale/index.tsx index 2bd07bd6bb..425ac76ef2 100644 --- a/src/Components/CreateImageWizard/steps/Locale/index.tsx +++ b/src/Components/CreateImageWizard/steps/Locale/index.tsx @@ -3,7 +3,7 @@ import React, { useMemo } from 'react'; import { Content, Spinner, Title } from '@patternfly/react-core'; import { CustomizationLabels } from '@/Components/sharedComponents/CustomizationLabels'; -import { useGetArchitecturesQuery } from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; import { useSearchLanguagePacks } from '@/store/api/contentSources'; import { useAppSelector } from '@/store/hooks'; import { @@ -17,6 +17,9 @@ import KeyboardDropDown from './components/KeyboardDropDown'; import LanguagesDropDown from './components/LanguagesDropDown'; const LocaleStep = () => { + const { + queries: { useGetArchitecturesQuery }, + } = usePlatform(); const distribution = useAppSelector(selectDistribution); const arch = useAppSelector(selectArchitecture); const candidateLangpacks = useAppSelector(selectLocaleLangpackCandidates); diff --git a/src/Components/CreateImageWizard/steps/Oscap/components/ProfileSelector.tsx b/src/Components/CreateImageWizard/steps/Oscap/components/ProfileSelector.tsx index f7317a74d9..a664e7ecae 100644 --- a/src/Components/CreateImageWizard/steps/Oscap/components/ProfileSelector.tsx +++ b/src/Components/CreateImageWizard/steps/Oscap/components/ProfileSelector.tsx @@ -15,14 +15,12 @@ import { } from '@patternfly/react-core'; import { TimesIcon } from '@patternfly/react-icons'; +import { usePlatform } from '@/context/platform'; import { DistributionProfileItem, DistributionProfileResponse, OpenScap, OpenScapProfile, - useBackendPrefetch, - useGetOscapCustomizationsQuery, - useLazyGetOscapCustomizationsQuery, } from '@/store/api/backend'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { selectIsOnPremise } from '@/store/slices/env'; @@ -60,6 +58,13 @@ const ProfileSelector = ({ isSuccess, refetch, }: ProfileSelectorProps) => { + const { + queries: { + useGetOscapCustomizationsQuery, + useLazyGetOscapCustomizationsQuery, + }, + api: { useBackendPrefetch }, + } = usePlatform(); const isOnPremise = useAppSelector(selectIsOnPremise); const profileID = useAppSelector(selectComplianceProfileID); const release = removeBetaFromRelease(useAppSelector(selectDistribution)); diff --git a/src/Components/CreateImageWizard/steps/Oscap/index.tsx b/src/Components/CreateImageWizard/steps/Oscap/index.tsx index 35bcd524e9..4156c141fb 100644 --- a/src/Components/CreateImageWizard/steps/Oscap/index.tsx +++ b/src/Components/CreateImageWizard/steps/Oscap/index.tsx @@ -26,12 +26,8 @@ import { FIRST_BOOT_SERVICE, OSCAP_URL, } from '@/constants'; +import { usePlatform } from '@/context/platform'; import { useGetUser } from '@/Hooks'; -import { - useBackendPrefetch, - useGetOscapCustomizationsQuery, - useGetOscapProfilesQuery, -} from '@/store/api/backend'; import { useSecuritySummary } from '@/store/api/backend/hooks'; import { usePoliciesQuery } from '@/store/api/compliance'; import { useCustomizationRestrictions } from '@/store/api/distributions'; @@ -71,6 +67,10 @@ import { removeBetaFromRelease } from './removeBetaFromRelease'; import ExternalLinkButton from '../../utilities/ExternalLinkButton'; const OscapContent = () => { + const { + queries: { useGetOscapProfilesQuery, useGetOscapCustomizationsQuery }, + api: { useBackendPrefetch }, + } = usePlatform(); const dispatch = useAppDispatch(); const registrationType = useAppSelector(selectRegistrationType); const complianceType = useAppSelector(selectComplianceType); diff --git a/src/Components/CreateImageWizard/steps/Packages/components/PackageSearch.tsx b/src/Components/CreateImageWizard/steps/Packages/components/PackageSearch.tsx index 5baf48103b..9a97c611ad 100644 --- a/src/Components/CreateImageWizard/steps/Packages/components/PackageSearch.tsx +++ b/src/Components/CreateImageWizard/steps/Packages/components/PackageSearch.tsx @@ -30,6 +30,7 @@ import { ContentOrigin, EPEL_10_REPO_DEFINITION, } from '@/constants'; +import { usePlatform } from '@/context/platform'; import { Module, useGetArchitecturesQuery, @@ -42,7 +43,6 @@ import { useListRepositoriesQuery, useSearchPackageGroupMutation, useSearchRepositoryModuleStreamsMutation, - useSearchRpmMutation, } from '@/store/api/contentSources'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { selectIsOnPremise } from '@/store/slices/env'; @@ -100,6 +100,9 @@ const PackageSearch = ({ activeStream, setActiveStream, }: PackageSearchProps) => { + const { + mutations: { useSearchRpmMutation }, + } = usePlatform(); const dispatch = useAppDispatch(); const { analytics, isBeta } = useChrome(); @@ -254,13 +257,16 @@ const PackageSearch = ({ } if (debouncedSearchTerm.length > 1 && isSuccessDistroRepositories) { if (isOnPremise) { + // On-prem uses a different request shape with packages/architecture/distribution + // fields that don't exist in the hosted ApiContentUnitSearchRequest type. + // The on-prem implementation accepts these fields at runtime via PlatformContext. searchDistroRpms({ apiContentUnitSearchRequest: { packages: [debouncedSearchTerm], architecture: arch, distribution, }, - }); + } as Parameters[0]); } else { searchDistroRpms({ apiContentUnitSearchRequest: { diff --git a/src/Components/CreateImageWizard/steps/Repositories/components/Repositories.tsx b/src/Components/CreateImageWizard/steps/Repositories/components/Repositories.tsx index 421b03c033..c3cf0061c0 100644 --- a/src/Components/CreateImageWizard/steps/Repositories/components/Repositories.tsx +++ b/src/Components/CreateImageWizard/steps/Repositories/components/Repositories.tsx @@ -16,12 +16,12 @@ import { ExternalLinkAltIcon } from '@patternfly/react-icons'; import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; import { CONTENT_URL, ContentOrigin } from '@/constants'; +import { usePlatform } from '@/context/platform'; import { ApiRepositoryResponseRead, useGetTemplateQuery, useListRepositoriesQuery, useListRepositoryParametersQuery, - useListSnapshotsByDateMutation, } from '@/store/api/contentSources'; import { useAppDispatch, useAppSelector } from '@/store/hooks'; import { @@ -70,6 +70,9 @@ import { } from '../repositoriesUtilities'; const Repositories = () => { + const { + mutations: { useListSnapshotsByDateMutation }, + } = usePlatform(); const dispatch = useAppDispatch(); const arch = useAppSelector(selectArchitecture); diff --git a/src/Components/CreateImageWizard/steps/Review/Footer/CreateDropdown.tsx b/src/Components/CreateImageWizard/steps/Review/Footer/CreateDropdown.tsx index af852c65f7..9d7282e8b3 100644 --- a/src/Components/CreateImageWizard/steps/Review/Footer/CreateDropdown.tsx +++ b/src/Components/CreateImageWizard/steps/Review/Footer/CreateDropdown.tsx @@ -82,7 +82,7 @@ export const CreateSaveAndBuildBtn = ({ } if (requestBody) { const blueprint = (await createBlueprint({ - createBlueprintRequest: requestBody, + createBlueprintRequest: requestBody as CreateBlueprintRequest, })) as CreateBlueprintResponse; buildBlueprint({ id: blueprint.id, body: {} }); @@ -181,7 +181,7 @@ export const CreateSaveButton = ({ } if (requestBody) { const blueprint = (await createBlueprint({ - createBlueprintRequest: requestBody, + createBlueprintRequest: requestBody as CreateBlueprintRequest, })) as CreateBlueprintResponse; dispatch(setBlueprintId(blueprint.id)); } diff --git a/src/Components/CreateImageWizard/steps/Review/Footer/EditDropdown.tsx b/src/Components/CreateImageWizard/steps/Review/Footer/EditDropdown.tsx index 158464bde6..3cf86cd7bd 100644 --- a/src/Components/CreateImageWizard/steps/Review/Footer/EditDropdown.tsx +++ b/src/Components/CreateImageWizard/steps/Review/Footer/EditDropdown.tsx @@ -78,7 +78,7 @@ export const EditSaveAndBuildBtn = ({ if (requestBody) { await updateBlueprint({ id: blueprintId, - createBlueprintRequest: requestBody, + createBlueprintRequest: requestBody as CreateBlueprintRequest, }); } buildBlueprint({ id: blueprintId, body: {} }); @@ -127,7 +127,7 @@ export const EditSaveButton = ({ if (requestBody) { updateBlueprint({ id: blueprintId, - createBlueprintRequest: requestBody, + createBlueprintRequest: requestBody as CreateBlueprintRequest, }); } }; diff --git a/src/Components/CreateImageWizard/steps/Services/components/ServicesInputs.tsx b/src/Components/CreateImageWizard/steps/Services/components/ServicesInputs.tsx index 7d7b515d9a..fa446ec6bb 100644 --- a/src/Components/CreateImageWizard/steps/Services/components/ServicesInputs.tsx +++ b/src/Components/CreateImageWizard/steps/Services/components/ServicesInputs.tsx @@ -5,7 +5,7 @@ import { FormGroup, HelperText, HelperTextItem } from '@patternfly/react-core'; import LabelInput from '@/Components/CreateImageWizard/LabelInput'; import { useServicesValidation } from '@/Components/CreateImageWizard/utilities/useValidation'; import { isServiceValid } from '@/Components/CreateImageWizard/validators'; -import { useGetOscapCustomizationsQuery } from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; import { useAppSelector } from '@/store/hooks'; import { addDisabledService, @@ -20,6 +20,9 @@ import { } from '@/store/slices/wizard'; const ServicesInput = () => { + const { + queries: { useGetOscapCustomizationsQuery }, + } = usePlatform(); const disabledServices = useAppSelector(selectServices).disabled; const maskedServices = useAppSelector(selectServices).masked; const enabledServices = useAppSelector(selectServices).enabled; diff --git a/src/Components/CreateImageWizard/tests/helpers.tsx b/src/Components/CreateImageWizard/tests/helpers.tsx index 25a452f32d..b7378504f9 100644 --- a/src/Components/CreateImageWizard/tests/helpers.tsx +++ b/src/Components/CreateImageWizard/tests/helpers.tsx @@ -5,6 +5,8 @@ import { render, screen } from '@testing-library/react'; import { Provider } from 'react-redux'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; +import { PlatformProvider } from '@/context/platform'; +import { hostedPlatform } from '@/context/platform/hosted'; import { RootState, serviceMiddleware, serviceReducer } from '@/store'; import CreateImageWizard from '../CreateImageWizard'; @@ -36,12 +38,14 @@ export const renderWithQueryParams = async ( render( - + + + , ); diff --git a/src/Components/CreateImageWizard/utilities/useValidation.tsx b/src/Components/CreateImageWizard/utilities/useValidation.tsx index c3e24692b7..a9f357d8ca 100644 --- a/src/Components/CreateImageWizard/utilities/useValidation.tsx +++ b/src/Components/CreateImageWizard/utilities/useValidation.tsx @@ -3,10 +3,8 @@ import React, { useEffect, useState } from 'react'; import { CheckCircleIcon } from '@patternfly/react-icons'; import { jwtDecode } from 'jwt-decode'; -import { - BlueprintsResponse, - useLazyGetBlueprintsQuery, -} from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; +import { BlueprintsResponse } from '@/store/api/backend'; import { useShowActivationKeyQuery } from '@/store/api/rhsm'; import { selectIsOnPremise } from '@/store/slices/env'; import { @@ -1098,6 +1096,9 @@ const countCharacterTypes = (value: string) => { }; export function useDetailsValidation(): StepValidation { + const { + queries: { useLazyGetBlueprintsQuery }, + } = usePlatform(); const name = useAppSelector(selectBlueprintName); const description = useAppSelector(selectBlueprintDescription); const blueprintId = useAppSelector(selectBlueprintId); diff --git a/src/Components/ImagesTable/ImageDetails.tsx b/src/Components/ImagesTable/ImageDetails.tsx index 9a720fb3c4..a838218198 100644 --- a/src/Components/ImagesTable/ImageDetails.tsx +++ b/src/Components/ImagesTable/ImageDetails.tsx @@ -12,10 +12,10 @@ import { import { ExternalLinkAltIcon } from '@patternfly/react-icons'; import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; +import { usePlatform } from '@/context/platform'; import { ComposesResponseItem, GcpUploadRequestOptions, - useGetComposeStatusQuery, } from '@/store/api/backend'; import { selectIsOnPremise } from '@/store/slices/env'; @@ -45,6 +45,9 @@ type AwsDetailsPropTypes = { }; export const AwsDetails = ({ compose }: AwsDetailsPropTypes) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const options = compose.request.image_requests[0].upload_request.options; const { analytics, auth } = useChrome(); @@ -176,6 +179,9 @@ type AzureDetailsPropTypes = { }; export const AzureDetails = ({ compose }: AzureDetailsPropTypes) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const { data: composeStatus } = useGetComposeStatusQuery({ composeId: compose.id, }); @@ -256,6 +262,9 @@ type GcpDetailsPropTypes = { }; export const GcpDetails = ({ compose }: GcpDetailsPropTypes) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const { data: composeStatus } = useGetComposeStatusQuery({ composeId: compose.id, }); @@ -344,6 +353,9 @@ type OciDetailsPropTypes = { }; export const OciDetails = ({ compose }: OciDetailsPropTypes) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const { data: composeStatus } = useGetComposeStatusQuery({ composeId: compose.id, }); diff --git a/src/Components/ImagesTable/ImagesTable.tsx b/src/Components/ImagesTable/ImagesTable.tsx index 6c87300b24..b2788dd739 100644 --- a/src/Components/ImagesTable/ImagesTable.tsx +++ b/src/Components/ImagesTable/ImagesTable.tsx @@ -31,16 +31,13 @@ import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; import cockpit from 'cockpit'; import { useDispatch } from 'react-redux'; +import { usePlatform } from '@/context/platform'; import { BlueprintItem, ComposesResponseItem, GetBlueprintComposesApiArg, GetBlueprintsApiArg, LocalUploadStatus, - useGetBlueprintComposesQuery, - useGetBlueprintsQuery, - useGetComposesQuery, - useGetComposeStatusQuery, } from '@/store/api/backend'; import { selectBlueprintSearchInput, @@ -96,6 +93,13 @@ import { GcpLaunchModal } from '../Launch/GcpLaunchModal'; import { OciLaunchModal } from '../Launch/OciLaunchModal'; const ImagesTable = () => { + const { + queries: { + useGetBlueprintsQuery, + useGetBlueprintComposesQuery, + useGetComposesQuery, + }, + } = usePlatform(); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(10); @@ -322,6 +326,9 @@ type ImagesTableRowPropTypes = { }; const ImagesTableRow = ({ compose, rowIndex }: ImagesTableRowPropTypes) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const [pollingInterval, setPollingInterval] = useState( STATUS_POLLING_INTERVAL, ); @@ -580,6 +587,9 @@ const Row = ({ details, instance, }: RowPropTypes) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const { analytics, auth } = useChrome(); const { userData } = useGetUser(auth); const isOnPremise = useAppSelector(selectIsOnPremise); diff --git a/src/Components/ImagesTable/ImagesTableToolbar.tsx b/src/Components/ImagesTable/ImagesTableToolbar.tsx index 86e1e0e3a4..081cd2b613 100644 --- a/src/Components/ImagesTable/ImagesTableToolbar.tsx +++ b/src/Components/ImagesTable/ImagesTableToolbar.tsx @@ -13,13 +13,11 @@ import { ToolbarItem, } from '@patternfly/react-core'; +import { usePlatform } from '@/context/platform'; import { BlueprintItem, Distributions, GetBlueprintComposesApiArg, - useGetBlueprintComposesQuery, - useGetBlueprintQuery, - useGetBlueprintsQuery, } from '@/store/api/backend'; import { selectBlueprintSearchInput, @@ -58,6 +56,13 @@ const ImagesTableToolbar: React.FC = ({ setPage, onPerPageSelect, }: imagesTableToolbarProps) => { + const { + queries: { + useGetBlueprintQuery, + useGetBlueprintComposesQuery, + useGetBlueprintsQuery, + }, + } = usePlatform(); const isOnPremise = useAppSelector(selectIsOnPremise); const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDiffModal, setShowDiffModal] = useState(false); diff --git a/src/Components/ImagesTable/Instance.tsx b/src/Components/ImagesTable/Instance.tsx index f1073cc89c..a012644512 100644 --- a/src/Components/ImagesTable/Instance.tsx +++ b/src/Components/ImagesTable/Instance.tsx @@ -6,11 +6,11 @@ import { Button, Skeleton } from '@patternfly/react-core'; import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; import cockpit from 'cockpit'; +import { usePlatform } from '@/context/platform'; import { ComposesResponseItem, ImageTypes, LocalUploadStatus, - useGetComposeStatusQuery, } from '@/store/api/backend'; import { selectIsOnPremise } from '@/store/slices/env'; @@ -32,6 +32,9 @@ export const AwsS3Instance = ({ compose, isExpired, }: AwsS3InstancePropTypes) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const { analytics } = useChrome(); const isOnPremise = useAppSelector(selectIsOnPremise); @@ -121,6 +124,9 @@ const VM_INSTALLABLE_IMAGE_TYPES: ImageTypes[] = [ ]; export const LocalInstance = ({ compose }: LocalInstancePropTypes) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const isMachinesAvailable = useCockpitMachinesAvailable(); const { data: composeStatus, isSuccess } = useGetComposeStatusQuery({ composeId: compose.id, diff --git a/src/Components/ImagesTable/Status.tsx b/src/Components/ImagesTable/Status.tsx index 3c2d75bf39..fdec39757d 100644 --- a/src/Components/ImagesTable/Status.tsx +++ b/src/Components/ImagesTable/Status.tsx @@ -24,12 +24,12 @@ import { PendingIcon, } from '@patternfly/react-icons'; +import { usePlatform } from '@/context/platform'; import { ComposerComposesResponseItem, ComposesResponseItem, ComposeStatus, ComposeStatusError, - useGetComposeStatusQuery, } from '@/store/api/backend'; import { @@ -42,6 +42,9 @@ type ComposeStatusPropTypes = { }; export const AwsDetailsStatus = ({ compose }: ComposeStatusPropTypes) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const { data, isSuccess } = useGetComposeStatusQuery({ composeId: compose.id, }); @@ -77,6 +80,9 @@ type CloudStatusPropTypes = { }; export const CloudStatus = ({ compose }: CloudStatusPropTypes) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const { data, isSuccess } = useGetComposeStatusQuery({ composeId: compose.id, }); @@ -145,6 +151,9 @@ export const ExpiringStatus = ({ isExpired, timeToExpiration, }: ExpiringStatusPropTypes) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const { data: composeStatus, isSuccess } = useGetComposeStatusQuery({ composeId: compose.id, }); @@ -237,6 +246,9 @@ type LocalStatusPropTypes = { }; export const LocalStatus = ({ compose }: LocalStatusPropTypes) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const { data: composeStatus, isSuccess } = useGetComposeStatusQuery({ composeId: compose.id, }); diff --git a/src/Components/LandingPage/tests/helpers.tsx b/src/Components/LandingPage/tests/helpers.tsx index 3302a5f269..6df6354742 100644 --- a/src/Components/LandingPage/tests/helpers.tsx +++ b/src/Components/LandingPage/tests/helpers.tsx @@ -4,6 +4,9 @@ import { render } from '@testing-library/react'; import { Provider } from 'react-redux'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; +import { PlatformProvider } from '@/context/platform'; +import { hostedPlatform } from '@/context/platform/hosted'; +import { onPremPlatform } from '@/context/platform/onprem'; import { createTestStore, type RenderWithReduxOptions } from '@/test/testUtils'; import LandingPage from '../LandingPage'; @@ -22,14 +25,18 @@ export const renderLandingPage = (options: RenderWithReduxOptions = {}) => { initialEntries: ['/'], }); + const isOnPremise = options.preloadedState?.env?.isOnPremise; + const platform = isOnPremise ? onPremPlatform : hostedPlatform; const view = render( - + + + , ); diff --git a/src/Components/Launch/AWSLaunchModal.tsx b/src/Components/Launch/AWSLaunchModal.tsx index 5038365b44..a70df493cb 100644 --- a/src/Components/Launch/AWSLaunchModal.tsx +++ b/src/Components/Launch/AWSLaunchModal.tsx @@ -15,10 +15,8 @@ import { } from '@patternfly/react-core'; import { ExternalLinkAltIcon } from '@patternfly/react-icons'; -import { - ComposesResponseItem, - useGetComposeStatusQuery, -} from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; +import { ComposesResponseItem } from '@/store/api/backend'; import { isAwsUploadRequestOptions } from '../../store/typeGuards'; @@ -27,6 +25,9 @@ type LaunchProps = { }; export const AWSLaunchModal = ({ compose }: LaunchProps) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const [isModalOpen, setIsModalOpen] = useState(false); const { data, isSuccess, isFetching } = useGetComposeStatusQuery({ composeId: compose.id, diff --git a/src/Components/Launch/AzureLaunchModal.tsx b/src/Components/Launch/AzureLaunchModal.tsx index 739c340871..bb6966f54b 100644 --- a/src/Components/Launch/AzureLaunchModal.tsx +++ b/src/Components/Launch/AzureLaunchModal.tsx @@ -15,10 +15,8 @@ import { } from '@patternfly/react-core'; import { ExternalLinkAltIcon } from '@patternfly/react-icons'; -import { - ComposesResponseItem, - useGetComposeStatusQuery, -} from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; +import { ComposesResponseItem } from '@/store/api/backend'; import { isAzureUploadStatus } from '../../store/typeGuards'; @@ -27,6 +25,9 @@ type LaunchProps = { }; export const AzureLaunchModal = ({ compose }: LaunchProps) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const [isModalOpen, setIsModalOpen] = useState(false); const { data, isSuccess, isFetching } = useGetComposeStatusQuery({ composeId: compose.id, diff --git a/src/Components/Launch/GcpLaunchModal.tsx b/src/Components/Launch/GcpLaunchModal.tsx index 3fc0aa8f84..a95bf0903f 100644 --- a/src/Components/Launch/GcpLaunchModal.tsx +++ b/src/Components/Launch/GcpLaunchModal.tsx @@ -18,10 +18,8 @@ import { } from '@patternfly/react-core'; import { ExternalLinkAltIcon } from '@patternfly/react-icons'; -import { - ComposesResponseItem, - useGetComposeStatusQuery, -} from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; +import { ComposesResponseItem } from '@/store/api/backend'; import { generateDefaultName } from './useGenerateDefaultName'; @@ -34,6 +32,9 @@ import { parseGcpSharedWith } from '../ImagesTable/ImageDetails'; type LaunchProps = { compose: ComposesResponseItem }; export const GcpLaunchModal = ({ compose }: LaunchProps) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const [isModalOpen, setIsModalOpen] = useState(false); const [customerProjectId, setCustomerProjectId] = useState(''); const { data, isSuccess, isFetching } = useGetComposeStatusQuery({ diff --git a/src/Components/Launch/OciLaunchModal.tsx b/src/Components/Launch/OciLaunchModal.tsx index d6a72e270d..3933da4230 100644 --- a/src/Components/Launch/OciLaunchModal.tsx +++ b/src/Components/Launch/OciLaunchModal.tsx @@ -18,10 +18,8 @@ import { import { ExternalLinkAltIcon } from '@patternfly/react-icons'; import { useNavigate } from 'react-router-dom'; -import { - ComposesResponseItem, - useGetComposeStatusQuery, -} from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; +import { ComposesResponseItem } from '@/store/api/backend'; import { selectPathResolver } from '@/store/slices/env'; import { useAppSelector } from '../../store/hooks'; @@ -33,6 +31,9 @@ type LaunchProps = { }; export const OciLaunchModal = ({ isExpired, compose }: LaunchProps) => { + const { + queries: { useGetComposeStatusQuery }, + } = usePlatform(); const [isModalOpen, setIsModalOpen] = useState(false); const { data, isSuccess, isFetching } = useGetComposeStatusQuery({ composeId: compose.id, diff --git a/src/Components/sharedComponents/ImageBuilderHeader.tsx b/src/Components/sharedComponents/ImageBuilderHeader.tsx index e301a29981..bb1e4322b1 100644 --- a/src/Components/sharedComponents/ImageBuilderHeader.tsx +++ b/src/Components/sharedComponents/ImageBuilderHeader.tsx @@ -8,7 +8,7 @@ import { PageHeaderTitle, } from '@redhat-cloud-services/frontend-components'; -import { useBackendPrefetch } from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; import { selectIsOnPremise } from '@/store/slices/env'; import { selectDistribution } from '@/store/slices/wizard'; import { openWizardModal } from '@/store/slices/wizardModal'; @@ -72,6 +72,9 @@ export const ImageBuilderHeader = ({ const dispatch = useAppDispatch(); const isOnPremise = useAppSelector(selectIsOnPremise); + const { + api: { useBackendPrefetch }, + } = usePlatform(); const distribution = useAppSelector(selectDistribution); const prefetchTargets = useBackendPrefetch('getArchitectures'); diff --git a/src/Hooks/MutationNotifications/useComposeBPWithNotification.tsx b/src/Hooks/MutationNotifications/useComposeBPWithNotification.tsx index 00f49ee123..a1cfed5da3 100644 --- a/src/Hooks/MutationNotifications/useComposeBPWithNotification.tsx +++ b/src/Hooks/MutationNotifications/useComposeBPWithNotification.tsx @@ -1,8 +1,11 @@ -import { useComposeBlueprintMutation } from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; import { useMutationWithNotification } from './useMutationWithNotification'; export const useComposeBPWithNotification = () => { + const { + mutations: { useComposeBlueprintMutation }, + } = usePlatform(); const { trigger: composeBlueprint, ...rest } = useMutationWithNotification( useComposeBlueprintMutation, { diff --git a/src/Hooks/MutationNotifications/useCreateBPWithNotification.tsx b/src/Hooks/MutationNotifications/useCreateBPWithNotification.tsx index de8c71b36d..9a637314af 100644 --- a/src/Hooks/MutationNotifications/useCreateBPWithNotification.tsx +++ b/src/Hooks/MutationNotifications/useCreateBPWithNotification.tsx @@ -1,4 +1,4 @@ -import { useCreateBlueprintMutation } from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; import { HookOptions, @@ -6,8 +6,10 @@ import { } from './useMutationWithNotification'; export const useCreateBPWithNotification = (options?: HookOptions) => { + const { + mutations: { useCreateBlueprintMutation }, + } = usePlatform(); const { trigger: createBlueprint, ...rest } = useMutationWithNotification( - // @ts-expect-error TODO: this will need to be revisited useCreateBlueprintMutation, { options, diff --git a/src/Hooks/MutationNotifications/useDeleteBPWithNotification.tsx b/src/Hooks/MutationNotifications/useDeleteBPWithNotification.tsx index 43dd18cd7a..48f755337a 100644 --- a/src/Hooks/MutationNotifications/useDeleteBPWithNotification.tsx +++ b/src/Hooks/MutationNotifications/useDeleteBPWithNotification.tsx @@ -1,4 +1,4 @@ -import { useDeleteBlueprintMutation } from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; import { HookOptions, @@ -6,6 +6,9 @@ import { } from './useMutationWithNotification'; export const useDeleteBPWithNotification = (options?: HookOptions) => { + const { + mutations: { useDeleteBlueprintMutation }, + } = usePlatform(); const { trigger: deleteBlueprint, ...rest } = useMutationWithNotification( useDeleteBlueprintMutation, { diff --git a/src/Hooks/MutationNotifications/useUpdateBPWithNotification.tsx b/src/Hooks/MutationNotifications/useUpdateBPWithNotification.tsx index b273e9bf99..fc901307f3 100644 --- a/src/Hooks/MutationNotifications/useUpdateBPWithNotification.tsx +++ b/src/Hooks/MutationNotifications/useUpdateBPWithNotification.tsx @@ -1,4 +1,4 @@ -import { useUpdateBlueprintMutation } from '@/store/api/backend'; +import { usePlatform } from '@/context/platform'; import { HookOptions, @@ -6,8 +6,10 @@ import { } from './useMutationWithNotification'; export const useUpdateBPWithNotification = (options?: HookOptions) => { + const { + mutations: { useUpdateBlueprintMutation }, + } = usePlatform(); const { trigger: updateBlueprint, ...rest } = useMutationWithNotification( - // @ts-expect-error TODO: this will need to be revisited useUpdateBlueprintMutation, { options, diff --git a/src/context/platform/hosted.ts b/src/context/platform/hosted.ts new file mode 100644 index 0000000000..3bdfaa46d9 --- /dev/null +++ b/src/context/platform/hosted.ts @@ -0,0 +1,77 @@ +import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; +import { useFlag as useUnleashFlag } from '@unleash/proxy-client-react'; + +import { imageBuilderApi } from '@/store/api/backend/hosted/enhancedImageBuilderApi'; +import { + useComposeBlueprintMutation, + useCreateBlueprintMutation, + useDeleteBlueprintMutation, + useGetArchitecturesQuery, + useGetBlueprintComposesQuery, + useGetBlueprintQuery, + useGetBlueprintsQuery, + useGetComposesQuery, + useGetComposeStatusQuery, + useGetDistributionsQuery, + useGetOscapCustomizationsQuery, + useGetOscapProfilesQuery, + useLazyGetBlueprintsQuery, + useLazyGetOscapCustomizationsQuery, + useUpdateBlueprintMutation, +} from '@/store/api/backend/hosted/imageBuilderApi'; +import { + contentSourcesApi, + useListSnapshotsByDateMutation, + useSearchRpmMutation, +} from '@/store/api/contentSources/hosted'; + +import type { PlatformHooks } from './types'; + +// Hoisted stable function references to avoid per-call allocations +const isBetaTrue = () => true; +const isBetaFalse = () => false; + +const useHostedGetEnvironment = () => { + const { isBeta, isProd, getEnvironment } = useChrome(); + if (isBeta() || getEnvironment() === 'qa') { + return { isBeta: isBetaTrue, isProd }; + } + return { isBeta: isBetaFalse, isProd }; +}; + +export const hostedPlatform: PlatformHooks = { + queries: { + // PlatformHooks uses the on-prem type which accepts wider input. + // The hosted version is narrower, but runtime behavior is identical. + useGetArchitecturesQuery: + useGetArchitecturesQuery as unknown as PlatformHooks['queries']['useGetArchitecturesQuery'], + useGetDistributionsQuery: + useGetDistributionsQuery as unknown as PlatformHooks['queries']['useGetDistributionsQuery'], + useGetBlueprintQuery, + useGetBlueprintsQuery, + useLazyGetBlueprintsQuery, + useGetOscapProfilesQuery, + useGetOscapCustomizationsQuery, + useLazyGetOscapCustomizationsQuery, + useGetComposesQuery, + useGetBlueprintComposesQuery, + useGetComposeStatusQuery, + }, + mutations: { + useCreateBlueprintMutation, + useUpdateBlueprintMutation, + useDeleteBlueprintMutation, + useComposeBlueprintMutation, + useSearchRpmMutation, + useListSnapshotsByDateMutation, + }, + env: { + useFlag: useUnleashFlag, + useGetEnvironment: useHostedGetEnvironment, + }, + api: { + backendApi: imageBuilderApi, + contentSourcesApi, + useBackendPrefetch: imageBuilderApi.usePrefetch, + }, +}; diff --git a/src/context/platform/index.ts b/src/context/platform/index.ts new file mode 100644 index 0000000000..a8466a90a3 --- /dev/null +++ b/src/context/platform/index.ts @@ -0,0 +1,15 @@ +import { createContext, useContext } from 'react'; + +import type { PlatformHooks } from './types'; + +const PlatformContext = createContext(null); + +export const usePlatform = (): PlatformHooks => { + const ctx = useContext(PlatformContext); + if (ctx === null) { + throw new Error('usePlatform must be used within a PlatformProvider'); + } + return ctx; +}; + +export const PlatformProvider = PlatformContext.Provider; diff --git a/src/context/platform/onprem.ts b/src/context/platform/onprem.ts new file mode 100644 index 0000000000..7732891e42 --- /dev/null +++ b/src/context/platform/onprem.ts @@ -0,0 +1,74 @@ +import { + useComposeBlueprintMutation, + useCreateBlueprintMutation, + useDeleteBlueprintMutation, + useGetArchitecturesQuery, + useGetBlueprintComposesQuery, + useGetBlueprintQuery, + useGetBlueprintsQuery, + useGetComposesQuery, + useGetComposeStatusQuery, + useGetDistributionsQuery, + useGetOscapCustomizationsQuery, + useGetOscapProfilesQuery, + useLazyGetBlueprintsQuery, + useLazyGetOscapCustomizationsQuery, + useUpdateBlueprintMutation, +} from '@/store/api/backend/onprem/composerApi'; +import { composerApi } from '@/store/api/backend/onprem/enhancedComposerApi'; +import { + contentSourcesApi, + useListSnapshotsByDateMutation, + useSearchRpmMutation, +} from '@/store/api/contentSources/onprem'; + +import type { PlatformHooks } from './types'; + +// Feature flag defaults for on-premises — Unleash is not available. +// All flags default to false (default-deny). Add cases here when a +// flag-gated feature needs to be enabled for on-prem users. +// NOTE: useGetEnvironment.ts has its own onPremFlag with a switch statement. +// That copy will be removed when Task 7 migrates useFlag to usePlatform().env. +export const onPremFlag = (_flag: string): boolean => { + return false; +}; + +// On-prem environment is static — hoist to avoid allocations per call +const onPremEnv = { isBeta: () => false, isProd: () => true }; + +// On-prem hooks use a different base query and reducer path than hosted, +// making them structurally incompatible with the hosted-anchored PlatformHooks +// type despite identical arg/response shapes. UseQuery is invariant in D +// so a single object-level cast is needed. +export const onPremPlatform = { + queries: { + useGetArchitecturesQuery, + useGetDistributionsQuery, + useGetBlueprintQuery, + useGetBlueprintsQuery, + useLazyGetBlueprintsQuery, + useGetOscapProfilesQuery, + useGetOscapCustomizationsQuery, + useLazyGetOscapCustomizationsQuery, + useGetComposesQuery, + useGetBlueprintComposesQuery, + useGetComposeStatusQuery, + }, + mutations: { + useCreateBlueprintMutation, + useUpdateBlueprintMutation, + useDeleteBlueprintMutation, + useComposeBlueprintMutation, + useSearchRpmMutation, + useListSnapshotsByDateMutation, + }, + env: { + useFlag: onPremFlag, + useGetEnvironment: () => onPremEnv, + }, + api: { + backendApi: composerApi, + contentSourcesApi, + useBackendPrefetch: composerApi.usePrefetch, + }, +} as unknown as PlatformHooks; diff --git a/src/context/platform/tests/PlatformContext.test.tsx b/src/context/platform/tests/PlatformContext.test.tsx new file mode 100644 index 0000000000..2a7ce2bf7b --- /dev/null +++ b/src/context/platform/tests/PlatformContext.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import { renderHook } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import { PlatformProvider, usePlatform } from '@/context/platform'; + +import { mockPlatform } from './mocks'; + +describe('PlatformContext', () => { + it('throws when usePlatform is called outside PlatformProvider', () => { + // React logs to console.error before the error propagates; + // suppress it so the global setup spy doesn't treat it as a failure. + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + expect(() => { + renderHook(() => usePlatform()); + }).toThrow('usePlatform must be used within a PlatformProvider'); + + spy.mockRestore(); + }); + + it('provides the platform object when wrapped in PlatformProvider', () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + const { result } = renderHook(() => usePlatform(), { wrapper }); + expect(result.current).toBe(mockPlatform); + expect(result.current.env.useFlag('test')).toBe(false); + }); +}); diff --git a/src/context/platform/tests/mocks/fixtures.ts b/src/context/platform/tests/mocks/fixtures.ts new file mode 100644 index 0000000000..667656a0d0 --- /dev/null +++ b/src/context/platform/tests/mocks/fixtures.ts @@ -0,0 +1,11 @@ +import type { PlatformHooks } from '@/context/platform/types'; + +export const mockPlatform = { + queries: {}, + mutations: {}, + env: { + useFlag: () => false, + useGetEnvironment: () => ({ isBeta: () => false, isProd: () => true }), + }, + api: {}, +} as unknown as PlatformHooks; diff --git a/src/context/platform/tests/mocks/index.ts b/src/context/platform/tests/mocks/index.ts new file mode 100644 index 0000000000..995b5bc6e1 --- /dev/null +++ b/src/context/platform/tests/mocks/index.ts @@ -0,0 +1 @@ +export * from './fixtures'; diff --git a/src/context/platform/types.ts b/src/context/platform/types.ts new file mode 100644 index 0000000000..52e7c5f7be --- /dev/null +++ b/src/context/platform/types.ts @@ -0,0 +1,66 @@ +import type { imageBuilderApi } from '@/store/api/backend/hosted/enhancedImageBuilderApi'; +import type { + useComposeBlueprintMutation as useHostedComposeBlueprintMutation, + useCreateBlueprintMutation as useHostedCreateBlueprintMutation, + useDeleteBlueprintMutation as useHostedDeleteBlueprintMutation, + useGetBlueprintComposesQuery as useHostedGetBlueprintComposesQuery, + useGetBlueprintQuery as useHostedGetBlueprintQuery, + useGetBlueprintsQuery as useHostedGetBlueprintsQuery, + useGetComposesQuery as useHostedGetComposesQuery, + useGetComposeStatusQuery as useHostedGetComposeStatusQuery, + useGetOscapCustomizationsQuery as useHostedGetOscapCustomizationsQuery, + useGetOscapProfilesQuery as useHostedGetOscapProfilesQuery, + useLazyGetBlueprintsQuery as useHostedLazyGetBlueprintsQuery, + useLazyGetOscapCustomizationsQuery as useHostedLazyGetOscapCustomizationsQuery, + useUpdateBlueprintMutation as useHostedUpdateBlueprintMutation, +} from '@/store/api/backend/hosted/imageBuilderApi'; +import type { + useGetArchitecturesQuery as useComposerGetArchitecturesQuery, + useGetDistributionsQuery as useComposerGetDistributionsQuery, +} from '@/store/api/backend/onprem/composerApi'; +import type { + contentSourcesApi, + useListSnapshotsByDateMutation as useHostedListSnapshotsByDateMutation, + useSearchRpmMutation as useHostedSearchRpmMutation, +} from '@/store/api/contentSources/hosted/contentSourcesApi'; + +// The useGetArchitecturesQuery and useGetDistributionsQuery types are +// widened to the composer (on-prem) versions because they accept wider +// input types. The response shapes are identical across platforms. +type UseGetArchitecturesQuery = typeof useComposerGetArchitecturesQuery; +type UseGetDistributionsQuery = typeof useComposerGetDistributionsQuery; + +type PlatformHooks = { + queries: { + useGetArchitecturesQuery: UseGetArchitecturesQuery; + useGetDistributionsQuery: UseGetDistributionsQuery; + useGetBlueprintQuery: typeof useHostedGetBlueprintQuery; + useGetBlueprintsQuery: typeof useHostedGetBlueprintsQuery; + useLazyGetBlueprintsQuery: typeof useHostedLazyGetBlueprintsQuery; + useGetOscapProfilesQuery: typeof useHostedGetOscapProfilesQuery; + useGetOscapCustomizationsQuery: typeof useHostedGetOscapCustomizationsQuery; + useLazyGetOscapCustomizationsQuery: typeof useHostedLazyGetOscapCustomizationsQuery; + useGetComposesQuery: typeof useHostedGetComposesQuery; + useGetBlueprintComposesQuery: typeof useHostedGetBlueprintComposesQuery; + useGetComposeStatusQuery: typeof useHostedGetComposeStatusQuery; + }; + mutations: { + useCreateBlueprintMutation: typeof useHostedCreateBlueprintMutation; + useUpdateBlueprintMutation: typeof useHostedUpdateBlueprintMutation; + useDeleteBlueprintMutation: typeof useHostedDeleteBlueprintMutation; + useComposeBlueprintMutation: typeof useHostedComposeBlueprintMutation; + useSearchRpmMutation: typeof useHostedSearchRpmMutation; + useListSnapshotsByDateMutation: typeof useHostedListSnapshotsByDateMutation; + }; + env: { + useFlag: (flag: string) => boolean; + useGetEnvironment: () => { isBeta: () => boolean; isProd: () => boolean }; + }; + api: { + backendApi: typeof imageBuilderApi; + contentSourcesApi: typeof contentSourcesApi; + useBackendPrefetch: typeof imageBuilderApi.usePrefetch; + }; +}; + +export type { PlatformHooks }; diff --git a/src/test/renderUtils.jsx b/src/test/renderUtils.jsx index 964b5f206e..b3e9d50e2e 100644 --- a/src/test/renderUtils.jsx +++ b/src/test/renderUtils.jsx @@ -6,6 +6,9 @@ import { Provider } from 'react-redux'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import LandingPage from '../Components/LandingPage/LandingPage'; +import { PlatformProvider } from '../context/platform'; +import { hostedPlatform } from '../context/platform/hosted'; +import { onPremPlatform } from '../context/platform/onprem'; import { serviceMiddleware as middleware, onPremMiddleware as onPremMiddleware, @@ -52,14 +55,18 @@ export const renderCustomRoutesWithReduxRouter = async ( initialEntries: [resolveRelPath(route)], }); + const platform = process.env.IS_ON_PREMISE ? onPremPlatform : hostedPlatform; + render( - + + + , ); diff --git a/src/test/testUtils/renderUtils.tsx b/src/test/testUtils/renderUtils.tsx index 4882b933f9..633686f496 100644 --- a/src/test/testUtils/renderUtils.tsx +++ b/src/test/testUtils/renderUtils.tsx @@ -4,6 +4,9 @@ import { configureStore, EnhancedStore } from '@reduxjs/toolkit'; import { render, RenderResult } from '@testing-library/react'; import { Provider } from 'react-redux'; +import { PlatformProvider } from '@/context/platform'; +import { hostedPlatform } from '@/context/platform/hosted'; +import { onPremPlatform } from '@/context/platform/onprem'; import { onPremMiddleware, onPremReducer, @@ -29,7 +32,13 @@ export const renderWithRedux = ( options: RenderWithReduxOptions = {}, ): RenderWithReduxResult => { const store = createTestStore(wizardStateOverrides, options); - const view = render({component}); + const isOnPremise = options.preloadedState?.env?.isOnPremise; + const platform = isOnPremise ? onPremPlatform : hostedPlatform; + const view = render( + + {component} + , + ); return { ...view, store }; };