Skip to content

Commit

Permalink
Merge pull request #2176 from DaoDaoNoCode/upstream-issue-2135
Browse files Browse the repository at this point in the history
Add spinner, enhance route and error handling for global model serving page
  • Loading branch information
openshift-merge-bot[bot] authored Nov 17, 2023
2 parents a0de3fd + 356c6bf commit e8b1c06
Show file tree
Hide file tree
Showing 13 changed files with 220 additions and 139 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ test('Empty State No Inference Service', async ({ page }) => {
await expect(await page.getByRole('button', { name: 'Deploy', exact: true })).toBeDisabled();
});

test('No serving platform installed', async ({ page }) => {
await page.goto(
navigateToStory('pages-modelserving-modelservingglobal', 'no-platform-installed'),
);

// wait for page to load
await page.waitForSelector('text=Problem loading model serving page');

// Test that the button is enabled
await page.getByRole('button', { name: 'View my projects' }).isEnabled();

// test that you can not submit on empty
await expect(await page.getByText('No model serving platform installed')).toBeVisible();
});

test('Delete model', async ({ page }) => {
await page.goto(navigateToStory('pages-modelserving-modelservingglobal', 'delete-model'));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
mockInferenceServicek8sError,
} from '~/__mocks__/mockInferenceServiceK8sResource';
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';
Expand All @@ -30,6 +29,7 @@ import useDetectUser from '~/utilities/useDetectUser';
import { useApplicationSettings } from '~/app/useApplicationSettings';
import { AppContext } from '~/app/AppContext';
import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes';
import GlobalModelServingCoreLoader from '~/pages/modelServing/screens/global/GlobalModelServingCoreLoader';

type HandlersProps = {
disableKServeConfig?: boolean;
Expand Down Expand Up @@ -145,39 +145,55 @@ export default {
},
} as Meta<typeof ModelServingGlobal>;

const Template: StoryFn<typeof ModelServingGlobal> = (args) => {
useDetectUser();
const { dashboardConfig, loaded } = useApplicationSettings();
type TemplateProps = {
kServeInstalled?: boolean;
modelMeshInstalled?: boolean;
};

return loaded && dashboardConfig ? (
<AppContext.Provider value={{ buildStatuses: [], dashboardConfig }}>
<AreaContext.Provider
value={{
dscStatus: mockDscStatus({
installedComponents: {
[StackComponent.K_SERVE]: true,
[StackComponent.MODEL_MESH]: true,
},
}),
}}
>
<ProjectsContextProvider>
<Routes>
<Route path="/" element={<ModelServingContextProvider />}>
<Route index element={<ModelServingGlobal {...args} />} />
<Route path="/:namespace?/*" element={<ModelServingGlobal {...args} />} />
</Route>
</Routes>
</ProjectsContextProvider>
</AreaContext.Provider>
</AppContext.Provider>
) : (
<Spinner />
);
const Template: (props: TemplateProps) => StoryFn<typeof ModelServingGlobal> = ({
kServeInstalled = true,
modelMeshInstalled = true,
}) => {
const InnerTemplate: StoryFn<typeof ModelServingGlobal> = (args) => {
useDetectUser();
const { dashboardConfig, loaded } = useApplicationSettings();
return loaded && dashboardConfig ? (
<AppContext.Provider value={{ buildStatuses: [], dashboardConfig }}>
<AreaContext.Provider
value={{
dscStatus: mockDscStatus({
installedComponents: {
[StackComponent.K_SERVE]: kServeInstalled,
[StackComponent.MODEL_MESH]: modelMeshInstalled,
},
}),
}}
>
<ProjectsContextProvider>
<Routes>
<Route
path="/:namespace?/*"
element={
<GlobalModelServingCoreLoader
getInvalidRedirectPath={(namespace) => `/modelServing/${namespace}`}
/>
}
>
<Route index element={<ModelServingGlobal {...args} />} />
</Route>
</Routes>
</ProjectsContextProvider>
</AreaContext.Provider>
</AppContext.Provider>
) : (
<Spinner />
);
};
return InnerTemplate;
};

export const EmptyStateNoServingRuntime: StoryObj = {
render: Template,
render: Template({}),

parameters: {
msw: {
Expand All @@ -193,7 +209,7 @@ export const EmptyStateNoServingRuntime: StoryObj = {
};

export const EmptyStateNoInferenceServices: StoryObj = {
render: Template,
render: Template({}),

parameters: {
msw: {
Expand All @@ -205,8 +221,22 @@ export const EmptyStateNoInferenceServices: StoryObj = {
},
};

export const NoPlatformInstalled: StoryObj = {
render: Template({ kServeInstalled: false, modelMeshInstalled: false }),

parameters: {
msw: {
handlers: getHandlers({
projectEnableModelMesh: true,
servingRuntimes: [],
inferenceServices: [],
}),
},
},
};

export const EditModel: StoryObj = {
render: Template,
render: Template({}),

parameters: {
a11y: {
Expand All @@ -230,7 +260,7 @@ export const EditModel: StoryObj = {
};

export const DeleteModel: StoryObj = {
render: Template,
render: Template({}),

parameters: {
a11y: {
Expand All @@ -253,7 +283,7 @@ export const DeleteModel: StoryObj = {
};

export const DeployModelModelMesh: StoryObj = {
render: Template,
render: Template({}),

parameters: {
a11y: {
Expand All @@ -278,7 +308,7 @@ export const DeployModelModelMesh: StoryObj = {
};

export const DeployModelModelKServe: StoryObj = {
render: Template,
render: Template({}),

parameters: {
a11y: {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/clusterSettings/ClusterSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +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 modelServingEnabled = useIsAreaAvailable(SupportedArea.MODEL_SERVING).status;
const isJupyterEnabled = useCheckJupyterEnabled();
const [notebookTolerationSettings, setNotebookTolerationSettings] =
React.useState<NotebookTolerationFormSettings>({
Expand Down
52 changes: 30 additions & 22 deletions frontend/src/pages/modelServing/ModelServingContext.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import * as React from 'react';
import { Outlet, useParams } from 'react-router-dom';
import {
Bullseye,
Button,
EmptyState,
EmptyStateBody,
EmptyStateIcon,
Spinner,
Title,
} from '@patternfly/react-core';
import { useNavigate } from 'react-router-dom';
import { ExclamationCircleIcon } from '@patternfly/react-icons';
import { ServingRuntimeKind, InferenceServiceKind, TemplateKind } from '~/k8sTypes';
import { ServingRuntimeKind, InferenceServiceKind, TemplateKind, ProjectKind } from '~/k8sTypes';
import { DEFAULT_CONTEXT_DATA } from '~/utilities/const';
import { ContextResourceData } from '~/types';
import { useContextResourceData } from '~/utilities/useContextResourceData';
Expand All @@ -20,6 +18,8 @@ import { DataConnection } from '~/pages/projects/types';
import useDataConnections from '~/pages/projects/screens/detail/data-connections/useDataConnections';
import useSyncPreferredProject from '~/concepts/projects/useSyncPreferredProject';
import { ProjectsContext, byName } from '~/concepts/projects/ProjectsContext';
import { SupportedArea, conditionalArea } from '~/concepts/areas';
import useServingPlatformStatuses from '~/pages/modelServing/useServingPlatformStatuses';
import useInferenceServices from './useInferenceServices';
import useServingRuntimes from './useServingRuntimes';
import useTemplates from './customServingRuntimes/useTemplates';
Expand All @@ -34,6 +34,12 @@ type ModelServingContextType = {
servingRuntimeTemplateDisablement: ContextResourceData<string>;
servingRuntimes: ContextResourceData<ServingRuntimeKind>;
inferenceServices: ContextResourceData<InferenceServiceKind>;
project: ProjectKind | null;
};

type ModelServingContextProviderProps = {
children: React.ReactNode;
namespace?: string;
};

export const ModelServingContext = React.createContext<ModelServingContextType>({
Expand All @@ -44,12 +50,15 @@ export const ModelServingContext = React.createContext<ModelServingContextType>(
servingRuntimeTemplateDisablement: DEFAULT_CONTEXT_DATA,
servingRuntimes: DEFAULT_CONTEXT_DATA,
inferenceServices: DEFAULT_CONTEXT_DATA,
project: null,
});

const ModelServingContextProvider: React.FC = () => {
const ModelServingContextProvider = conditionalArea<ModelServingContextProviderProps>(
SupportedArea.MODEL_SERVING,
true,
)(({ children, namespace }) => {
const { dashboardNamespace } = useDashboardNamespace();
const navigate = useNavigate();
const { namespace } = useParams<{ namespace: string }>();
const { projects } = React.useContext(ProjectsContext);
const project = projects.find(byName(namespace)) ?? null;
useSyncPreferredProject(project);
Expand Down Expand Up @@ -77,7 +86,18 @@ const ModelServingContextProvider: React.FC = () => {
dataConnectionRefresh();
}, [servingRuntimeRefresh, inferenceServiceRefresh, dataConnectionRefresh]);

const {
kServe: { installed: kServeInstalled },
modelMesh: { installed: modelMeshInstalled },
} = useServingPlatformStatuses();

const notInstalledError =
!kServeInstalled && !modelMeshInstalled
? new Error('No model serving platform installed')
: undefined;

if (
notInstalledError ||
servingRuntimes.error ||
inferenceServices.error ||
servingRuntimeTemplates.error ||
Expand All @@ -93,7 +113,8 @@ const ModelServingContextProvider: React.FC = () => {
Problem loading model serving page
</Title>
<EmptyStateBody>
{servingRuntimes.error?.message ||
{notInstalledError?.message ||
servingRuntimes.error?.message ||
inferenceServices.error?.message ||
servingRuntimeTemplates.error?.message ||
servingRuntimeTemplateOrder.error?.message ||
Expand All @@ -108,20 +129,6 @@ const ModelServingContextProvider: React.FC = () => {
);
}

if (
!servingRuntimes.loaded ||
!inferenceServices.loaded ||
!servingRuntimeTemplates.loaded ||
!servingRuntimeTemplateOrder.loaded ||
!servingRuntimeTemplateDisablement.loaded
) {
return (
<Bullseye>
<Spinner />
</Bullseye>
);
}

return (
<ModelServingContext.Provider
value={{
Expand All @@ -132,11 +139,12 @@ const ModelServingContextProvider: React.FC = () => {
servingRuntimeTemplateDisablement,
dataConnections,
refreshAllData,
project,
}}
>
<Outlet />
{children}
</ModelServingContext.Provider>
);
};
});

export default ModelServingContextProvider;
14 changes: 10 additions & 4 deletions frontend/src/pages/modelServing/ModelServingRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import { Navigate, Route } from 'react-router-dom';
import ProjectsRoutes from '~/concepts/projects/ProjectsRoutes';
import ModelServingContextProvider from './ModelServingContext';
import GlobalModelServingCoreLoader from '~/pages/modelServing/screens/global/GlobalModelServingCoreLoader';
import ModelServingMetricsWrapper from './screens/metrics/ModelServingMetricsWrapper';
import ModelServingGlobal from './screens/global/ModelServingGlobal';
import useModelMetricsEnabled from './useModelMetricsEnabled';
Expand All @@ -11,11 +11,17 @@ const ModelServingRoutes: React.FC = () => {

return (
<ProjectsRoutes>
<Route path="/" element={<ModelServingContextProvider />}>
<Route
path="/:namespace?/*"
element={
<GlobalModelServingCoreLoader
getInvalidRedirectPath={(namespace) => `/modelServing/${namespace}`}
/>
}
>
<Route index element={<ModelServingGlobal />} />
<Route path="/:namespace?/*" element={<ModelServingGlobal />} />
<Route
path="/metrics/:project/:inferenceService"
path="metrics/:inferenceService"
element={
modelMetricsEnabled ? <ModelServingMetricsWrapper /> : <Navigate replace to="/" />
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@ import {
EmptyStateSecondaryActions,
Title,
} from '@patternfly/react-core';
import { useParams } from 'react-router-dom';
import { PlusCircleIcon, WrenchIcon } from '@patternfly/react-icons';
import { useNavigate } from 'react-router-dom';
import { ModelServingContext } from '~/pages/modelServing/ModelServingContext';
import { getProjectModelServingPlatform } from '~/pages/modelServing/screens/projects/utils';
import { ServingRuntimePlatform } from '~/types';
import { byName, ProjectsContext } from '~/concepts/projects/ProjectsContext';
import useServingPlatformStatuses from '~/pages/modelServing/useServingPlatformStatuses';
import { getProjectDisplayName } from '~/pages/projects/utils';
import ServeModelButton from './ServeModelButton';
Expand All @@ -23,13 +21,10 @@ const EmptyModelServing: React.FC = () => {
const navigate = useNavigate();
const {
servingRuntimes: { data: servingRuntimes },
project,
} = React.useContext(ModelServingContext);
const { projects } = React.useContext(ProjectsContext);
const { namespace } = useParams<{ namespace: string }>();
const servingPlatformStatuses = useServingPlatformStatuses();

const project = projects.find(byName(namespace));

if (
getProjectModelServingPlatform(project, servingPlatformStatuses).platform !==
ServingRuntimePlatform.SINGLE &&
Expand Down
Loading

0 comments on commit e8b1c06

Please sign in to comment.