Skip to content

Commit

Permalink
Add views for Model Versions and Model Details (#409)
Browse files Browse the repository at this point in the history
Signed-off-by: Griffin-Sullivan <[email protected]>
  • Loading branch information
Griffin-Sullivan authored Sep 24, 2024
1 parent 3afc987 commit e0598fd
Show file tree
Hide file tree
Showing 26 changed files with 2,200 additions and 9 deletions.
4 changes: 1 addition & 3 deletions clients/ui/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions clients/ui/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"test:fix": "eslint --ext .js,.ts,.jsx,.tsx ./src --fix",
"test:lint": "eslint --max-warnings 0 --ext .js,.ts,.jsx,.tsx ./src",
"cypress:open": "cypress open --project src/__tests__/cypress",
"cypress:open:mock": "CY_MOCK=1 CY_WS_PORT=9002 npm run cypress:open -- ",
"cypress:open:mock": "CY_MOCK=1 npm run cypress:open -- ",
"cypress:run": "cypress run -b chrome --project src/__tests__/cypress",
"cypress:run:mock": "CY_MOCK=1 npm run cypress:run -- ",
"cypress:server:build": "POLL_INTERVAL=9999999 FAST_POLL_INTERVAL=9999999 npm run build",
Expand All @@ -40,12 +40,12 @@
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "14.5.2",
"@types/jest": "^29.5.12",
"@types/react-router-dom": "^5.3.3",
"@types/classnames": "^2.3.1",
"@types/dompurify": "^3.0.5",
"@types/showdown": "^2.0.3",
"@types/jest": "^29.5.12",
"@types/lodash-es": "^4.17.8",
"@types/react-router-dom": "^5.3.3",
"@types/showdown": "^2.0.3",
"chai-subset": "^1.6.0",
"copy-webpack-plugin": "^12.0.2",
"core-js": "^3.37.1",
Expand Down Expand Up @@ -98,6 +98,7 @@
"npm-run-all": "^4.1.5",
"react": "^18",
"react-dom": "^18",
"react-router": "^6.26.2",
"sass": "^1.78.0",
"dompurify": "^3.1.6",
"showdown": "^2.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ declare global {
options: {
path: { modelRegistryName: string; apiVersion: string; registeredModelId: number };
},
response: ApiResponse<RegisteredModel>,
response: ApiResponse<ModelRegistryResponse<RegisteredModel>>,
) => Cypress.Chainable<null>) &
((
type: 'PATCH /api/:apiVersion/model_registry/:modelRegistryName/registered_models/:registeredModelId',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/* eslint-disable camelcase */
import { mockModelVersionList } from '~/__mocks__/mockModelVersionList';
import { mockRegisteredModelList } from '~/__mocks__/mockRegisteredModelsList';
import { labelModal, modelRegistry } from '~/__tests__/cypress/cypress/pages/modelRegistry';
import { be } from '~/__tests__/cypress/cypress/utils/should';
import { mockRegisteredModel } from '~/__mocks__/mockRegisteredModel';
import type { ModelRegistry, ModelVersion } from '~/app/types';
import { verifyRelativeURL } from '~/__tests__/cypress/cypress/utils/url';
import { mockModelRegistry } from '~/__mocks__/mockModelRegistry';
import { mockModelVersion } from '~/__mocks__/mockModelVersion';
import { mockBFFResponse } from '~/__mocks__/utils';

const MODEL_REGISTRY_API_VERSION = 'v1';

type HandlersProps = {
registeredModelsSize?: number;
modelVersions?: ModelVersion[];
modelRegistries?: ModelRegistry[];
};

const initIntercepts = ({
registeredModelsSize = 4,
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',
}),
],
modelVersions = [
mockModelVersion({
author: 'Author 1',
id: '1',
labels: [
'Financial data',
'Fraud detection',
'Test label',
'Machine learning',
'Next data to be overflow',
'Test label x',
'Test label y',
'Test label z',
],
}),
mockModelVersion({ id: '2', name: 'model version' }),
],
}: HandlersProps) => {
cy.interceptApi(
`GET /api/:apiVersion/model_registry`,
{
path: { apiVersion: MODEL_REGISTRY_API_VERSION },
},
mockBFFResponse(modelRegistries),
);

cy.interceptApi(
`GET /api/:apiVersion/model_registry/:modelRegistryName/registered_models`,
{
path: { modelRegistryName: 'modelregistry-sample', apiVersion: MODEL_REGISTRY_API_VERSION },
},
mockBFFResponse(mockRegisteredModelList({ size: registeredModelsSize })),
);

cy.interceptApi(
`GET /api/:apiVersion/model_registry/:modelRegistryName/registered_models/:registeredModelId/versions`,
{
path: {
modelRegistryName: 'modelregistry-sample',
apiVersion: MODEL_REGISTRY_API_VERSION,
registeredModelId: 1,
},
},
mockBFFResponse(mockModelVersionList({ items: modelVersions })),
);

cy.interceptApi(
`GET /api/:apiVersion/model_registry/:modelRegistryName/registered_models/:registeredModelId`,
{
path: {
modelRegistryName: 'modelregistry-sample',
apiVersion: MODEL_REGISTRY_API_VERSION,
registeredModelId: 1,
},
},
mockBFFResponse(mockRegisteredModel({})),
);

cy.interceptApi(
`GET /api/:apiVersion/model_registry/:modelRegistryName/model_versions/:modelVersionId`,
{
path: {
modelRegistryName: 'modelregistry-sample',
apiVersion: MODEL_REGISTRY_API_VERSION,
modelVersionId: 1,
},
},
mockModelVersion({ id: '1', name: 'model version' }),
);
};

describe('Model Versions', () => {
it('No model versions in the selected registered model', () => {
initIntercepts({
modelVersions: [],
});

modelRegistry.visit();
const registeredModelRow = modelRegistry.getRow('Fraud detection model');
registeredModelRow.findName().contains('Fraud detection model').click();
verifyRelativeURL(`/modelRegistry/modelregistry-sample/registeredModels/1/versions`);
modelRegistry.shouldmodelVersionsEmpty();
});

it('Model versions table browser back button should lead to Registered models table', () => {
initIntercepts({
modelVersions: [],
});

modelRegistry.visit();
const registeredModelRow = modelRegistry.getRow('Fraud detection model');
registeredModelRow.findName().contains('Fraud detection model').click();
verifyRelativeURL(`/modelRegistry/modelregistry-sample/registeredModels/1/versions`);
cy.go('back');
verifyRelativeURL(`/modelRegistry/modelregistry-sample`);
registeredModelRow.findName().contains('Fraud detection model').should('exist');
});

it('Model versions table', () => {
// TODO: Uncomment when we fix finding dropdown items

initIntercepts({
modelRegistries: [
// mockModelRegistry({ name: 'modelRegistry-1', displayName: 'modelRegistry-1' }),
mockModelRegistry({}),
],
});

modelRegistry.visit();
//modelRegistry.findModelRegistry().findSelectOption('Model Registry Sample').click();
//cy.reload();
const registeredModelRow = modelRegistry.getRow('Fraud detection model');
registeredModelRow.findName().contains('Fraud detection model').click();
verifyRelativeURL(`/modelRegistry/modelregistry-sample/registeredModels/1/versions`);
modelRegistry.findModelBreadcrumbItem().contains('test');
//modelRegistry.findModelVersionsTableKebab().findDropdownItem('View archived versions');
//modelRegistry.findModelVersionsHeaderAction().findDropdownItem('Archive model');
modelRegistry.findModelVersionsTable().should('be.visible');
modelRegistry.findModelVersionsTableRows().should('have.length', 2);

// Label modal
const modelVersionRow = modelRegistry.getModelVersionRow('new model version');

modelVersionRow.findLabelModalText().contains('5 more');
modelVersionRow.findLabelModalText().click();
labelModal.shouldContainsModalLabels([
'Financial',
'Financial data',
'Fraud detection',
'Test label',
'Machine learning',
'Next data to be overflow',
'Test label x',
'Test label y',
'Test label y',
]);
labelModal.findModalSearchInput().type('Financial');
labelModal.shouldContainsModalLabels(['Financial', 'Financial data']);
labelModal.findCloseModal().click();

// sort by model version name
modelRegistry.findModelVersionsTableHeaderButton('Version name').click();
modelRegistry.findModelVersionsTableHeaderButton('Version name').should(be.sortAscending);
modelRegistry.findModelVersionsTableHeaderButton('Version name').click();
modelRegistry.findModelVersionsTableHeaderButton('Version name').should(be.sortDescending);

// sort by Last modified
modelRegistry.findModelVersionsTableHeaderButton('Last modified').click();
modelRegistry.findModelVersionsTableHeaderButton('Last modified').should(be.sortAscending);
modelRegistry.findModelVersionsTableHeaderButton('Last modified').click();
modelRegistry.findModelVersionsTableHeaderButton('Last modified').should(be.sortDescending);

// sort by model version author
modelRegistry.findModelVersionsTableHeaderButton('Author').click();
modelRegistry.findModelVersionsTableHeaderButton('Author').should(be.sortAscending);
modelRegistry.findModelVersionsTableHeaderButton('Author').click();
modelRegistry.findModelVersionsTableHeaderButton('Author').should(be.sortDescending);

// filtering by keyword
modelRegistry.findModelVersionsTableSearch().type('new model version');
modelRegistry.findModelVersionsTableRows().should('have.length', 1);
modelRegistry.findModelVersionsTableRows().contains('new model version');
modelRegistry.findModelVersionsTableSearch().focused().clear();

// filtering by model version author
modelRegistry.findModelVersionsTableFilter().findSelectOption('Author').click();
modelRegistry.findModelVersionsTableSearch().type('Test author');
modelRegistry.findModelVersionsTableRows().should('have.length', 1);
modelRegistry.findModelVersionsTableRows().contains('Test author');
});

it('Model version details back button should lead to versions table', () => {
initIntercepts({});

modelRegistry.visit();
const registeredModelRow = modelRegistry.getRow('Fraud detection model');
registeredModelRow.findName().contains('Fraud detection model').click();
verifyRelativeURL(`/modelRegistry/modelregistry-sample/registeredModels/1/versions`);
// TODO: Uncomment when we have model version details
// const modelVersionRow = modelRegistry.getModelVersionRow('model version');
// modelVersionRow.findModelVersionName().contains('model version').click();
// verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/1/versions/1/details');
// cy.findByTestId('app-page-title').should('have.text', 'model version');
// cy.findByTestId('breadcrumb-version-name').should('have.text', 'model version');
// cy.go('back');
// verifyRelativeURL('/modelRegistry/modelregistry-sample/registeredModels/1/versions');
});
});
12 changes: 12 additions & 0 deletions clients/ui/frontend/src/__tests__/cypress/cypress/utils/url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Verify the relative route to the cypress host
* e.g. If page is running on `https://localhost:9001/pipelines`
* calling verifyRelativeURL('/pipelines') will check whether the full URL matches the URL above
*/
export const verifyRelativeURL = (relativeURL: string): Cypress.Chainable<string> => {
return cy
.location()
.then((location) =>
cy.url().should('eq', `${location.protocol}//${location.host}${relativeURL}`),
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import ModelRegistry from './screens/ModelRegistry';
import ModelRegistryCoreLoader from './ModelRegistryCoreLoader';
import { modelRegistryUrl } from './screens/routeUtils';
import RegisteredModelsArchive from './screens/RegisteredModelsArchive/RegisteredModelsArchive';
import { ModelVersionsTab } from './screens/ModelVersions/const';
import ModelVersions from './screens/ModelVersions/ModelVersions';

const ModelRegistryRoutes: React.FC = () => (
<Routes>
Expand All @@ -16,6 +18,18 @@ const ModelRegistryRoutes: React.FC = () => (
}
>
<Route index element={<ModelRegistry empty={false} />} />
<Route path="registeredModels/:registeredModelId">
<Route index element={<Navigate to={ModelVersionsTab.VERSIONS} replace />} />
<Route
path={ModelVersionsTab.VERSIONS}
element={<ModelVersions tab={ModelVersionsTab.VERSIONS} empty={false} />}
/>
<Route
path={ModelVersionsTab.DETAILS}
element={<ModelVersions tab={ModelVersionsTab.DETAILS} empty={false} />}
/>
<Route path="*" element={<Navigate to="." />} />
</Route>
<Route path="registeredModels/archive">
<Route index element={<RegisteredModelsArchive empty={false} />} />
<Route path="*" element={<Navigate to="." />} />
Expand Down
Loading

0 comments on commit e0598fd

Please sign in to comment.