From 82408ab49189e28c4340d9e50843ed6aa775c304 Mon Sep 17 00:00:00 2001 From: Juntao Wang Date: Wed, 8 Nov 2023 14:37:27 -0500 Subject: [PATCH] Add empty state when no serving platform is enabled --- .../ClusterSettings.stories.tsx | 15 ++- .../ModelServingGlobal.stories.tsx | 21 +++- .../modelServing/ServingRuntimeList.spec.ts | 11 +++ .../ServingRuntimeList.stories.tsx | 38 ++++++- .../pages/projects/ProjectDetails.spec.ts | 2 +- .../pages/projects/ProjectDetails.stories.tsx | 37 +++---- .../concepts/areas/__tests__/utils.spec.ts | 61 ++++-------- frontend/src/concepts/areas/const.ts | 5 +- frontend/src/concepts/areas/index.ts | 2 +- .../pages/clusterSettings/ClusterSettings.tsx | 18 ++-- .../ModelServingPlatformSettings.tsx | 68 ++++++++++++- .../CustomServingRuntimeHeaderLabels.tsx | 17 ++-- .../screens/global/ModelServingGlobal.tsx | 12 +++ .../projects/EmptyModelServingPlatform.tsx | 18 ++++ .../screens/projects/ModelServingPlatform.tsx | 33 +++---- .../projects/ModelServingPlatformSelect.tsx | 7 ++ .../getProjectModelServingPlatform.spec.ts | 99 +++++++++++-------- .../modelServing/screens/projects/utils.ts | 35 ++++--- .../src/pages/modelServing/screens/types.ts | 11 +++ .../useServingPlatformStatuses.ts | 24 +++++ 20 files changed, 367 insertions(+), 167 deletions(-) create mode 100644 frontend/src/pages/modelServing/screens/projects/EmptyModelServingPlatform.tsx create mode 100644 frontend/src/pages/modelServing/useServingPlatformStatuses.ts diff --git a/frontend/src/__tests__/integration/pages/clusterSettings/ClusterSettings.stories.tsx b/frontend/src/__tests__/integration/pages/clusterSettings/ClusterSettings.stories.tsx index 20326a91f6..56605c1ac1 100644 --- a/frontend/src/__tests__/integration/pages/clusterSettings/ClusterSettings.stories.tsx +++ b/frontend/src/__tests__/integration/pages/clusterSettings/ClusterSettings.stories.tsx @@ -4,6 +4,9 @@ import { rest } from 'msw'; import { within } from '@storybook/testing-library'; import { mockClusterSettings } from '~/__mocks__/mockClusterSettings'; import ClusterSettings from '~/pages/clusterSettings/ClusterSettings'; +import { AreaContext } from '~/concepts/areas/AreaContext'; +import { mockDscStatus } from '~/__mocks__/mockDscStatus'; +import { StackComponent } from '~/concepts/areas'; export default { component: ClusterSettings, @@ -18,7 +21,17 @@ export default { }, } as Meta; -const Template: StoryFn = (args) => ; +const Template: StoryFn = (args) => ( + + + +); export const Default: StoryObj = { render: Template, diff --git a/frontend/src/__tests__/integration/pages/modelServing/ModelServingGlobal.stories.tsx b/frontend/src/__tests__/integration/pages/modelServing/ModelServingGlobal.stories.tsx index 5700620b71..bdbd766da5 100644 --- a/frontend/src/__tests__/integration/pages/modelServing/ModelServingGlobal.stories.tsx +++ b/frontend/src/__tests__/integration/pages/modelServing/ModelServingGlobal.stories.tsx @@ -15,6 +15,9 @@ import { import { mockSecretK8sResource } from '~/__mocks__/mockSecretK8sResource'; import ModelServingContextProvider from '~/pages/modelServing/ModelServingContext'; import ModelServingGlobal from '~/pages/modelServing/screens/global/ModelServingGlobal'; +import { AreaContext } from '~/concepts/areas/AreaContext'; +import { mockDscStatus } from '~/__mocks__/mockDscStatus'; +import { StackComponent } from '~/concepts/areas'; export default { component: ModelServingGlobal, @@ -56,11 +59,19 @@ export default { } as Meta; const Template: StoryFn = (args) => ( - - }> - } /> - - + + + }> + } /> + + + ); export const EmptyStateNoServingRuntime: StoryObj = { diff --git a/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.spec.ts b/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.spec.ts index 275858b84f..dbf3ae9e50 100644 --- a/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.spec.ts +++ b/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.spec.ts @@ -89,6 +89,17 @@ test('Deploy KServe model', async ({ page }) => { await expect(await page.getByRole('button', { name: 'Deploy', exact: true })).toBeEnabled(); }); +test('No model serving platform available', async ({ page }) => { + await page.goto( + navigateToStory( + 'pages-modelserving-servingruntimelist', + 'neither-platform-enabled-and-project-not-labelled', + ), + ); + + expect(page.getByText('No model serving platform selected')).toBeTruthy(); +}); + test('ModelMesh ServingRuntime list', async ({ page }) => { await page.goto( navigateToStory('pages-modelserving-servingruntimelist', 'model-mesh-list-available-models'), diff --git a/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.stories.tsx b/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.stories.tsx index 0c967b325a..79caea3908 100644 --- a/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.stories.tsx +++ b/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.stories.tsx @@ -33,6 +33,9 @@ import { AppContext } from '~/app/AppContext'; import { useApplicationSettings } from '~/app/useApplicationSettings'; import { ServingRuntimeKind } from '~/k8sTypes'; import { ServingRuntimePlatform } from '~/types'; +import { AreaContext } from '~/concepts/areas/AreaContext'; +import { mockDscStatus } from '~/__mocks__/mockDscStatus'; +import { StackComponent } from '~/concepts/areas'; type HandlersProps = { disableKServeConfig?: boolean; @@ -181,11 +184,22 @@ const Template: StoryFn = (args) => { const { dashboardConfig, loaded } = useApplicationSettings(); return loaded && dashboardConfig ? ( - - }> - } /> - - + + + }> + } /> + + + ) : ( @@ -220,6 +234,20 @@ export const OnlyEnabledModelMeshAndProjectNotLabelled: StoryObj = { }, }; +export const NeitherPlatformEnabledAndProjectNotLabelled: StoryObj = { + render: Template, + + parameters: { + msw: { + handlers: getHandlers({ + disableModelMeshConfig: false, + disableKServeConfig: false, + servingRuntimes: [], + }), + }, + }, +}; + export const ModelMeshListAvailableModels: StoryObj = { render: Template, diff --git a/frontend/src/__tests__/integration/pages/projects/ProjectDetails.spec.ts b/frontend/src/__tests__/integration/pages/projects/ProjectDetails.spec.ts index a89ca6bf86..34c75331d2 100644 --- a/frontend/src/__tests__/integration/pages/projects/ProjectDetails.spec.ts +++ b/frontend/src/__tests__/integration/pages/projects/ProjectDetails.spec.ts @@ -5,7 +5,7 @@ test('Empty project', async ({ page }) => { await page.goto(navigateToStory('pages-projects-projectdetails', 'empty-details-page')); // wait for page to load - await page.waitForSelector('text=No model servers'); + await page.waitForSelector('text=Models and model servers'); // the dividers number should always 1 less than the section number const sections = await page.locator('[data-id="details-page-section"]').all(); diff --git a/frontend/src/__tests__/integration/pages/projects/ProjectDetails.stories.tsx b/frontend/src/__tests__/integration/pages/projects/ProjectDetails.stories.tsx index 724cd4f217..606d42effd 100644 --- a/frontend/src/__tests__/integration/pages/projects/ProjectDetails.stories.tsx +++ b/frontend/src/__tests__/integration/pages/projects/ProjectDetails.stories.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { StoryFn, Meta, StoryObj } from '@storybook/react'; import { DefaultBodyType, MockedRequest, rest, RestHandler } from 'msw'; -import { within } from '@storybook/testing-library'; import { Route } from 'react-router-dom'; import { mockRouteK8sResource, @@ -25,6 +24,9 @@ import { mockStatus } from '~/__mocks__/mockStatus'; import { mockServingRuntimeTemplateK8sResource } from '~/__mocks__/mockServingRuntimeTemplateK8sResource'; import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; import ProjectDetails from '~/pages/projects/screens/detail/ProjectDetails'; +import { AreaContext } from '~/concepts/areas/AreaContext'; +import { mockDscStatus } from '~/__mocks__/mockDscStatus'; +import { StackComponent } from '~/concepts/areas'; const handlers = (isEmpty: boolean): RestHandler>[] => [ rest.get('/api/status', (req, res, ctx) => res(ctx.json(mockStatus()))), @@ -144,21 +146,28 @@ export default { const Template: StoryFn = (args) => { useDetectUser(); return ( - - }> - } /> - - + + + }> + } /> + + + ); }; export const Default: StoryObj = { render: Template, - play: async ({ canvasElement }) => { - // load page and wait until settled - const canvas = within(canvasElement); - await canvas.findByText('Test Notebook', undefined, { timeout: 5000 }); - }, }; export const EmptyDetailsPage: StoryObj = { @@ -169,10 +178,4 @@ export const EmptyDetailsPage: StoryObj = { handlers: handlers(true), }, }, - - play: async ({ canvasElement }) => { - // load page and wait until settled - const canvas = within(canvasElement); - await canvas.findByText('No model servers', undefined, { timeout: 5000 }); - }, }; diff --git a/frontend/src/concepts/areas/__tests__/utils.spec.ts b/frontend/src/concepts/areas/__tests__/utils.spec.ts index af92b7510a..8dc10110fd 100644 --- a/frontend/src/concepts/areas/__tests__/utils.spec.ts +++ b/frontend/src/concepts/areas/__tests__/utils.spec.ts @@ -128,71 +128,50 @@ describe('isAreaAvailable', () => { }); /** - * These tests rely on Model Serving being in a specific configuration, we may need to replace + * These tests rely on Custom Serving Runtime being in a specific configuration, we may need to replace * these tests if these become obsolete. */ describe('reliantAreas', () => { it('should enable area if at least one reliant area is enabled', () => { // Make sure this test is valid - expect(SupportedAreasStateMap[SupportedArea.MODEL_SERVING].reliantAreas).toEqual([ - SupportedArea.K_SERVE, - SupportedArea.MODEL_MESH, + expect(SupportedAreasStateMap[SupportedArea.CUSTOM_RUNTIMES].reliantAreas).toEqual([ + SupportedArea.MODEL_SERVING, ]); // Test both reliant areas - const isAvailableReliantModelMesh = isAreaAvailable( - SupportedArea.MODEL_SERVING, + const isAvailableReliantCustomRuntimes = isAreaAvailable( + SupportedArea.CUSTOM_RUNTIMES, mockDashboardConfig({ disableModelServing: false }).spec, - mockDscStatus({ installedComponents: { [StackComponent.MODEL_MESH]: true } }), + mockDscStatus({}), ); - expect(isAvailableReliantModelMesh.status).toBe(true); - expect(isAvailableReliantModelMesh.featureFlags).toEqual({ ['disableModelServing']: 'on' }); - expect(isAvailableReliantModelMesh.reliantAreas).toEqual({ - [SupportedArea.K_SERVE]: false, - [SupportedArea.MODEL_MESH]: true, + expect(isAvailableReliantCustomRuntimes.status).toBe(true); + expect(isAvailableReliantCustomRuntimes.featureFlags).toEqual({ + ['disableCustomServingRuntimes']: 'on', }); - expect(isAvailableReliantModelMesh.requiredComponents).toBe(null); - - const isAvailableReliantKServe = isAreaAvailable( - SupportedArea.MODEL_SERVING, - mockDashboardConfig({ disableModelServing: false }).spec, - mockDscStatus({ installedComponents: { [StackComponent.K_SERVE]: true } }), - ); - - expect(isAvailableReliantKServe.status).toBe(true); - expect(isAvailableReliantKServe.featureFlags).toEqual({ ['disableModelServing']: 'on' }); - expect(isAvailableReliantKServe.reliantAreas).toEqual({ - [SupportedArea.K_SERVE]: true, - [SupportedArea.MODEL_MESH]: false, + expect(isAvailableReliantCustomRuntimes.reliantAreas).toEqual({ + [SupportedArea.MODEL_SERVING]: true, }); - expect(isAvailableReliantKServe.requiredComponents).toBe(null); + expect(isAvailableReliantCustomRuntimes.requiredComponents).toBe(null); }); it('should disable area if reliant areas are all disabled', () => { // Make sure this test is valid - expect(SupportedAreasStateMap[SupportedArea.MODEL_SERVING].reliantAreas).toEqual([ - SupportedArea.K_SERVE, - SupportedArea.MODEL_MESH, + expect(SupportedAreasStateMap[SupportedArea.CUSTOM_RUNTIMES].reliantAreas).toEqual([ + SupportedArea.MODEL_SERVING, ]); - // Test both areas disabled + // Test areas disabled const isAvailable = isAreaAvailable( - SupportedArea.MODEL_SERVING, - mockDashboardConfig({ disableModelServing: false }).spec, - mockDscStatus({ - installedComponents: { - [StackComponent.K_SERVE]: false, - [StackComponent.MODEL_MESH]: false, - }, - }), + SupportedArea.CUSTOM_RUNTIMES, + mockDashboardConfig({ disableModelServing: true }).spec, + mockDscStatus({}), ); expect(isAvailable.status).not.toBe(true); - expect(isAvailable.featureFlags).toEqual({ ['disableModelServing']: 'on' }); + expect(isAvailable.featureFlags).toEqual({ ['disableCustomServingRuntimes']: 'on' }); expect(isAvailable.reliantAreas).toEqual({ - [SupportedArea.K_SERVE]: false, - [SupportedArea.MODEL_MESH]: false, + [SupportedArea.MODEL_SERVING]: false, }); expect(isAvailable.requiredComponents).toBe(null); }); diff --git a/frontend/src/concepts/areas/const.ts b/frontend/src/concepts/areas/const.ts index 31c9b2b30b..a26b8ed072 100644 --- a/frontend/src/concepts/areas/const.ts +++ b/frontend/src/concepts/areas/const.ts @@ -23,16 +23,15 @@ export const SupportedAreasStateMap: SupportedAreasState = { reliantAreas: [SupportedArea.DS_PROJECTS_VIEW], }, [SupportedArea.K_SERVE]: { - //featureFlags: ['disableKServe'], // TODO: validate KServe feature flag + featureFlags: ['disableKServe'], requiredComponents: [StackComponent.K_SERVE], }, [SupportedArea.MODEL_MESH]: { - //featureFlags: ['disableModelMesh'], // TODO: validate ModelMesh feature flag + featureFlags: ['disableModelMesh'], requiredComponents: [StackComponent.MODEL_MESH], }, [SupportedArea.MODEL_SERVING]: { featureFlags: ['disableModelServing'], - reliantAreas: [SupportedArea.K_SERVE, SupportedArea.MODEL_MESH], }, [SupportedArea.USER_MANAGEMENT]: { featureFlags: ['disableUserManagement'], diff --git a/frontend/src/concepts/areas/index.ts b/frontend/src/concepts/areas/index.ts index d4ab163880..cf05395a9b 100644 --- a/frontend/src/concepts/areas/index.ts +++ b/frontend/src/concepts/areas/index.ts @@ -6,6 +6,6 @@ determine the state we are in. */ export { default as AreaComponent, conditionalArea } from './AreaComponent'; -export { SupportedArea } from './types'; +export { SupportedArea, StackComponent } from './types'; export { default as useIsAreaAvailable } from './useIsAreaAvailable'; export { isAreaAvailable } from './utils'; diff --git a/frontend/src/pages/clusterSettings/ClusterSettings.tsx b/frontend/src/pages/clusterSettings/ClusterSettings.tsx index c326e246a5..d7dbda7c28 100644 --- a/frontend/src/pages/clusterSettings/ClusterSettings.tsx +++ b/frontend/src/pages/clusterSettings/ClusterSettings.tsx @@ -17,6 +17,7 @@ import CullerSettings from '~/pages/clusterSettings/CullerSettings'; import TelemetrySettings from '~/pages/clusterSettings/TelemetrySettings'; import TolerationSettings from '~/pages/clusterSettings/TolerationSettings'; import ModelServingPlatformSettings from '~/pages/clusterSettings/ModelServingPlatformSettings'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import { DEFAULT_CONFIG, DEFAULT_PVC_SIZE, @@ -34,6 +35,7 @@ const ClusterSettings: React.FC = () => { const [userTrackingEnabled, setUserTrackingEnabled] = React.useState(false); const [cullerTimeout, setCullerTimeout] = React.useState(DEFAULT_CULLER_TIMEOUT); const { dashboardConfig } = useAppContext(); + const modelServingEnabled = useIsAreaAvailable(SupportedArea.MODEL_SERVING); const isJupyterEnabled = useCheckJupyterEnabled(); const [notebookTolerationSettings, setNotebookTolerationSettings] = React.useState({ @@ -140,13 +142,15 @@ const ClusterSettings: React.FC = () => { provideChildrenPadding > - - - + {modelServingEnabled && ( + + + + )} void; }; +const accessReviewResource: AccessReviewResourceAttributes = { + group: 'datasciencecluster.opendatahub.io/v1', + resource: 'DataScienceCluster', + verb: 'update', +}; + const ModelServingPlatformSettings: React.FC = ({ initialValue, enabledPlatforms, setEnabledPlatforms, }) => { const [alert, setAlert] = React.useState<{ variant: AlertVariant; message: string }>(); + const { + kServe: { installed: kServeInstalled }, + modelMesh: { installed: modelMeshInstalled }, + } = useServingPlatformStatuses(); + + const [allowUpdate] = useAccessReview(accessReviewResource); + const url = useOpenShiftURL(); React.useEffect(() => { - if (!enabledPlatforms.kServe && !enabledPlatforms.modelMesh) { + const kServeDisabled = !enabledPlatforms.kServe || !kServeInstalled; + const modelMeshDisabled = !enabledPlatforms.modelMesh || !modelMeshInstalled; + if (kServeDisabled && modelMeshDisabled) { setAlert({ variant: AlertVariant.warning, message: @@ -47,18 +71,51 @@ const ModelServingPlatformSettings: React.FC setAlert(undefined); } } - }, [enabledPlatforms, initialValue]); + }, [enabledPlatforms, initialValue, kServeInstalled, modelMeshInstalled]); return ( + + Select the serving platforms that projects on this cluster can use for deploying models. + + + To modify the availability of model serving platforms, ask your cluster admin to + manage the respective components in the{' '} + {allowUpdate && url ? ( + + ) : ( + 'DataScienceCluster' + )}{' '} + resource. + + } + > + + + + } > { const newEnabledPlatforms: ModelServingPlatformEnabled = { ...enabledPlatforms, @@ -75,7 +132,8 @@ const ModelServingPlatformSettings: React.FC { const newEnabledPlatforms: ModelServingPlatformEnabled = { ...enabledPlatforms, diff --git a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeHeaderLabels.tsx b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeHeaderLabels.tsx index c069eb83f9..fd08bda513 100644 --- a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeHeaderLabels.tsx +++ b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeHeaderLabels.tsx @@ -2,27 +2,22 @@ import * as React from 'react'; import { Button, Icon, Label, LabelGroup, Popover } from '@patternfly/react-core'; import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; import { Link } from 'react-router-dom'; -import { useAppContext } from '~/app/AppContext'; import DashboardPopupIconButton from '~/concepts/dashboard/DashboardPopupIconButton'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; const CustomServingRuntimeHeaderLabels: React.FC = () => { - const { - dashboardConfig: { - spec: { - dashboardConfig: { disableKServe, disableModelMesh }, - }, - }, - } = useAppContext(); + const kServeEnabled = useIsAreaAvailable(SupportedArea.K_SERVE).status; + const modelMeshEnabled = useIsAreaAvailable(SupportedArea.MODEL_MESH).status; - if (disableKServe && disableModelMesh) { + if (!kServeEnabled && !modelMeshEnabled) { return null; } return ( <> - {!disableKServe && } - {!disableModelMesh && } + {kServeEnabled && } + {modelMeshEnabled && } { inferenceServices: { data: inferenceServices }, } = React.useContext(ModelServingContext); + const { + kServe: { installed: kServeInstalled }, + modelMesh: { installed: modelMeshInstalled }, + } = useServingPlatformStatuses(); + + const loadError = + !kServeInstalled && !modelMeshInstalled + ? new Error('No model serving platform installed') + : undefined; + return ( } diff --git a/frontend/src/pages/modelServing/screens/projects/EmptyModelServingPlatform.tsx b/frontend/src/pages/modelServing/screens/projects/EmptyModelServingPlatform.tsx new file mode 100644 index 0000000000..02789b6225 --- /dev/null +++ b/frontend/src/pages/modelServing/screens/projects/EmptyModelServingPlatform.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { EmptyState, EmptyStateBody, EmptyStateIcon, Title } from '@patternfly/react-core'; +import { WrenchIcon } from '@patternfly/react-icons'; + +const EmptyModelServingPlatform: React.FC = () => ( + + + + No model serving platform selected + + + To enable model serving, an administrator must first select a model serving platform in the + cluster settings. + + +); + +export default EmptyModelServingPlatform; diff --git a/frontend/src/pages/modelServing/screens/projects/ModelServingPlatform.tsx b/frontend/src/pages/modelServing/screens/projects/ModelServingPlatform.tsx index 62446f0333..d2600cf68e 100644 --- a/frontend/src/pages/modelServing/screens/projects/ModelServingPlatform.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ModelServingPlatform.tsx @@ -14,26 +14,24 @@ import { import { ServingRuntimePlatform } from '~/types'; import ModelServingPlatformSelect from '~/pages/modelServing/screens/projects/ModelServingPlatformSelect'; import { getProjectModelServingPlatform } from '~/pages/modelServing/screens/projects/utils'; -import { useAppContext } from '~/app/AppContext'; import { ProjectsContext } from '~/concepts/projects/ProjectsContext'; import KServeInferenceServiceTable from '~/pages/modelServing/screens/projects/KServeSection/KServeInferenceServiceTable'; +import useServingPlatformStatuses from '~/pages/modelServing/useServingPlatformStatuses'; import ManageServingRuntimeModal from './ServingRuntimeModal/ManageServingRuntimeModal'; import ModelMeshServingRuntimeTable from './ModelMeshSection/ServingRuntimeTable'; import ModelServingPlatformButtonAction from './ModelServingPlatformButtonAction'; import ManageKServeModal from './kServeModal/ManageKServeModal'; const ModelServingPlatform: React.FC = () => { - const { - dashboardConfig: { - spec: { - dashboardConfig: { disableKServe, disableModelMesh }, - }, - }, - } = useAppContext(); const [platformSelected, setPlatformSelected] = React.useState< ServingRuntimePlatform | undefined >(undefined); + const servingPlatformStatuses = useServingPlatformStatuses(); + + const kServeEnabled = servingPlatformStatuses.kServe.enabled; + const modelMeshEnabled = servingPlatformStatuses.modelMesh.enabled; + const { servingRuntimes: { data: servingRuntimes, @@ -60,16 +58,14 @@ const ModelServingPlatform: React.FC = () => { const emptyTemplates = templatesEnabled.length === 0; const emptyModelServer = servingRuntimes.length === 0; - const currentProjectServingPlatform = getProjectModelServingPlatform( - currentProject, - disableKServe, - disableModelMesh, - ); - - const isProjectModelMesh = currentProjectServingPlatform === ServingRuntimePlatform.MULTI; + const { platform: currentProjectServingPlatform, error: platformError } = + getProjectModelServingPlatform(currentProject, servingPlatformStatuses); const shouldShowPlatformSelection = - !disableKServe && !disableModelMesh && !currentProjectServingPlatform; + ((kServeEnabled && modelMeshEnabled) || (!kServeEnabled && !modelMeshEnabled)) && + !currentProjectServingPlatform; + + const isProjectModelMesh = currentProjectServingPlatform === ServingRuntimePlatform.MULTI; const onSubmit = (submit: boolean) => { setPlatformSelected(undefined); @@ -87,7 +83,7 @@ const ModelServingPlatform: React.FC = () => { id={ProjectSectionID.MODEL_SERVER} title={ProjectSectionTitles[ProjectSectionID.MODEL_SERVER]} actions={ - shouldShowPlatformSelection + shouldShowPlatformSelection || platformError ? undefined : [ { } isLoading={!servingRuntimesLoaded && !templatesLoaded} isEmpty={!shouldShowPlatformSelection && emptyModelServer} - loadError={servingRuntimeError || templateError} + loadError={servingRuntimeError || templateError || platformError} emptyState={ { setPlatformSelected(selectedPlatform); }} emptyTemplates={emptyTemplates} + emptyPlatforms={!modelMeshEnabled && !kServeEnabled} /> ) : isProjectModelMesh ? ( diff --git a/frontend/src/pages/modelServing/screens/projects/ModelServingPlatformSelect.tsx b/frontend/src/pages/modelServing/screens/projects/ModelServingPlatformSelect.tsx index 8c45a2e5d2..7d30dd8364 100644 --- a/frontend/src/pages/modelServing/screens/projects/ModelServingPlatformSelect.tsx +++ b/frontend/src/pages/modelServing/screens/projects/ModelServingPlatformSelect.tsx @@ -10,17 +10,24 @@ import { import { ServingRuntimePlatform } from '~/types'; import ModelServingPlatformCard from '~/pages/modelServing/screens/projects/ModelServingPlatformCard'; import ModelServingPlatformButtonAction from '~/pages/modelServing/screens/projects/ModelServingPlatformButtonAction'; +import EmptyModelServingPlatform from '~/pages/modelServing/screens/projects/EmptyModelServingPlatform'; type ModelServingPlatformSelectProps = { onSelect: (platform: ServingRuntimePlatform) => void; emptyTemplates: boolean; + emptyPlatforms: boolean; }; const ModelServingPlatformSelect: React.FC = ({ onSelect, emptyTemplates, + emptyPlatforms, }) => { const [alertShown, setAlertShown] = React.useState(true); + if (emptyPlatforms) { + return ; + } + return ( diff --git a/frontend/src/pages/modelServing/screens/projects/__tests__/getProjectModelServingPlatform.spec.ts b/frontend/src/pages/modelServing/screens/projects/__tests__/getProjectModelServingPlatform.spec.ts index 62c84db55d..42ea1419c0 100644 --- a/frontend/src/pages/modelServing/screens/projects/__tests__/getProjectModelServingPlatform.spec.ts +++ b/frontend/src/pages/modelServing/screens/projects/__tests__/getProjectModelServingPlatform.spec.ts @@ -1,80 +1,99 @@ import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; import { getProjectModelServingPlatform } from '~/pages/modelServing/screens/projects/utils'; +import { ServingPlatformStatuses } from '~/pages/modelServing/screens/types'; import { ServingRuntimePlatform } from '~/types'; +const getMockServingPlatformStatuses = ({ + kServeEnabled = true, + kServeInstalled = true, + modelMeshEnabled = true, + modelMeshInstalled = true, +}): ServingPlatformStatuses => ({ + kServe: { + enabled: kServeEnabled, + installed: kServeInstalled, + }, + modelMesh: { + enabled: modelMeshEnabled, + installed: modelMeshInstalled, + }, +}); + describe('getProjectModelServingPlatform', () => { it('should return undefined if both KServe and ModelMesh are disabled, and project has no platform label', () => { - expect(getProjectModelServingPlatform(mockProjectK8sResource({}), true, true)).toBeUndefined(); - }); - it('should return undefined if both KServe and ModelMesh are enabled, and project has no platform label', () => { expect( - getProjectModelServingPlatform(mockProjectK8sResource({}), false, false), - ).toBeUndefined(); + getProjectModelServingPlatform( + mockProjectK8sResource({}), + getMockServingPlatformStatuses({ kServeEnabled: false, modelMeshEnabled: false }), + ), + ).toStrictEqual({}); }); - it('should return Single Platform if has platform label set to false, no matter whether the feature flags are enabled or not', () => { + it('should return undefined if both KServe and ModelMesh are enabled, and project has no platform label', () => { expect( getProjectModelServingPlatform( - mockProjectK8sResource({ enableModelMesh: false }), - true, - true, + mockProjectK8sResource({}), + getMockServingPlatformStatuses({}), ), - ).toBe(ServingRuntimePlatform.SINGLE); + ).toStrictEqual({}); + }); + it('should return Single Platform if has platform label set to false and KServe is installed', () => { expect( getProjectModelServingPlatform( mockProjectK8sResource({ enableModelMesh: false }), - true, - false, + getMockServingPlatformStatuses({}), ), - ).toBe(ServingRuntimePlatform.SINGLE); + ).toStrictEqual({ platform: ServingRuntimePlatform.SINGLE, error: undefined }); expect( getProjectModelServingPlatform( mockProjectK8sResource({ enableModelMesh: false }), - false, - true, + getMockServingPlatformStatuses({ kServeEnabled: false }), ), - ).toBe(ServingRuntimePlatform.SINGLE); + ).toStrictEqual({ platform: ServingRuntimePlatform.SINGLE, error: undefined }); + }); + it('should give error if has platform label set to false and KServe is not installed', () => { expect( getProjectModelServingPlatform( mockProjectK8sResource({ enableModelMesh: false }), - false, - false, - ), - ).toBe(ServingRuntimePlatform.SINGLE); + getMockServingPlatformStatuses({ kServeEnabled: false, kServeInstalled: false }), + ).error, + ).not.toBeUndefined(); }); - it('should return Multi Platform if has platform label set to true, no matter whether the feature flags are enabled or not', () => { - expect( - getProjectModelServingPlatform(mockProjectK8sResource({ enableModelMesh: true }), true, true), - ).toBe(ServingRuntimePlatform.MULTI); + it('should return Multi Platform if has platform label set to true and ModelMesh is installed', () => { expect( getProjectModelServingPlatform( mockProjectK8sResource({ enableModelMesh: true }), - true, - false, + getMockServingPlatformStatuses({}), ), - ).toBe(ServingRuntimePlatform.MULTI); + ).toStrictEqual({ platform: ServingRuntimePlatform.MULTI, error: undefined }); expect( getProjectModelServingPlatform( mockProjectK8sResource({ enableModelMesh: true }), - false, - true, + getMockServingPlatformStatuses({ modelMeshEnabled: false }), ), - ).toBe(ServingRuntimePlatform.MULTI); + ).toStrictEqual({ platform: ServingRuntimePlatform.MULTI, error: undefined }); + }); + it('should give error if has platform label set to true and ModelMesh is not installed', () => { expect( getProjectModelServingPlatform( mockProjectK8sResource({ enableModelMesh: true }), - false, - false, - ), - ).toBe(ServingRuntimePlatform.MULTI); + getMockServingPlatformStatuses({ modelMeshEnabled: false, modelMeshInstalled: false }), + ).error, + ).not.toBeUndefined(); }); it('should return Single Platform if only KServe is enabled, and project has no platform label', () => { - expect(getProjectModelServingPlatform(mockProjectK8sResource({}), false, true)).toBe( - ServingRuntimePlatform.SINGLE, - ); + expect( + getProjectModelServingPlatform( + mockProjectK8sResource({}), + getMockServingPlatformStatuses({ modelMeshEnabled: false }), + ), + ).toStrictEqual({ platform: ServingRuntimePlatform.SINGLE }); }); it('should return Multi Platform if only ModelMesh is enabled, and project has no platform label', () => { - expect(getProjectModelServingPlatform(mockProjectK8sResource({}), true, false)).toBe( - ServingRuntimePlatform.MULTI, - ); + expect( + getProjectModelServingPlatform( + mockProjectK8sResource({}), + getMockServingPlatformStatuses({ kServeEnabled: false }), + ), + ).toStrictEqual({ platform: ServingRuntimePlatform.MULTI }); }); }); diff --git a/frontend/src/pages/modelServing/screens/projects/utils.ts b/frontend/src/pages/modelServing/screens/projects/utils.ts index 16ccc9b479..2e786ca725 100644 --- a/frontend/src/pages/modelServing/screens/projects/utils.ts +++ b/frontend/src/pages/modelServing/screens/projects/utils.ts @@ -13,6 +13,7 @@ import { CreatingInferenceServiceObject, CreatingServingRuntimeObject, InferenceServiceStorageType, + ServingPlatformStatuses, ServingRuntimeEditInfo, ServingRuntimeSize, } from '~/pages/modelServing/screens/types'; @@ -201,23 +202,33 @@ export const useCreateInferenceServiceObject = ( export const getProjectModelServingPlatform = ( project: ProjectKind, - disableKServe: boolean, - disableModelMesh: boolean, -) => { + platformStatuses: ServingPlatformStatuses, +): { platform?: ServingRuntimePlatform; error?: Error } => { + const { + kServe: { enabled: kServeEnabled, installed: kServeInstalled }, + modelMesh: { enabled: modelMeshEnabled, installed: modelMeshInstalled }, + } = platformStatuses; if (project.metadata.labels[KnownLabels.MODEL_SERVING_PROJECT] === undefined) { - if ((!disableKServe && !disableModelMesh) || (disableKServe && disableModelMesh)) { - return undefined; + if ((kServeEnabled && modelMeshEnabled) || (!kServeEnabled && !modelMeshEnabled)) { + return {}; } - if (disableKServe) { - return ServingRuntimePlatform.MULTI; + if (modelMeshEnabled) { + return { platform: ServingRuntimePlatform.MULTI }; } - if (disableModelMesh) { - return ServingRuntimePlatform.SINGLE; + if (kServeEnabled) { + return { platform: ServingRuntimePlatform.SINGLE }; } } - return project.metadata.labels[KnownLabels.MODEL_SERVING_PROJECT] === 'true' - ? ServingRuntimePlatform.MULTI - : ServingRuntimePlatform.SINGLE; + if (project.metadata.labels[KnownLabels.MODEL_SERVING_PROJECT] === 'true') { + return { + platform: ServingRuntimePlatform.MULTI, + error: modelMeshInstalled ? undefined : new Error('Multi-model platform is not installed'), + }; + } + return { + platform: ServingRuntimePlatform.SINGLE, + error: kServeInstalled ? undefined : new Error('Single model platform is not installed'), + }; }; export const createAWSSecret = (createData: CreatingInferenceServiceObject): Promise => diff --git a/frontend/src/pages/modelServing/screens/types.ts b/frontend/src/pages/modelServing/screens/types.ts index 8069142da2..9bad580601 100644 --- a/frontend/src/pages/modelServing/screens/types.ts +++ b/frontend/src/pages/modelServing/screens/types.ts @@ -81,3 +81,14 @@ export type ServingRuntimeEditInfo = { servingRuntime?: ServingRuntimeKind; secrets: SecretKind[]; }; + +export type ServingPlatformStatuses = { + kServe: { + enabled: boolean; + installed: boolean; + }; + modelMesh: { + enabled: boolean; + installed: boolean; + }; +}; diff --git a/frontend/src/pages/modelServing/useServingPlatformStatuses.ts b/frontend/src/pages/modelServing/useServingPlatformStatuses.ts new file mode 100644 index 0000000000..fb78dda67b --- /dev/null +++ b/frontend/src/pages/modelServing/useServingPlatformStatuses.ts @@ -0,0 +1,24 @@ +import { StackComponent, SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import { ServingPlatformStatuses } from '~/pages/modelServing/screens/types'; + +const useServingPlatformStatuses = (): ServingPlatformStatuses => { + const kServeStatus = useIsAreaAvailable(SupportedArea.K_SERVE); + const modelMeshStatus = useIsAreaAvailable(SupportedArea.MODEL_MESH); + const kServeEnabled = kServeStatus.status; + const modelMeshEnabled = modelMeshStatus.status; + const kServeInstalled = !!kServeStatus.requiredComponents?.[StackComponent.K_SERVE]; + const modelMeshInstalled = !!modelMeshStatus.requiredComponents?.[StackComponent.MODEL_MESH]; + + return { + kServe: { + enabled: kServeEnabled, + installed: kServeInstalled, + }, + modelMesh: { + enabled: modelMeshEnabled, + installed: modelMeshInstalled, + }, + }; +}; + +export default useServingPlatformStatuses;