Skip to content

Commit

Permalink
Refactor model registry routes and configuration in ModelRegistryCore…
Browse files Browse the repository at this point in the history
…Loader.tsx, ModelRegistryRoutes.tsx, and InvalidModelRegistry.tsx
  • Loading branch information
lucferbux committed Apr 25, 2024
1 parent 5a3a6b2 commit 009f36f
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 66 deletions.
9 changes: 6 additions & 3 deletions frontend/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { logout } from './appUtils';
import QuickStarts from './QuickStarts';

import './App.scss';
import { ModelRegistrySelectorContextProvider } from '~/concepts/modelRegistry/context/ModelRegistrySelectorContext';

Check failure on line 34 in frontend/src/app/App.tsx

View workflow job for this annotation

GitHub Actions / Tests (18.x)

`~/concepts/modelRegistry/context/ModelRegistrySelectorContext` import should occur before import of `./Header`

const App: React.FC = () => {
const [notificationsOpen, setNotificationsOpen] = React.useState(false);
Expand Down Expand Up @@ -116,9 +117,11 @@ const App: React.FC = () => {
>
<ErrorBoundary>
<ProjectsContextProvider>
<QuickStarts>
<AppRoutes />
</QuickStarts>
<ModelRegistrySelectorContextProvider>
<QuickStarts>
<AppRoutes />
</QuickStarts>
</ModelRegistrySelectorContextProvider>
</ProjectsContextProvider>
<ToastNotifications />
<TelemetrySetup />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import * as React from 'react';
import { Alert, Bullseye } from '@patternfly/react-core';
import { SupportedArea, conditionalArea } from '~/concepts/areas';
import { ModelRegistryKind } from '~/k8sTypes';
import useModelRegistries from '~/concepts/modelRegistry/apiHooks/useModelRegistries';
import { MODEL_REGISTRY_DEFAULT_NAMESPACE } from '~/concepts/modelRegistry/const';
import useModelRegistryAPIState, { ModelRegistryAPIState } from './useModelRegistryAPIState';
import {
Expand All @@ -19,9 +17,6 @@ export type ModelRegistryContextType = {
ignoreTimedOut: () => void;
refreshState: () => Promise<undefined>;
refreshAPIState: () => void;
modelRegistries: ModelRegistryKind[];
preferredModelRegistry: ModelRegistryKind | undefined;
updatePreferredModelRegistry: (modelRegistry: ModelRegistryKind | undefined) => void;
};

type ModelRegistryContextProviderProps = {
Expand All @@ -37,25 +32,18 @@ export const ModelRegistryContext = React.createContext<ModelRegistryContextType
ignoreTimedOut: () => undefined,
refreshState: async () => undefined,
refreshAPIState: () => undefined,
modelRegistries: [],
preferredModelRegistry: undefined,
updatePreferredModelRegistry: () => undefined,
});

export const ModelRegistryContextProvider = conditionalArea<ModelRegistryContextProviderProps>(
SupportedArea.MODEL_REGISTRY,
true,
)(({ children, modelRegistryName }) => {
const [modelRegistries] = useModelRegistries();
const [preferredModelRegistry, setPreferredModelRegistry] =
React.useState<ModelRegistryContextType['preferredModelRegistry']>(undefined);

const crState = useModelRegistryNamespaceCR(MODEL_REGISTRY_DEFAULT_NAMESPACE, modelRegistryName);
const [modelRegistryNamespaceCR, crLoaded, crLoadError, refreshCR] = crState;
const isCRReady = isModelRegistryAvailable(crState);
const state = useModelRegistryNamespaceCR(MODEL_REGISTRY_DEFAULT_NAMESPACE, modelRegistryName);
const [modelRegistryCR, crLoaded, crLoadError, refreshCR] = state;
const isCRReady = isModelRegistryAvailable(state);

const [disableTimeout, setDisableTimeout] = React.useState(false);
const serverTimedOut = !disableTimeout && hasServerTimedOut(crState, isCRReady);
const serverTimedOut = !disableTimeout && hasServerTimedOut(state, isCRReady);
const ignoreTimedOut = React.useCallback(() => {
setDisableTimeout(true);
}, []);
Expand All @@ -64,29 +52,16 @@ export const ModelRegistryContextProvider = conditionalArea<ModelRegistryContext

const [apiState, refreshAPIState] = useModelRegistryAPIState(hostPath);

React.useEffect(() => {
if (modelRegistries.length > 0 && !preferredModelRegistry) {
setPreferredModelRegistry(modelRegistries[0]);
}
}, [modelRegistries, preferredModelRegistry]);

const refreshState = React.useCallback(
() => Promise.all([refreshCR()]).then(() => undefined),
[refreshCR],
);

const updatePreferredModelRegistry = React.useCallback<
ModelRegistryContextType['updatePreferredModelRegistry']
>((modelRegistry) => {
setPreferredModelRegistry(modelRegistry);
}, []);

const error = crLoadError;
if (error) {
if (crLoadError) {
return (
<Bullseye>
<Alert title="Model registry load error" variant="danger" isInline>
{error.message}
{crLoadError.message}
</Alert>
</Bullseye>
);
Expand All @@ -95,16 +70,13 @@ export const ModelRegistryContextProvider = conditionalArea<ModelRegistryContext
return (
<ModelRegistryContext.Provider
value={{
hasCR: !!modelRegistryNamespaceCR,
hasCR: !!modelRegistryCR,
crInitializing: !crLoaded,
serverTimedOut,
apiState,
ignoreTimedOut,
refreshState,
refreshAPIState,
modelRegistries,
preferredModelRegistry,
updatePreferredModelRegistry,
}}
>
{children}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as React from 'react';
import { Alert, Bullseye } from '@patternfly/react-core';
import { SupportedArea, conditionalArea } from '~/concepts/areas';
import { ModelRegistryKind } from '~/k8sTypes';
import useModelRegistries from '~/concepts/modelRegistry/apiHooks/useModelRegistries';

export type ModelRegistrySelectorContextType = {
modelRegistries: ModelRegistryKind[];
preferredModelRegistry: ModelRegistryKind | undefined;
updatePreferredModelRegistry: (modelRegistry: ModelRegistryKind | undefined) => void;
};

type ModelRegistrySelectorContextProviderProps = {
children: React.ReactNode;
};

export const ModelRegistrySelectorContext = React.createContext<ModelRegistrySelectorContextType>({
modelRegistries: [],
preferredModelRegistry: undefined,
updatePreferredModelRegistry: () => undefined,
});

export const ModelRegistrySelectorContextProvider =
conditionalArea<ModelRegistrySelectorContextProviderProps>(
SupportedArea.MODEL_REGISTRY,
true,
)(({ children }) => {
const [modelRegistries, isLoaded, error] = useModelRegistries();
const [preferredModelRegistry, setPreferredModelRegistry] =
React.useState<ModelRegistrySelectorContextType['preferredModelRegistry']>(undefined);

const firstModelRegistry = modelRegistries.length > 0 ? modelRegistries[0] : null;

React.useEffect(() => {
if (firstModelRegistry && !preferredModelRegistry) {
setPreferredModelRegistry(firstModelRegistry);
}
}, [firstModelRegistry, preferredModelRegistry]);

const updatePreferredModelRegistry = React.useCallback<
ModelRegistrySelectorContextType['updatePreferredModelRegistry']
>((modelRegistry) => {
setPreferredModelRegistry(modelRegistry);
}, []);

if (!isLoaded) {
return <Bullseye>Loading model registries...</Bullseye>;
}

if (error) {
return (
<Bullseye>
<Alert title="Model registry load error" variant="danger" isInline>
{error.message}
</Alert>
</Bullseye>
);
}

return (
<ModelRegistrySelectorContext.Provider
value={{
modelRegistries,
preferredModelRegistry,
updatePreferredModelRegistry,
}}
>
{children}
</ModelRegistrySelectorContext.Provider>
);
});
79 changes: 72 additions & 7 deletions frontend/src/pages/modelRegistry/ModelRegistryCoreLoader.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,77 @@
import * as React from 'react';
import { Outlet } from 'react-router';

import { Navigate, Outlet, useParams } from 'react-router';
import { ModelRegistryContextProvider } from '~/concepts/modelRegistry/context/ModelRegistryContext';
import ApplicationsPage from '~/pages/ApplicationsPage';
import TitleWithIcon from '~/concepts/design/TitleWithIcon';
import { ProjectObjectType } from '~/concepts/design/utils';

import { ModelRegistrySelectorContext } from '~/concepts/modelRegistry/context/ModelRegistrySelectorContext';
import InvalidModelRegistry from './screens/InvalidModelRegistry';
import ModelRegistrySelector from './screens/ModelRegistrySelector';
import EmptyRegisteredModels from './screens/EmptyRegisteredModels';

type ApplicationPageProps = React.ComponentProps<typeof ApplicationsPage>;
type EmptyStateProps = 'emptyStatePage' | 'empty';

type ModelRegistryCoreLoaderProps = {
getInvalidRedirectPath: (modelRegistry: string) => string;
};

type ApplicationPageRenderState = Pick<ApplicationPageProps, EmptyStateProps>;

// TODO: Parametrize this to make the route dynamic
const ModelRegistryCoreLoader: React.FC = () => (
<ModelRegistryContextProvider modelRegistryName="modelregistry-sample">
<Outlet />
</ModelRegistryContextProvider>
);
const ModelRegistryCoreLoader: React.FC<ModelRegistryCoreLoaderProps> = ({
getInvalidRedirectPath,
}) => {
const { modelRegistry } = useParams<{ modelRegistry: string }>();
const { modelRegistries, preferredModelRegistry } = React.useContext(
ModelRegistrySelectorContext,
);

let renderStateProps: ApplicationPageRenderState & { children?: React.ReactNode };
if (modelRegistries.length === 0) {
renderStateProps = {
empty: true,
emptyStatePage: <EmptyRegisteredModels preferredModelRegistry="TODO: Change" />,
};
} else if (modelRegistry) {
const foundModelRegistry = modelRegistries.find((mr) => mr.metadata.name === modelRegistry);
if (foundModelRegistry) {
// Render the content
return (
<ModelRegistryContextProvider modelRegistryName={modelRegistry}>
<Outlet />
</ModelRegistryContextProvider>
);
}

// They ended up on a non-valid project path
renderStateProps = {
empty: true,
emptyStatePage: (
<InvalidModelRegistry
modelRegistry={modelRegistry}
getRedirectPath={getInvalidRedirectPath}
/>
),
};
} else {
// Redirect the namespace suffix into the URL
const redirectModelRegistry = preferredModelRegistry ?? modelRegistries[0];
return <Navigate to={getInvalidRedirectPath(redirectModelRegistry.metadata.name)} replace />;
}

return (
<ApplicationsPage
title={
<TitleWithIcon title="Registered models" objectType={ProjectObjectType.registeredModels} />
}
{...renderStateProps}
loaded
headerContent={<ModelRegistrySelector />}
provideChildrenPadding
/>
);
};

export default ModelRegistryCoreLoader;
16 changes: 11 additions & 5 deletions frontend/src/pages/modelRegistry/ModelRegistryRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import * as React from 'react';
import { Navigate, Route } from 'react-router-dom';
import ProjectsRoutes from '~/concepts/projects/ProjectsRoutes';
import { Navigate, Route, Routes } from 'react-router-dom';
import ModelRegistryCoreLoader from './ModelRegistryCoreLoader';
import ModelRegistry from './screens/ModelRegistry';

const ModelServingRoutes: React.FC = () => (
<ProjectsRoutes>
<Route path={'/:modelRegistry?/*'} element={<ModelRegistryCoreLoader />}>
<Routes>
<Route
path={'/:modelRegistry?/*'}
element={
<ModelRegistryCoreLoader
getInvalidRedirectPath={(modelRegistry) => `/modelRegistry/${modelRegistry}`}
/>
}
>
<Route index element={<ModelRegistry />} />
<Route path="*" element={<Navigate to="." />} />
</Route>
</ProjectsRoutes>
</Routes>
);

export default ModelServingRoutes;
14 changes: 14 additions & 0 deletions frontend/src/pages/modelRegistry/screens/EmptyRegisteredModels.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
Button,
EmptyState,
EmptyStateBody,
EmptyStateFooter,
EmptyStateHeader,
EmptyStateIcon,
EmptyStateVariant,
Expand All @@ -22,6 +24,18 @@ const EmptyRegisteredModels: React.FC<EmptyRegisteredModelsType> = ({ preferredM
<br />
registry or select a different one.
</EmptyStateBody>
<EmptyStateFooter>
<Button
id="register-model-empty-button"
key="register-model-empty-button"
data-testid="register-model-empty-button"
aria-label="Register model"
onClick={() => undefined}
>
Register model
</Button>
,
</EmptyStateFooter>
</EmptyState>
);

Expand Down
31 changes: 31 additions & 0 deletions frontend/src/pages/modelRegistry/screens/InvalidModelRegistry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import EmptyStateErrorMessage from '~/components/EmptyStateErrorMessage';
import ProjectSelectorNavigator from '~/concepts/projects/ProjectSelectorNavigator';

type InvalidModelRegistryProps = {
title?: string;
modelRegistry?: string;
getRedirectPath: (namespace: string) => string;
};

const InvalidModelRegistry: React.FC<InvalidModelRegistryProps> = ({
title,
modelRegistry,
getRedirectPath,
}) => (
<EmptyStateErrorMessage
title={title || 'Model Registry not found'}
bodyText={`${
modelRegistry ? `Model Registry ${modelRegistry}` : 'The Model Registry'
} was not found.`}
>
{/* TODO: Replace this with a model registry selector */}
<ProjectSelectorNavigator
getRedirectPath={getRedirectPath}
invalidDropdownPlaceholder="Select project"
primary
/>
</EmptyStateErrorMessage>
);

export default InvalidModelRegistry;
Loading

0 comments on commit 009f36f

Please sign in to comment.