diff --git a/frontend/src/__tests__/cypress/cypress/pages/connectionTypes.ts b/frontend/src/__tests__/cypress/cypress/pages/connectionTypes.ts new file mode 100644 index 0000000000..f5244eb18c --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/pages/connectionTypes.ts @@ -0,0 +1,67 @@ +import { TableRow } from './components/table'; + +class CreateConnectionTypeTableRow extends TableRow { + findSectionHeading() { + return this.find().findByTestId(['section-heading']); + } + + findType() { + return this.find().findByTestId(['field-type']); + } + + findDefault() { + return this.find().findByTestId(['field-default']); + } + + findEnvVar() { + return this.find().findByTestId(['field-env']); + } + + findRequired() { + return this.find().findByTestId(['field-required']); + } +} + +class CreateConnectionTypePage { + visitCreatePage() { + cy.visitWithLogin('/connectionTypes/create'); + cy.findAllByText('Create connection type').should('exist'); + } + + visitDuplicatePage(name = 'existing') { + cy.visitWithLogin(`/connectionTypes/duplicate/${name}`); + cy.findAllByText('Create connection type').should('exist'); + } + + connectionTypeName() { + return cy.findByTestId('connection-type-name'); + } + + connectionTypeDesc() { + return cy.findByTestId('connection-type-description'); + } + + connectionTypeEnable() { + return cy.findByTestId('connection-type-enable'); + } + + findFieldsTable() { + return cy.findByTestId('connection-type-fields-table'); + } + + getFieldsTableRow(name = '') { + return new CreateConnectionTypeTableRow(() => + this.findFieldsTable().findByText(name).parents('tr'), + ); + } + + findAllFieldsTableRows() { + return this.findFieldsTable().findAllByTestId(['row']); + } + + findSubmitButton() { + return cy.findByTestId('submit-button'); + } +} + +export const createConnectionTypePage = new CreateConnectionTypePage(); diff --git a/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts b/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts index 88688563d9..4c96700e9e 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts @@ -167,10 +167,6 @@ class ModelRegistry { findModelVersionsTableFilter() { return cy.findByTestId('model-versions-table-filter'); } - - findRegisterModelButton() { - return cy.findByRole('button', { name: 'Register model' }); - } } export const modelRegistry = new ModelRegistry(); diff --git a/frontend/src/__tests__/cypress/cypress/pages/modelRegistry/registerModelPage.ts b/frontend/src/__tests__/cypress/cypress/pages/modelRegistry/registerModelPage.ts deleted file mode 100644 index d420cdff09..0000000000 --- a/frontend/src/__tests__/cypress/cypress/pages/modelRegistry/registerModelPage.ts +++ /dev/null @@ -1,41 +0,0 @@ -export enum FormFieldSelector { - MODEL_NAME = '#model-name', - MODEL_DESCRIPTION = '#model-description', - VERSION_NAME = '#version-name', - VERSION_DESCRIPTION = '#version-description', - SOURCE_MODEL_FORMAT = '#source-model-format', - SOURCE_MODEL_FORMAT_VERSION = '#source-model-format-version', - LOCATION_TYPE_OBJECT_STORAGE = '#location-type-object-storage', - LOCATION_ENDPOINT = '#location-endpoint', - LOCATION_BUCKET = '#location-bucket', - LOCATION_REGION = '#location-region', - LOCATION_PATH = '#location-path', - LOCATION_TYPE_URI = '#location-type-uri', - LOCATION_URI = '#location-uri', -} - -class RegisterModelPage { - visit() { - const preferredModelRegistry = 'modelregistry-sample'; - cy.visitWithLogin(`/modelRegistry/${preferredModelRegistry}/registerModel`); - this.wait(); - } - - private wait() { - const preferredModelRegistry = 'modelregistry-sample'; - cy.findByTestId('app-page-title').should('exist'); - cy.findByTestId('app-page-title').contains('Register model'); - cy.findByText(`Model registry - ${preferredModelRegistry}`).should('exist'); - cy.testA11y(); - } - - findFormField(selector: FormFieldSelector) { - return cy.get(selector); - } - - findSubmitButton() { - return cy.findByTestId('create-button'); - } -} - -export const registerModelPage = new RegisterModelPage(); diff --git a/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts b/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts index 51838eb9a3..3deaccdf8b 100644 --- a/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts +++ b/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts @@ -2,7 +2,6 @@ import type { K8sResourceListResult } from '@openshift/dynamic-plugin-sdk-utils' import type { GenericStaticResponse, RouteHandlerController } from 'cypress/types/net-stubbing'; import type { BaseMetricCreationResponse, BaseMetricListResponse } from '~/api'; import type { - ModelArtifact, ModelArtifactList, ModelVersion, ModelVersionList, @@ -50,6 +49,7 @@ import type { } from '~/concepts/pipelines/kfTypes'; import type { GrpcResponse } from '~/__mocks__/mlmd/utils'; import type { BuildMockPipelinveVersionsType } from '~/__mocks__'; +import type { ConnectionTypeConfigMap } from '~/concepts/connectionTypes/types'; type SuccessErrorResponse = { success: boolean; @@ -295,21 +295,11 @@ declare global { options: { path: { serviceName: string; apiVersion: string } }, response: OdhResponse, ) => Cypress.Chainable) & - (( - type: 'POST /api/service/modelregistry/:serviceName/api/model_registry/:apiVersion/registered_models', - options: { path: { serviceName: string; apiVersion: string } }, - response: OdhResponse, - ) => Cypress.Chainable) & (( type: 'GET /api/service/modelregistry/:serviceName/api/model_registry/:apiVersion/registered_models/:registeredModelId/versions', options: { path: { serviceName: string; apiVersion: string; registeredModelId: number } }, response: OdhResponse, ) => Cypress.Chainable) & - (( - type: 'POST /api/service/modelregistry/:serviceName/api/model_registry/:apiVersion/registered_models/:registeredModelId/versions', - options: { path: { serviceName: string; apiVersion: string; registeredModelId: number } }, - response: OdhResponse, - ) => Cypress.Chainable) & (( type: 'GET /api/service/modelregistry/:serviceName/api/model_registry/:apiVersion/registered_models/:registeredModelId', options: { path: { serviceName: string; apiVersion: string; registeredModelId: number } }, @@ -332,11 +322,6 @@ declare global { options: { path: { serviceName: string; apiVersion: string; modelVersionId: number } }, response: OdhResponse, ) => Cypress.Chainable) & - (( - type: 'POST /api/service/modelregistry/:serviceName/api/model_registry/:apiVersion/model_versions/:modelVersionId/artifacts', - options: { path: { serviceName: string; apiVersion: string; modelVersionId: number } }, - response: OdhResponse, - ) => Cypress.Chainable) & (( type: 'PATCH /api/service/modelregistry/:serviceName/api/model_registry/:apiVersion/model_versions/:modelVersionId', options: { path: { serviceName: string; apiVersion: string; modelVersionId: number } }, @@ -581,6 +566,24 @@ declare global { path: { namespace: string }; }, response: OdhResponse, + ) => Cypress.Chainable) & + (( + type: 'GET /api/connection-types', + response: ConnectionTypeConfigMap[], + ) => Cypress.Chainable) & + (( + type: 'PATCH /api/connection-types/:name', + options: { + path: { name: string }; + }, + response: { success: boolean; error: string }, + ) => Cypress.Chainable) & + (( + type: 'GET /api/connection-types/:name', + options: { + path: { name: string }; + }, + response: ConnectionTypeConfigMap, ) => Cypress.Chainable); } } diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/connectionTypes/createConnectionType.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/connectionTypes/createConnectionType.cy.ts new file mode 100644 index 0000000000..dd319713b3 --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/connectionTypes/createConnectionType.cy.ts @@ -0,0 +1,85 @@ +import { + mockConnectionTypeConfigMap, + mockConnectionTypeConfigMapObj, +} from '~/__mocks__/mockConnectionType'; +import { createConnectionTypePage } from '~/__tests__/cypress/cypress/pages/connectionTypes'; +import { asClusterAdminUser } from '~/__tests__/cypress/cypress/utils/mockUsers'; +import type { SectionField, ShortTextField } from '~/concepts/connectionTypes/types'; + +describe('create', () => { + it('Display base page', () => { + asClusterAdminUser(); + createConnectionTypePage.visitCreatePage(); + + createConnectionTypePage.connectionTypeName().should('exist'); + createConnectionTypePage.connectionTypeDesc().should('exist'); + createConnectionTypePage.connectionTypeEnable().should('exist'); + createConnectionTypePage.findFieldsTable().should('exist'); + }); + + it('Allows create button with valid name', () => { + asClusterAdminUser(); + createConnectionTypePage.visitCreatePage(); + + createConnectionTypePage.connectionTypeName().should('have.value', ''); + createConnectionTypePage.findSubmitButton().should('be.disabled'); + + createConnectionTypePage.connectionTypeName().type('hello'); + createConnectionTypePage.findSubmitButton().should('be.enabled'); + }); +}); + +describe('duplicate', () => { + const existing = mockConnectionTypeConfigMapObj({ name: 'existing' }); + + beforeEach(() => { + asClusterAdminUser(); + cy.interceptOdh( + 'GET /api/connection-types/:name', + { path: { name: 'existing' } }, + mockConnectionTypeConfigMap({ name: 'existing' }), + ); + }); + + it('Prefill details from existing connection', () => { + createConnectionTypePage.visitDuplicatePage('existing'); + + createConnectionTypePage + .connectionTypeName() + .should( + 'have.value', + `Duplicate of ${existing.metadata.annotations['openshift.io/display-name']}`, + ); + createConnectionTypePage + .connectionTypeDesc() + .should('have.value', existing.metadata.annotations['openshift.io/description']); + createConnectionTypePage.connectionTypeEnable().should('be.checked'); + }); + + it('Prefill fields table from existing connection', () => { + createConnectionTypePage.visitDuplicatePage('existing'); + + createConnectionTypePage + .findAllFieldsTableRows() + .should('have.length', existing.data?.fields?.length); + + // Row 0 - Section + const dataField0 = existing.data?.fields?.[0] as SectionField; + const row0 = createConnectionTypePage.getFieldsTableRow(dataField0.name); + row0.findSectionHeading().should('exist'); + + // Row 1 - Short text field + const dataField1 = existing.data?.fields?.[1] as ShortTextField; + const row1 = createConnectionTypePage.getFieldsTableRow(dataField1.name); + row1.findType().should('have.text', dataField1.type); + row1.findDefault().should('have.text', '-'); + row1.findRequired().not('be.checked'); + + // Row 2 - Short text field + const dataField2 = existing.data?.fields?.[2] as ShortTextField; + const row2 = createConnectionTypePage.getFieldsTableRow(dataField2.name); + row2.findType().should('have.text', dataField2.type); + row2.findDefault().should('have.text', dataField2.properties.defaultValue); + row2.findRequired().should('be.checked'); + }); +}); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts index 24c8dd0a70..1e110dff25 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/modelRegistry.cy.ts @@ -101,7 +101,7 @@ const initIntercepts = ({ ); }; -describe('Model Registry core', () => { +describe('Model Registry', () => { it('Model Registry Disabled in the cluster', () => { initIntercepts({ disableModelRegistryFeature: true, @@ -134,81 +134,57 @@ describe('Model Registry core', () => { modelRegistry.shouldregisteredModelsEmpty(); }); - describe('Registered model table', () => { - beforeEach(() => { - initIntercepts({ disableModelRegistryFeature: false }); - modelRegistry.visit(); - }); - - it('Renders row contents', () => { - const registeredModelRow = modelRegistry.getRow('Fraud detection model'); - registeredModelRow.findName().contains('Fraud detection model'); - registeredModelRow - .findDescription() - .contains( - 'A machine learning model trained to detect fraudulent transactions in financial data', - ); - registeredModelRow.findOwner().contains('Author 1'); - - // Label popover - registeredModelRow.findLabelPopoverText().contains('2 more'); - registeredModelRow.findLabelPopoverText().click(); - registeredModelRow.shouldContainsPopoverLabels([ - 'Machine learning', - 'Next data to be overflow', - ]); - }); - - it('Renders labels in modal', () => { - const registeredModelRow2 = modelRegistry.getRow('Label modal'); - registeredModelRow2.findLabelModalText().contains('6 more'); - registeredModelRow2.findLabelModalText().click(); - labelModal.shouldContainsModalLabels([ - 'Testing label', - 'Financial', - 'Financial data', - 'Fraud detection', - 'Machine learning', - 'Next data to be overflow', - 'Label x', - 'Label y', - 'Label z', - ]); - labelModal.findModalSearchInput().type('Financial'); - labelModal.shouldContainsModalLabels(['Financial', 'Financial data']); - labelModal.findCloseModal().click(); - }); - - it('Sorts by model name', () => { - modelRegistry.findRegisteredModelTableHeaderButton('Model name').should(be.sortAscending); - modelRegistry.findRegisteredModelTableHeaderButton('Model name').click(); - modelRegistry.findRegisteredModelTableHeaderButton('Model name').should(be.sortDescending); - }); - - it('Filters by keyword', () => { - modelRegistry.findTableSearch().type('Fraud detection model'); - modelRegistry.findTableRows().should('have.length', 1); - modelRegistry.findTableRows().contains('Fraud detection model'); + it('Registered model table', () => { + initIntercepts({ + disableModelRegistryFeature: false, }); - }); -}); -describe('Register Model button', () => { - it('Navigates to register page from empty state', () => { - initIntercepts({ disableModelRegistryFeature: false, registeredModels: [] }); modelRegistry.visit(); - modelRegistry.findRegisterModelButton().click(); - cy.findByTestId('app-page-title').should('exist'); - cy.findByTestId('app-page-title').contains('Register model'); - cy.findByText('Model registry - modelregistry-sample').should('exist'); - }); - it('Navigates to register page from table toolbar', () => { - initIntercepts({ disableModelRegistryFeature: false }); - modelRegistry.visit(); - modelRegistry.findRegisterModelButton().click(); - cy.findByTestId('app-page-title').should('exist'); - cy.findByTestId('app-page-title').contains('Register model'); - cy.findByText('Model registry - modelregistry-sample').should('exist'); + const registeredModelRow = modelRegistry.getRow('Fraud detection model'); + registeredModelRow.findName().contains('Fraud detection model'); + registeredModelRow + .findDescription() + .contains( + 'A machine learning model trained to detect fraudulent transactions in financial data', + ); + registeredModelRow.findOwner().contains('Author 1'); + + // Label popover + registeredModelRow.findLabelPopoverText().contains('2 more'); + registeredModelRow.findLabelPopoverText().click(); + registeredModelRow.shouldContainsPopoverLabels([ + 'Machine learning', + 'Next data to be overflow', + ]); + + // Label modal + const registeredModelRow2 = modelRegistry.getRow('Label modal'); + registeredModelRow2.findLabelModalText().contains('6 more'); + registeredModelRow2.findLabelModalText().click(); + labelModal.shouldContainsModalLabels([ + 'Testing label', + 'Financial', + 'Financial data', + 'Fraud detection', + 'Machine learning', + 'Next data to be overflow', + 'Label x', + 'Label y', + 'Label z', + ]); + labelModal.findModalSearchInput().type('Financial'); + labelModal.shouldContainsModalLabels(['Financial', 'Financial data']); + labelModal.findCloseModal().click(); + + // sort by modelName + modelRegistry.findRegisteredModelTableHeaderButton('Model name').should(be.sortAscending); + modelRegistry.findRegisteredModelTableHeaderButton('Model name').click(); + modelRegistry.findRegisteredModelTableHeaderButton('Model name').should(be.sortDescending); + + // filtering by keyword + modelRegistry.findTableSearch().type('Fraud detection model'); + modelRegistry.findTableRows().should('have.length', 1); + modelRegistry.findTableRows().contains('Fraud detection model'); }); }); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/registerModel.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/registerModel.cy.ts deleted file mode 100644 index e108df61f0..0000000000 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/modelRegistry/registerModel.cy.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { mockDashboardConfig, mockDscStatus, mockK8sResourceList } from '~/__mocks__'; -import { mockDsciStatus } from '~/__mocks__/mockDsciStatus'; -import { StackCapability, StackComponent } from '~/concepts/areas/types'; -import { ModelRegistryModel } from '~/__tests__/cypress/cypress/utils/models'; -import { - FormFieldSelector, - registerModelPage, -} from '~/__tests__/cypress/cypress/pages/modelRegistry/registerModelPage'; -import { mockModelRegistry } from '~/__mocks__/mockModelRegistry'; -import { mockRegisteredModel } from '~/__mocks__/mockRegisteredModel'; -import { mockModelVersion } from '~/__mocks__/mockModelVersion'; -import { mockModelArtifact } from '~/__mocks__/mockModelArtifact'; -import { - ModelArtifactState, - ModelState, - type RegisteredModel, - type ModelVersion, - type ModelArtifact, -} from '~/concepts/modelRegistry/types'; - -const MODEL_REGISTRY_API_VERSION = 'v1alpha3'; - -const initIntercepts = () => { - cy.interceptOdh( - 'GET /api/config', - mockDashboardConfig({ - disableModelRegistry: false, - }), - ); - cy.interceptOdh( - 'GET /api/dsc/status', - mockDscStatus({ - installedComponents: { - [StackComponent.MODEL_REGISTRY]: true, - [StackComponent.MODEL_MESH]: true, - }, - }), - ); - cy.interceptOdh( - 'GET /api/dsci/status', - mockDsciStatus({ - requiredCapabilities: [StackCapability.SERVICE_MESH, StackCapability.SERVICE_MESH_AUTHZ], - }), - ); - - // TODO replace these with a mock list of services when https://github.com/opendatahub-io/odh-dashboard/pull/3034 is merged - cy.interceptK8sList( - ModelRegistryModel, - mockK8sResourceList([mockModelRegistry({ name: 'modelregistry-sample' })]), - ); - cy.interceptK8s(ModelRegistryModel, mockModelRegistry({ name: 'modelregistry-sample' })); - - cy.interceptOdh( - 'POST /api/service/modelregistry/:serviceName/api/model_registry/:apiVersion/registered_models', - { - path: { - serviceName: 'modelregistry-sample', - apiVersion: MODEL_REGISTRY_API_VERSION, - }, - }, - mockRegisteredModel({ id: '1', name: 'Test model name' }), - ).as('createRegisteredModel'); - - cy.interceptOdh( - 'POST /api/service/modelregistry/:serviceName/api/model_registry/:apiVersion/registered_models/:registeredModelId/versions', - { - path: { - serviceName: 'modelregistry-sample', - apiVersion: MODEL_REGISTRY_API_VERSION, - registeredModelId: 1, - }, - }, - mockModelVersion({ id: '2', name: 'Test version name' }), - ).as('createModelVersion'); - - cy.interceptOdh( - 'POST /api/service/modelregistry/:serviceName/api/model_registry/:apiVersion/model_versions/:modelVersionId/artifacts', - { - path: { - serviceName: 'modelregistry-sample', - apiVersion: MODEL_REGISTRY_API_VERSION, - modelVersionId: 2, - }, - }, - mockModelArtifact(), - ).as('createModelArtifact'); -}; - -describe('Register model page', () => { - beforeEach(() => { - initIntercepts(); - registerModelPage.visit(); - }); - - it('Disables submit until required fields are filled in object storage mode', () => { - registerModelPage.findSubmitButton().should('be.disabled'); - registerModelPage.findFormField(FormFieldSelector.MODEL_NAME).type('Test model name'); - registerModelPage.findFormField(FormFieldSelector.VERSION_NAME).type('Test version name'); - registerModelPage.findFormField(FormFieldSelector.LOCATION_TYPE_OBJECT_STORAGE).click(); - registerModelPage - .findFormField(FormFieldSelector.LOCATION_ENDPOINT) - .type('http://s3.amazonaws.com/'); - registerModelPage.findFormField(FormFieldSelector.LOCATION_BUCKET).type('test-bucket'); - registerModelPage - .findFormField(FormFieldSelector.LOCATION_PATH) - .type('demo-models/flan-t5-small-caikit'); - registerModelPage.findSubmitButton().should('be.enabled'); - }); - - it('Creates expected resources on submit in object storage mode', () => { - registerModelPage.findFormField(FormFieldSelector.MODEL_NAME).type('Test model name'); - registerModelPage - .findFormField(FormFieldSelector.MODEL_DESCRIPTION) - .type('Test model description'); - registerModelPage.findFormField(FormFieldSelector.VERSION_NAME).type('Test version name'); - registerModelPage - .findFormField(FormFieldSelector.VERSION_DESCRIPTION) - .type('Test version description'); - registerModelPage.findFormField(FormFieldSelector.SOURCE_MODEL_FORMAT).type('caikit'); - registerModelPage.findFormField(FormFieldSelector.SOURCE_MODEL_FORMAT_VERSION).type('1'); - registerModelPage.findFormField(FormFieldSelector.LOCATION_TYPE_OBJECT_STORAGE).click(); - registerModelPage - .findFormField(FormFieldSelector.LOCATION_ENDPOINT) - .type('http://s3.amazonaws.com/'); - registerModelPage.findFormField(FormFieldSelector.LOCATION_BUCKET).type('test-bucket'); - registerModelPage.findFormField(FormFieldSelector.LOCATION_REGION).type('us-east-1'); - registerModelPage - .findFormField(FormFieldSelector.LOCATION_PATH) - .type('demo-models/flan-t5-small-caikit'); - - registerModelPage.findSubmitButton().click(); - - cy.wait('@createRegisteredModel').then((interception) => { - expect(interception.request.body).to.containSubset({ - name: 'Test model name', - description: 'Test model description', - customProperties: {}, - state: ModelState.LIVE, - } satisfies Partial); - }); - cy.wait('@createModelVersion').then((interception) => { - expect(interception.request.body).to.containSubset({ - name: 'Test version name', - description: 'Test version description', - customProperties: {}, - state: ModelState.LIVE, - author: 'test-user', - registeredModelId: '1', - } satisfies Partial); - }); - cy.wait('@createModelArtifact').then((interception) => { - expect(interception.request.body).to.containSubset({ - name: 'Test model name-Test version name-artifact', - description: 'Test version description', - customProperties: {}, - state: ModelArtifactState.LIVE, - author: 'test-user', - modelFormatName: 'caikit', - modelFormatVersion: '1', - uri: 's3://test-bucket/demo-models/flan-t5-small-caikit?endpoint=http%3A%2F%2Fs3.amazonaws.com%2F&defaultRegion=us-east-1', - artifactType: 'model-artifact', - } satisfies Partial); - }); - - cy.url().should('include', '/modelRegistry/modelregistry-sample/registeredModels/1'); - }); - - it('Disables submit until required fields are filled in URI mode', () => { - registerModelPage.findSubmitButton().should('be.disabled'); - registerModelPage.findFormField(FormFieldSelector.MODEL_NAME).type('Test model name'); - registerModelPage.findFormField(FormFieldSelector.VERSION_NAME).type('Test version name'); - registerModelPage.findFormField(FormFieldSelector.LOCATION_TYPE_URI).click(); - registerModelPage - .findFormField(FormFieldSelector.LOCATION_URI) - .type( - 's3://test-bucket/demo-models/flan-t5-small-caikit?endpoint=http%3A%2F%2Fs3.amazonaws.com%2F&defaultRegion=us-east-1', - ); - registerModelPage.findSubmitButton().should('be.enabled'); - }); - - it('Creates expected resources on submit in URI mode', () => { - registerModelPage.findFormField(FormFieldSelector.MODEL_NAME).type('Test model name'); - registerModelPage - .findFormField(FormFieldSelector.MODEL_DESCRIPTION) - .type('Test model description'); - registerModelPage.findFormField(FormFieldSelector.VERSION_NAME).type('Test version name'); - registerModelPage - .findFormField(FormFieldSelector.VERSION_DESCRIPTION) - .type('Test version description'); - registerModelPage.findFormField(FormFieldSelector.SOURCE_MODEL_FORMAT).type('caikit'); - registerModelPage.findFormField(FormFieldSelector.SOURCE_MODEL_FORMAT_VERSION).type('1'); - registerModelPage.findFormField(FormFieldSelector.LOCATION_TYPE_URI).click(); - registerModelPage - .findFormField(FormFieldSelector.LOCATION_URI) - .type( - 's3://test-bucket/demo-models/flan-t5-small-caikit?endpoint=http%3A%2F%2Fs3.amazonaws.com%2F&defaultRegion=us-east-1', - ); - - registerModelPage.findSubmitButton().click(); - - cy.wait('@createRegisteredModel').then((interception) => { - expect(interception.request.body).to.containSubset({ - name: 'Test model name', - description: 'Test model description', - customProperties: {}, - state: ModelState.LIVE, - } satisfies Partial); - }); - cy.wait('@createModelVersion').then((interception) => { - expect(interception.request.body).to.containSubset({ - name: 'Test version name', - description: 'Test version description', - customProperties: {}, - state: ModelState.LIVE, - author: 'test-user', - registeredModelId: '1', - } satisfies Partial); - }); - cy.wait('@createModelArtifact').then((interception) => { - expect(interception.request.body).to.containSubset({ - name: 'Test model name-Test version name-artifact', - description: 'Test version description', - customProperties: {}, - state: ModelArtifactState.LIVE, - author: 'test-user', - modelFormatName: 'caikit', - modelFormatVersion: '1', - uri: 's3://test-bucket/demo-models/flan-t5-small-caikit?endpoint=http%3A%2F%2Fs3.amazonaws.com%2F&defaultRegion=us-east-1', - artifactType: 'model-artifact', - } satisfies Partial); - }); - - cy.url().should('include', '/modelRegistry/modelregistry-sample/registeredModels/1'); - }); -}); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelineCreateRuns.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelineCreateRuns.cy.ts index 106b8c804d..d00bce64a9 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelineCreateRuns.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelineCreateRuns.cy.ts @@ -549,47 +549,56 @@ describe('Pipeline create runs', () => { }); it('creates a schedule', () => { - createScheduleRunCommonTest(); - createSchedulePage.findExperimentSelect().should('contain.text', 'Default'); - createSchedulePage.findExperimentSelect().should('not.be.disabled').click(); - createSchedulePage.selectExperimentByName('Test experiment 1'); - createSchedulePage - .mockCreateRecurringRun(projectName, mockPipelineVersion, createRecurringRunParams) - .as('createSchedule'); - createSchedulePage.submit(); + visitLegacyRunsPage(); + pipelineRunsGlobal.findSchedulesTab().click(); - cy.wait('@createSchedule').then((interception) => { - expect(interception.request.body).to.eql({ - display_name: 'New recurring run', - description: 'New recurring run description', - pipeline_version_reference: { - pipeline_id: 'test-pipeline', - pipeline_version_id: 'test-pipeline-version', - }, - runtime_config: { - parameters: { min_max_scaler: false, neighbors: 1, standard_scaler: 'no' }, + const createRecurringRunParams: Partial = { + display_name: 'New recurring run', + description: 'New recurring run description', + recurring_run_id: 'new-recurring-run-id', + runtime_config: { + parameters: { + min_max_scaler: false, + neighbors: 1, + standard_scaler: 'no', }, - trigger: { periodic_schedule: { interval_second: '604800' } }, - max_concurrency: '10', - mode: 'ENABLE', - no_catchup: false, - service_account: '', - experiment_id: 'experiment-1', - }); - }); + }, + }; - // Navigate to the 'Create run' page + // Mock experiments, pipelines & versions for form select dropdowns + createSchedulePage.mockGetExperiments(projectName, mockExperiments); + createSchedulePage.mockGetExperiments(projectName, mockExperiments); + createSchedulePage.mockGetPipelines(projectName, [mockPipeline]); + createSchedulePage.mockGetPipelineVersions( + projectName, + [mockPipelineVersion], + mockPipelineVersion.pipeline_id, + ); + // Navigate to the 'Create run' page + pipelineRunsGlobal.findScheduleRunButton().click(); verifyRelativeURL( - `/pipelines/${projectName}/${mockPipelineVersion.pipeline_id}/${mockPipelineVersion.pipeline_version_id}/schedules/${createRecurringRunParams.recurring_run_id}`, + `/pipelines/${projectName}/${mockPipelineVersion.pipeline_id}/${mockPipelineVersion.pipeline_version_id}/schedules/create`, ); - }); + createSchedulePage.find(); - it('creates a schedule with trigger type cron without whitespace', () => { // Fill out the form with a schedule and submit - createScheduleRunCommonTest(); - createSchedulePage.findScheduledRunTypeSelector().findDropdownItem('Cron').click(); - createSchedulePage.findScheduledRunCron().fill('@every 5m'); + createRunPage.fillName(initialMockRecurringRuns[0].display_name); + cy.findByTestId('duplicate-name-help-text').should('be.visible'); + createSchedulePage.fillName('New recurring run'); + createSchedulePage.fillDescription('New recurring run description'); + createSchedulePage.findExperimentSelect().should('contain.text', 'Default'); + createSchedulePage.findExperimentSelect().should('not.be.disabled').click(); + createSchedulePage.selectExperimentByName('Test experiment 1'); + createSchedulePage.findPipelineSelect().should('not.be.disabled').click(); + createSchedulePage.selectPipelineByName('Test pipeline'); + createSchedulePage.findPipelineVersionSelect().should('not.be.disabled'); + + const parameters = createRecurringRunParams.runtime_config?.parameters || {}; + const paramsSection = createRunPage.getParamsSection(); + paramsSection.findParamById('radio-min_max_scaler-false').click(); + paramsSection.fillParamInputById('neighbors', String(parameters.neighbors)); + paramsSection.fillParamInputById('standard_scaler', String(parameters.standard_scaler)); createSchedulePage .mockCreateRecurringRun(projectName, mockPipelineVersion, createRecurringRunParams) .as('createSchedule'); @@ -606,12 +615,12 @@ describe('Pipeline create runs', () => { runtime_config: { parameters: { min_max_scaler: false, neighbors: 1, standard_scaler: 'no' }, }, - trigger: { cron_schedule: { cron: '@every 5m' } }, + trigger: { periodic_schedule: { interval_second: '604800' } }, max_concurrency: '10', - mode: 'DISABLE', + mode: 'ENABLE', no_catchup: false, service_account: '', - experiment_id: 'default', + experiment_id: 'experiment-1', }); }); @@ -621,36 +630,6 @@ describe('Pipeline create runs', () => { ); }); - it('creates a schedule with trigger type cron with whitespace', () => { - createScheduleRunCommonTest(); - createSchedulePage.findScheduledRunTypeSelector().findDropdownItem('Cron').click(); - createSchedulePage.findScheduledRunCron().fill('@every 5m '); - createSchedulePage - .mockCreateRecurringRun(projectName, mockPipelineVersion, createRecurringRunParams) - .as('createSchedule'); - createSchedulePage.submit(); - - cy.wait('@createSchedule').then((interception) => { - expect(interception.request.body).to.eql({ - display_name: 'New recurring run', - description: 'New recurring run description', - pipeline_version_reference: { - pipeline_id: 'test-pipeline', - pipeline_version_id: 'test-pipeline-version', - }, - runtime_config: { - parameters: { min_max_scaler: false, neighbors: 1, standard_scaler: 'no' }, - }, - trigger: { cron_schedule: { cron: '@every 5m' } }, - max_concurrency: '10', - mode: 'DISABLE', - no_catchup: false, - service_account: '', - experiment_id: 'default', - }); - }); - }); - it('duplicates a schedule', () => { const [mockRecurringRun] = initialMockRecurringRuns; const mockExperiment = mockExperiments[0]; @@ -878,48 +857,3 @@ const initIntercepts = () => { ); }); }; - -const createRecurringRunParams: Partial = { - display_name: 'New recurring run', - description: 'New recurring run description', - recurring_run_id: 'new-recurring-run-id', - runtime_config: { - parameters: { - min_max_scaler: false, - neighbors: 1, - standard_scaler: 'no', - }, - }, -}; - -const createScheduleRunCommonTest = () => { - visitLegacyRunsPage(); - pipelineRunsGlobal.findSchedulesTab().click(); - // Mock experiments, pipelines & versions for form select dropdowns - createSchedulePage.mockGetExperiments(projectName, mockExperiments); - createSchedulePage.mockGetPipelines(projectName, [mockPipeline]); - createSchedulePage.mockGetPipelineVersions( - projectName, - [mockPipelineVersion], - mockPipelineVersion.pipeline_id, - ); - - // Navigate to the 'Create run' page - pipelineRunsGlobal.findScheduleRunButton().click(); - verifyRelativeURL( - `/pipelines/${projectName}/${mockPipelineVersion.pipeline_id}/${mockPipelineVersion.pipeline_version_id}/schedules/create`, - ); - createSchedulePage.find(); - createRunPage.fillName(initialMockRecurringRuns[0].display_name); - cy.findByTestId('duplicate-name-help-text').should('be.visible'); - createSchedulePage.fillName('New recurring run'); - createSchedulePage.fillDescription('New recurring run description'); - createSchedulePage.findPipelineSelect().should('not.be.disabled').click(); - createSchedulePage.selectPipelineByName('Test pipeline'); - createSchedulePage.findPipelineVersionSelect().should('not.be.disabled'); - const parameters = createRecurringRunParams.runtime_config?.parameters || {}; - const paramsSection = createRunPage.getParamsSection(); - paramsSection.findParamById('radio-min_max_scaler-false').click(); - paramsSection.fillParamInputById('neighbors', String(parameters.neighbors)); - paramsSection.fillParamInputById('standard_scaler', String(parameters.standard_scaler)); -}; diff --git a/frontend/src/api/modelRegistry/__tests__/custom.spec.ts b/frontend/src/api/modelRegistry/__tests__/custom.spec.ts index 200a6f1ba5..5efcfcce41 100644 --- a/frontend/src/api/modelRegistry/__tests__/custom.spec.ts +++ b/frontend/src/api/modelRegistry/__tests__/custom.spec.ts @@ -16,8 +16,6 @@ import { patchModelVersion, patchRegisteredModel, getModelArtifactsByModelVersion, - createModelVersionForRegisteredModel, - createModelArtifactForModelVersion, } from '~/api/modelRegistry/custom'; import { MODEL_REGISTRY_API_VERSION } from '~/concepts/modelRegistry/const'; @@ -106,40 +104,6 @@ describe('createModelVersion', () => { }); }); -describe('createModelVersionForRegisteredModel', () => { - it('should call proxyCREATE and handleModelRegistryFailures to create model version for a model', () => { - expect( - createModelVersionForRegisteredModel('hostPath')(K8sAPIOptionsMock, '1', { - description: 'test', - externalID: '1', - author: 'test author', - registeredModelId: '1', - name: 'test new model version', - state: ModelState.LIVE, - customProperties: {}, - }), - ).toBe(mockResultPromise); - expect(proxyCREATEMock).toHaveBeenCalledTimes(1); - expect(proxyCREATEMock).toHaveBeenCalledWith( - 'hostPath', - `/api/model_registry/${MODEL_REGISTRY_API_VERSION}/registered_models/1/versions`, - { - description: 'test', - externalID: '1', - author: 'test author', - registeredModelId: '1', - name: 'test new model version', - state: ModelState.LIVE, - customProperties: {}, - }, - {}, - K8sAPIOptionsMock, - ); - expect(handleModelRegistryFailuresMock).toHaveBeenCalledTimes(1); - expect(handleModelRegistryFailuresMock).toHaveBeenCalledWith(mockProxyPromise); - }); -}); - describe('createModelArtifact', () => { it('should call proxyCREATE and handleModelRegistryFailures to create model artifact', () => { expect( @@ -155,7 +119,6 @@ describe('createModelArtifact', () => { modelFormatVersion: 'testmodelFormatVersion', serviceAccountName: 'testserviceAccountname', customProperties: {}, - artifactType: 'model-artifact', }), ).toBe(mockResultPromise); expect(proxyCREATEMock).toHaveBeenCalledTimes(1); @@ -174,51 +137,6 @@ describe('createModelArtifact', () => { modelFormatVersion: 'testmodelFormatVersion', serviceAccountName: 'testserviceAccountname', customProperties: {}, - artifactType: 'model-artifact', - }, - {}, - K8sAPIOptionsMock, - ); - expect(handleModelRegistryFailuresMock).toHaveBeenCalledTimes(1); - expect(handleModelRegistryFailuresMock).toHaveBeenCalledWith(mockProxyPromise); - }); -}); - -describe('createModelArtifactForModelVersion', () => { - it('should call proxyCREATE and handleModelRegistryFailures to create model artifact for version', () => { - expect( - createModelArtifactForModelVersion('hostPath')(K8sAPIOptionsMock, '2', { - description: 'test', - externalID: 'test', - uri: 'test-uri', - state: ModelArtifactState.LIVE, - name: 'test-name', - modelFormatName: 'test-modelformatname', - storageKey: 'teststoragekey', - storagePath: 'teststoragePath', - modelFormatVersion: 'testmodelFormatVersion', - serviceAccountName: 'testserviceAccountname', - customProperties: {}, - artifactType: 'model-artifact', - }), - ).toBe(mockResultPromise); - expect(proxyCREATEMock).toHaveBeenCalledTimes(1); - expect(proxyCREATEMock).toHaveBeenCalledWith( - 'hostPath', - `/api/model_registry/${MODEL_REGISTRY_API_VERSION}/model_versions/2/artifacts`, - { - description: 'test', - externalID: 'test', - uri: 'test-uri', - state: ModelArtifactState.LIVE, - name: 'test-name', - modelFormatName: 'test-modelformatname', - storageKey: 'teststoragekey', - storagePath: 'teststoragePath', - modelFormatVersion: 'testmodelFormatVersion', - serviceAccountName: 'testserviceAccountname', - customProperties: {}, - artifactType: 'model-artifact', }, {}, K8sAPIOptionsMock, diff --git a/frontend/src/api/modelRegistry/custom.ts b/frontend/src/api/modelRegistry/custom.ts index 3604ed6357..8282cc51ff 100644 --- a/frontend/src/api/modelRegistry/custom.ts +++ b/frontend/src/api/modelRegistry/custom.ts @@ -39,22 +39,6 @@ export const createModelVersion = opts, ), ); -export const createModelVersionForRegisteredModel = - (hostpath: string) => - ( - opts: K8sAPIOptions, - registeredModelId: string, - data: CreateModelVersionData, - ): Promise => - handleModelRegistryFailures( - proxyCREATE( - hostpath, - `/api/model_registry/${MODEL_REGISTRY_API_VERSION}/registered_models/${registeredModelId}/versions`, - data, - {}, - opts, - ), - ); export const createModelArtifact = (hostPath: string) => @@ -69,23 +53,6 @@ export const createModelArtifact = ), ); -export const createModelArtifactForModelVersion = - (hostPath: string) => - ( - opts: K8sAPIOptions, - modelVersionId: string, - data: CreateModelArtifactData, - ): Promise => - handleModelRegistryFailures( - proxyCREATE( - hostPath, - `/api/model_registry/${MODEL_REGISTRY_API_VERSION}/model_versions/${modelVersionId}/artifacts`, - data, - {}, - opts, - ), - ); - export const getRegisteredModel = (hostPath: string) => (opts: K8sAPIOptions, registeredModelId: string): Promise => diff --git a/frontend/src/app/AppRoutes.tsx b/frontend/src/app/AppRoutes.tsx index cc6ee4516d..bd227c0b21 100644 --- a/frontend/src/app/AppRoutes.tsx +++ b/frontend/src/app/AppRoutes.tsx @@ -14,6 +14,7 @@ import { useCheckJupyterEnabled } from '~/utilities/notebookControllerUtils'; import { SupportedArea } from '~/concepts/areas'; import useIsAreaAvailable from '~/concepts/areas/useIsAreaAvailable'; import ModelRegistrySettingsRoutes from '~/pages/modelRegistrySettings/ModelRegistrySettingsRoutes'; +import ConnectionTypeRoutes from '~/pages/connectionTypes/ConnectionTypeRoutes'; const HomePage = React.lazy(() => import('../pages/home/Home')); @@ -125,6 +126,7 @@ const AppRoutes: React.FC = () => { } /> } /> } /> + } /> )} diff --git a/frontend/src/concepts/connectionTypes/useConnectionType.ts b/frontend/src/concepts/connectionTypes/useConnectionType.ts new file mode 100644 index 0000000000..49462210e6 --- /dev/null +++ b/frontend/src/concepts/connectionTypes/useConnectionType.ts @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { ConnectionTypeConfigMapObj } from '~/concepts/connectionTypes/types'; +import { fetchConnectionType } from '~/services/connectionTypesService'; + +export const useConnectionType = ( + name?: string, +): [boolean, Error | undefined, ConnectionTypeConfigMapObj | undefined] => { + const [loaded, setLoaded] = React.useState(false); + const [error, setError] = React.useState(); + const [connectionType, setConnectionType] = React.useState(); + + React.useEffect(() => { + if (name) { + fetchConnectionType(name) + .then((res) => { + setLoaded(true); + setConnectionType(res); + }) + .catch((err) => { + setLoaded(true); + setError(err); + }); + } + }, [name]); + + return [loaded, error, connectionType]; +}; diff --git a/frontend/src/concepts/k8s/NameDescriptionField.tsx b/frontend/src/concepts/k8s/NameDescriptionField.tsx index 3514e74d56..f294edbdcc 100644 --- a/frontend/src/concepts/k8s/NameDescriptionField.tsx +++ b/frontend/src/concepts/k8s/NameDescriptionField.tsx @@ -16,10 +16,13 @@ import { isValidK8sName, translateDisplayNameForK8s } from '~/concepts/k8s/utils type NameDescriptionFieldProps = { nameFieldId: string; + nameFieldLabel?: string; descriptionFieldId: string; + descriptionFieldLabel?: string; data: NameDescType; setData: (data: NameDescType) => void; autoFocusName?: boolean; + K8sLabelName?: string; showK8sName?: boolean; disableK8sName?: boolean; maxLength?: number; @@ -29,10 +32,13 @@ type NameDescriptionFieldProps = { const NameDescriptionField: React.FC = ({ nameFieldId, + nameFieldLabel = 'Name', descriptionFieldId, + descriptionFieldLabel = 'Description', data, setData, autoFocusName, + K8sLabelName = 'Resource name', showK8sName, disableK8sName, maxLength, @@ -58,7 +64,7 @@ const NameDescriptionField: React.FC = ({ return ( - + = ({ {showK8sName && ( = ({ )} - +