Skip to content

Commit

Permalink
[RHOAIENG-7572] Registered Models - Empty View Redesign
Browse files Browse the repository at this point in the history
  • Loading branch information
jpuzz0 committed Aug 13, 2024
1 parent bd0f9a9 commit ac10d58
Show file tree
Hide file tree
Showing 13 changed files with 106 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class ModelRegistry {

private wait() {
cy.findByTestId('app-page-title').should('exist');
cy.findByTestId('app-page-title').contains('Registered models');
cy.findByTestId('app-page-title').contains('Model registry');
cy.testA11y();
}

Expand All @@ -79,6 +79,10 @@ class ModelRegistry {
return this;
}

findModelRegistryEmptyState() {
return cy.findByTestId('empty-model-registries-state');
}

shouldregisteredModelsEmpty() {
cy.findByTestId('empty-registered-models').should('exist');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,30 @@ import {
} from '~/__tests__/cypress/cypress/utils/models';
import { mockModelVersionList } from '~/__mocks__/mockModelVersionList';
import { mockModelVersion } from '~/__mocks__/mockModelVersion';
import type { ModelVersion, RegisteredModel } from '~/concepts/modelRegistry/types';
import { mockRegisteredModel } from '~/__mocks__/mockRegisteredModel';
import { mockModelRegistryService } from '~/__mocks__/mockModelRegistryService';
import { asProjectEditUser } from '~/__tests__/cypress/cypress/utils/mockUsers';
import { mockSelfSubjectRulesReview } from '~/__mocks__/mockSelfSubjectRulesReview';
import { mockSelfSubjectAccessReview } from '~/__mocks__/mockSelfSubjectAccessReview';
import type { ModelVersion, RegisteredModel } from '~/concepts/modelRegistry/types';
import type { ServiceKind } from '~/k8sTypes';

const MODEL_REGISTRY_API_VERSION = 'v1alpha3';

type HandlersProps = {
disableModelRegistryFeature?: boolean;
modelRegistries?: ServiceKind[];
registeredModels?: RegisteredModel[];
modelVersions?: ModelVersion[];
allowed?: boolean;
};

const initIntercepts = ({
disableModelRegistryFeature = false,
modelRegistries = [
mockModelRegistryService({ name: 'modelregistry-sample' }),
mockModelRegistryService({ name: 'modelregistry-sample-2' }),
],
registeredModels = [
mockRegisteredModel({
name: 'Fraud detection model',
Expand Down Expand Up @@ -76,13 +82,7 @@ const initIntercepts = ({

cy.interceptK8s('POST', SelfSubjectRulesReviewModel, mockSelfSubjectRulesReview());

cy.interceptK8sList(
ServiceModel,
mockK8sResourceList([
mockModelRegistryService({ name: 'modelregistry-sample' }),
mockModelRegistryService({ name: 'modelregistry-sample-2' }),
]),
);
cy.interceptK8sList(ServiceModel, mockK8sResourceList(modelRegistries));

cy.interceptK8s(ServiceModel, mockModelRegistryService({ name: 'modelregistry-sample' }));

Expand Down Expand Up @@ -141,6 +141,17 @@ describe('Model Registry core', () => {
modelRegistry.tabEnabled();
});

it('Renders empty state with no model registries', () => {
initIntercepts({
disableModelRegistryFeature: false,
modelRegistries: [],
});

modelRegistry.visit();
modelRegistry.navigate();
modelRegistry.findModelRegistryEmptyState().should('exist');
});

it('No registered models in the selected Model Registry', () => {
initIntercepts({
disableModelRegistryFeature: false,
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/concepts/design/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import pipelineEmptyStateImg from '~/images/empty-state-pipelines.svg';
import clusterStorageEmptyStateImg from '~/images/empty-state-cluster-storage.svg';
import modelServerEmptyStateImg from '~/images/empty-state-model-serving.svg';
import dataConnectionEmptyStateImg from '~/images/empty-state-data-connections.svg';
import modelRegistryEmptyStateImg from '~/images/empty-state-model-registries.svg';

import './vars.scss';

Expand Down Expand Up @@ -114,8 +115,9 @@ export const typedEmptyImage = (objectType: ProjectObjectType): string => {
case ProjectObjectType.clusterStorage:
return clusterStorageEmptyStateImg;
case ProjectObjectType.modelServer:
case ProjectObjectType.registeredModels:
return modelServerEmptyStateImg;
case ProjectObjectType.registeredModels:
return modelRegistryEmptyStateImg;
case ProjectObjectType.dataConnection:
return dataConnectionEmptyStateImg;
default:
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/images/empty-state-model-registries.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 41 additions & 18 deletions frontend/src/pages/modelRegistry/ModelRegistryCoreLoader.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import * as React from 'react';
import { Navigate, Outlet, useParams } from 'react-router';
import { Bullseye, Alert } from '@patternfly/react-core';

import { Bullseye, Alert, Popover, List, ListItem, Button } from '@patternfly/react-core';
import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';

import { conditionalArea, SupportedArea } from '~/concepts/areas';
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 { ProjectObjectType, typedEmptyImage } from '~/concepts/design/utils';
import { ModelRegistrySelectorContext } from '~/concepts/modelRegistry/context/ModelRegistrySelectorContext';
import InvalidModelRegistry from './screens/InvalidModelRegistry';
import EmptyModelRegistryState from './screens/components/EmptyModelRegistryState';
import ModelRegistrySelectorNavigator from './screens/ModelRegistrySelectorNavigator';

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

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

type ApplicationPageRenderState = Pick<ApplicationPageProps, EmptyStateProps>;
type ApplicationPageRenderState = Pick<
ApplicationPageProps,
'emptyStatePage' | 'empty' | 'headerContent'
>;

const ModelRegistryCoreLoader: React.FC<ModelRegistryCoreLoaderProps> =
conditionalArea<ModelRegistryCoreLoaderProps>(
Expand Down Expand Up @@ -52,16 +56,37 @@ const ModelRegistryCoreLoader: React.FC<ModelRegistryCoreLoaderProps> =
renderStateProps = {
empty: true,
emptyStatePage: (
// TODO: Replace this with a component for empty registries once we have the designs
<EmptyModelRegistryState
title="No model registries found"
description="No model registries found in the cluster. Configure a new one before registering models."
primaryActionText="Configure model registry"
primaryActionOnClick={() => {
// TODO: Add primary action
}}
testid="empty-model-registries-state"
title="Request access to model registries"
description="To request a new model registry, or to request permission to access an existing model registry, contact your administrator."
headerIcon={() => (
<img src={typedEmptyImage(ProjectObjectType.registeredModels)} alt="" />
)}
customAction={
<Popover
showClose
position="bottom"
headerContent="Your administrator might be:"
bodyContent={
<List>
<ListItem>
The person who gave you your username, or who helped you to log in for the
first time
</ListItem>
<ListItem>Someone in your IT department or help desk</ListItem>
<ListItem>A project manager or developer</ListItem>
</List>
}
>
<Button variant="link" icon={<OutlinedQuestionCircleIcon />}>
Who&apos;s my administrator?
</Button>
</Popover>
}
/>
),
headerContent: null,
};
} else if (modelRegistry) {
const foundModelRegistry = modelRegistryServices.find(
Expand Down Expand Up @@ -90,18 +115,16 @@ const ModelRegistryCoreLoader: React.FC<ModelRegistryCoreLoaderProps> =
return (
<ApplicationsPage
title={
<TitleWithIcon
title="Registered models"
objectType={ProjectObjectType.registeredModels}
/>
<TitleWithIcon title="Model registry" objectType={ProjectObjectType.registeredModels} />
}
{...renderStateProps}
loaded
description="View and manage all of your registered models. Registering models to model registry allows you to manage their content, metadata, versions, and user access settings."
headerContent={
<ModelRegistrySelectorNavigator
getRedirectPath={(modelRegistryName) => `/modelRegistry/${modelRegistryName}`}
/>
}
{...renderStateProps}
loaded
provideChildrenPadding
/>
);
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/pages/modelRegistry/screens/ModelRegistry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@ const ModelRegistry: React.FC<ModelRegistryProps> = ({ ...pageProps }) => {
return (
<ApplicationsPage
{...pageProps}
title={
<TitleWithIcon title="Registered models" objectType={ProjectObjectType.deployedModels} />
}
description="View and manage your registered models."
title={<TitleWithIcon title="Model registry" objectType={ProjectObjectType.deployedModels} />}
description="View and manage all of your registered models. Registering models to model registry allows you to manage their content, metadata, versions, and user access settings."
headerContent={
<ModelRegistrySelectorNavigator
getRedirectPath={(modelRegistryName) => `/modelRegistry/${modelRegistryName}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const ModelVersionsDetails: React.FC<ModelVersionsDetailProps> = ({ tab, ...page
<BreadcrumbItem
render={() => (
<Link to="/modelRegistry">
Registered models - {preferredModelRegistry?.metadata.name}
Model registry - {preferredModelRegistry?.metadata.name}
</Link>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const ModelVersions: React.FC<ModelVersionsProps> = ({ tab, ...pageProps }) => {
<BreadcrumbItem
render={() => (
<Link to="/modelRegistry">
Registered models - {preferredModelRegistry?.metadata.name}
Model registry - {preferredModelRegistry?.metadata.name}
</Link>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const ModelVersionArchiveDetailsBreadcrumb: React.FC<ModelVersionArchiveDetailsB
}) => (
<Breadcrumb>
<BreadcrumbItem
render={() => <Link to="/modelRegistry">Registered models - {preferredModelRegistry}</Link>}
render={() => <Link to="/modelRegistry">Model registry - {preferredModelRegistry}</Link>}
/>
<BreadcrumbItem
render={() => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const ModelVersionsArchive: React.FC<ModelVersionsArchiveProps> = ({ ...pageProp
<BreadcrumbItem
render={() => (
<Link to="/modelRegistry">
Registered models - {preferredModelRegistry?.metadata.name}
Model registry - {preferredModelRegistry?.metadata.name}
</Link>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const RegisteredModelArchiveDetailsBreadcrumb: React.FC<
> = ({ preferredModelRegistry, registeredModel }) => (
<Breadcrumb>
<BreadcrumbItem
render={() => <Link to="/modelRegistry">Registered models - {preferredModelRegistry}</Link>}
render={() => <Link to="/modelRegistry">Model registry - {preferredModelRegistry}</Link>}
/>
<BreadcrumbItem
render={() => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const RegisteredModelsArchive: React.FC<RegisteredModelsArchiveProps> = ({ ...pa
<BreadcrumbItem
render={() => (
<Link to="/modelRegistry">
Registered models - {preferredModelRegistry?.metadata.name}
Model registry - {preferredModelRegistry?.metadata.name}
</Link>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react';
import {
Button,
ButtonVariant,
Expand All @@ -7,10 +8,10 @@ import {
EmptyStateFooter,
EmptyStateHeader,
EmptyStateIcon,
EmptyStateIconProps,
EmptyStateVariant,
} from '@patternfly/react-core';
import { PlusCircleIcon } from '@patternfly/react-icons';
import * as React from 'react';

type EmptyModelRegistryStateType = {
testid?: string;
Expand All @@ -20,6 +21,8 @@ type EmptyModelRegistryStateType = {
primaryActionOnClick?: () => void;
secondaryActionText?: string;
secondaryActionOnClick?: () => void;
headerIcon?: EmptyStateIconProps['icon'];
customAction?: React.ReactNode;
};

const EmptyModelRegistryState: React.FC<EmptyModelRegistryStateType> = ({
Expand All @@ -30,33 +33,41 @@ const EmptyModelRegistryState: React.FC<EmptyModelRegistryStateType> = ({
secondaryActionText,
primaryActionOnClick,
secondaryActionOnClick,
headerIcon,
customAction,
}) => (
<EmptyState variant={EmptyStateVariant.sm} data-testid={testid}>
<EmptyStateHeader titleText={title} icon={<EmptyStateIcon icon={PlusCircleIcon} />} />
<EmptyStateHeader
titleText={title}
icon={<EmptyStateIcon icon={headerIcon ?? PlusCircleIcon} />}
/>
<EmptyStateBody>{description}</EmptyStateBody>
<EmptyStateFooter>
<EmptyStateActions>
{primaryActionText && (
{primaryActionText && (
<EmptyStateActions>
<Button
data-testid="empty-model-registry-primary-action"
variant={ButtonVariant.primary}
onClick={primaryActionOnClick}
>
{primaryActionText}
</Button>
)}
</EmptyStateActions>
<EmptyStateActions>
{secondaryActionText && (
</EmptyStateActions>
)}

{secondaryActionText && (
<EmptyStateActions>
<Button
data-testid="empty-model-registry-secondary-action"
variant="link"
onClick={secondaryActionOnClick}
>
{secondaryActionText}
</Button>
)}
</EmptyStateActions>
</EmptyStateActions>
)}

{customAction && <EmptyStateActions>{customAction}</EmptyStateActions>}
</EmptyStateFooter>
</EmptyState>
);
Expand Down

0 comments on commit ac10d58

Please sign in to comment.