diff --git a/plugins/sandbox/src/api/RegistrationBackendClient.tsx b/plugins/sandbox/src/api/RegistrationBackendClient.tsx index 7713ad8..df0c270 100644 --- a/plugins/sandbox/src/api/RegistrationBackendClient.tsx +++ b/plugins/sandbox/src/api/RegistrationBackendClient.tsx @@ -28,6 +28,7 @@ export type RegistrationBackendClientOptions = { export interface UIConfig { workatoWebHookURL?: string; + disabledIntegrations?: string[]; } export interface RegistrationService { diff --git a/plugins/sandbox/src/api/__tests__/RegistrationBackendClient.test.tsx b/plugins/sandbox/src/api/__tests__/RegistrationBackendClient.test.tsx index 7a47fb3..4ae269d 100644 --- a/plugins/sandbox/src/api/__tests__/RegistrationBackendClient.test.tsx +++ b/plugins/sandbox/src/api/__tests__/RegistrationBackendClient.test.tsx @@ -289,6 +289,28 @@ describe('RegistrationBackendClient', () => { ); }); + it('should return UI config with disabledIntegrations', async () => { + const mockUIConfig = { + workatoWebHookURL: 'https://webhooks.test', + disabledIntegrations: ['openshift-console', 'devspaces'], + }; + + mockSecureFetchApi.fetch.mockResolvedValue( + createMockResponse({ + ok: true, + json: () => Promise.resolve(mockUIConfig), + }), + ); + + const result = await client.getUIConfig(); + + expect(result).toEqual(mockUIConfig); + expect(result.disabledIntegrations).toEqual([ + 'openshift-console', + 'devspaces', + ]); + }); + it('should return empty config if workatoWebHookURL is not present', async () => { const mockUIConfig = {}; @@ -303,6 +325,7 @@ describe('RegistrationBackendClient', () => { expect(result).toEqual({}); expect(result.workatoWebHookURL).toBeUndefined(); + expect(result.disabledIntegrations).toBeUndefined(); }); it('should return empty config on unsuccessful response', async () => { diff --git a/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogGrid.tsx b/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogGrid.tsx index acefafb..b14f469 100644 --- a/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogGrid.tsx +++ b/plugins/sandbox/src/components/SandboxCatalog/SandboxCatalogGrid.tsx @@ -13,16 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import Box from '@mui/material/Box'; import Grid from '@mui/material/Grid'; import { Product, productData } from './productData'; import useGreenCorners from '../../hooks/useGreenCorners'; import { SandboxCatalogCard } from './SandboxCatalogCard'; import useProductURLs from '../../hooks/useProductURLs'; +import { useSandboxContext } from '../../hooks/useSandboxContext'; export const SandboxCatalogGrid: React.FC = () => { - const { greenCorners, setGreenCorners } = useGreenCorners(productData); + const { disabledIntegrations } = useSandboxContext(); + const enabledProducts = useMemo( + () => productData.filter(p => !(disabledIntegrations ?? []).includes(p.id)), + [disabledIntegrations], + ); + const { greenCorners, setGreenCorners } = useGreenCorners(enabledProducts); const productURLs = useProductURLs(); const showGreenCorner = (id: Product) => { @@ -31,9 +37,17 @@ export const SandboxCatalogGrid: React.FC = () => { ); }; + // Do not load the grid until we have a list of which integrations are + // disabled. Otherwise what happens is that the whole integration catalog + // is loaded, and once the UI configuration is fetched, the disabled + // integrations disappear. + if (disabledIntegrations === undefined) { + return null; + } + return ( - {productData?.map(product => ( + {enabledProducts.map(product => ( ({ @@ -80,6 +82,10 @@ describe('SandboxCatalogGrid', () => { beforeEach(() => { jest.clearAllMocks(); + (useSandboxContext as jest.Mock).mockReturnValue({ + disabledIntegrations: [], + }); + // Setup mock return values for hooks (useGreenCorners as jest.Mock).mockReturnValue({ greenCorners: mockGreenCorners, @@ -107,7 +113,7 @@ describe('SandboxCatalogGrid', () => { expect(cards).toHaveLength(productData.length); }); - it('calls useGreenCorners hook with productData', () => { + it('calls useGreenCorners hook with enabled products', () => { renderGrid(); expect(useGreenCorners).toHaveBeenCalledWith(productData); }); @@ -170,4 +176,57 @@ describe('SandboxCatalogGrid', () => { { id: 'openshift-ai', show: true }, // Already true, unchanged ]); }); + + it('renders nothing while UI config is loading', () => { + (useSandboxContext as jest.Mock).mockReturnValue({ + disabledIntegrations: undefined, + }); + + (useGreenCorners as jest.Mock).mockReturnValue({ + greenCorners: mockGreenCorners, + setGreenCorners: mockSetGreenCorners, + }); + + const { container } = renderGrid(); + + const cards = screen.queryAllByTestId('catalog-card'); + expect(cards).toHaveLength(0); + expect(container.querySelector('.MuiGrid-container')).toBeNull(); + expect(useGreenCorners).toHaveBeenCalledWith(productData); + }); + + it('hides cards for disabled integrations', () => { + (useSandboxContext as jest.Mock).mockReturnValue({ + disabledIntegrations: ['openshift-ai'], + }); + + (useGreenCorners as jest.Mock).mockReturnValue({ + greenCorners: [{ id: 'openshift-console', show: false }], + setGreenCorners: mockSetGreenCorners, + }); + + renderGrid(); + + const cards = screen.getAllByTestId('catalog-card'); + expect(cards).toHaveLength(1); + + const firstCallProps = (SandboxCatalogCard as jest.Mock).mock.calls[0][0]; + expect(firstCallProps.id).toBe('openshift-console'); + }); + + it('hides all cards when all integrations are disabled', () => { + (useSandboxContext as jest.Mock).mockReturnValue({ + disabledIntegrations: ['openshift-console', 'openshift-ai'], + }); + + (useGreenCorners as jest.Mock).mockReturnValue({ + greenCorners: [], + setGreenCorners: mockSetGreenCorners, + }); + + renderGrid(); + + const cards = screen.queryAllByTestId('catalog-card'); + expect(cards).toHaveLength(0); + }); }); diff --git a/plugins/sandbox/src/hooks/useSandboxContext.tsx b/plugins/sandbox/src/hooks/useSandboxContext.tsx index 747b196..590aded 100644 --- a/plugins/sandbox/src/hooks/useSandboxContext.tsx +++ b/plugins/sandbox/src/hooks/useSandboxContext.tsx @@ -48,6 +48,7 @@ interface SandboxContextType { ansibleStatus: AnsibleStatus; segmentTrackClick?: (data: SegmentTrackingData) => Promise; marketoWebhookURL?: string; + disabledIntegrations?: string[]; } const SandboxContext = createContext(undefined); @@ -73,6 +74,9 @@ export const SandboxProvider: React.FC<{ children: React.ReactNode }> = ({ const registerApi = useApi(registerApiRef); const [segmentWriteKey, setSegmentWriteKey] = useState(); const [marketoWebhookURL, setMarketoWebhookURL] = useState(); + const [disabledIntegrations, setDisabledIntegrations] = useState< + string[] | undefined + >(); const [statusUnknown, setStatusUnknown] = React.useState(true); const [userFound, setUserFound] = useState(false); const [userData, setData] = useState(undefined); @@ -228,12 +232,24 @@ export const SandboxProvider: React.FC<{ children: React.ReactNode }> = ({ fetchSegmentWriteKey(); }, [registerApi, isProd]); - // Fetch Marketo webhook URL from UI config + // Fetch the marketo URL and the disabled integrations from the registration + // service. useEffect(() => { const fetchUIConfig = async () => { - const uiConfig = await registerApi.getUIConfig(); - if (uiConfig.workatoWebHookURL) { - setMarketoWebhookURL(uiConfig.workatoWebHookURL); + try { + const uiConfig = await registerApi.getUIConfig(); + if (uiConfig.workatoWebHookURL) { + setMarketoWebhookURL(uiConfig.workatoWebHookURL); + } + setDisabledIntegrations( + Array.isArray(uiConfig.disabledIntegrations) + ? uiConfig.disabledIntegrations + : [], + ); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Error fetching UI config:', err); + setDisabledIntegrations([]); } }; fetchUIConfig(); @@ -291,6 +307,7 @@ export const SandboxProvider: React.FC<{ children: React.ReactNode }> = ({ ansibleStatus, segmentTrackClick: segmentAnalytics.trackClick, marketoWebhookURL, + disabledIntegrations, }} > {children}