From 58092736e5ae95b00201e1fceaf63b5f6afb624b Mon Sep 17 00:00:00 2001 From: ppadti Date: Mon, 27 May 2024 18:12:39 +0530 Subject: [PATCH] Convert templates to use websocket --- .../customServingRuntimes.cy.ts | 71 +++++++--- .../customServingRuntimesUtils.ts | 5 +- frontend/src/api/k8s/__tests__/groups.spec.ts | 52 +++++--- .../src/api/k8s/__tests__/projects.spec.ts | 16 ++- .../src/api/k8s/__tests__/templates.spec.ts | 124 +++++++++++++----- frontend/src/api/k8s/groups.ts | 15 ++- frontend/src/api/k8s/projects.ts | 8 +- frontend/src/api/k8s/templates.ts | 54 ++++++-- .../src/concepts/projects/ProjectsContext.tsx | 3 +- .../modelServing/ModelServingContext.tsx | 21 ++- .../CustomServingRuntimeContext.tsx | 29 ++-- .../CustomServingRuntimeEditTemplate.tsx | 2 +- .../CustomServingRuntimeEnabledToggle.tsx | 2 +- .../CustomServingRuntimeListView.tsx | 2 +- .../CustomServingRuntimeView.tsx | 2 +- .../DeleteCustomServingRuntimeModal.tsx | 2 +- .../customServingRuntimes/useTemplates.ts | 61 --------- .../screens/global/ServeModelButton.tsx | 2 +- .../projects/EmptyMultiModelServingCard.tsx | 2 +- .../projects/EmptySingleModelServingCard.tsx | 2 +- .../screens/projects/ModelServingPlatform.tsx | 2 +- .../ModelServingPlatformButtonAction.tsx | 2 +- .../pages/projects/ProjectDetailsContext.tsx | 21 ++- .../overview/serverModels/AddModelFooter.tsx | 2 +- frontend/src/types.ts | 12 ++ .../__tests__/useK8sWatchResourceList.spec.ts | 87 ++++++++++++ frontend/src/utilities/const.ts | 14 +- .../src/utilities/useK8sWatchResourceList.ts | 40 ++++++ 28 files changed, 436 insertions(+), 219 deletions(-) delete mode 100644 frontend/src/pages/modelServing/customServingRuntimes/useTemplates.ts create mode 100644 frontend/src/utilities/__tests__/useK8sWatchResourceList.spec.ts create mode 100644 frontend/src/utilities/useK8sWatchResourceList.ts diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/customServingRuntimes/customServingRuntimes.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/customServingRuntimes/customServingRuntimes.cy.ts index 0e22582789..dabd9e5512 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/customServingRuntimes/customServingRuntimes.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/customServingRuntimes/customServingRuntimes.cy.ts @@ -1,4 +1,3 @@ -import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; import { mockServingRuntimeTemplateK8sResource } from '~/__mocks__/mockServingRuntimeTemplateK8sResource'; import { servingRuntimes } from '~/__tests__/cypress/cypress/pages/servingRuntimes'; import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; @@ -6,10 +5,8 @@ import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/Delete import { mockServingRuntimeK8sResource } from '~/__mocks__/mockServingRuntimeK8sResource'; import { asProductAdminUser, asProjectAdminUser } from '~/__tests__/cypress/cypress/utils/users'; import { pageNotfound } from '~/__tests__/cypress/cypress/pages/pageNotFound'; -import { - customServingRuntimesInitialMock, - customServingRuntimesIntercept, -} from '~/__tests__/cypress/cypress/tests/mocked/customServingRuntimes/customServingRuntimesUtils'; +import { customServingRuntimesIntercept } from '~/__tests__/cypress/cypress/tests/mocked/customServingRuntimes/customServingRuntimesUtils'; +import { TemplateModel } from '~/__tests__/cypress/cypress/utils/models'; const addfilePath = '../../__mocks__/mock-custom-serving-runtime-add.yaml'; const editfilePath = '../../__mocks__/mock-custom-serving-runtime-edit.yaml'; @@ -81,7 +78,6 @@ describe('Custom serving runtimes', () => { servingRuntimes.findSubmitButton().should('be.enabled'); servingRuntimes.findSubmitButton().click(); - cy.wait('@createSingleModelServingRuntime').then((interception) => { expect(interception.request.url).to.include('?dryRun=All'); expect(interception.request.body).to.containSubset({ @@ -112,6 +108,19 @@ describe('Custom serving runtimes', () => { ], }); }); + + cy.wsK8s( + 'ADDED', + TemplateModel, + mockServingRuntimeTemplateK8sResource({ + name: 'template-new', + displayName: 'New OVMS Server', + platforms: [ServingRuntimePlatform.SINGLE], + apiProtocol: ServingRuntimeAPIProtocol.REST, + }), + ); + + servingRuntimes.getRowById('template-new').shouldBeSingleModel(true); }); it('should add a new multi model serving runtime', () => { @@ -172,6 +181,18 @@ describe('Custom serving runtimes', () => { ], }); }); + + cy.wsK8s( + 'ADDED', + TemplateModel, + mockServingRuntimeTemplateK8sResource({ + name: 'template-new', + displayName: 'New OVMS Server', + platforms: [ServingRuntimePlatform.MULTI], + }), + ); + + servingRuntimes.getRowById('template-new').shouldBeMultiModel(true); }); it('should duplicate a serving runtime', () => { @@ -185,19 +206,6 @@ describe('Custom serving runtimes', () => { 'duplicateTemplate', ); - const ServingRuntimeTemplateMock = mockServingRuntimeTemplateK8sResource({ - name: 'serving-runtime-template-1', - displayName: 'Multi platform', - platforms: [ServingRuntimePlatform.SINGLE], - apiProtocol: ServingRuntimeAPIProtocol.GRPC, - }); - - cy.interceptOdh( - 'GET /api/templates/:namespace', - { path: { namespace: 'opendatahub' } }, - mockK8sResourceList([...customServingRuntimesInitialMock, ServingRuntimeTemplateMock]), - ).as('refreshServingRuntime'); - servingRuntimes.getRowById('template-1').find().findKebabAction('Duplicate').click(); servingRuntimes.findAppTitle().should('have.text', 'Duplicate serving runtime'); cy.url().should('include', '/addServingRuntime'); @@ -236,10 +244,20 @@ describe('Custom serving runtimes', () => { ], }); }); - cy.wait('@refreshServingRuntime'); + + cy.wsK8s( + 'ADDED', + TemplateModel, + mockServingRuntimeTemplateK8sResource({ + name: 'template-1-copy', + displayName: 'Copy of Multi platform', + platforms: [ServingRuntimePlatform.SINGLE], + apiProtocol: ServingRuntimeAPIProtocol.GRPC, + }), + ); servingRuntimes - .getRowById('serving-runtime-template-1') + .getRowById('template-1-copy') .shouldHaveAPIProtocol(ServingRuntimeAPIProtocol.GRPC); }); @@ -310,5 +328,16 @@ describe('Custom serving runtimes', () => { deleteModal.findSubmitButton().should('be.enabled').click(); cy.wait('@deleteServingRuntime'); + cy.wsK8s( + 'DELETED', + TemplateModel, + mockServingRuntimeTemplateK8sResource({ + name: 'template-1', + displayName: 'Multi platform', + platforms: [ServingRuntimePlatform.SINGLE], + apiProtocol: ServingRuntimeAPIProtocol.REST, + }), + ); + servingRuntimes.getRowById('template-1').find().should('not.exist'); }); }); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/customServingRuntimes/customServingRuntimesUtils.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/customServingRuntimes/customServingRuntimesUtils.ts index 2036d8d18b..edddd79d0f 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/customServingRuntimes/customServingRuntimesUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/customServingRuntimes/customServingRuntimesUtils.ts @@ -1,7 +1,7 @@ import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; import { mockServingRuntimeTemplateK8sResource } from '~/__mocks__/mockServingRuntimeTemplateK8sResource'; import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; -import { ProjectModel } from '~/__tests__/cypress/cypress/utils/models'; +import { ProjectModel, TemplateModel } from '~/__tests__/cypress/cypress/utils/models'; import { mockProjectK8sResource } from '~/__mocks__'; export const customServingRuntimesInitialMock = [ @@ -28,10 +28,11 @@ export const customServingRuntimesInitialMock = [ ]; export const customServingRuntimesIntercept = (): void => { + cy.interceptK8sList(TemplateModel, mockK8sResourceList(customServingRuntimesInitialMock)); + cy.interceptK8sList(ProjectModel, mockK8sResourceList([mockProjectK8sResource({})])); cy.interceptOdh( 'GET /api/templates/:namespace', { path: { namespace: 'opendatahub' } }, mockK8sResourceList(customServingRuntimesInitialMock), ); - cy.interceptK8sList(ProjectModel, mockK8sResourceList([mockProjectK8sResource({})])); }; diff --git a/frontend/src/api/k8s/__tests__/groups.spec.ts b/frontend/src/api/k8s/__tests__/groups.spec.ts index b56c502bf8..becf99f656 100644 --- a/frontend/src/api/k8s/__tests__/groups.spec.ts +++ b/frontend/src/api/k8s/__tests__/groups.spec.ts @@ -1,11 +1,13 @@ -import { useK8sWatchResource } from '@openshift/dynamic-plugin-sdk-utils'; import { groupVersionKind, useAccessReview, useGroups } from '~/api'; import { testHook } from '~/__tests__/unit/testUtils/hooks'; import { GroupModel } from '~/api/models'; import { mockGroup } from '~/__mocks__/mockGroup'; +import useK8sWatchResourceList from '~/utilities/useK8sWatchResourceList'; +import { GroupKind } from '~/k8sTypes'; -jest.mock('@openshift/dynamic-plugin-sdk-utils', () => ({ - useK8sWatchResource: jest.fn(), +jest.mock('~/utilities/useK8sWatchResourceList', () => ({ + __esModule: true, + default: jest.fn(), })); jest.mock('~/api/useAccessReview', () => ({ @@ -13,17 +15,17 @@ jest.mock('~/api/useAccessReview', () => ({ })); const useAccessReviewMock = jest.mocked(useAccessReview); -const useK8sWatchResourceMock = useK8sWatchResource as jest.Mock; +const useK8sWatchResourceListMock = jest.mocked(useK8sWatchResourceList); describe('useGroups', () => { it('should wrap useK8sWatchResource to watch groups', async () => { - const mockReturnValue: ReturnType = [[], false, undefined]; + const mockReturnValue: ReturnType = [[], false, undefined]; useAccessReviewMock.mockReturnValue([true, true]); - useK8sWatchResourceMock.mockReturnValue(mockReturnValue); + useK8sWatchResourceListMock.mockReturnValue(mockReturnValue); const { result } = testHook(useGroups)(); - expect(useK8sWatchResourceMock).toHaveBeenCalledTimes(1); - expect(useK8sWatchResourceMock).toHaveBeenCalledWith( + expect(useK8sWatchResourceListMock).toHaveBeenCalledTimes(1); + expect(useK8sWatchResourceListMock).toHaveBeenCalledWith( { isList: true, groupVersionKind: groupVersionKind(GroupModel), @@ -34,16 +36,16 @@ describe('useGroups', () => { }); it('should render list of groups', () => { - const mockReturnValue: ReturnType = [ + const mockReturnValue: ReturnType = [ [mockGroup({})], true, undefined, ]; useAccessReviewMock.mockReturnValue([true, true]); - useK8sWatchResourceMock.mockReturnValue(mockReturnValue); + useK8sWatchResourceListMock.mockReturnValue(mockReturnValue); const { result } = testHook(useGroups)(); - expect(useK8sWatchResourceMock).toHaveBeenCalledTimes(1); - expect(useK8sWatchResourceMock).toHaveBeenCalledWith( + expect(useK8sWatchResourceListMock).toHaveBeenCalledTimes(1); + expect(useK8sWatchResourceListMock).toHaveBeenCalledWith( { isList: true, groupVersionKind: groupVersionKind(GroupModel), @@ -55,10 +57,30 @@ describe('useGroups', () => { it('should handle 403 error', () => { useAccessReviewMock.mockReturnValue([false, true]); - useK8sWatchResourceMock.mockReturnValue([undefined, true, undefined]); + useK8sWatchResourceListMock.mockReturnValue([[], true, undefined]); const { result } = testHook(useGroups)(); - expect(useK8sWatchResourceMock).toHaveBeenCalledTimes(1); - expect(useK8sWatchResourceMock).toHaveBeenCalledWith(null, GroupModel); + expect(useK8sWatchResourceListMock).toHaveBeenCalledTimes(1); + expect(useK8sWatchResourceListMock).toHaveBeenCalledWith(null, GroupModel); expect(result.current).toStrictEqual([[], true, undefined]); }); + + it('should handle errors and rethrow', () => { + const mockReturnValue: ReturnType = [ + [], + true, + new Error('Unknown error occured'), + ]; + useAccessReviewMock.mockReturnValue([true, true]); + useK8sWatchResourceListMock.mockReturnValue(mockReturnValue); + const { result } = testHook(useGroups)(); + expect(useK8sWatchResourceListMock).toHaveBeenCalledTimes(1); + expect(useK8sWatchResourceListMock).toHaveBeenCalledWith( + { + isList: true, + groupVersionKind: groupVersionKind(GroupModel), + }, + GroupModel, + ); + expect(result.current).toStrictEqual(mockReturnValue); + }); }); diff --git a/frontend/src/api/k8s/__tests__/projects.spec.ts b/frontend/src/api/k8s/__tests__/projects.spec.ts index 36446a94eb..297f7a578f 100644 --- a/frontend/src/api/k8s/__tests__/projects.spec.ts +++ b/frontend/src/api/k8s/__tests__/projects.spec.ts @@ -3,7 +3,6 @@ import { k8sCreateResource, k8sUpdateResource, k8sDeleteResource, - useK8sWatchResource, } from '@openshift/dynamic-plugin-sdk-utils'; import axios from 'axios'; import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; @@ -23,13 +22,18 @@ import { ODH_PRODUCT_NAME } from '~/utilities/const'; import { NamespaceApplicationCase } from '~/pages/projects/types'; import { ProjectKind } from '~/k8sTypes'; import { groupVersionKind } from '~/api/k8sUtils'; +import useK8sWatchResourceList from '~/utilities/useK8sWatchResourceList'; jest.mock('@openshift/dynamic-plugin-sdk-utils', () => ({ k8sListResource: jest.fn(), k8sCreateResource: jest.fn(), k8sUpdateResource: jest.fn(), k8sDeleteResource: jest.fn(), - useK8sWatchResource: jest.fn(), +})); + +jest.mock('~/utilities/useK8sWatchResourceList', () => ({ + __esModule: true, + default: jest.fn(), })); jest.mock('~/api/k8s/servingRuntimes.ts', () => ({ @@ -43,14 +47,14 @@ const k8sListResourceMock = jest.mocked(k8sListResource); const k8sCreateResourceMock = jest.mocked(k8sCreateResource); const k8sUpdateResourceMock = jest.mocked(k8sUpdateResource); const k8sDeleteResourceMock = jest.mocked(k8sDeleteResource); -const useK8sWatchResourceMock = jest.mocked(useK8sWatchResource); +const useK8sWatchResourceListMock = jest.mocked(useK8sWatchResourceList); describe('useProjects', () => { it('should wrap useK8sWatchResource to watch projects', async () => { - const mockReturnValue: ReturnType = [[], false, false]; - useK8sWatchResourceMock.mockReturnValue(mockReturnValue); + const mockReturnValue: ReturnType = [[], false, undefined]; + useK8sWatchResourceListMock.mockReturnValue(mockReturnValue); expect(useProjects()).toBe(mockReturnValue); - expect(useK8sWatchResourceMock).toHaveBeenCalledWith( + expect(useK8sWatchResourceListMock).toHaveBeenCalledWith( { isList: true, groupVersionKind: groupVersionKind(ProjectModel), diff --git a/frontend/src/api/k8s/__tests__/templates.spec.ts b/frontend/src/api/k8s/__tests__/templates.spec.ts index 23df1ddc5c..3b1d2278e5 100644 --- a/frontend/src/api/k8s/__tests__/templates.spec.ts +++ b/frontend/src/api/k8s/__tests__/templates.spec.ts @@ -1,13 +1,21 @@ -import { K8sStatus, k8sDeleteResource, k8sListResource } from '@openshift/dynamic-plugin-sdk-utils'; -import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; +import { K8sStatus, k8sDeleteResource } from '@openshift/dynamic-plugin-sdk-utils'; import { mock200Status, mock404Error } from '~/__mocks__/mockK8sStatus'; import { mockServingRuntimeTemplateK8sResource } from '~/__mocks__/mockServingRuntimeTemplateK8sResource'; -import { assembleServingRuntimeTemplate, deleteTemplate, listTemplates } from '~/api'; +import { testHook } from '~/__tests__/unit/testUtils/hooks'; +import { + assembleServingRuntimeTemplate, + deleteTemplate, + groupVersionKind, + useTemplates, +} from '~/api'; import { TemplateModel } from '~/api/models'; import { K8sDSGResource, TemplateKind } from '~/k8sTypes'; +import useCustomServingRuntimesEnabled from '~/pages/modelServing/customServingRuntimes/useCustomServingRuntimesEnabled'; +import useModelServingEnabled from '~/pages/modelServing/useModelServingEnabled'; import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; import { genRandomChars } from '~/utilities/string'; +import useK8sWatchResourceList from '~/utilities/useK8sWatchResourceList'; jest.mock('@openshift/dynamic-plugin-sdk-utils', () => ({ k8sListResource: jest.fn(), @@ -18,9 +26,26 @@ jest.mock('~/utilities/string', () => ({ genRandomChars: jest.fn(), })); -const k8sListResourceMock = jest.mocked(k8sListResource); +jest.mock('~/utilities/useK8sWatchResourceList', () => ({ + __esModule: true, + default: jest.fn(), +})); + +jest.mock('~/pages/modelServing/useModelServingEnabled', () => ({ + __esModule: true, + default: jest.fn(), +})); + +jest.mock('~/pages/modelServing/customServingRuntimes/useCustomServingRuntimesEnabled', () => ({ + __esModule: true, + default: jest.fn(), +})); + +const useModelServingEnabledMock = jest.mocked(useModelServingEnabled); +const useCustomServingRuntimesEnabledMock = jest.mocked(useCustomServingRuntimesEnabled); const genRandomCharsMock = jest.mocked(genRandomChars); const k8sDeleteResourceMock = jest.mocked(k8sDeleteResource); +const useK8sWatchResourceListMock = jest.mocked(useK8sWatchResourceList); const templateMock = mockServingRuntimeTemplateK8sResource({}); const { namespace } = templateMock.metadata; @@ -75,37 +100,74 @@ describe('assembleServingRuntimeTemplate', () => { }); }); -describe('listTemplates', () => { - it('should list templates without namespace and label selector', async () => { - k8sListResourceMock.mockResolvedValue(mockK8sResourceList([templateMock])); - const result = await listTemplates(); - expect(k8sListResourceMock).toHaveBeenCalledWith({ - model: TemplateModel, - queryOptions: {}, - }); - expect(k8sListResourceMock).toHaveBeenCalledTimes(1); - expect(result).toStrictEqual([templateMock]); +describe('useTemplates', () => { + it('should wrap useK8sWatchResource to watch templates', () => { + useModelServingEnabledMock.mockReturnValue(true); + useCustomServingRuntimesEnabledMock.mockReturnValue(true); + const mockReturnValue: ReturnType = [[], false, undefined]; + useK8sWatchResourceListMock.mockReturnValue(mockReturnValue); + const { result } = testHook(useTemplates)(namespace); + expect(result.current).toStrictEqual(mockReturnValue); + expect(useK8sWatchResourceListMock).toHaveBeenCalledWith( + { + isList: true, + groupVersionKind: groupVersionKind(TemplateModel), + namespace, + }, + TemplateModel, + ); }); - it('should list templates with namespace and label selector', async () => { - k8sListResourceMock.mockResolvedValue(mockK8sResourceList([templateMock])); - const result = await listTemplates(namespace, 'labelSelector'); - expect(k8sListResourceMock).toHaveBeenCalledWith({ - model: TemplateModel, - queryOptions: { ns: namespace, queryParams: { labelSelector: 'labelSelector' } }, - }); - expect(k8sListResourceMock).toHaveBeenCalledTimes(1); - expect(result).toStrictEqual([templateMock]); + it('should throw error when namespace is not provided', () => { + useModelServingEnabledMock.mockReturnValue(true); + useCustomServingRuntimesEnabledMock.mockReturnValue(true); + const mockReturnValue: ReturnType = [[], false, undefined]; + useK8sWatchResourceListMock.mockReturnValue(mockReturnValue); + const { result } = testHook(useTemplates)(); + expect(result.current).toStrictEqual(mockReturnValue); }); - it('should handle errors and rethrow', async () => { - k8sListResourceMock.mockRejectedValue(new Error('error1')); - await expect(listTemplates()).rejects.toThrow('error1'); - expect(k8sListResourceMock).toHaveBeenCalledTimes(1); - expect(k8sListResourceMock).toHaveBeenCalledWith({ - model: TemplateModel, - queryOptions: {}, - }); + it('should throw error when model serving is not enabled', () => { + useModelServingEnabledMock.mockReturnValue(false); + useCustomServingRuntimesEnabledMock.mockReturnValue(true); + const mockReturnValue: ReturnType = [[], false, undefined]; + useK8sWatchResourceListMock.mockReturnValue(mockReturnValue); + const { result } = testHook(useTemplates)(namespace); + expect(result.current).toStrictEqual(mockReturnValue); + }); + + it('should return empty array when template data is undefined', () => { + useModelServingEnabledMock.mockReturnValue(false); + useCustomServingRuntimesEnabledMock.mockReturnValue(true); + const mockReturnValue: ReturnType = [[], false, undefined]; + useK8sWatchResourceListMock.mockReturnValue(mockReturnValue); + const { result } = testHook(useTemplates)(namespace); + expect(result.current).toStrictEqual(mockReturnValue); + }); + + it('should filter templates when custom serving runtime is not enabled', () => { + const templatesMock = { + ...templateMock, + metadata: { ...templateMock.metadata, labels: { 'opendatahub.io/ootb': 'true' } }, + }; + useModelServingEnabledMock.mockReturnValue(true); + useCustomServingRuntimesEnabledMock.mockReturnValue(false); + const mockReturnValue: ReturnType = [ + [templatesMock], + false, + undefined, + ]; + useK8sWatchResourceListMock.mockReturnValue(mockReturnValue); + const { result } = testHook(useTemplates)(namespace); + expect(result.current).toStrictEqual(mockReturnValue); + expect(useK8sWatchResourceListMock).toHaveBeenCalledWith( + { + isList: true, + groupVersionKind: groupVersionKind(TemplateModel), + namespace, + }, + TemplateModel, + ); }); }); diff --git a/frontend/src/api/k8s/groups.ts b/frontend/src/api/k8s/groups.ts index 27dc86ed9d..33a3909478 100644 --- a/frontend/src/api/k8s/groups.ts +++ b/frontend/src/api/k8s/groups.ts @@ -1,9 +1,11 @@ -import { WatchK8sResult, useK8sWatchResource } from '@openshift/dynamic-plugin-sdk-utils'; import React from 'react'; +import { WatchK8sResource } from '@openshift/dynamic-plugin-sdk-utils'; import { AccessReviewResourceAttributes, GroupKind } from '~/k8sTypes'; import { GroupModel } from '~/api/models'; import { groupVersionKind } from '~/api/k8sUtils'; import { useAccessReview } from '~/api/useAccessReview'; +import useK8sWatchResourceList from '~/utilities/useK8sWatchResourceList'; +import { CustomWatchK8sResult } from '~/types'; const accessReviewResource: AccessReviewResourceAttributes = { group: 'user.openshift.io', @@ -11,17 +13,18 @@ const accessReviewResource: AccessReviewResourceAttributes = { verb: 'list', }; -export const useGroups = (): WatchK8sResult => { +export const useGroups = (): CustomWatchK8sResult => { const [allowList, accessReviewLoaded] = useAccessReview(accessReviewResource); - const [groupData, loaded, error] = useK8sWatchResource( + const initResource: WatchK8sResource | null = allowList && accessReviewLoaded ? { isList: true, groupVersionKind: groupVersionKind(GroupModel), } - : null, - GroupModel, - ); + : null; + + const [groupData, loaded, error] = useK8sWatchResourceList(initResource, GroupModel); + return React.useMemo(() => { if (!allowList) { return [[], true, undefined]; diff --git a/frontend/src/api/k8s/projects.ts b/frontend/src/api/k8s/projects.ts index 47948d843c..c4b7a8df6e 100644 --- a/frontend/src/api/k8s/projects.ts +++ b/frontend/src/api/k8s/projects.ts @@ -5,9 +5,8 @@ import { k8sListResource, K8sResourceCommon, k8sUpdateResource, - useK8sWatchResource, - WatchK8sResult, } from '@openshift/dynamic-plugin-sdk-utils'; +import { CustomWatchK8sResult } from '~/types'; import { K8sAPIOptions, ProjectKind } from '~/k8sTypes'; import { ProjectModel, ProjectRequestModel } from '~/api/models'; import { throwErrorFromAxios } from '~/api/errorUtils'; @@ -17,9 +16,10 @@ import { LABEL_SELECTOR_DASHBOARD_RESOURCE, LABEL_SELECTOR_MODEL_SERVING_PROJECT import { NamespaceApplicationCase } from '~/pages/projects/types'; import { applyK8sAPIOptions } from '~/api/apiMergeUtils'; import { groupVersionKind } from '~/api/k8sUtils'; +import useK8sWatchResourceList from '~/utilities/useK8sWatchResourceList'; -export const useProjects = (): WatchK8sResult => - useK8sWatchResource( +export const useProjects = (): CustomWatchK8sResult => + useK8sWatchResourceList( { isList: true, groupVersionKind: groupVersionKind(ProjectModel), diff --git a/frontend/src/api/k8s/templates.ts b/frontend/src/api/k8s/templates.ts index 5d90ea50cd..ea025e2fe5 100644 --- a/frontend/src/api/k8s/templates.ts +++ b/frontend/src/api/k8s/templates.ts @@ -1,9 +1,14 @@ import YAML from 'yaml'; -import { k8sDeleteResource, k8sListResource } from '@openshift/dynamic-plugin-sdk-utils'; +import React from 'react'; +import { WatchK8sResource, k8sDeleteResource } from '@openshift/dynamic-plugin-sdk-utils'; import { ServingRuntimeKind, TemplateKind } from '~/k8sTypes'; import { TemplateModel } from '~/api/models'; import { genRandomChars } from '~/utilities/string'; -import { ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; +import { CustomWatchK8sResult, ServingRuntimeAPIProtocol, ServingRuntimePlatform } from '~/types'; +import useModelServingEnabled from '~/pages/modelServing/useModelServingEnabled'; +import useCustomServingRuntimesEnabled from '~/pages/modelServing/customServingRuntimes/useCustomServingRuntimesEnabled'; +import { groupVersionKind } from '~/api/k8sUtils'; +import useK8sWatchResourceList from '~/utilities/useK8sWatchResourceList'; export const assembleServingRuntimeTemplate = ( body: string, @@ -39,18 +44,39 @@ export const assembleServingRuntimeTemplate = ( }; }; -export const listTemplates = async ( - namespace?: string, - labelSelector?: string, -): Promise => { - const queryOptions = { - ...(namespace && { ns: namespace }), - ...(labelSelector && { queryParams: { labelSelector } }), - }; - return k8sListResource({ - model: TemplateModel, - queryOptions, - }).then((listResource) => listResource.items); +export const useTemplates = (namespace?: string): CustomWatchK8sResult => { + const modelServingEnabled = useModelServingEnabled(); + const customServingRuntimesEnabled = useCustomServingRuntimesEnabled(); + + const initResource: WatchK8sResource | null = + namespace && modelServingEnabled + ? { + isList: true, + groupVersionKind: groupVersionKind(TemplateModel), + namespace, + } + : null; + + const [templatesData, loaded, error] = useK8sWatchResourceList( + initResource, + TemplateModel, + ); + + const templates = React.useMemo( + () => + customServingRuntimesEnabled + ? templatesData + : templatesData.filter( + (template) => template.metadata.labels?.['opendatahub.io/ootb'] === 'true', + ), + [templatesData, customServingRuntimesEnabled], + ); + + if (!namespace || !modelServingEnabled) { + return [templates, false, undefined]; + } + + return [templates, loaded, error]; }; export const deleteTemplate = (name: string, namespace: string): Promise => diff --git a/frontend/src/concepts/projects/ProjectsContext.tsx b/frontend/src/concepts/projects/ProjectsContext.tsx index dce7d107cc..7b60048453 100644 --- a/frontend/src/concepts/projects/ProjectsContext.tsx +++ b/frontend/src/concepts/projects/ProjectsContext.tsx @@ -53,8 +53,7 @@ type ProjectsProviderProps = { const ProjectsContextProvider: React.FC = ({ children }) => { const [preferredProject, setPreferredProject] = React.useState(null); - const [projectData, loaded, error] = useProjects(); - const loadError = error as Error | undefined; + const [projectData, loaded, loadError] = useProjects(); const { dashboardNamespace } = useDashboardNamespace(); const { projects, dataScienceProjects, modelServingProjects, nonActiveProjects } = React.useMemo( diff --git a/frontend/src/pages/modelServing/ModelServingContext.tsx b/frontend/src/pages/modelServing/ModelServingContext.tsx index 2a3dfd3a24..6673725346 100644 --- a/frontend/src/pages/modelServing/ModelServingContext.tsx +++ b/frontend/src/pages/modelServing/ModelServingContext.tsx @@ -17,8 +17,8 @@ import { ProjectKind, SecretKind, } from '~/k8sTypes'; -import { DEFAULT_CONTEXT_DATA } from '~/utilities/const'; -import { ContextResourceData } from '~/types'; +import { DEFAULT_CONTEXT_DATA, DEFAULT_LIST_WATCH_RESULT } from '~/utilities/const'; +import { ContextResourceData, CustomWatchK8sResult } from '~/types'; import { useContextResourceData } from '~/utilities/useContextResourceData'; import { useDashboardNamespace } from '~/redux/selectors'; import { DataConnection } from '~/pages/projects/types'; @@ -27,9 +27,9 @@ import useSyncPreferredProject from '~/concepts/projects/useSyncPreferredProject import { ProjectsContext, byName } from '~/concepts/projects/ProjectsContext'; import { SupportedArea, conditionalArea } from '~/concepts/areas'; import useServingPlatformStatuses from '~/pages/modelServing/useServingPlatformStatuses'; +import { useTemplates } from '~/api'; import useInferenceServices from './useInferenceServices'; import useServingRuntimes from './useServingRuntimes'; -import useTemplates from './customServingRuntimes/useTemplates'; import useTemplateOrder from './customServingRuntimes/useTemplateOrder'; import useTemplateDisablement from './customServingRuntimes/useTemplateDisablement'; import { getTokenNames } from './utils'; @@ -39,7 +39,7 @@ type ModelServingContextType = { refreshAllData: () => void; filterTokens: (servingRuntime?: string) => SecretKind[]; dataConnections: ContextResourceData; - servingRuntimeTemplates: ContextResourceData; + servingRuntimeTemplates: CustomWatchK8sResult; servingRuntimeTemplateOrder: ContextResourceData; servingRuntimeTemplateDisablement: ContextResourceData; servingRuntimes: ContextResourceData; @@ -60,7 +60,7 @@ export const ModelServingContext = React.createContext( refreshAllData: () => undefined, filterTokens: () => [], dataConnections: DEFAULT_CONTEXT_DATA, - servingRuntimeTemplates: DEFAULT_CONTEXT_DATA, + servingRuntimeTemplates: DEFAULT_LIST_WATCH_RESULT, servingRuntimeTemplateOrder: DEFAULT_CONTEXT_DATA, servingRuntimeTemplateDisablement: DEFAULT_CONTEXT_DATA, servingRuntimes: DEFAULT_CONTEXT_DATA, @@ -80,9 +80,8 @@ const ModelServingContextProvider = conditionalArea( - useTemplates(dashboardNamespace), - ); + const servingRuntimeTemplates = useTemplates(dashboardNamespace); + const servingRuntimeTemplateOrder = useContextResourceData( useTemplateOrder(dashboardNamespace), ); @@ -137,7 +136,7 @@ const ModelServingContextProvider = conditionalArea void; - servingRuntimeTemplates: ContextResourceData; + servingRuntimeTemplates: CustomWatchK8sResult; servingRuntimeTemplateOrder: ContextResourceData; servingRuntimeTemplateDisablement: ContextResourceData; }; export const CustomServingRuntimeContext = React.createContext({ refreshData: () => undefined, - servingRuntimeTemplates: DEFAULT_CONTEXT_DATA, + servingRuntimeTemplates: DEFAULT_LIST_WATCH_RESULT, servingRuntimeTemplateOrder: DEFAULT_CONTEXT_DATA, servingRuntimeTemplateDisablement: DEFAULT_CONTEXT_DATA, }); @@ -35,10 +35,7 @@ export const CustomServingRuntimeContext = React.createContext { const { dashboardNamespace } = useDashboardNamespace(); - // TODO: Disable backend workaround when we migrate admin panel to Passthrough API - const servingRuntimeTemplates = useContextResourceData( - useTemplates(dashboardNamespace, true), - ); + const servingRuntimeTemplates = useTemplates(dashboardNamespace); // TODO: Disable backend workaround when we migrate admin panel to Passthrough API const servingRuntimeTemplateOrder = useContextResourceData( @@ -52,19 +49,13 @@ const CustomServingRuntimeContextProvider: React.FC = () => { 2 * 60 * 1000, ); - const servingRuntimeTemplateRefresh = servingRuntimeTemplates.refresh; const servingRuntimeTemplateOrderRefresh = servingRuntimeTemplateOrder.refresh; const servingRuntimeTemplateDisablementRefresh = servingRuntimeTemplateOrder.refresh; const refreshData = React.useCallback(() => { - servingRuntimeTemplateRefresh(); servingRuntimeTemplateOrderRefresh(); servingRuntimeTemplateDisablementRefresh(); - }, [ - servingRuntimeTemplateRefresh, - servingRuntimeTemplateOrderRefresh, - servingRuntimeTemplateDisablementRefresh, - ]); + }, [servingRuntimeTemplateOrderRefresh, servingRuntimeTemplateDisablementRefresh]); const contextValue = React.useMemo( () => ({ @@ -82,7 +73,7 @@ const CustomServingRuntimeContextProvider: React.FC = () => { ); if ( - servingRuntimeTemplates.error || + servingRuntimeTemplates[2] || servingRuntimeTemplateOrder.error || servingRuntimeTemplateDisablement.error ) { @@ -95,7 +86,7 @@ const CustomServingRuntimeContextProvider: React.FC = () => { headingLevel="h2" /> - {servingRuntimeTemplates.error?.message || servingRuntimeTemplateOrder.error?.message} + {servingRuntimeTemplates[2]?.message || servingRuntimeTemplateOrder.error?.message} @@ -103,7 +94,7 @@ const CustomServingRuntimeContextProvider: React.FC = () => { } if ( - !servingRuntimeTemplates.loaded || + !servingRuntimeTemplates[1] || !servingRuntimeTemplateOrder.loaded || !servingRuntimeTemplateDisablement.loaded ) { diff --git a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeEditTemplate.tsx b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeEditTemplate.tsx index 42f60586bc..1e6a29353d 100644 --- a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeEditTemplate.tsx +++ b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeEditTemplate.tsx @@ -18,7 +18,7 @@ import { CustomServingRuntimeContext } from './CustomServingRuntimeContext'; const CustomServingRuntimeEditTemplate: React.FC = () => { const { - servingRuntimeTemplates: { data }, + servingRuntimeTemplates: [data], } = React.useContext(CustomServingRuntimeContext); const navigate = useNavigate(); const { servingRuntimeName } = useParams(); diff --git a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeEnabledToggle.tsx b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeEnabledToggle.tsx index b220d0fca1..8d26b7bae9 100644 --- a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeEnabledToggle.tsx +++ b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeEnabledToggle.tsx @@ -20,7 +20,7 @@ const CustomServingRuntimeEnabledToggle: React.FC { const { servingRuntimeTemplateOrder: { data: templateOrder, refresh: refreshOrder }, - servingRuntimeTemplates: { data: unsortedTemplates }, + servingRuntimeTemplates: [unsortedTemplates], refreshData, } = React.useContext(CustomServingRuntimeContext); const { dashboardNamespace } = useDashboardNamespace(); diff --git a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeView.tsx b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeView.tsx index ce3330b091..f296119987 100644 --- a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeView.tsx +++ b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeView.tsx @@ -7,7 +7,7 @@ import { CustomServingRuntimeContext } from './CustomServingRuntimeContext'; const CustomServingRuntimeView: React.FC = () => { const { - servingRuntimeTemplates: { data: servingRuntimeTemplates }, + servingRuntimeTemplates: [servingRuntimeTemplates], } = React.useContext(CustomServingRuntimeContext); return ( diff --git a/frontend/src/pages/modelServing/customServingRuntimes/DeleteCustomServingRuntimeModal.tsx b/frontend/src/pages/modelServing/customServingRuntimes/DeleteCustomServingRuntimeModal.tsx index 9fc485630f..c6489820d3 100644 --- a/frontend/src/pages/modelServing/customServingRuntimes/DeleteCustomServingRuntimeModal.tsx +++ b/frontend/src/pages/modelServing/customServingRuntimes/DeleteCustomServingRuntimeModal.tsx @@ -25,7 +25,7 @@ const DeleteCustomServingRuntimeModal: React.FC => { - const customServingRuntimesEnabled = useCustomServingRuntimesEnabled(); - const modelServingEnabled = useModelServingEnabled(); - - const getTemplates = React.useCallback(() => { - if (!namespace) { - return Promise.reject(new NotReadyError('No namespace provided')); - } - - if (!modelServingEnabled) { - return Promise.reject(new NotReadyError('Model serving is not enabled')); - } - - // TODO: Remove this when we migrate admin panel to Passthrough API - if (adminPanel) { - return listTemplatesBackend(namespace, 'opendatahub.io/dashboard=true') - .catch((e) => { - if (e.statusObject?.code === 404) { - throw new Error('Serving Runtime templates is not properly configured.'); - } - throw e; - }) - .then((templates) => { - if (!customServingRuntimesEnabled) { - return templates.filter( - (template) => template.metadata.labels?.['opendatahub.io/ootb'] === 'true', - ); - } - return templates; - }); - } - - return listTemplates(namespace, 'opendatahub.io/dashboard=true') - .catch((e) => { - if (e.statusObject?.code === 404) { - throw new Error('Serving Runtime templates is not properly configured.'); - } - throw e; - }) - .then((templates) => { - if (!customServingRuntimesEnabled) { - return templates.filter( - (template) => template.metadata.labels?.['opendatahub.io/ootb'] === 'true', - ); - } - return templates; - }); - }, [namespace, customServingRuntimesEnabled, modelServingEnabled, adminPanel]); - - return useFetchState(getTemplates, []); -}; - -export default useTemplates; diff --git a/frontend/src/pages/modelServing/screens/global/ServeModelButton.tsx b/frontend/src/pages/modelServing/screens/global/ServeModelButton.tsx index 5b0fd26468..c42491dfeb 100644 --- a/frontend/src/pages/modelServing/screens/global/ServeModelButton.tsx +++ b/frontend/src/pages/modelServing/screens/global/ServeModelButton.tsx @@ -21,7 +21,7 @@ const ServeModelButton: React.FC = () => { const { inferenceServices: { refresh: refreshInferenceServices }, servingRuntimes: { refresh: refreshServingRuntimes }, - servingRuntimeTemplates: { data: templates }, + servingRuntimeTemplates: [templates], servingRuntimeTemplateOrder: { data: templateOrder }, servingRuntimeTemplateDisablement: { data: templateDisablement }, dataConnections: { data: dataConnections }, diff --git a/frontend/src/pages/modelServing/screens/projects/EmptyMultiModelServingCard.tsx b/frontend/src/pages/modelServing/screens/projects/EmptyMultiModelServingCard.tsx index 828dcb360f..d9b670de84 100644 --- a/frontend/src/pages/modelServing/screens/projects/EmptyMultiModelServingCard.tsx +++ b/frontend/src/pages/modelServing/screens/projects/EmptyMultiModelServingCard.tsx @@ -24,7 +24,7 @@ const EmptyMultiModelServingCard: React.FC = () => { const { servingRuntimes: { refresh: refreshServingRuntime }, - servingRuntimeTemplates: { data: templates }, + servingRuntimeTemplates: [templates], servingRuntimeTemplateOrder: { data: templateOrder }, servingRuntimeTemplateDisablement: { data: templateDisablement }, serverSecrets: { refresh: refreshTokens }, diff --git a/frontend/src/pages/modelServing/screens/projects/EmptySingleModelServingCard.tsx b/frontend/src/pages/modelServing/screens/projects/EmptySingleModelServingCard.tsx index 761e17b6e9..e02b81d93e 100644 --- a/frontend/src/pages/modelServing/screens/projects/EmptySingleModelServingCard.tsx +++ b/frontend/src/pages/modelServing/screens/projects/EmptySingleModelServingCard.tsx @@ -27,7 +27,7 @@ const EmptySingleModelServingCard: React.FC = () => { const { servingRuntimes: { refresh: refreshServingRuntime }, - servingRuntimeTemplates: { data: templates }, + servingRuntimeTemplates: [templates], servingRuntimeTemplateOrder: { data: templateOrder }, servingRuntimeTemplateDisablement: { data: templateDisablement }, serverSecrets: { refresh: refreshTokens }, diff --git a/frontend/src/pages/modelServing/screens/projects/ModelServingPlatform.tsx b/frontend/src/pages/modelServing/screens/projects/ModelServingPlatform.tsx index ee1fb728a7..c0b3cfd5da 100644 --- a/frontend/src/pages/modelServing/screens/projects/ModelServingPlatform.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ModelServingPlatform.tsx @@ -54,7 +54,7 @@ const ModelServingPlatform: React.FC = () => { error: servingRuntimeError, refresh: refreshServingRuntime, }, - servingRuntimeTemplates: { data: templates, loaded: templatesLoaded, error: templateError }, + servingRuntimeTemplates: [templates, templatesLoaded, templateError], servingRuntimeTemplateOrder: { data: templateOrder }, servingRuntimeTemplateDisablement: { data: templateDisablement }, dataConnections: { data: dataConnections }, diff --git a/frontend/src/pages/modelServing/screens/projects/ModelServingPlatformButtonAction.tsx b/frontend/src/pages/modelServing/screens/projects/ModelServingPlatformButtonAction.tsx index 1e3c999648..73ee8b5a10 100644 --- a/frontend/src/pages/modelServing/screens/projects/ModelServingPlatformButtonAction.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ModelServingPlatformButtonAction.tsx @@ -16,7 +16,7 @@ const ModelServingPlatformButtonAction: React.FC { const { - servingRuntimeTemplates: { loaded: templatesLoaded }, + servingRuntimeTemplates: [, templatesLoaded], } = React.useContext(ProjectDetailsContext); const actionButton = () => ( diff --git a/frontend/src/pages/projects/ProjectDetailsContext.tsx b/frontend/src/pages/projects/ProjectDetailsContext.tsx index f7e64820a5..825fb866ec 100644 --- a/frontend/src/pages/projects/ProjectDetailsContext.tsx +++ b/frontend/src/pages/projects/ProjectDetailsContext.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { Navigate, Outlet, useParams } from 'react-router-dom'; -import { WatchK8sResult } from '@openshift/dynamic-plugin-sdk-utils'; import { GroupKind, InferenceServiceKind, @@ -14,20 +13,19 @@ import { import { DEFAULT_CONTEXT_DATA, DEFAULT_LIST_WATCH_RESULT } from '~/utilities/const'; import useServingRuntimes from '~/pages/modelServing/useServingRuntimes'; import useInferenceServices from '~/pages/modelServing/useInferenceServices'; -import { ContextResourceData } from '~/types'; +import { ContextResourceData, CustomWatchK8sResult } from '~/types'; import { useContextResourceData } from '~/utilities/useContextResourceData'; import useServingRuntimeSecrets from '~/pages/modelServing/screens/projects/useServingRuntimeSecrets'; import { PipelineContextProvider } from '~/concepts/pipelines/context'; import { byName, ProjectsContext } from '~/concepts/projects/ProjectsContext'; import InvalidProject from '~/concepts/projects/InvalidProject'; import useSyncPreferredProject from '~/concepts/projects/useSyncPreferredProject'; -import useTemplates from '~/pages/modelServing/customServingRuntimes/useTemplates'; import useTemplateOrder from '~/pages/modelServing/customServingRuntimes/useTemplateOrder'; import useTemplateDisablement from '~/pages/modelServing/customServingRuntimes/useTemplateDisablement'; import { useDashboardNamespace } from '~/redux/selectors'; import { getTokenNames } from '~/pages/modelServing/utils'; import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; -import { useGroups } from '~/api'; +import { useGroups, useTemplates } from '~/api'; import { NotebookState } from './notebook/types'; import { DataConnection } from './types'; import useDataConnections from './screens/detail/data-connections/useDataConnections'; @@ -43,13 +41,13 @@ type ProjectDetailsContextType = { pvcs: ContextResourceData; dataConnections: ContextResourceData; servingRuntimes: ContextResourceData; - servingRuntimeTemplates: ContextResourceData; + servingRuntimeTemplates: CustomWatchK8sResult; servingRuntimeTemplateOrder: ContextResourceData; servingRuntimeTemplateDisablement: ContextResourceData; inferenceServices: ContextResourceData; serverSecrets: ContextResourceData; projectSharingRB: ContextResourceData; - groups: WatchK8sResult; + groups: CustomWatchK8sResult; }; export const ProjectDetailsContext = React.createContext({ @@ -61,7 +59,7 @@ export const ProjectDetailsContext = React.createContext { const pvcs = useContextResourceData(useProjectPvcs(namespace)); const dataConnections = useContextResourceData(useDataConnections(namespace)); const servingRuntimes = useContextResourceData(useServingRuntimes(namespace)); - const servingRuntimeTemplates = useContextResourceData( - useTemplates(dashboardNamespace), - ); + const servingRuntimeTemplates = useTemplates(dashboardNamespace); + const servingRuntimeTemplateOrder = useContextResourceData( useTemplateOrder(dashboardNamespace), ); @@ -100,7 +97,6 @@ const ProjectDetailsContextProvider: React.FC = () => { const pvcRefresh = pvcs.refresh; const dataConnectionRefresh = dataConnections.refresh; const servingRuntimeRefresh = servingRuntimes.refresh; - const servingRuntimeTemplateRefresh = servingRuntimeTemplates.refresh; const servingRuntimeTemplateOrderRefresh = servingRuntimeTemplateOrder.refresh; const servingRuntimeTemplateDisablementRefresh = servingRuntimeTemplateDisablement.refresh; const inferenceServiceRefresh = inferenceServices.refresh; @@ -113,7 +109,7 @@ const ProjectDetailsContextProvider: React.FC = () => { servingRuntimeRefresh(); inferenceServiceRefresh(); projectSharingRefresh(); - servingRuntimeTemplateRefresh(); + servingRuntimeTemplateOrderRefresh(); servingRuntimeTemplateDisablementRefresh(); }, [ @@ -121,7 +117,6 @@ const ProjectDetailsContextProvider: React.FC = () => { pvcRefresh, dataConnectionRefresh, servingRuntimeRefresh, - servingRuntimeTemplateRefresh, servingRuntimeTemplateOrderRefresh, servingRuntimeTemplateDisablementRefresh, inferenceServiceRefresh, diff --git a/frontend/src/pages/projects/screens/detail/overview/serverModels/AddModelFooter.tsx b/frontend/src/pages/projects/screens/detail/overview/serverModels/AddModelFooter.tsx index bbb05ab1b4..fe5498c9b8 100644 --- a/frontend/src/pages/projects/screens/detail/overview/serverModels/AddModelFooter.tsx +++ b/frontend/src/pages/projects/screens/detail/overview/serverModels/AddModelFooter.tsx @@ -24,7 +24,7 @@ const AddModelFooter: React.FC = ({ selectedPlatform }) => const { servingRuntimes: { refresh: refreshServingRuntime }, - servingRuntimeTemplates: { data: templates }, + servingRuntimeTemplates: [templates], servingRuntimeTemplateOrder: { data: templateOrder }, servingRuntimeTemplateDisablement: { data: templateDisablement }, dataConnections: { data: dataConnections }, diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 2dee3d3c3e..44bfa5a5fc 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,6 +1,11 @@ /* * Common types, should be kept up to date with backend types */ + +import { + WatchK8sResult, + K8sResourceCommon as SDKK8sResourceCommon, +} from '@openshift/dynamic-plugin-sdk-utils'; import { AxiosError } from 'axios'; import { EnvironmentFromVariable } from '~/pages/projects/types'; import { AcceleratorProfileKind, ImageStreamKind, ImageStreamSpecTagType } from './k8sTypes'; @@ -662,6 +667,13 @@ export type ImageStreamAndVersion = { imageVersion?: ImageStreamSpecTagType; }; +// This is the workaround to use K8sResourceCommon | K8sResourceCommon[] from SDK to work with utils. +export type CustomWatchK8sResult = [ + data: WatchK8sResult[0], + loaded: WatchK8sResult[1], + loadError: Error | undefined, +]; + export type FetchStateObject = { data: T; loaded: boolean; diff --git a/frontend/src/utilities/__tests__/useK8sWatchResourceList.spec.ts b/frontend/src/utilities/__tests__/useK8sWatchResourceList.spec.ts new file mode 100644 index 0000000000..383caca4ed --- /dev/null +++ b/frontend/src/utilities/__tests__/useK8sWatchResourceList.spec.ts @@ -0,0 +1,87 @@ +import { WatchK8sResource, useK8sWatchResource } from '@openshift/dynamic-plugin-sdk-utils'; +import { testHook } from '~/__tests__/unit/testUtils/hooks'; +import { TemplateModel, groupVersionKind } from '~/api'; +import useK8sWatchResourceList from '~/utilities/useK8sWatchResourceList'; + +jest.mock('@openshift/dynamic-plugin-sdk-utils', () => ({ + useK8sWatchResource: jest.fn(), +})); + +const useK8sWatchResourceMock = useK8sWatchResource as jest.Mock; + +const namespace = 'opendatahub'; + +describe('useK8sWatchResourceList', () => { + it('should wrap useK8sWatchResource', () => { + const mockReturnValue: ReturnType = [[], false, undefined]; + useK8sWatchResourceMock.mockReturnValue(mockReturnValue); + const initResource: WatchK8sResource | null = { + isList: true, + groupVersionKind: groupVersionKind(TemplateModel), + namespace, + }; + const renderResult = testHook(useK8sWatchResourceList)(initResource, TemplateModel); + useK8sWatchResourceMock.mockReturnValue(mockReturnValue); + expect(renderResult.result.current).toStrictEqual(mockReturnValue); + }); + + it('should return empty array when initResource is null', () => { + const mockReturnValue: ReturnType = [ + undefined, + false, + undefined, + ]; + useK8sWatchResourceMock.mockReturnValue(mockReturnValue); + const initResource: WatchK8sResource | null = null; + const renderResult = testHook(useK8sWatchResourceList)(initResource, TemplateModel); + useK8sWatchResourceMock.mockReturnValue(mockReturnValue); + expect(renderResult.result.current).toStrictEqual([[], false, undefined]); + }); + + it('should return error when it is instance of error', () => { + const mockReturnValue: ReturnType = [ + [], + false, + new Error('error'), + ]; + useK8sWatchResourceMock.mockReturnValue(mockReturnValue); + const initResource: WatchK8sResource | null = { + isList: true, + groupVersionKind: groupVersionKind(TemplateModel), + namespace, + }; + const renderResult = testHook(useK8sWatchResourceList)(initResource, TemplateModel); + useK8sWatchResourceMock.mockReturnValue(mockReturnValue); + expect(renderResult.result.current).toStrictEqual(mockReturnValue); + }); + + it('should return error object when it is not instance of error', () => { + const mockReturnValue: ReturnType = [[], false, 'error']; + useK8sWatchResourceMock.mockReturnValue(mockReturnValue); + const initResource: WatchK8sResource | null = { + isList: true, + groupVersionKind: groupVersionKind(TemplateModel), + namespace, + }; + const renderResult = testHook(useK8sWatchResourceList)(initResource, TemplateModel); + useK8sWatchResourceMock.mockReturnValue(mockReturnValue); + expect(renderResult.result.current).toStrictEqual([ + [], + false, + new Error('Unknown error occured'), + ]); + }); + + it('should return undefined when error is an empty string', () => { + const mockReturnValue: ReturnType = [[], false, '']; + useK8sWatchResourceMock.mockReturnValue(mockReturnValue); + const initResource: WatchK8sResource | null = { + isList: true, + groupVersionKind: groupVersionKind(TemplateModel), + namespace, + }; + const renderResult = testHook(useK8sWatchResourceList)(initResource, TemplateModel); + useK8sWatchResourceMock.mockReturnValue(mockReturnValue); + expect(renderResult.result.current).toStrictEqual([[], false, undefined]); + }); +}); diff --git a/frontend/src/utilities/const.ts b/frontend/src/utilities/const.ts index 4a18492343..7e6dbc57e7 100644 --- a/frontend/src/utilities/const.ts +++ b/frontend/src/utilities/const.ts @@ -1,5 +1,9 @@ -import { WatchK8sResult } from '@openshift/dynamic-plugin-sdk-utils'; -import { ContextResourceData, FetchStateObject, OdhDocumentType } from '~/types'; +import { + ContextResourceData, + CustomWatchK8sResult, + FetchStateObject, + OdhDocumentType, +} from '~/types'; const WS_HOSTNAME = process.env.WS_HOSTNAME || location.host; const DEV_MODE = process.env.APP_ENV === 'development'; @@ -52,7 +56,11 @@ export const DEFAULT_CONTEXT_DATA: ContextResourceData = { refresh: () => undefined, }; -export const DEFAULT_LIST_WATCH_RESULT: WatchK8sResult = [[], false, undefined]; +export const DEFAULT_LIST_WATCH_RESULT: CustomWatchK8sResult = [ + [], + false, + undefined, +]; export const DEFAULT_LIST_FETCH_STATE: FetchStateObject = { data: [], diff --git a/frontend/src/utilities/useK8sWatchResourceList.ts b/frontend/src/utilities/useK8sWatchResourceList.ts new file mode 100644 index 0000000000..b837320cda --- /dev/null +++ b/frontend/src/utilities/useK8sWatchResourceList.ts @@ -0,0 +1,40 @@ +import { + K8sModelCommon, + K8sResourceCommon, + WatchK8sResource, + WebSocketOptions, + useK8sWatchResource, +} from '@openshift/dynamic-plugin-sdk-utils'; +import React from 'react'; +import { CustomWatchK8sResult } from '~/types'; + +const useK8sWatchResourceList = ( + initResource: WatchK8sResource | null, + initModel?: K8sModelCommon, + options?: Partial, +): CustomWatchK8sResult => { + const initListResource = React.useMemo( + () => (initResource != null ? { ...initResource, isList: true } : null), + [initResource], + ); + + const [data, loaded, error] = useK8sWatchResource(initListResource, initModel, options); + + const loadError = React.useMemo(() => { + if (error instanceof Error) { + return error; + } + + if (!error) { + return undefined; + } + + return new Error('Unknown error occured'); + }, [error]); + + // disable as data can be `undefined` by the type in the SDK is incorrect + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return [React.useMemo(() => data ?? [], [data]), loaded, loadError]; +}; + +export default useK8sWatchResourceList;