From 390a3a11d2ace296754fa9df0e0d34c0845db0f0 Mon Sep 17 00:00:00 2001 From: Juntao Wang Date: Thu, 12 Oct 2023 10:45:04 -0400 Subject: [PATCH] Adapt custom serving runtimes to KServe --- .../mockServingRuntimeTemplateK8sResource.ts | 18 +-- .../CustomServingRuntimes.spec.ts | 24 ++++ .../CustomServingRuntimes.stories.tsx | 73 ++++++++++ .../ServingRuntimeList.stories.tsx | 4 +- .../pages/projects/ProjectDetails.stories.tsx | 5 +- frontend/src/api/k8s/templates.ts | 27 +++- frontend/src/k8sTypes.ts | 1 + .../CustomServingRuntimeAddTemplate.tsx | 125 ++++++++++++------ .../CustomServingRuntimeHeaderLabels.tsx | 47 +++++++ .../CustomServingRuntimeListView.tsx | 1 + ...ustomServingRuntimePlatformsLabelGroup.tsx | 34 +++++ .../CustomServingRuntimePlatformsSelector.tsx | 71 ++++++++++ .../CustomServingRuntimeTableRow.tsx | 6 +- .../CustomServingRuntimeView.tsx | 2 + .../customServingRuntimes/templatedData.tsx | 5 + .../customServingRuntimes/utils.ts | 15 +++ frontend/src/services/dashboardService.ts | 4 +- frontend/src/services/templateService.ts | 23 +++- frontend/src/types.ts | 5 + 19 files changed, 427 insertions(+), 63 deletions(-) create mode 100644 frontend/src/__tests__/integration/pages/customServingRuntimes/CustomServingRuntimes.spec.ts create mode 100644 frontend/src/__tests__/integration/pages/customServingRuntimes/CustomServingRuntimes.stories.tsx create mode 100644 frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeHeaderLabels.tsx create mode 100644 frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimePlatformsLabelGroup.tsx create mode 100644 frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimePlatformsSelector.tsx diff --git a/frontend/src/__mocks__/mockServingRuntimeTemplateK8sResource.ts b/frontend/src/__mocks__/mockServingRuntimeTemplateK8sResource.ts index d5ab4a42ab..84dbebc0db 100644 --- a/frontend/src/__mocks__/mockServingRuntimeTemplateK8sResource.ts +++ b/frontend/src/__mocks__/mockServingRuntimeTemplateK8sResource.ts @@ -1,27 +1,29 @@ import { TemplateKind } from '~/k8sTypes'; +import { ServingRuntimePlatform } from '~/types'; type MockResourceConfigType = { name?: string; namespace?: string; + displayName?: string; + platforms?: ServingRuntimePlatform[]; }; -export const mockTemplateK8sResource = ({ - name = 'test-model', +export const mockServingRuntimeTemplateK8sResource = ({ + name = 'template-1', namespace = 'opendatahub', + displayName = 'New OVMS Server', + platforms, }: MockResourceConfigType): TemplateKind => ({ apiVersion: 'template.openshift.io/v1', kind: 'Template', metadata: { - name: 'template-ar2pcc', + name, namespace, - uid: '31277020-b60a-40c9-91bc-5ee3e2bb25ec', - resourceVersion: '164740435', - creationTimestamp: '2023-05-03T21:58:17Z', labels: { 'opendatahub.io/dashboard': 'true', }, annotations: { - tags: 'new-one,servingruntime', + 'opendatahub.io/modelServingSupport': JSON.stringify(platforms), }, }, objects: [ @@ -31,7 +33,7 @@ export const mockTemplateK8sResource = ({ metadata: { name, annotations: { - 'openshift.io/display-name': 'New OVMS Server', + 'openshift.io/display-name': displayName, }, labels: { 'opendatahub.io/dashboard': 'true', diff --git a/frontend/src/__tests__/integration/pages/customServingRuntimes/CustomServingRuntimes.spec.ts b/frontend/src/__tests__/integration/pages/customServingRuntimes/CustomServingRuntimes.spec.ts new file mode 100644 index 0000000000..f78badf283 --- /dev/null +++ b/frontend/src/__tests__/integration/pages/customServingRuntimes/CustomServingRuntimes.spec.ts @@ -0,0 +1,24 @@ +import { test, expect } from '@playwright/test'; + +test('Custom serving runtimes', async ({ page }) => { + await page.goto( + './iframe.html?args=&id=tests-integration-pages-customservingruntimes-customservingruntimes--default&viewMode=story', + ); + // wait for page to load + await page.waitForSelector('text=Serving runtimes'); + + // check the platform setting labels in the header + await expect(page.getByText('Single model serving enabled')).toBeVisible(); + await expect(page.getByText('Multi-model serving enabled')).toBeHidden(); + + // check the platform labels in the table row + await expect(page.locator('#template-1').getByLabel('Label group category')).toHaveText( + 'Single modelMulti-model', + ); + await expect(page.locator('#template-2').getByLabel('Label group category')).toHaveText( + 'Multi-model', + ); + await expect(page.locator('#template-3').getByLabel('Label group category')).toHaveText( + 'Single modelMulti-model', + ); +}); diff --git a/frontend/src/__tests__/integration/pages/customServingRuntimes/CustomServingRuntimes.stories.tsx b/frontend/src/__tests__/integration/pages/customServingRuntimes/CustomServingRuntimes.stories.tsx new file mode 100644 index 0000000000..d6f1f56943 --- /dev/null +++ b/frontend/src/__tests__/integration/pages/customServingRuntimes/CustomServingRuntimes.stories.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import { StoryFn, Meta, StoryObj } from '@storybook/react'; +import { rest } from 'msw'; +import { within } from '@storybook/testing-library'; +import { Route, Routes } from 'react-router-dom'; +import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; +import CustomServingRuntimeView from '~/pages/modelServing/customServingRuntimes/CustomServingRuntimeView'; +import { mockServingRuntimeTemplateK8sResource } from '~/__mocks__/mockServingRuntimeTemplateK8sResource'; +import CustomServingRuntimeContextProvider from '~/pages/modelServing/customServingRuntimes/CustomServingRuntimeContext'; +import { mockStatus } from '~/__mocks__/mockStatus'; +import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; +import useDetectUser from '~/utilities/useDetectUser'; +import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; +import { ServingRuntimePlatform } from '~/types'; + +export default { + component: CustomServingRuntimeView, + parameters: { + msw: { + handlers: [ + rest.get('/api/status', (req, res, ctx) => res(ctx.json(mockStatus()))), + rest.get('/api/templates/opendatahub', (req, res, ctx) => + res( + ctx.json( + mockK8sResourceList([ + mockServingRuntimeTemplateK8sResource({ + platforms: [ServingRuntimePlatform.SINGLE, ServingRuntimePlatform.MULTI], + }), + mockServingRuntimeTemplateK8sResource({ + name: 'template-2', + displayName: 'Multi-model Serving Runtime', + platforms: [ServingRuntimePlatform.MULTI], + }), + mockServingRuntimeTemplateK8sResource({ + name: 'template-3', + displayName: 'Serving Runtime with No Annotations', + }), + ]), + ), + ), + ), + rest.get('/api/k8s/apis/project.openshift.io/v1/projects', (req, res, ctx) => + res(ctx.json(mockK8sResourceList([mockProjectK8sResource({})]))), + ), + rest.get('/api/dashboardConfig/opendatahub/odh-dashboard-config', (req, res, ctx) => + res(ctx.json(mockDashboardConfig({}))), + ), + ], + }, + }, +} as Meta; + +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('Serving runtimes', undefined, { timeout: 5000 }); + }, +}; diff --git a/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.stories.tsx b/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.stories.tsx index c3297d7544..4d65cd314c 100644 --- a/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.stories.tsx +++ b/frontend/src/__tests__/integration/pages/modelServing/ServingRuntimeList.stories.tsx @@ -22,7 +22,7 @@ import { mockPVCK8sResource } from '~/__mocks__/mockPVCK8sResource'; import ProjectsRoutes from '~/concepts/projects/ProjectsRoutes'; import { mockInvalidTemplateK8sResource, - mockTemplateK8sResource, + mockServingRuntimeTemplateK8sResource, } from '~/__mocks__/mockServingRuntimeTemplateK8sResource'; import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; import { mockStatus } from '~/__mocks__/mockStatus'; @@ -114,7 +114,7 @@ export default { res( ctx.json( mockK8sResourceList([ - mockTemplateK8sResource({}), + mockServingRuntimeTemplateK8sResource({}), mockInvalidTemplateK8sResource({}), ]), ), diff --git a/frontend/src/__tests__/integration/pages/projects/ProjectDetails.stories.tsx b/frontend/src/__tests__/integration/pages/projects/ProjectDetails.stories.tsx index 747c391d33..2a20e33276 100644 --- a/frontend/src/__tests__/integration/pages/projects/ProjectDetails.stories.tsx +++ b/frontend/src/__tests__/integration/pages/projects/ProjectDetails.stories.tsx @@ -22,7 +22,7 @@ import { mockPVCK8sResource } from '~/__mocks__/mockPVCK8sResource'; import useDetectUser from '~/utilities/useDetectUser'; import ProjectsRoutes from '~/concepts/projects/ProjectsRoutes'; import { mockStatus } from '~/__mocks__/mockStatus'; -import { mockTemplateK8sResource } from '~/__mocks__/mockServingRuntimeTemplateK8sResource'; +import { mockServingRuntimeTemplateK8sResource } from '~/__mocks__/mockServingRuntimeTemplateK8sResource'; import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; import ProjectDetails from '~/pages/projects/screens/detail/ProjectDetails'; @@ -119,7 +119,8 @@ const handlers = (isEmpty: boolean): RestHandler> ), rest.get( '/api/k8s/apis/template.openshift.io/v1/namespaces/opendatahub/templates', - (req, res, ctx) => res(ctx.json(mockK8sResourceList([mockTemplateK8sResource({})]))), + (req, res, ctx) => + res(ctx.json(mockK8sResourceList([mockServingRuntimeTemplateK8sResource({})]))), ), rest.get( '/api/k8s/apis/opendatahub.io/v1alpha/namespaces/opendatahub/odhdashboardconfigs/odh-dashboard-config', diff --git a/frontend/src/api/k8s/templates.ts b/frontend/src/api/k8s/templates.ts index 694ed95d39..1b1aac2501 100644 --- a/frontend/src/api/k8s/templates.ts +++ b/frontend/src/api/k8s/templates.ts @@ -9,10 +9,12 @@ import { ServingRuntimeKind, TemplateKind } from '~/k8sTypes'; import { ServingRuntimeModel, TemplateModel } from '~/api/models'; import { applyK8sAPIOptions } from '~/api/apiMergeUtils'; import { genRandomChars } from '~/utilities/string'; +import { ServingRuntimePlatform } from '~/types'; export const assembleServingRuntimeTemplate = ( body: string, namespace: string, + platforms: ServingRuntimePlatform[], templateName?: string, ): TemplateKind & { objects: ServingRuntimeKind[] } => { const servingRuntime: ServingRuntimeKind = YAML.parse(body); @@ -32,6 +34,9 @@ export const assembleServingRuntimeTemplate = ( labels: { 'opendatahub.io/dashboard': 'true', }, + annotations: { + 'opendatahub.io/modelServingSupport': JSON.stringify(platforms), + }, }, objects: [servingRuntime], parameters: [], @@ -86,9 +91,10 @@ const dryRunServingRuntimeForTemplateCreation = ( export const createServingRuntimeTemplate = async ( body: string, namespace: string, + platforms: ServingRuntimePlatform[], ): Promise => { try { - const template = assembleServingRuntimeTemplate(body, namespace); + const template = assembleServingRuntimeTemplate(body, namespace, platforms); const servingRuntime = template.objects[0]; const servingRuntimeName = servingRuntime.metadata.name; @@ -109,12 +115,14 @@ export const createServingRuntimeTemplate = async ( }; export const updateServingRuntimeTemplate = ( - templateName: string, - servingRuntimeName: string, + existingTemplate: TemplateKind, body: string, namespace: string, + platforms: ServingRuntimePlatform[], ): Promise => { try { + const templateName = existingTemplate.metadata.name; + const servingRuntimeName = existingTemplate.objects[0].metadata.name; const servingRuntime: ServingRuntimeKind = YAML.parse(body); if (!servingRuntime.metadata.name) { throw new Error('Serving runtime name is required.'); @@ -134,6 +142,19 @@ export const updateServingRuntimeTemplate = ( path: '/objects/0', value: servingRuntime, }, + existingTemplate.metadata.annotations + ? { + op: 'replace', + path: '/metadata/annotations/opendatahub.io~1modelServingSupport', + value: JSON.stringify(platforms), + } + : { + op: 'add', + path: '/metadata/annotations', + value: { + 'opendatahub.io/modelServingSupport': JSON.stringify(platforms), + }, + }, ], }), ); diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index 329d5008be..d2f4e7a83f 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -705,6 +705,7 @@ export type TemplateKind = K8sResourceCommon & { tags: string; iconClass?: string; 'opendatahub.io/template-enabled': string; + 'opendatahub.io/modelServingSupport': string; }>; name: string; namespace: string; diff --git a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeAddTemplate.tsx b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeAddTemplate.tsx index e2f7f204e9..3e1f5be997 100644 --- a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeAddTemplate.tsx +++ b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeAddTemplate.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; import { - ActionList, - ActionListItem, + ActionGroup, Alert, AlertActionCloseButton, Breadcrumb, BreadcrumbItem, Button, + Form, Stack, StackItem, } from '@patternfly/react-core'; @@ -21,7 +21,10 @@ import { createServingRuntimeTemplateBackend, updateServingRuntimeTemplateBackend, } from '~/services/templateService'; +import { ServingRuntimePlatform } from '~/types'; +import CustomServingRuntimePlatformsSelector from '~/pages/modelServing/customServingRuntimes/CustomServingRuntimePlatformsSelector'; import { + getEnabledPlatformsFromTemplate, getServingRuntimeDisplayNameFromTemplate, getServingRuntimeNameFromTemplate, isServingRuntimeKind, @@ -61,16 +64,43 @@ const CustomServingRuntimeAddTemplate: React.FC (state ? getEnabledPlatformsFromTemplate(state.template) : []), + [state], + ); + const stringifiedTemplate = React.useMemo( () => existingTemplate ? YAML.stringify(existingTemplate.objects[0]) : copiedServingRuntimeString, [copiedServingRuntimeString, existingTemplate], ); + + const enabledPlatforms: ServingRuntimePlatform[] = React.useMemo( + () => + existingTemplate + ? getEnabledPlatformsFromTemplate(existingTemplate) + : copiedServingRuntimePlatforms, + [existingTemplate, copiedServingRuntimePlatforms], + ); + const [code, setCode] = React.useState(stringifiedTemplate); + const [selectedPlatforms, setSelectedPlatforms] = + React.useState(enabledPlatforms); + const isSinglePlatformEnabled = selectedPlatforms.includes(ServingRuntimePlatform.SINGLE); + const isMultiPlatformEnabled = selectedPlatforms.includes(ServingRuntimePlatform.MULTI); const [loading, setIsLoading] = React.useState(false); const [error, setError] = React.useState(undefined); const navigate = useNavigate(); + const isDisabled = + (!state && + code === stringifiedTemplate && + enabledPlatforms.includes(ServingRuntimePlatform.SINGLE) === isSinglePlatformEnabled && + enabledPlatforms.includes(ServingRuntimePlatform.MULTI) === isMultiPlatformEnabled) || + code === '' || + selectedPlatforms.length === 0 || + loading; + return ( @@ -101,40 +131,47 @@ const CustomServingRuntimeAddTemplate: React.FC - - - { - setCode(codeChanged); - }} - /> - - {error && ( +
+ - setError(undefined)} />} - > - {error.message} - + - )} - - - + + { + setCode(codeChanged); + }} + /> + + {error && ( + + setError(undefined)} />} + > + {error.message} + + + )} + + - - - - - - + + + +
); }; diff --git a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeHeaderLabels.tsx b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeHeaderLabels.tsx new file mode 100644 index 0000000000..c069eb83f9 --- /dev/null +++ b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeHeaderLabels.tsx @@ -0,0 +1,47 @@ +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'; + +const CustomServingRuntimeHeaderLabels: React.FC = () => { + const { + dashboardConfig: { + spec: { + dashboardConfig: { disableKServe, disableModelMesh }, + }, + }, + } = useAppContext(); + + if (disableKServe && disableModelMesh) { + return null; + } + + return ( + <> + + {!disableKServe && } + {!disableModelMesh && } + + + You can change which model serving platforms are enabled in the{' '} + + . + + } + > + + } aria-label="More info" /> + + + + ); +}; + +export default CustomServingRuntimeHeaderLabels; diff --git a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeListView.tsx b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeListView.tsx index c8fba8316a..cab542ef6d 100644 --- a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeListView.tsx +++ b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeListView.tsx @@ -24,6 +24,7 @@ const CustomServingRuntimeListView: React.FC = () => { const navigate = useNavigate(); const [deleteTemplate, setDeleteTemplate] = React.useState(); + const sortedTemplates = React.useMemo( () => getSortedTemplates(unsortedTemplates, templateOrder), [unsortedTemplates, templateOrder], diff --git a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimePlatformsLabelGroup.tsx b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimePlatformsLabelGroup.tsx new file mode 100644 index 0000000000..c014a6e95b --- /dev/null +++ b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimePlatformsLabelGroup.tsx @@ -0,0 +1,34 @@ +import { Label, LabelGroup } from '@patternfly/react-core'; +import * as React from 'react'; +import { TemplateKind } from '~/k8sTypes'; +import { getEnabledPlatformsFromTemplate } from '~/pages/modelServing/customServingRuntimes/utils'; +import { ServingRuntimePlatform } from '~/types'; + +type CustomServingRuntimePlatformsLabelGroupProps = { + template: TemplateKind; +}; + +const ServingRuntimePlatformLabels = { + [ServingRuntimePlatform.SINGLE]: 'Single model', + [ServingRuntimePlatform.MULTI]: 'Multi-model', +}; + +const CustomServingRuntimePlatformsLabelGroup: React.FC< + CustomServingRuntimePlatformsLabelGroupProps +> = ({ template }) => { + const platforms = getEnabledPlatformsFromTemplate(template); + + if (platforms.length === 0) { + return null; + } + + return ( + + {platforms.map((platform, i) => ( + + ))} + + ); +}; + +export default CustomServingRuntimePlatformsLabelGroup; diff --git a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimePlatformsSelector.tsx b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimePlatformsSelector.tsx new file mode 100644 index 0000000000..d18fdcf4c9 --- /dev/null +++ b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimePlatformsSelector.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { FormGroup } from '@patternfly/react-core'; +import { ServingRuntimePlatform } from '~/types'; +import SimpleDropdownSelect from '~/components/SimpleDropdownSelect'; + +type CustomServingRuntimePlatformsSelectorProps = { + isSinglePlatformEnabled: boolean; + isMultiPlatformEnabled: boolean; + setSelectedPlatforms: (platforms: ServingRuntimePlatform[]) => void; +}; + +const RuntimePlatformSelectOptionLabels = { + [ServingRuntimePlatform.SINGLE]: 'Single model serving platform', + [ServingRuntimePlatform.MULTI]: 'Multi-model serving platform', + both: 'Both single and multi-model serving platforms', +}; + +const CustomServingRuntimePlatformsSelector: React.FC< + CustomServingRuntimePlatformsSelectorProps +> = ({ isSinglePlatformEnabled, isMultiPlatformEnabled, setSelectedPlatforms }) => { + const options = [ + { + key: ServingRuntimePlatform.SINGLE, + label: RuntimePlatformSelectOptionLabels[ServingRuntimePlatform.SINGLE], + }, + { + key: ServingRuntimePlatform.MULTI, + label: RuntimePlatformSelectOptionLabels[ServingRuntimePlatform.MULTI], + }, + { + key: 'both', + label: RuntimePlatformSelectOptionLabels['both'], + }, + ]; + + const selection = + isSinglePlatformEnabled && isMultiPlatformEnabled + ? 'both' + : isSinglePlatformEnabled + ? ServingRuntimePlatform.SINGLE + : isMultiPlatformEnabled + ? ServingRuntimePlatform.MULTI + : ''; + return ( + + { + if (key === 'both') { + setSelectedPlatforms([ServingRuntimePlatform.SINGLE, ServingRuntimePlatform.MULTI]); + } else if ( + key === ServingRuntimePlatform.SINGLE || + key === ServingRuntimePlatform.MULTI + ) { + setSelectedPlatforms([key]); + } + }} + /> + + ); +}; + +export default CustomServingRuntimePlatformsSelector; diff --git a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeTableRow.tsx b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeTableRow.tsx index 4a991b205e..eed5607a9c 100644 --- a/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeTableRow.tsx +++ b/frontend/src/pages/modelServing/customServingRuntimes/CustomServingRuntimeTableRow.tsx @@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { Label } from '@patternfly/react-core'; import { TemplateKind } from '~/k8sTypes'; import ResourceNameTooltip from '~/components/ResourceNameTooltip'; +import CustomServingRuntimePlatformsLabelGroup from '~/pages/modelServing/customServingRuntimes/CustomServingRuntimePlatformsLabelGroup'; import CustomServingRuntimeEnabledToggle from './CustomServingRuntimeEnabledToggle'; import { getServingRuntimeDisplayNameFromTemplate, @@ -34,7 +35,7 @@ const CustomServingRuntimeTableRow: React.FC id: `draggable-row-${servingRuntimeName}`, }} /> - + {getServingRuntimeDisplayNameFromTemplate(template)} @@ -43,6 +44,9 @@ const CustomServingRuntimeTableRow: React.FC + + + { empty={servingRuntimeTemplates.length === 0} emptyStatePage={} provideChildrenPadding + headerContent={} > diff --git a/frontend/src/pages/modelServing/customServingRuntimes/templatedData.tsx b/frontend/src/pages/modelServing/customServingRuntimes/templatedData.tsx index 921d4eef68..fb8928785a 100644 --- a/frontend/src/pages/modelServing/customServingRuntimes/templatedData.tsx +++ b/frontend/src/pages/modelServing/customServingRuntimes/templatedData.tsx @@ -23,6 +23,11 @@ export const columns: SortableData[] = [ }, }, }, + { + field: 'platforms', + label: 'Serving platforms supported', + sortable: false, + }, { field: 'kebab', label: '', diff --git a/frontend/src/pages/modelServing/customServingRuntimes/utils.ts b/frontend/src/pages/modelServing/customServingRuntimes/utils.ts index cc9322f354..2c5613d038 100644 --- a/frontend/src/pages/modelServing/customServingRuntimes/utils.ts +++ b/frontend/src/pages/modelServing/customServingRuntimes/utils.ts @@ -1,6 +1,7 @@ import { K8sResourceCommon } from '@openshift/dynamic-plugin-sdk-utils'; import { ServingRuntimeKind, TemplateKind } from '~/k8sTypes'; import { getDisplayNameFromK8sResource } from '~/pages/projects/utils'; +import { ServingRuntimePlatform } from '~/types'; export const getTemplateEnabled = (template: TemplateKind, templateDisablement: string[]) => !templateDisablement.includes(getServingRuntimeNameFromTemplate(template)); @@ -93,3 +94,17 @@ export const getDisplayNameFromServingRuntimeTemplate = (resource: ServingRuntim return templateName || legacyTemplateName || 'Unknown Serving Runtime'; }; + +export const getEnabledPlatformsFromTemplate = ( + template: TemplateKind, +): ServingRuntimePlatform[] => { + if (!template.metadata.annotations?.['opendatahub.io/modelServingSupport']) { + return [ServingRuntimePlatform.SINGLE, ServingRuntimePlatform.MULTI]; + } + + try { + return JSON.parse(template.metadata.annotations?.['opendatahub.io/modelServingSupport']); + } catch (e) { + return [ServingRuntimePlatform.SINGLE, ServingRuntimePlatform.MULTI]; + } +}; diff --git a/frontend/src/services/dashboardService.ts b/frontend/src/services/dashboardService.ts index 5a0adee1a8..c3e054501b 100644 --- a/frontend/src/services/dashboardService.ts +++ b/frontend/src/services/dashboardService.ts @@ -10,7 +10,9 @@ export const getDashboardConfigBackend = (namespace: string): Promise Promise.reject(e)); export const getDashboardConfigTemplateOrderBackend = (ns: string): Promise => - getDashboardConfigBackend(ns).then((dashboardConfig) => dashboardConfig.spec.templateOrder || []); + getDashboardConfigBackend(ns).then( + (dashboardConfig) => dashboardConfig.spec?.templateOrder || [], + ); export const getDashboardConfigTemplateDisablementBackend = (ns: string): Promise => getDashboardConfigBackend(ns).then( diff --git a/frontend/src/services/templateService.ts b/frontend/src/services/templateService.ts index 5713a7cf73..c21371e355 100644 --- a/frontend/src/services/templateService.ts +++ b/frontend/src/services/templateService.ts @@ -3,6 +3,7 @@ import axios from 'axios'; import YAML from 'yaml'; import { assembleServingRuntimeTemplate } from '~/api'; import { ServingRuntimeKind, TemplateKind } from '~/k8sTypes'; +import { ServingRuntimePlatform } from '~/types'; import { addTypesToK8sListedResources } from '~/utilities/addTypesToK8sListedResources'; export const listTemplatesBackend = async ( @@ -30,9 +31,10 @@ const dryRunServingRuntimeForTemplateCreationBackend = ( export const createServingRuntimeTemplateBackend = async ( body: string, namespace: string, + platforms: ServingRuntimePlatform[], ): Promise => { try { - const template = assembleServingRuntimeTemplate(body, namespace); + const template = assembleServingRuntimeTemplate(body, namespace, platforms); const servingRuntime = template.objects[0]; const servingRuntimeName = servingRuntime.metadata.name; @@ -55,12 +57,14 @@ export const createServingRuntimeTemplateBackend = async ( }; export const updateServingRuntimeTemplateBackend = ( - name: string, - servingRuntimeName: string, + existingTemplate: TemplateKind, body: string, namespace: string, + platforms: ServingRuntimePlatform[], ): Promise => { try { + const name = existingTemplate.metadata.name; + const servingRuntimeName = existingTemplate.objects[0].metadata.name; const servingRuntime: ServingRuntimeKind = YAML.parse(body); if (!servingRuntime.metadata.name) { throw new Error('Serving runtime name is required.'); @@ -78,6 +82,19 @@ export const updateServingRuntimeTemplateBackend = ( path: '/objects/0', value: servingRuntime, }, + existingTemplate.metadata.annotations + ? { + op: 'replace', + path: '/metadata/annotations/opendatahub.io~1modelServingSupport', + value: JSON.stringify(platforms), + } + : { + op: 'add', + path: '/metadata/annotations', + value: { + 'opendatahub.io/modelServingSupport': JSON.stringify(platforms), + }, + }, ]) .then((response) => response.data), ); diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 122f211107..2d3dc86463 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -749,3 +749,8 @@ export type AcceleratorInfo = { total: { [key: string]: number }; allocated: { [key: string]: number }; }; + +export enum ServingRuntimePlatform { + SINGLE = 'single', + MULTI = 'multi', +}