Skip to content

Commit

Permalink
feat: added logic for displaying the tile correctly
Browse files Browse the repository at this point in the history
Signed-off-by: Olga Lavtar <[email protected]>
  • Loading branch information
olavtar committed Nov 14, 2024
1 parent 3c770de commit 7899002
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 67 deletions.
32 changes: 22 additions & 10 deletions backend/src/routes/api/integrations/nim/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) => {
Expand All @@ -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.',
});
}
});
}),
Expand Down
13 changes: 2 additions & 11 deletions backend/src/routes/api/integrations/nim/nimUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const isAppEnabled = (app: NIMAccountKind): boolean => {
export const getNIMAccount = async (
fastify: KubeFastifyInstance,
namespace: string,
): Promise<NIMAccountKind> => {
): Promise<NIMAccountKind | undefined> => {
const { customObjectsApi } = fastify.kube;
try {
const response = await customObjectsApi.listNamespacedCustomObject(
Expand All @@ -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);
}
Expand Down
17 changes: 7 additions & 10 deletions frontend/src/pages/exploreApplication/ExploreApplications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -99,12 +98,10 @@ const ExploreApplications: React.FC = () => {
const selectedId = queryParams.get('selectId');
const [selectedComponent, setSelectedComponent] = React.useState<OdhApplication>();
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) {
Expand All @@ -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<OdhApplication[]>(
() =>
_.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 (
<ExploreApplicationsInner
loaded={loaded && isIntegrationComponentsChecked}
loaded={loaded}
isEmpty={isEmpty}
loadError={loadError}
exploreComponents={exploreComponents}
Expand Down
20 changes: 15 additions & 5 deletions frontend/src/pages/exploreApplication/GetStartedPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import MarkdownView from '~/components/MarkdownView';
import { markdownConverter } from '~/utilities/markdown';
import { useAppContext } from '~/app/AppContext';
import { fireMiscTrackingEvent } from '~/concepts/analyticsTracking/segmentIOUtils';
import { useWatchIntegrationApp } from '~/utilities/useWatchIntegrationApp';
import { useIntegratedAppStatus } from '~/pages/exploreApplication/useIntegratedAppStatus';

const DEFAULT_BETA_TEXT =
'This application is available for early access prior to official ' +
Expand All @@ -37,22 +37,32 @@ type GetStartedPanelProps = {
const GetStartedPanel: React.FC<GetStartedPanelProps> = ({ 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 = (
<Button
variant={ButtonVariant.secondary}
onClick={onEnable}
isDisabled={!enablement || (isIntegrationAppInstalled && isintegrationAppChecked)}
isDisabled={!enablement || !canInstall}
isLoading={!loaded && !error}
>
Enable
</Button>
Expand Down
30 changes: 30 additions & 0 deletions frontend/src/pages/exploreApplication/useIntegratedAppStatus.ts
Original file line number Diff line number Diff line change
@@ -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<IntegrationAppStatus> => {
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: '',
});
};
10 changes: 8 additions & 2 deletions frontend/src/services/integrationAppService.ts
Original file line number Diff line number Diff line change
@@ -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 },
Expand All @@ -16,9 +22,9 @@ export const enableIntegrationApp = (

export const getIntegrationAppEnablementStatus = (
internalRoute: string,
): Promise<{ isAppEnabled: boolean; canEnable: boolean; error: string }> =>
): Promise<IntegrationAppStatus> =>
axios
.get(internalRoute)
.get<IntegrationAppStatus>(internalRoute)
.then((res) => res.data)
.catch((e) => {
throw new Error(e.response.data?.message || e.message);
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?: {
Expand Down Expand Up @@ -638,3 +640,10 @@ export type KeyValuePair = {
key: string;
value: string;
};

export type IntegrationAppStatus = {
isInstalled: boolean;
isEnabled: boolean;
canInstall: boolean;
error: string;
};
4 changes: 2 additions & 2 deletions frontend/src/utilities/useEnableApplication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: '',
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/utilities/useWatchIntegrationApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const useWatchIntegrationApp = (
setIsIntegrationAppInstalled(false);
setLoadError(new Error(response.error));
}
if (response.isAppEnabled) {
if (response.isEnabled) {
setIsIntegrationAppEnabled(true);
}
})
Expand Down
58 changes: 32 additions & 26 deletions frontend/src/utilities/useWatchIntegrationComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,37 @@ export const useWatchIntegrationComponents = (
);
const [newComponents, setNewComponents] = React.useState<OdhApplication[]>([]);

const updateComponentEnablementStatus = (
const updateComponentEnablementStatus = async (
integrationComponentList: OdhApplication[],
componentList: OdhApplication[],
) => {
integrationComponentList.forEach((component) => {
): Promise<void> => {
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(() => {
Expand All @@ -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();
}
Expand All @@ -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]);
Expand Down

0 comments on commit 7899002

Please sign in to comment.