diff --git a/frontend/src/const.ts b/frontend/src/const.ts index 73be687c75..6785cec682 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -1,6 +1,6 @@ import { KnownLabels } from '~/k8sTypes'; export const LABEL_SELECTOR_DASHBOARD_RESOURCE = `${KnownLabels.DASHBOARD_RESOURCE}=true`; -export const LABEL_SELECTOR_MODEL_SERVING_PROJECT = `${KnownLabels.MODEL_SERVING_PROJECT}=true`; +export const LABEL_SELECTOR_MODEL_SERVING_PROJECT = KnownLabels.MODEL_SERVING_PROJECT; export const LABEL_SELECTOR_DATA_CONNECTION_AWS = `${KnownLabels.DATA_CONNECTION_AWS}=true`; export const LABEL_SELECTOR_PROJECT_SHARING = `${KnownLabels.PROJECT_SHARING}=true`; diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index 71f16d7b48..2575ae18d7 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -101,6 +101,7 @@ export type ServingRuntimeAnnotations = Partial<{ 'opendatahub.io/accelerator-name': string; 'enable-route': string; 'enable-auth': string; + 'modelmesh-enabled': 'true' | 'false'; }>; export type BuildConfigKind = K8sResourceCommon & { @@ -357,6 +358,10 @@ export type InferenceServiceKind = K8sResourceCommon & { metadata: { name: string; namespace: string; + annotations?: DisplayNameAnnotations & + Partial<{ + 'serving.kserve.io/deploymentMode': 'ModelMesh'; + }>; }; spec: { predictor: { diff --git a/frontend/src/pages/modelServing/__tests__/utils.spec.ts b/frontend/src/pages/modelServing/__tests__/utils.spec.ts index 4814e493b1..271fe8e200 100644 --- a/frontend/src/pages/modelServing/__tests__/utils.spec.ts +++ b/frontend/src/pages/modelServing/__tests__/utils.spec.ts @@ -5,6 +5,12 @@ import { checkPlatformAvailability, resourcesArePositive, } from '~/pages/modelServing/utils'; +import { + mockServingRuntimeK8sResource, + mockServingRuntimeK8sResourceLegacy, +} from '~/__mocks__/mockServingRuntimeK8sResource'; +import { ServingRuntimeKind } from '~/k8sTypes'; +import { getDisplayNameFromServingRuntimeTemplate } from '~/pages/modelServing/customServingRuntimes/utils'; import { ContainerResources } from '~/types'; describe('resourcesArePositive', () => { @@ -156,3 +162,34 @@ describe('servingPlatformsInstallaed', () => { expect(checkModelMeshFailureStatus(mockedDataScienceStatusKserve)).toEqual(errorMessage); }); }); + +describe('getDisplayNameFromServingRuntimeTemplate', () => { + it('should provide default name if not found', () => { + const servingRuntime = getDisplayNameFromServingRuntimeTemplate({ + metadata: {}, + spec: {}, + } as ServingRuntimeKind); + expect(servingRuntime).toBe('Unknown Serving Runtime'); + }); + + it('should prioritize name from annotation "opendatahub.io/template-display-name"', () => { + const servingRuntime = getDisplayNameFromServingRuntimeTemplate( + mockServingRuntimeK8sResource({}), + ); + expect(servingRuntime).toBe('OpenVINO Serving Runtime (Supports GPUs)'); + }); + + it('should fallback first to name from annotation "opendatahub.io/template-name"', () => { + const mockServingRuntime = mockServingRuntimeK8sResource({}); + delete mockServingRuntime.metadata.annotations?.['opendatahub.io/template-display-name']; + const servingRuntime = getDisplayNameFromServingRuntimeTemplate(mockServingRuntime); + expect(servingRuntime).toBe('ovms'); + }); + + it('should fallback to ovms serverType', () => { + const servingRuntime = getDisplayNameFromServingRuntimeTemplate( + mockServingRuntimeK8sResourceLegacy({}), + ); + expect(servingRuntime).toBe('OpenVINO Model Server'); + }); +}); diff --git a/frontend/src/pages/modelServing/screens/global/DeleteInferenceServiceModal.tsx b/frontend/src/pages/modelServing/screens/global/DeleteInferenceServiceModal.tsx index e8d18b45cb..a7b26e5d07 100644 --- a/frontend/src/pages/modelServing/screens/global/DeleteInferenceServiceModal.tsx +++ b/frontend/src/pages/modelServing/screens/global/DeleteInferenceServiceModal.tsx @@ -7,11 +7,13 @@ import { getInferenceServiceDisplayName } from './utils'; type DeleteInferenceServiceModalProps = { inferenceService?: InferenceServiceKind; onClose: (deleted: boolean) => void; + isOpen?: boolean; }; const DeleteInferenceServiceModal: React.FC = ({ inferenceService, onClose, + isOpen = false, }) => { const [isDeleting, setIsDeleting] = React.useState(false); const [error, setError] = React.useState(); @@ -29,7 +31,7 @@ const DeleteInferenceServiceModal: React.FC = return ( onBeforeClose(false)} submitButtonLabel="Delete deployed model" onDelete={() => { diff --git a/frontend/src/pages/modelServing/screens/global/InferenceServiceEndpoint.tsx b/frontend/src/pages/modelServing/screens/global/InferenceServiceEndpoint.tsx index 2221bc3ab2..0c4a0016df 100644 --- a/frontend/src/pages/modelServing/screens/global/InferenceServiceEndpoint.tsx +++ b/frontend/src/pages/modelServing/screens/global/InferenceServiceEndpoint.tsx @@ -44,7 +44,7 @@ const InferenceServiceEndpoint: React.FC = ({ ); } - if (!routeLink || !loaded) { + if (!loaded) { return ; } diff --git a/frontend/src/pages/modelServing/screens/global/InferenceServiceModel.tsx b/frontend/src/pages/modelServing/screens/global/InferenceServiceModel.tsx deleted file mode 100644 index ff7f73286b..0000000000 --- a/frontend/src/pages/modelServing/screens/global/InferenceServiceModel.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from 'react'; -import { HelperText, HelperTextItem, Skeleton } from '@patternfly/react-core'; -import { InferenceServiceKind } from '~/k8sTypes'; -import { getDisplayNameFromK8sResource } from '~/pages/projects/utils'; -import { ModelServingContext } from '~/pages/modelServing/ModelServingContext'; - -type InferenceServiceModelProps = { - inferenceService: InferenceServiceKind; -}; - -const InferenceServiceModel: React.FC = ({ inferenceService }) => { - const { - servingRuntimes: { data: servingRuntimes, loaded, error }, - } = React.useContext(ModelServingContext); - const servingRuntime = servingRuntimes.find( - ({ metadata: { name } }) => name === inferenceService.spec.predictor.model.runtime, - ); - - if (!loaded) { - return ; - } - - if (error) { - return ( - - - Failed to get model server for this deployed model. {error.message}. - - - ); - } - - return <>{servingRuntime ? getDisplayNameFromK8sResource(servingRuntime) : 'Unknown'}; -}; - -export default InferenceServiceModel; diff --git a/frontend/src/pages/modelServing/screens/global/InferenceServiceProject.tsx b/frontend/src/pages/modelServing/screens/global/InferenceServiceProject.tsx index 797195b60e..9a20826851 100644 --- a/frontend/src/pages/modelServing/screens/global/InferenceServiceProject.tsx +++ b/frontend/src/pages/modelServing/screens/global/InferenceServiceProject.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { HelperText, HelperTextItem, Skeleton } from '@patternfly/react-core'; +import { HelperText, HelperTextItem, Label, Skeleton } from '@patternfly/react-core'; import { InferenceServiceKind } from '~/k8sTypes'; import { getProjectDisplayName } from '~/pages/projects/utils'; import { byName, ProjectsContext } from '~/concepts/projects/ProjectsContext'; @@ -27,7 +27,22 @@ const InferenceServiceProject: React.FC = ({ infer const project = modelServingProjects.find(byName(inferenceService.metadata.namespace)); - return <>{project ? getProjectDisplayName(project) : 'Unknown'}; + return ( + <> + {project ? ( + <> + {getProjectDisplayName(project)}{' '} + + + ) : ( + 'Unknown' + )} + + ); }; export default InferenceServiceProject; diff --git a/frontend/src/pages/modelServing/screens/global/InferenceServiceServingRuntime.tsx b/frontend/src/pages/modelServing/screens/global/InferenceServiceServingRuntime.tsx new file mode 100644 index 0000000000..2365eae69e --- /dev/null +++ b/frontend/src/pages/modelServing/screens/global/InferenceServiceServingRuntime.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { ServingRuntimeKind } from '~/k8sTypes'; +import { getDisplayNameFromServingRuntimeTemplate } from '~/pages/modelServing/customServingRuntimes/utils'; + +type Props = { + servingRuntime?: ServingRuntimeKind; +}; + +const InferenceServiceServingRuntime: React.FC = ({ servingRuntime }) => ( + <>{servingRuntime ? getDisplayNameFromServingRuntimeTemplate(servingRuntime) : 'Unknown'} +); + +export default InferenceServiceServingRuntime; diff --git a/frontend/src/pages/modelServing/screens/global/InferenceServiceTable.tsx b/frontend/src/pages/modelServing/screens/global/InferenceServiceTable.tsx index e6ec893a3f..c5bbf75014 100644 --- a/frontend/src/pages/modelServing/screens/global/InferenceServiceTable.tsx +++ b/frontend/src/pages/modelServing/screens/global/InferenceServiceTable.tsx @@ -63,6 +63,7 @@ const InferenceServiceTable: React.FC = ({ )} /> { if (deleted) { @@ -72,7 +73,7 @@ const InferenceServiceTable: React.FC = ({ }} /> { if (edited) { diff --git a/frontend/src/pages/modelServing/screens/global/InferenceServiceTableRow.tsx b/frontend/src/pages/modelServing/screens/global/InferenceServiceTableRow.tsx index 705e4152da..8b46d755b6 100644 --- a/frontend/src/pages/modelServing/screens/global/InferenceServiceTableRow.tsx +++ b/frontend/src/pages/modelServing/screens/global/InferenceServiceTableRow.tsx @@ -3,13 +3,14 @@ import { DropdownDirection } from '@patternfly/react-core'; import { ActionsColumn, Td, Tr } from '@patternfly/react-table'; import { Link } from 'react-router-dom'; import ResourceNameTooltip from '~/components/ResourceNameTooltip'; +import { isModelMesh } from '~/pages/modelServing/utils'; import useModelMetricsEnabled from '~/pages/modelServing/useModelMetricsEnabled'; import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes'; import { getInferenceServiceDisplayName } from './utils'; import InferenceServiceEndpoint from './InferenceServiceEndpoint'; import InferenceServiceProject from './InferenceServiceProject'; -import InferenceServiceModel from './InferenceServiceModel'; import InferenceServiceStatus from './InferenceServiceStatus'; +import InferenceServiceServingRuntime from './InferenceServiceServingRuntime'; type InferenceServiceTableRowProps = { obj: InferenceServiceKind; @@ -53,8 +54,8 @@ const InferenceServiceTableRow: React.FC = ({ )} {isGlobal && ( - - + + )} @@ -71,6 +72,8 @@ const InferenceServiceTableRow: React.FC = ({ dropdownDirection={isGlobal ? DropdownDirection.down : DropdownDirection.up} items={[ { + // TODO re-enable edit when supported + isDisabled: !isModelMesh(inferenceService), title: 'Edit', onClick: () => { onEditInferenceService(inferenceService); diff --git a/frontend/src/pages/modelServing/screens/global/ModelServingGlobal.tsx b/frontend/src/pages/modelServing/screens/global/ModelServingGlobal.tsx index 6d1d1fe044..abd7911b46 100644 --- a/frontend/src/pages/modelServing/screens/global/ModelServingGlobal.tsx +++ b/frontend/src/pages/modelServing/screens/global/ModelServingGlobal.tsx @@ -12,7 +12,7 @@ const ModelServingGlobal: React.FC = () => { return ( { + it('should render error if loading fails', () => { + const result = render( + , + { + wrapper: ({ children }) => ( + ['value'] + } + > + {children} + + ), + }, + ); + + expect(result.queryByText(/test loading error/)).toBeInTheDocument(); + }); + + it('should render modelmesh project', () => { + const result = render( + , + { + wrapper: ({ children }) => ( + ['value'] + } + > + {children} + + ), + }, + ); + + expect(result.queryByText('My Project')).toBeInTheDocument(); + expect(result.queryByText('Multi-model serving enabled')).toBeInTheDocument(); + }); + + it('should render kserve project', () => { + const result = render( + , + { + wrapper: ({ children }) => ( + ['value'] + } + > + {children} + + ), + }, + ); + + expect(result.queryByText('My Project')).toBeInTheDocument(); + expect(result.queryByText('Single model serving enabled')).toBeInTheDocument(); + }); + + it('should render kserve project', () => { + const result = render( + , + { + wrapper: ({ children }) => ( + ['value'] + } + > + {children} + + ), + }, + ); + + expect(result.queryByText('My Project')).not.toBeInTheDocument(); + expect(result.queryByText('Unknown')).toBeInTheDocument(); + expect(result.queryByText('Single model serving enabled')).not.toBeInTheDocument(); + expect(result.queryByText('Multi-model serving enabled')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/pages/modelServing/screens/global/__tests__/InferenceServiceServingRuntime.spec.tsx b/frontend/src/pages/modelServing/screens/global/__tests__/InferenceServiceServingRuntime.spec.tsx new file mode 100644 index 0000000000..37687ecfb8 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/global/__tests__/InferenceServiceServingRuntime.spec.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import InferenceServiceServingRuntime from '~/pages/modelServing/screens/global/InferenceServiceServingRuntime'; +import { mockServingRuntimeK8sResource } from '~/__mocks__/mockServingRuntimeK8sResource'; + +describe('InferenceServiceServingRuntime', () => { + it('should handle undefined serving runtime', () => { + const wrapper = render(); + expect(wrapper.container.textContent).toBe('Unknown'); + }); + + it('should display serving runtime name', () => { + const mockServingRuntime = mockServingRuntimeK8sResource({}); + const wrapper = render(); + expect(wrapper.container.textContent).toBe('OpenVINO Serving Runtime (Supports GPUs)'); + }); +}); diff --git a/frontend/src/pages/modelServing/screens/global/data.ts b/frontend/src/pages/modelServing/screens/global/data.ts index 50a9ec2ca0..7a4b6e3690 100644 --- a/frontend/src/pages/modelServing/screens/global/data.ts +++ b/frontend/src/pages/modelServing/screens/global/data.ts @@ -39,9 +39,9 @@ const COL_ENDPOINT: SortableData = { sortable: false, }; -const COL_MODEL_SERVER: SortableData = { - field: 'model', - label: 'Model server', +const COL_SERVING_RUNTIME: SortableData = { + field: 'servingRuntime', + label: 'Serving runtime', width: 20, sortable: false, }; @@ -62,7 +62,7 @@ export const getGlobalInferenceServiceColumns = ( ): SortableData[] => [ COL_NAME, buildProjectCol(projects), - COL_MODEL_SERVER, + COL_SERVING_RUNTIME, COL_ENDPOINT, COL_STATUS, COL_KEBAB, diff --git a/frontend/src/pages/modelServing/utils.ts b/frontend/src/pages/modelServing/utils.ts index 2ce70f8ec9..e32e8b589e 100644 --- a/frontend/src/pages/modelServing/utils.ts +++ b/frontend/src/pages/modelServing/utils.ts @@ -23,6 +23,7 @@ import { RoleBindingKind, ServingRuntimeKind, DataScienceClusterKindStatus, + InferenceServiceKind, } from '~/k8sTypes'; import { ContainerResources } from '~/types'; import { getDisplayNameFromK8sResource, translateDisplayNameForK8s } from '~/pages/projects/utils'; @@ -229,3 +230,6 @@ export const checkModelMeshFailureStatus = (status: DataScienceClusterKindStatus status.conditions.find( (condition) => condition.type === 'model-meshReady' && condition.status === 'False', )?.message || ''; + +export const isModelMesh = (inferenceService: InferenceServiceKind) => + inferenceService.metadata.annotations?.['serving.kserve.io/deploymentMode'] === 'ModelMesh';