Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add empty state when no serving platform is enabled #2109

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -18,7 +21,17 @@ export default {
},
} as Meta<typeof ClusterSettings>;

const Template: StoryFn<typeof ClusterSettings> = (args) => <ClusterSettings {...args} />;
const Template: StoryFn<typeof ClusterSettings> = (args) => (
<AreaContext.Provider
value={{
dscStatus: mockDscStatus({
installedComponents: { [StackComponent.K_SERVE]: true, [StackComponent.MODEL_MESH]: true },
}),
}}
>
<ClusterSettings {...args} />
</AreaContext.Provider>
);

export const Default: StoryObj = {
render: Template,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -56,11 +59,19 @@ export default {
} as Meta<typeof ModelServingGlobal>;

const Template: StoryFn<typeof ModelServingGlobal> = (args) => (
<Routes>
<Route path="/" element={<ModelServingContextProvider />}>
<Route index element={<ModelServingGlobal {...args} />} />
</Route>
</Routes>
<AreaContext.Provider
value={{
dscStatus: mockDscStatus({
installedComponents: { [StackComponent.K_SERVE]: true, [StackComponent.MODEL_MESH]: true },
}),
}}
>
<Routes>
<Route path="/" element={<ModelServingContextProvider />}>
<Route index element={<ModelServingGlobal {...args} />} />
</Route>
</Routes>
</AreaContext.Provider>
);

export const EmptyStateNoServingRuntime: StoryObj = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -181,11 +184,22 @@ const Template: StoryFn<typeof ModelServingPlatform> = (args) => {
const { dashboardConfig, loaded } = useApplicationSettings();
return loaded && dashboardConfig ? (
<AppContext.Provider value={{ buildStatuses: [], dashboardConfig }}>
<ProjectsRoutes>
<Route path="/" element={<ProjectDetailsContextProvider />}>
<Route index element={<ModelServingPlatform {...args} />} />
</Route>
</ProjectsRoutes>
<AreaContext.Provider
value={{
dscStatus: mockDscStatus({
installedComponents: {
[StackComponent.K_SERVE]: true,
[StackComponent.MODEL_MESH]: true,
},
}),
}}
>
<ProjectsRoutes>
<Route path="/" element={<ProjectDetailsContextProvider />}>
<Route index element={<ModelServingPlatform {...args} />} />
</Route>
</ProjectsRoutes>
</AreaContext.Provider>
</AppContext.Provider>
) : (
<Spinner />
Expand Down Expand Up @@ -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,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<MockedRequest<DefaultBodyType>>[] => [
rest.get('/api/status', (req, res, ctx) => res(ctx.json(mockStatus()))),
Expand Down Expand Up @@ -144,21 +146,28 @@ export default {
const Template: StoryFn<typeof ProjectDetails> = (args) => {
useDetectUser();
return (
<ProjectsRoutes>
<Route path="/" element={<ProjectDetailsContextProvider />}>
<Route index element={<ProjectDetails {...args} />} />
</Route>
</ProjectsRoutes>
<AreaContext.Provider
value={{
dscStatus: mockDscStatus({
installedComponents: {
[StackComponent.WORKBENCHES]: true,
[StackComponent.K_SERVE]: true,
[StackComponent.MODEL_MESH]: true,
},
}),
}}
>
<ProjectsRoutes>
<Route path="/" element={<ProjectDetailsContextProvider />}>
<Route index element={<ProjectDetails {...args} />} />
</Route>
</ProjectsRoutes>
</AreaContext.Provider>
);
};

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 = {
Expand All @@ -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 });
},
};
61 changes: 20 additions & 41 deletions frontend/src/concepts/areas/__tests__/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/concepts/areas/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/concepts/areas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
18 changes: 11 additions & 7 deletions frontend/src/pages/clusterSettings/ClusterSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<NotebookTolerationFormSettings>({
Expand Down Expand Up @@ -140,13 +142,15 @@ const ClusterSettings: React.FC = () => {
provideChildrenPadding
>
<Stack hasGutter>
<StackItem>
<ModelServingPlatformSettings
initialValue={clusterSettings.modelServingPlatformEnabled}
enabledPlatforms={modelServingEnabledPlatforms}
setEnabledPlatforms={setModelServingEnabledPlatforms}
/>
</StackItem>
{modelServingEnabled && (
<StackItem>
<ModelServingPlatformSettings
initialValue={clusterSettings.modelServingPlatformEnabled}
enabledPlatforms={modelServingEnabledPlatforms}
setEnabledPlatforms={setModelServingEnabledPlatforms}
/>
</StackItem>
)}
<StackItem>
<PVCSizeSettings
initialValue={clusterSettings.pvcSize}
Expand Down
Loading
Loading