diff --git a/backend/src/types.ts b/backend/src/types.ts index 1b8e0e6e1e..613d85ebdc 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -29,6 +29,7 @@ export type DashboardConfig = K8sResourceCommon & { disableCustomServingRuntimes: boolean; modelMetricsNamespace: string; disablePipelines: boolean; + disableAcceleratorProfiles: boolean; }; groupsConfig?: { adminGroups: string; @@ -254,7 +255,6 @@ export type KubeDecorator = KubeStatus & { customObjectsApi: k8s.CustomObjectsApi; rbac: k8s.RbacAuthorizationV1Api; currentToken: string; - }; export type KubeFastifyInstance = FastifyInstance & { @@ -759,10 +759,10 @@ export type GPUInfo = { export type AcceleratorInfo = { configured: boolean; - available: {[key: string]: number}; - total: {[key: string]: number}; - allocated: {[key: string]: number}; -} + available: { [key: string]: number }; + total: { [key: string]: number }; + allocated: { [key: string]: number }; +}; export type EnvironmentVariable = EitherNotBoth< { value: string | number }, @@ -882,7 +882,6 @@ export type SupportedModelFormats = { autoSelect?: boolean; }; - export enum ContainerResourceAttributes { CPU = 'cpu', MEMORY = 'memory', diff --git a/backend/src/utils/constants.ts b/backend/src/utils/constants.ts index 1699c0eac1..d73e1e625b 100644 --- a/backend/src/utils/constants.ts +++ b/backend/src/utils/constants.ts @@ -53,6 +53,7 @@ export const blankDashboardCR: DashboardConfig = { disableCustomServingRuntimes: false, modelMetricsNamespace: '', disablePipelines: false, + disableAcceleratorProfiles: false, }, notebookController: { enabled: true, diff --git a/docs/dashboard-config.md b/docs/dashboard-config.md index cf20d49269..faa9076074 100644 --- a/docs/dashboard-config.md +++ b/docs/dashboard-config.md @@ -2,14 +2,14 @@ # Dashboard Config -By default the ODH Dashboard comes with a set of core features enabled that are design to work for most scenarios. The dashboard can be configured from its OdhDashboard CR, `odh-dashboard-config`. +By default the ODH Dashboard comes with a set of core features enabled that are design to work for most scenarios. The dashboard can be configured from its OdhDashboard CR, `odh-dashboard-config`. ## Features The following are a list of features that are supported, along with there default settings. | Feature | Default | Description | -|------------------------------|---------|------------------------------------------------------------------------------------------------------| +| ---------------------------- | ------- | ---------------------------------------------------------------------------------------------------- | | enablement | true | Enables the ability to enable ISVs to the dashboard | | disableInfo | false | Removes the information panel in Explore Application section | | disableSupport | false | Disables components related to support. | @@ -24,6 +24,7 @@ The following are a list of features that are supported, along with there defaul | disableModelServing | false | Disables Model Serving from the dashboard and from Data Science Projects. | | disableProjectSharing | false | Disables Project Sharing from Data Science Projects. | | disableCustomServingRuntimes | false | Disables Custom Serving Runtimes from the Admin Panel. | +| disableAcceleratorProfiles | false | Disables Accelerator profiles from the Admin Panel. | | modelMetricsNamespace | false | Enables the namespace in which the Model Serving Metrics' Prometheus Operator is installed. | ## Defaults @@ -47,6 +48,7 @@ spec: disableModelServing: false disableProjectSharing: false disableCustomServingRuntimes: false + disableAcceleratorProfiles: false modelMetricsNamespace: '' ``` @@ -72,12 +74,12 @@ Note: These sizes must follow conventions such as requests smaller than limits ```yaml notebookSizes: -- name: XSmall - resources: - requests: + - name: XSmall + resources: + requests: memory: 0.5Gi cpu: '0.1' - limits: + limits: memory: 2Gi cpu: '0.1' ``` @@ -103,11 +105,11 @@ We make use of the Notebook resource as a source of truth for what the user has New annotations we created are: -| Annotation name | What it represents | -| --------------- | ------------------ | -| `opendatahub.io/username` | The untranslated username behind the notebook`*` | -| `notebooks.opendatahub.io/last-image-selection` | The last image the user selected (on create notebook) | -| `notebooks.opendatahub.io/last-size-selection` | The last notebook size the user selected (on create notebook) | +| Annotation name | What it represents | +| ----------------------------------------------- | ------------------------------------------------------------- | +| `opendatahub.io/username` | The untranslated username behind the notebook`*` | +| `notebooks.opendatahub.io/last-image-selection` | The last image the user selected (on create notebook) | +| `notebooks.opendatahub.io/last-size-selection` | The last notebook size the user selected (on create notebook) | `*` - We need the original user's name (we translate their name to kube safe characters for notebook name and for the label) for some functionality. If this is omitted from the Notebook (or they don't have one yet) we try to make a validation against the current logged in user. This will work most of the time (and we assume logged in user when they don't have a Notebook), if this fails because you're an Admin and we don't have this state, we consider this an invalid state - should be rare though as it requires the subset of users that are Admins to have a bad-state Notebook they are trying to impersonate (to start or view that users Notebook information). @@ -136,6 +138,7 @@ spec: disableModelServing: true disableProjectSharing: true disableCustomServingRuntimes: false + disableAcceleratorProfiles: true modelMetricsNamespace: '' notebookController: enabled: true @@ -143,51 +146,51 @@ spec: - name: Small resources: limits: - cpu: "2" + cpu: '2' memory: 2Gi requests: - cpu: "1" + cpu: '1' memory: 1Gi - name: Medium resources: limits: - cpu: "4" + cpu: '4' memory: 4Gi requests: - cpu: "2" + cpu: '2' memory: 2Gi - name: Large resources: limits: - cpu: "8" + cpu: '8' memory: 8Gi requests: - cpu: "4" + cpu: '4' memory: 4Gi modelServerSizes: - name: Small resources: limits: - cpu: "2" + cpu: '2' memory: 8Gi requests: - cpu: "1" + cpu: '1' memory: 4Gi - name: Medium resources: limits: - cpu: "8" + cpu: '8' memory: 10Gi requests: - cpu: "4" + cpu: '4' memory: 8Gi - name: Large resources: limits: - cpu: "10" + cpu: '10' memory: 20Gi requests: - cpu: "6" + cpu: '6' memory: 16Gi groupsConfig: adminGroups: 'odh-admins' @@ -196,5 +199,4 @@ spec: - 'ovms' templateDisablement: - 'ovms' - ``` diff --git a/frontend/src/__mocks__/mockAcceleratorProfile.ts b/frontend/src/__mocks__/mockAcceleratorProfile.ts new file mode 100644 index 0000000000..4679cbe811 --- /dev/null +++ b/frontend/src/__mocks__/mockAcceleratorProfile.ts @@ -0,0 +1,22 @@ +import { AcceleratorKind } from '~/k8sTypes'; + +export const mockAcceleratorProfile = (): AcceleratorKind => ({ + apiVersion: 'dashboard.opendatahub.io/v1', + kind: 'AcceleratorProfile', + metadata: { + name: 'test-accelerator', + }, + spec: { + displayName: 'test-accelerator', + enabled: true, + identifier: 'nvidia.com/gpu', + description: 'Test description', + tolerations: [ + { + key: 'nvidia.com/gpu', + operator: 'Exists', + effect: 'NoSchedule', + }, + ], + }, +}); diff --git a/frontend/src/__mocks__/mockDashboardConfig.ts b/frontend/src/__mocks__/mockDashboardConfig.ts index b388678b38..70d9eb0f6f 100644 --- a/frontend/src/__mocks__/mockDashboardConfig.ts +++ b/frontend/src/__mocks__/mockDashboardConfig.ts @@ -13,6 +13,7 @@ type MockDashboardConfigType = { disableProjects?: boolean; disableModelServing?: boolean; disableCustomServingRuntimes?: boolean; + disableAcceleratorProfiles?: boolean; }; export const mockDashboardConfig = ({ @@ -27,6 +28,7 @@ export const mockDashboardConfig = ({ disableProjects = false, disableModelServing = false, disableCustomServingRuntimes = false, + disableAcceleratorProfiles = false, }: MockDashboardConfigType): DashboardConfig => ({ apiVersion: 'opendatahub.io/v1alpha', kind: 'OdhDashboardConfig', @@ -54,6 +56,7 @@ export const mockDashboardConfig = ({ modelMetricsNamespace: 'test-project', disablePipelines: false, disableProjectSharing: false, + disableAcceleratorProfiles, }, notebookController: { enabled: true, diff --git a/frontend/src/__tests__/integration/pages/acceleratorProfiles/AcceleratorProfiles.spec.ts b/frontend/src/__tests__/integration/pages/acceleratorProfiles/AcceleratorProfiles.spec.ts new file mode 100644 index 0000000000..98b9924715 --- /dev/null +++ b/frontend/src/__tests__/integration/pages/acceleratorProfiles/AcceleratorProfiles.spec.ts @@ -0,0 +1,17 @@ +import { test, expect } from '@playwright/test'; +import { navigateToStory } from '~/__tests__/integration/utils'; + +test('Empty State no Accelerator profile', async ({ page }) => { + await page.goto( + navigateToStory( + 'pages-acceleratorprofiles-acceleratorprofiles', + 'empty-state-no-accelerator-profile', + ), + ); + + // wait for page to load + await page.waitForSelector('text=No available accelerator profiles yet'); + + // Test that the button is enabled + await expect(page.getByRole('button', { name: 'Add new accelerator profile' })).toBeEnabled(); +}); diff --git a/frontend/src/__tests__/integration/pages/acceleratorProfiles/AcceleratorProfiles.stories.tsx b/frontend/src/__tests__/integration/pages/acceleratorProfiles/AcceleratorProfiles.stories.tsx new file mode 100644 index 0000000000..27001e575b --- /dev/null +++ b/frontend/src/__tests__/integration/pages/acceleratorProfiles/AcceleratorProfiles.stories.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { StoryFn, Meta, StoryObj } from '@storybook/react'; +import { rest } from 'msw'; +import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; +import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; +import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; +import useDetectUser from '~/utilities/useDetectUser'; +import { mockStatus } from '~/__mocks__/mockStatus'; + +import AcceleratorProfiles from '~/pages/acceleratorProfiles/AcceleratorProfiles'; + +export default { + component: AcceleratorProfiles, + parameters: { + msw: { + handlers: { + status: [ + rest.get('/api/k8s/apis/project.openshift.io/v1/projects', (req, res, ctx) => + res(ctx.json(mockK8sResourceList([mockProjectK8sResource({})]))), + ), + rest.get('/api/status', (req, res, ctx) => res(ctx.json(mockStatus()))), + ], + accelerators: rest.get( + '/api/k8s/apis/dashboard.opendatahub.io/v1/namespaces/opendatahub/acceleratorprofiles', + (req, res, ctx) => res(ctx.json(mockK8sResourceList([mockAcceleratorProfile()]))), + ), + }, + }, + }, +} as Meta; + +const Template: StoryFn = (args) => { + useDetectUser(); + return ; +}; + +export const EmptyStateNoAcceleratorProfile: StoryObj = { + render: Template, + + parameters: { + msw: { + handlers: { + accelerators: rest.get( + '/api/k8s/apis/dashboard.opendatahub.io/v1/namespaces/opendatahub/acceleratorprofiles', + (req, res, ctx) => res(ctx.json(mockK8sResourceList([]))), + ), + }, + }, + }, +}; diff --git a/frontend/src/app/AppRoutes.tsx b/frontend/src/app/AppRoutes.tsx index fc24d81c50..88ecd597ae 100644 --- a/frontend/src/app/AppRoutes.tsx +++ b/frontend/src/app/AppRoutes.tsx @@ -37,6 +37,10 @@ const DependencyMissingPage = React.lazy( () => import('../pages/dependencies/DependencyMissingPage'), ); +const AcceleratorProfiles = React.lazy( + () => import('../pages/acceleratorProfiles/AcceleratorProfiles'), +); + const AppRoutes: React.FC = () => { const { isAdmin, isAllowed } = useUser(); @@ -76,6 +80,7 @@ const AppRoutes: React.FC = () => { <> } /> } /> + } /> } /> } /> diff --git a/frontend/src/pages/acceleratorProfiles/AcceleratorProfiles.tsx b/frontend/src/pages/acceleratorProfiles/AcceleratorProfiles.tsx new file mode 100644 index 0000000000..8cd2938307 --- /dev/null +++ b/frontend/src/pages/acceleratorProfiles/AcceleratorProfiles.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { + Button, + ButtonVariant, + Flex, + FlexItem, + EmptyState, + EmptyStateIcon, + EmptyStateVariant, + EmptyStateBody, + PageSection, + PageSectionVariants, + Title, +} from '@patternfly/react-core'; +import { PlusCircleIcon } from '@patternfly/react-icons'; +import ApplicationsPage from '~/pages/ApplicationsPage'; +import useAccelerators from '~/pages/notebookController/screens/server/useAccelerators'; +import { useDashboardNamespace } from '~/redux/selectors'; + +const description = `Manage accelerator profile settings for users in your organization`; + +const AcceleratorProfiles: React.FC = () => { + const { dashboardNamespace } = useDashboardNamespace(); + const [accelerators, loaded, loadError] = useAccelerators(dashboardNamespace); + + const isEmpty = !accelerators || accelerators.length === 0; + + const noAcceleratorProfilePageSection = ( + + + + + No available accelerator profiles yet + + + You don't have any accelerator profiles yet. To get started, please ask your cluster + administrator about the accelerator availability in your cluster and create corresponding + profiles in Openshift Data Science. + + + + + ); + + return ( + + + + {/* Todo: Create accelerator table */} + + + + ); +}; + +export default AcceleratorProfiles; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 50898f5b44..8fd14271b1 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -91,6 +91,7 @@ export type DashboardCommonConfig = { disableCustomServingRuntimes: boolean; modelMetricsNamespace: string; disablePipelines: boolean; + disableAcceleratorProfiles: boolean; }; export type NotebookControllerUserState = { diff --git a/frontend/src/utilities/NavData.tsx b/frontend/src/utilities/NavData.tsx index b0adb2931c..064367e4ea 100644 --- a/frontend/src/utilities/NavData.tsx +++ b/frontend/src/utilities/NavData.tsx @@ -53,6 +53,14 @@ const getSettingsNav = ( }); } + if (featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disableAcceleratorProfiles)) { + settingsNavs.push({ + id: 'accelerator-profile', + label: 'Accelerator profiles', + href: '/acceleratorProfiles', + }); + } + if ( featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disableCustomServingRuntimes) && featureFlagEnabled(dashboardConfig.spec.dashboardConfig.disableModelServing) diff --git a/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml b/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml index 8108ce32aa..9faadcba2c 100644 --- a/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml +++ b/manifests/crd/odhdashboardconfigs.opendatahub.io.crd.yaml @@ -53,6 +53,8 @@ spec: type: string disablePipelines: type: boolean + disableAcceleratorProfiles: + type: boolean groupsConfig: type: object required: @@ -134,10 +136,10 @@ spec: notebookTolerationSettings: type: object properties: - enabled: - type: boolean - key: - type: string + enabled: + type: boolean + key: + type: string storageClassName: type: string templateOrder: diff --git a/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml b/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml index 677c300dc9..6ce2946c92 100644 --- a/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml +++ b/manifests/overlays/odhdashboardconfig/odh-dashboard-config.yaml @@ -18,6 +18,7 @@ spec: disableModelServing: true disableProjectSharing: true disableCustomServingRuntimes: true + disableAcceleratorProfiles: true modelMetricsNamespace: '' notebookController: enabled: true @@ -25,51 +26,51 @@ spec: - name: Small resources: limits: - cpu: "2" + cpu: '2' memory: 2Gi requests: - cpu: "1" + cpu: '1' memory: 1Gi - name: Medium resources: limits: - cpu: "4" + cpu: '4' memory: 4Gi requests: - cpu: "2" + cpu: '2' memory: 2Gi - name: Large resources: limits: - cpu: "8" + cpu: '8' memory: 8Gi requests: - cpu: "4" + cpu: '4' memory: 4Gi modelServerSizes: - name: Small resources: limits: - cpu: "2" + cpu: '2' memory: 8Gi requests: - cpu: "1" + cpu: '1' memory: 4Gi - name: Medium resources: limits: - cpu: "8" + cpu: '8' memory: 10Gi requests: - cpu: "4" + cpu: '4' memory: 8Gi - name: Large resources: limits: - cpu: "10" + cpu: '10' memory: 20Gi requests: - cpu: "6" + cpu: '6' memory: 16Gi groupsConfig: adminGroups: 'odh-admins'