From 009f36f95f3530a0da7b51a8300a3aa465a54da7 Mon Sep 17 00:00:00 2001 From: Lucas Fernandez Date: Thu, 18 Apr 2024 13:14:36 +0200 Subject: [PATCH] Refactor model registry routes and configuration in ModelRegistryCoreLoader.tsx, ModelRegistryRoutes.tsx, and InvalidModelRegistry.tsx --- frontend/src/app/App.tsx | 9 ++- .../context/ModelRegistryContext.tsx | 42 ++-------- .../context/ModelRegistrySelectorContext.tsx | 71 +++++++++++++++++ .../modelRegistry/ModelRegistryCoreLoader.tsx | 79 +++++++++++++++++-- .../modelRegistry/ModelRegistryRoutes.tsx | 16 ++-- .../screens/EmptyRegisteredModels.tsx | 14 ++++ .../screens/InvalidModelRegistry.tsx | 31 ++++++++ .../modelRegistry/screens/ModelRegistry.tsx | 18 +++-- .../screens/ModelRegistrySelector.tsx | 30 ++++++- .../screens/RegisteredModelsTableToolbar.tsx | 4 - 10 files changed, 248 insertions(+), 66 deletions(-) create mode 100644 frontend/src/concepts/modelRegistry/context/ModelRegistrySelectorContext.tsx create mode 100644 frontend/src/pages/modelRegistry/screens/InvalidModelRegistry.tsx diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index 6988612dc8..1c2a770cad 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -31,6 +31,7 @@ import { logout } from './appUtils'; import QuickStarts from './QuickStarts'; import './App.scss'; +import { ModelRegistrySelectorContextProvider } from '~/concepts/modelRegistry/context/ModelRegistrySelectorContext'; const App: React.FC = () => { const [notificationsOpen, setNotificationsOpen] = React.useState(false); @@ -116,9 +117,11 @@ const App: React.FC = () => { > - - - + + + + + diff --git a/frontend/src/concepts/modelRegistry/context/ModelRegistryContext.tsx b/frontend/src/concepts/modelRegistry/context/ModelRegistryContext.tsx index b5ddda600d..eb3bdce52d 100644 --- a/frontend/src/concepts/modelRegistry/context/ModelRegistryContext.tsx +++ b/frontend/src/concepts/modelRegistry/context/ModelRegistryContext.tsx @@ -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 { @@ -19,9 +17,6 @@ export type ModelRegistryContextType = { ignoreTimedOut: () => void; refreshState: () => Promise; refreshAPIState: () => void; - modelRegistries: ModelRegistryKind[]; - preferredModelRegistry: ModelRegistryKind | undefined; - updatePreferredModelRegistry: (modelRegistry: ModelRegistryKind | undefined) => void; }; type ModelRegistryContextProviderProps = { @@ -37,25 +32,18 @@ export const ModelRegistryContext = React.createContext undefined, refreshState: async () => undefined, refreshAPIState: () => undefined, - modelRegistries: [], - preferredModelRegistry: undefined, - updatePreferredModelRegistry: () => undefined, }); export const ModelRegistryContextProvider = conditionalArea( SupportedArea.MODEL_REGISTRY, true, )(({ children, modelRegistryName }) => { - const [modelRegistries] = useModelRegistries(); - const [preferredModelRegistry, setPreferredModelRegistry] = - React.useState(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); }, []); @@ -64,29 +52,16 @@ export const ModelRegistryContextProvider = conditionalArea { - 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 ( - {error.message} + {crLoadError.message} ); @@ -95,16 +70,13 @@ export const ModelRegistryContextProvider = conditionalArea {children} diff --git a/frontend/src/concepts/modelRegistry/context/ModelRegistrySelectorContext.tsx b/frontend/src/concepts/modelRegistry/context/ModelRegistrySelectorContext.tsx new file mode 100644 index 0000000000..155adea01a --- /dev/null +++ b/frontend/src/concepts/modelRegistry/context/ModelRegistrySelectorContext.tsx @@ -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({ + modelRegistries: [], + preferredModelRegistry: undefined, + updatePreferredModelRegistry: () => undefined, +}); + +export const ModelRegistrySelectorContextProvider = + conditionalArea( + SupportedArea.MODEL_REGISTRY, + true, + )(({ children }) => { + const [modelRegistries, isLoaded, error] = useModelRegistries(); + const [preferredModelRegistry, setPreferredModelRegistry] = + React.useState(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 Loading model registries...; + } + + if (error) { + return ( + + + {error.message} + + + ); + } + + return ( + + {children} + + ); + }); diff --git a/frontend/src/pages/modelRegistry/ModelRegistryCoreLoader.tsx b/frontend/src/pages/modelRegistry/ModelRegistryCoreLoader.tsx index a5094c5c51..31db81476c 100644 --- a/frontend/src/pages/modelRegistry/ModelRegistryCoreLoader.tsx +++ b/frontend/src/pages/modelRegistry/ModelRegistryCoreLoader.tsx @@ -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; +type EmptyStateProps = 'emptyStatePage' | 'empty'; + +type ModelRegistryCoreLoaderProps = { + getInvalidRedirectPath: (modelRegistry: string) => string; +}; + +type ApplicationPageRenderState = Pick; // TODO: Parametrize this to make the route dynamic -const ModelRegistryCoreLoader: React.FC = () => ( - - - -); +const ModelRegistryCoreLoader: React.FC = ({ + 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: , + }; + } else if (modelRegistry) { + const foundModelRegistry = modelRegistries.find((mr) => mr.metadata.name === modelRegistry); + if (foundModelRegistry) { + // Render the content + return ( + + + + ); + } + + // They ended up on a non-valid project path + renderStateProps = { + empty: true, + emptyStatePage: ( + + ), + }; + } else { + // Redirect the namespace suffix into the URL + const redirectModelRegistry = preferredModelRegistry ?? modelRegistries[0]; + return ; + } + + return ( + + } + {...renderStateProps} + loaded + headerContent={} + provideChildrenPadding + /> + ); +}; + export default ModelRegistryCoreLoader; diff --git a/frontend/src/pages/modelRegistry/ModelRegistryRoutes.tsx b/frontend/src/pages/modelRegistry/ModelRegistryRoutes.tsx index c8760b5c86..af1360dea1 100644 --- a/frontend/src/pages/modelRegistry/ModelRegistryRoutes.tsx +++ b/frontend/src/pages/modelRegistry/ModelRegistryRoutes.tsx @@ -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 = () => ( - - }> + + `/modelRegistry/${modelRegistry}`} + /> + } + > } /> } /> - + ); export default ModelServingRoutes; diff --git a/frontend/src/pages/modelRegistry/screens/EmptyRegisteredModels.tsx b/frontend/src/pages/modelRegistry/screens/EmptyRegisteredModels.tsx index e5fc317e0c..22e894bf57 100644 --- a/frontend/src/pages/modelRegistry/screens/EmptyRegisteredModels.tsx +++ b/frontend/src/pages/modelRegistry/screens/EmptyRegisteredModels.tsx @@ -1,6 +1,8 @@ import { + Button, EmptyState, EmptyStateBody, + EmptyStateFooter, EmptyStateHeader, EmptyStateIcon, EmptyStateVariant, @@ -22,6 +24,18 @@ const EmptyRegisteredModels: React.FC = ({ preferredM
registry or select a different one. + + + , + ); diff --git a/frontend/src/pages/modelRegistry/screens/InvalidModelRegistry.tsx b/frontend/src/pages/modelRegistry/screens/InvalidModelRegistry.tsx new file mode 100644 index 0000000000..00c6057bfb --- /dev/null +++ b/frontend/src/pages/modelRegistry/screens/InvalidModelRegistry.tsx @@ -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 = ({ + title, + modelRegistry, + getRedirectPath, +}) => ( + + {/* TODO: Replace this with a model registry selector */} + + +); + +export default InvalidModelRegistry; diff --git a/frontend/src/pages/modelRegistry/screens/ModelRegistry.tsx b/frontend/src/pages/modelRegistry/screens/ModelRegistry.tsx index 3530581e98..8865f18da8 100644 --- a/frontend/src/pages/modelRegistry/screens/ModelRegistry.tsx +++ b/frontend/src/pages/modelRegistry/screens/ModelRegistry.tsx @@ -1,26 +1,28 @@ import React from 'react'; import ApplicationsPage from '~/pages/ApplicationsPage'; -import { ModelRegistryContext } from '~/concepts/modelRegistry/context/ModelRegistryContext'; import useRegisteredModels from '~/concepts/modelRegistry/apiHooks/useRegisteredModels'; +import { ModelRegistrySelectorContext } from '~/concepts/modelRegistry/context/ModelRegistrySelectorContext'; +import TitleWithIcon from '~/concepts/design/TitleWithIcon'; +import { ProjectObjectType } from '~/concepts/design/utils'; import EmptyRegisteredModels from './EmptyRegisteredModels'; -import RegisteredModelsTableToolbar from './RegisteredModelsTableToolbar'; import RegisteredModelListView from './RegisteredModelListView'; +import ModelRegistrySelector from './ModelRegistrySelector'; const ModelRegistry: React.FC = () => { - const { preferredModelRegistry } = React.useContext(ModelRegistryContext); + const { preferredModelRegistry } = React.useContext(ModelRegistrySelectorContext); const [registeredModels, loaded, loadError] = useRegisteredModels(); return ( - - - + + } + title={ + } - title="Registered models" description="View and manage your registered models." + headerContent={} loadError={loadError} loaded={loaded} provideChildrenPadding diff --git a/frontend/src/pages/modelRegistry/screens/ModelRegistrySelector.tsx b/frontend/src/pages/modelRegistry/screens/ModelRegistrySelector.tsx index 2be18d5ba4..77982143ec 100644 --- a/frontend/src/pages/modelRegistry/screens/ModelRegistrySelector.tsx +++ b/frontend/src/pages/modelRegistry/screens/ModelRegistrySelector.tsx @@ -5,23 +5,27 @@ import { SelectGroup, SelectOption, } from '@patternfly/react-core/deprecated'; -import { ModelRegistryContext } from '~/concepts/modelRegistry/context/ModelRegistryContext'; +import { useNavigate } from 'react-router'; +import { Bullseye, Flex, FlexItem } from '@patternfly/react-core'; import { useBrowserStorage } from '~/components/browserStorage'; +import { ModelRegistrySelectorContext } from '~/concepts/modelRegistry/context/ModelRegistrySelectorContext'; +import { ProjectObjectType, typedObjectImage } from '~/concepts/design/utils'; const MODEL_REGISTRY_FAVORITE_STORAGE_KEY = 'odh.dashboard.model.registry.favorite'; const ModelRegistrySelector: React.FC = () => { const { modelRegistries, preferredModelRegistry, updatePreferredModelRegistry } = - React.useContext(ModelRegistryContext); + React.useContext(ModelRegistrySelectorContext); const [favorites, setFavorites] = useBrowserStorage( MODEL_REGISTRY_FAVORITE_STORAGE_KEY, [], ); + const navigate = useNavigate(); const [isOpen, setIsOpen] = React.useState(false); const options = [ - + {modelRegistries.map((modelRegistry) => ( { , ]; - return ( + const selector = ( ); + + return ( + + + + + Model registry + + {selector} + + + ); }; export default ModelRegistrySelector; diff --git a/frontend/src/pages/modelRegistry/screens/RegisteredModelsTableToolbar.tsx b/frontend/src/pages/modelRegistry/screens/RegisteredModelsTableToolbar.tsx index bb6d0e964f..480a8c93cf 100644 --- a/frontend/src/pages/modelRegistry/screens/RegisteredModelsTableToolbar.tsx +++ b/frontend/src/pages/modelRegistry/screens/RegisteredModelsTableToolbar.tsx @@ -12,7 +12,6 @@ import { ToolbarToggleGroup, } from '@patternfly/react-core'; import { EllipsisVIcon, FilterIcon } from '@patternfly/react-icons'; -import ModelRegistrySelector from './ModelRegistrySelector'; type RegisteredModelsTableToolbarProps = { toggleGroupItems?: React.ReactNode; @@ -29,9 +28,6 @@ const RegisteredModelsTableToolbar: React.FC return ( - - - } breakpoint="xl"> {tableToggleGroupItems}