Skip to content

Commit

Permalink
Add Model Registry Settings view (#423)
Browse files Browse the repository at this point in the history
Signed-off-by: lucferbux <[email protected]>
  • Loading branch information
lucferbux authored Sep 25, 2024
1 parent e0598fd commit 89fdaff
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 8 deletions.
2 changes: 1 addition & 1 deletion clients/ui/bff/internal/api/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

type Envelope[D any, M any] struct {
Data D `json:"data,omitempty"`
Data D `json:"data"`
Metadata M `json:"metadata,omitempty"`
}

Expand Down
2 changes: 1 addition & 1 deletion clients/ui/bff/internal/data/model_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func (m ModelRegistryModel) FetchAllModelRegistries(client k8s.KubernetesClientI
return nil, fmt.Errorf("error fetching model registries: %w", err)
}

var registries []ModelRegistryModel
var registries []ModelRegistryModel = []ModelRegistryModel{}
for _, item := range resources {
registry := ModelRegistryModel{
Name: item.Name,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { appChrome } from './appChrome';

export enum FormFieldSelector {
NAME = '#mr-name',
RESOURCENAME = '#resource-mr-name',
HOST = '#mr-host',
PORT = '#mr-port',
USERNAME = '#mr-username',
PASSWORD = '#mr-password',
DATABASE = '#mr-database',
}

export enum FormErrorTestId {
HOST = 'mr-host-error',
PORT = 'mr-port-error',
USERNAME = 'mr-username-error',
PASSWORD = 'mr-password-error',
DATABASE = 'mr-database-error',
}

export enum DatabaseDetailsTestId {
HOST = 'mr-db-host',
PORT = 'mr-db-port',
USERNAME = 'mr-db-username',
PASSWORD = 'mr-db-password',
DATABASE = 'mr-db-database',
}

class ModelRegistrySettings {
visit(wait = true) {
cy.visit('/modelRegistrySettings');
if (wait) {
this.wait();
}
}

navigate() {
this.findNavItem().click();
this.wait();
}

private wait() {
this.findHeading();
cy.testA11y();
}

private findHeading() {
cy.findByTestId('app-page-title').should('exist');
cy.findByTestId('app-page-title').contains('Model Registry Settings');
}

findNavItem() {
return appChrome.findNavItem('Model registry settings', 'Settings');
}

findEmptyState() {
return cy.findByTestId('mr-settings-empty-state');
}

// findCreateButton() {
// return cy.findByText('Create model registry');
// }

findFormField(selector: FormFieldSelector) {
return cy.get(selector);
}

clearFormFields() {
Object.values(FormFieldSelector).forEach((selector) => {
this.findFormField(selector).clear();
this.findFormField(selector).blur();
});
}

findFormError(testId: FormErrorTestId) {
return cy.findByTestId(testId);
}

shouldHaveAllErrors() {
Object.values(FormErrorTestId).forEach((testId) => this.findFormError(testId).should('exist'));
}

shouldHaveNoErrors() {
Object.values(FormErrorTestId).forEach((testId) =>
this.findFormError(testId).should('not.exist'),
);
}

findSubmitButton() {
return cy.findByTestId('modal-submit-button');
}

findTable() {
return cy.findByTestId('model-registries-table');
}

findModelRegistryRow(registryName: string) {
return this.findTable().findByText(registryName).closest('tr');
}

findDatabaseDetail(testId: DatabaseDetailsTestId) {
return cy.findByTestId(testId);
}

findDatabasePasswordHiddenButton() {
return this.findDatabaseDetail(DatabaseDetailsTestId.PASSWORD).findByTestId(
'password-hidden-button',
);
}

findConfirmDeleteNameInput() {
return cy.findByTestId('confirm-delete-input');
}
}

export const modelRegistrySettings = new ModelRegistrySettings();
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { mockModelRegistry } from '~/__mocks__/mockModelRegistry';
import type { ModelRegistry } from '~/app/types';
import { mockBFFResponse } from '~/__mocks__/mockBFFResponse';
import { modelRegistrySettings } from '~/__tests__/cypress/cypress/pages/modelRegistrySettings';

type HandlersProps = {
modelRegistries?: ModelRegistry[];
};

const MODEL_REGISTRY_API_VERSION = 'v1';

const initIntercepts = ({
modelRegistries = [
mockModelRegistry({
name: 'modelregistry-sample',
description: 'New model registry',
displayName: 'Model Registry Sample',
}),
mockModelRegistry({
name: 'modelregistry-sample-2',
description: 'New model registry 2',
displayName: 'Model Registry Sample 2',
}),
],
}: HandlersProps) => {
cy.interceptApi(
`GET /api/:apiVersion/model_registry`,
{
path: { apiVersion: MODEL_REGISTRY_API_VERSION },
},
mockBFFResponse(modelRegistries),
);
};

it('Shows empty state when there are no registries', () => {
initIntercepts({ modelRegistries: [] });
modelRegistrySettings.visit(true);
modelRegistrySettings.findEmptyState().should('exist');
});

describe('ModelRegistriesTable', () => {
it('Shows table when there are registries', () => {
initIntercepts({});
modelRegistrySettings.visit(true);
modelRegistrySettings.findEmptyState().should('not.exist');
modelRegistrySettings.findTable().should('exist');
modelRegistrySettings.findModelRegistryRow('Model Registry Sample').should('exist');
});
});
6 changes: 4 additions & 2 deletions clients/ui/frontend/src/app/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const useAdminSettings = (): NavDataItem[] => {
return [
{
label: 'Settings',
children: [{ label: 'Model Registry', path: '/settings' }],
children: [{ label: 'Model Registry', path: '/modelRegistrySettings' }],
},
];
};
Expand All @@ -58,7 +58,9 @@ const AppRoutes: React.FC = () => {
{
// TODO: Remove the linter skip when we implement authentication
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
isAdmin && <Route path="/settings/*" element={<ModelRegistrySettingsRoutes />} />
isAdmin && (
<Route path="/modelRegistrySettings/*" element={<ModelRegistrySettingsRoutes />} />
)
}
</Routes>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { ModelRegistry } from '~/app/types';
import { Table } from '~/app/components/table';
import { modelRegistryColumns } from './columns';
import ModelRegistriesTableRow from './ModelRegistriesTableRow';

type ModelRegistriesTableProps = {
modelRegistries: ModelRegistry[];
};

const ModelRegistriesTable: React.FC<ModelRegistriesTableProps> = ({ modelRegistries }) => (
// TODO: Add toolbar once we manage permissions
<Table
data-testid="model-registries-table"
data={modelRegistries}
columns={modelRegistryColumns}
rowRenderer={(mr) => <ModelRegistriesTableRow key={mr.name} modelRegistry={mr} />}
variant="compact"
/>
);

export default ModelRegistriesTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { Td, Tr } from '@patternfly/react-table';
import { ModelRegistry } from '~/app/types';

type ModelRegistriesTableRowProps = {
modelRegistry: ModelRegistry;
};

const ModelRegistriesTableRow: React.FC<ModelRegistriesTableRowProps> = ({ modelRegistry: mr }) => (
<>
<Tr>
<Td dataLabel="Model registry name">
<strong>{mr.displayName || mr.name}</strong>
{mr.description && <p>{mr.description}</p>}
</Td>
</Tr>
</>
);

// TODO: Get rest of columns once we manage permissions

export default ModelRegistriesTableRow;
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@ import React from 'react';
import { EmptyState, EmptyStateBody, EmptyStateVariant } from '@patternfly/react-core';
import { PlusCircleIcon } from '@patternfly/react-icons';
import ApplicationsPage from '~/app/components/ApplicationsPage';
import useModelRegistries from '~/app/hooks/useModelRegistries';
import TitleWithIcon from '~/app/components/design/TitleWithIcon';
import { ProjectObjectType } from '~/app/components/design/utils';
import ModelRegistriesTable from './ModelRegistriesTable';

const ModelRegistrySettings: React.FC = () => {
const [modelRegistries, loaded, loadError] = [[], true, undefined]; // TODO: change to real values
const [modelRegistries, loaded, loadError] = useModelRegistries();
return (
<>
<ApplicationsPage
title="Model registry settings"
description="Manage model registry settings for all users in your organization."
title={
<TitleWithIcon
title="Model Registry Settings"
objectType={ProjectObjectType.registeredModels}
/>
}
description="List all the model registries deployed in your environment."
loaded={loaded}
loadError={loadError}
errorMessage="Unable to load model registries."
Expand All @@ -27,7 +36,7 @@ const ModelRegistrySettings: React.FC = () => {
}
provideChildrenPadding
>
TODO: Add model registry settings
<ModelRegistriesTable modelRegistries={modelRegistries} />
</ApplicationsPage>
</>
);
Expand Down
23 changes: 23 additions & 0 deletions clients/ui/frontend/src/app/pages/settings/columns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { SortableData } from '~/app/components/table';
import { ModelRegistry } from '~/app/types';

export const modelRegistryColumns: SortableData<ModelRegistry>[] = [
{
field: 'model regisry name',
label: 'Model registry name',
sortable: (a, b) => a.name.localeCompare(b.name),
width: 30,
},
// TODO: Add once we manage permissions
// {
// field: 'status',
// label: 'Status',
// sortable: false,
// },
// {
// field: 'manage permissions',
// label: '',
// sortable: false,
// },
// kebabTableColumn(),
];

0 comments on commit 89fdaff

Please sign in to comment.