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

[RHOAIENG-7572] Registered Models - Empty View Redesign #3069

Merged
merged 1 commit into from
Aug 13, 2024
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 @@ -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} />
jpuzz0 marked this conversation as resolved.
Show resolved Hide resolved
}
{...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
Loading