diff --git a/backend/src/routes/api/integrations/nim/index.ts b/backend/src/routes/api/integrations/nim/index.ts index d944464b00..f169a8bc0d 100644 --- a/backend/src/routes/api/integrations/nim/index.ts +++ b/backend/src/routes/api/integrations/nim/index.ts @@ -2,7 +2,7 @@ import { FastifyReply, FastifyRequest } from 'fastify'; import { secureAdminRoute } from '../../../../utils/route-security'; import { KubeFastifyInstance } from '../../../../types'; import { isString } from 'lodash'; -import { isAppEnabled, getNIMAccount, createNIMAccount, createNIMSecret } from './nimUtils'; +import { createNIMAccount, createNIMSecret, getNIMAccount, isAppEnabled } from './nimUtils'; module.exports = async (fastify: KubeFastifyInstance) => { const { namespace } = fastify.kube; @@ -13,10 +13,14 @@ module.exports = async (fastify: KubeFastifyInstance) => { secureAdminRoute(fastify)(async (request: FastifyRequest, reply: FastifyReply) => { await getNIMAccount(fastify, namespace) .then((response) => { - if (isAppEnabled(response)) { - reply.send({ isAppEnabled: true, canEnable: false, error: '' }); + if (response) { + // installed + const isEnabled = isAppEnabled(response); + reply.send({ isInstalled: true, isEnabled: isEnabled, canInstall: false, error: '' }); } else { - reply.send({ isAppEnabled: false, canEnable: true, error: '' }); + // Not installed + fastify.log.info(`NIM account does not exist`); + reply.send({ isInstalled: false, isEnabled: false, canInstall: true, error: '' }); } }) .catch((e) => { @@ -27,13 +31,21 @@ module.exports = async (fastify: KubeFastifyInstance) => { e.response.body.trim() === PAGE_NOT_FOUND_MESSAGE.trim() ) { fastify.log.error(`NIM not installed, ${e.response?.body}`); - reply - .status(404) - .send({ isAppEnabled: false, canEnable: false, error: 'NIM not installed' }); - } else { - fastify.log.error(`NIM account does not exist, ${e.response.body.message}`); - reply.send({ isAppEnabled: false, canEnable: true, error: '' }); + reply.status(404).send({ + isInstalled: false, + isAppEnabled: false, + canInstall: false, + error: 'NIM not installed', + }); } + } else { + fastify.log.error(`An unexpected error occurred: ${e.message || e}`); + reply.status(500).send({ + isInstalled: false, + isAppEnabled: false, + canInstall: false, + error: 'An unexpected error occurred. Please try again later.', + }); } }); }), diff --git a/backend/src/routes/api/integrations/nim/nimUtils.ts b/backend/src/routes/api/integrations/nim/nimUtils.ts index 371bf498d8..afc04c3b12 100644 --- a/backend/src/routes/api/integrations/nim/nimUtils.ts +++ b/backend/src/routes/api/integrations/nim/nimUtils.ts @@ -15,7 +15,7 @@ export const isAppEnabled = (app: NIMAccountKind): boolean => { export const getNIMAccount = async ( fastify: KubeFastifyInstance, namespace: string, -): Promise => { +): Promise => { const { customObjectsApi } = fastify.kube; try { const response = await customObjectsApi.listNamespacedCustomObject( @@ -29,16 +29,7 @@ export const getNIMAccount = async ( items: NIMAccountKind[]; }; - if (!accounts.items || accounts.items.length === 0) { - const error: any = new Error('NIM account does not exist'); - error.response = { - statusCode: 404, - body: { message: 'NIM account does not exist' }, - }; - return Promise.reject(error); - } - // Return the first account - return Promise.resolve(accounts.items[0]); + return accounts.items[0] || undefined; } catch (e) { return Promise.reject(e); } diff --git a/frontend/src/pages/exploreApplication/ExploreApplications.tsx b/frontend/src/pages/exploreApplication/ExploreApplications.tsx index ff003a3b4f..97e8ba6452 100644 --- a/frontend/src/pages/exploreApplication/ExploreApplications.tsx +++ b/frontend/src/pages/exploreApplication/ExploreApplications.tsx @@ -17,7 +17,6 @@ import { removeQueryArgument, setQueryArgument } from '~/utilities/router'; import { ODH_PRODUCT_NAME } from '~/utilities/const'; import { useAppContext } from '~/app/AppContext'; import { fireMiscTrackingEvent } from '~/concepts/analyticsTracking/segmentIOUtils'; -import { useWatchIntegrationComponents } from '~/utilities/useWatchIntegrationComponents'; import GetStartedPanel from './GetStartedPanel'; import './ExploreApplications.scss'; @@ -99,12 +98,10 @@ const ExploreApplications: React.FC = () => { const selectedId = queryParams.get('selectId'); const [selectedComponent, setSelectedComponent] = React.useState(); const isEmpty = components.length === 0; - const { checkedComponents, isIntegrationComponentsChecked } = - useWatchIntegrationComponents(components); const updateSelection = React.useCallback( (currentSelectedId?: string | null): void => { - const selection = checkedComponents.find( + const selection = components.find( (c) => c.metadata.name && c.metadata.name === currentSelectedId, ); if (currentSelectedId && selection) { @@ -117,26 +114,26 @@ const ExploreApplications: React.FC = () => { removeQueryArgument(navigate, 'selectId'); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [checkedComponents], + [components], ); const exploreComponents = React.useMemo( () => - _.cloneDeep(checkedComponents) + _.cloneDeep(components) .filter((component) => !component.spec.hidden) .toSorted((a, b) => a.spec.displayName.localeCompare(b.spec.displayName)), - [checkedComponents], + [components], ); React.useEffect(() => { - if (checkedComponents.length > 0) { + if (components.length > 0) { updateSelection(selectedId); } - }, [updateSelection, selectedId, checkedComponents]); + }, [updateSelection, selectedId, components]); return ( = ({ selectedApp, onClose, onEnable }) => { const { dashboardConfig } = useAppContext(); const { enablement } = dashboardConfig.spec.dashboardConfig; - const { isIntegrationAppInstalled, isintegrationAppChecked } = - useWatchIntegrationApp(selectedApp); + const [{ isInstalled, canInstall, error }, loaded] = useIntegratedAppStatus(selectedApp); if (!selectedApp) { return null; } + // console.log('Render state:', { + // loaded, + // canInstall, + // enablement, + // isInstalled, + // error, + // selectedAppEnable: selectedApp.spec.enable, + // isEnabled: selectedApp.spec.isEnabled, + // }); + const renderEnableButton = () => { - if (!selectedApp.spec.enable || selectedApp.spec.isEnabled) { + if (!selectedApp.spec.enable || selectedApp.spec.isEnabled || isInstalled) { return null; } const button = ( diff --git a/frontend/src/pages/exploreApplication/useIntegratedAppStatus.ts b/frontend/src/pages/exploreApplication/useIntegratedAppStatus.ts new file mode 100644 index 0000000000..9c40b83819 --- /dev/null +++ b/frontend/src/pages/exploreApplication/useIntegratedAppStatus.ts @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { IntegrationAppStatus, OdhApplication } from '~/types'; +import useFetchState, { FetchState, NotReadyError } from '~/utilities/useFetchState'; +import { getIntegrationAppEnablementStatus } from '~/services/integrationAppService'; + +export const useIntegratedAppStatus = (app?: OdhApplication): FetchState => { + const callback = React.useCallback(() => { + if (!app) { + return Promise.reject(new NotReadyError('Need an app to check')); + } + if (!app.spec.internalRoute) { + // Silently ignore apps who don't have an internal route -- the logic is not needed + return Promise.resolve({ + isInstalled: false, + isEnabled: false, + canInstall: true, + error: '', + }); + } + + return getIntegrationAppEnablementStatus(app.spec.internalRoute); + }, [app]); + + return useFetchState(callback, { + isInstalled: false, + isEnabled: false, + canInstall: false, + error: '', + }); +}; diff --git a/frontend/src/services/integrationAppService.ts b/frontend/src/services/integrationAppService.ts index a8d8e3eb1a..ab71474008 100644 --- a/frontend/src/services/integrationAppService.ts +++ b/frontend/src/services/integrationAppService.ts @@ -1,5 +1,11 @@ import axios from '~/utilities/axios'; +type IntegrationAppStatus = { + isInstalled: boolean; + isEnabled: boolean; + canInstall: boolean; + error: string; +}; export const enableIntegrationApp = ( internalRoute: string, enableValues: { [key: string]: string }, @@ -16,9 +22,9 @@ export const enableIntegrationApp = ( export const getIntegrationAppEnablementStatus = ( internalRoute: string, -): Promise<{ isAppEnabled: boolean; canEnable: boolean; error: string }> => +): Promise => axios - .get(internalRoute) + .get(internalRoute) .then((res) => res.data) .catch((e) => { throw new Error(e.response.data?.message || e.message); diff --git a/frontend/src/types.ts b/frontend/src/types.ts index d434fb3f0c..5de50a3ac2 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -159,6 +159,8 @@ export type OdhApplication = { betaText?: string | null; shownOnEnabledPage: boolean | null; isEnabled: boolean | null; + isInstalled: boolean | null; + canInstall: boolean | null; kfdefApplications?: string[]; csvName?: string; enable?: { @@ -638,3 +640,10 @@ export type KeyValuePair = { key: string; value: string; }; + +export type IntegrationAppStatus = { + isInstalled: boolean; + isEnabled: boolean; + canInstall: boolean; + error: string; +}; diff --git a/frontend/src/utilities/useEnableApplication.tsx b/frontend/src/utilities/useEnableApplication.tsx index 53e6bc10de..f6d849fc2e 100644 --- a/frontend/src/utilities/useEnableApplication.tsx +++ b/frontend/src/utilities/useEnableApplication.tsx @@ -68,11 +68,11 @@ export const useEnableApplication = ( if (internalRoute && isInternalRouteIntegrationsApp(internalRoute)) { getIntegrationAppEnablementStatus(internalRoute) .then((response) => { - if (!response.isAppEnabled && response.canEnable) { + if (!response.isEnabled && response.canInstall) { watchHandle = setTimeout(watchStatus, 10 * 1000); return; } - if (response.isAppEnabled && !response.canEnable) { + if (response.isEnabled && !response.canInstall) { setEnableStatus({ status: EnableApplicationStatus.SUCCESS, error: '', diff --git a/frontend/src/utilities/useWatchIntegrationApp.tsx b/frontend/src/utilities/useWatchIntegrationApp.tsx index 142528ee2e..8d6a386e6d 100644 --- a/frontend/src/utilities/useWatchIntegrationApp.tsx +++ b/frontend/src/utilities/useWatchIntegrationApp.tsx @@ -25,7 +25,7 @@ export const useWatchIntegrationApp = ( setIsIntegrationAppInstalled(false); setLoadError(new Error(response.error)); } - if (response.isAppEnabled) { + if (response.isEnabled) { setIsIntegrationAppEnabled(true); } }) diff --git a/frontend/src/utilities/useWatchIntegrationComponents.tsx b/frontend/src/utilities/useWatchIntegrationComponents.tsx index b5d6993d57..251e18f49a 100644 --- a/frontend/src/utilities/useWatchIntegrationComponents.tsx +++ b/frontend/src/utilities/useWatchIntegrationComponents.tsx @@ -17,33 +17,37 @@ export const useWatchIntegrationComponents = ( ); const [newComponents, setNewComponents] = React.useState([]); - const updateComponentEnablementStatus = ( + const updateComponentEnablementStatus = async ( integrationComponentList: OdhApplication[], componentList: OdhApplication[], - ) => { - integrationComponentList.forEach((component) => { + ): Promise => { + const updatePromises = integrationComponentList.map(async (component) => { if (component.spec.internalRoute) { - getIntegrationAppEnablementStatus(component.spec.internalRoute).then((response) => { - if (response.error) { - setNewComponents(componentList); - } else { - setNewComponents( - componentList.map((app) => - app.metadata.name === component.metadata.name - ? { - ...app, - spec: { - ...app.spec, - isAppEnabled: response.isAppEnabled, - }, - } - : app, - ), + const response = await getIntegrationAppEnablementStatus(component.spec.internalRoute); + + if (response.error) { + setNewComponents(componentList); + } else { + const updatedComponents = componentList + .filter( + (app) => !(app.metadata.name === component.metadata.name && !response.isInstalled), + ) + .map((app) => + app.metadata.name === component.metadata.name + ? { + ...app, + spec: { + ...app.spec, + isEnabled: response.isEnabled, + }, + } + : app, ); - } - }); + setNewComponents(updatedComponents); + } } }); + await Promise.all(updatePromises); }; React.useEffect(() => { @@ -52,9 +56,10 @@ export const useWatchIntegrationComponents = ( setIsIntegrationComponentsChecked(true); } else { const watchComponents = () => { - updateComponentEnablementStatus(integrationComponents, components); - setIsIntegrationComponentsChecked(true); - watchHandle = setTimeout(watchComponents, POLL_INTERVAL); + updateComponentEnablementStatus(integrationComponents, components).then(() => { + setIsIntegrationComponentsChecked(true); + watchHandle = setTimeout(watchComponents, POLL_INTERVAL); + }); }; watchComponents(); } @@ -69,8 +74,9 @@ export const useWatchIntegrationComponents = ( if (integrationComponents.length === 0) { setIsIntegrationComponentsChecked(true); } else { - updateComponentEnablementStatus(integrationComponents, components); - setIsIntegrationComponentsChecked(true); + updateComponentEnablementStatus(integrationComponents, components).then(() => { + setIsIntegrationComponentsChecked(true); + }); } } }, [forceUpdate, components, integrationComponents]);