Skip to content

Commit 7e4df91

Browse files
authored
Deploy from catalog using wizard e2e test (opendatahub-io#5497)
* Deploy from catalog using wizard e2e test * add uri to button state * gating navigation initialization * fixing nav bug * test working * pr comments * pr comments pt 2 * add TODO comment
1 parent 07f1c7a commit 7e4df91

8 files changed

Lines changed: 196 additions & 9 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
projectResourceName: "test-model-catalog"
2+
singleModelName: "catalog-model-test"

frontend/src/__tests__/cypress/cypress/pages/modelCatalog/modelCatalog.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,37 @@ class ModelCatalog {
5454
.contains('[data-testid~=model-catalog-card]', modelName);
5555
}
5656

57+
findFirstModelCatalogCard() {
58+
return cy.findAllByTestId('model-catalog-card').first();
59+
}
60+
61+
findFirstModelCatalogCardLink() {
62+
return this.findFirstModelCatalogCard().findByTestId('model-catalog-detail-link');
63+
}
64+
65+
findCatalogDeployButton() {
66+
return cy.findByTestId('deploy-button');
67+
}
68+
69+
clickDeployModelButtonWithRetry() {
70+
const maxRetries = 3;
71+
let attempt = 0;
72+
const tryClick = () => {
73+
attempt++;
74+
cy.log(`Click attempt #${attempt}`);
75+
this.findCatalogDeployButton().click();
76+
77+
cy.location('pathname').then((path) => {
78+
if (!path.includes('/ai-hub/deployments/deploy') && attempt < maxRetries) {
79+
cy.log('Wizard did not open, retrying...');
80+
tryClick();
81+
}
82+
});
83+
};
84+
tryClick();
85+
return this;
86+
}
87+
5788
expandCardLabelGroup(modelName: string) {
5889
this.findModelCatalogCard(modelName)
5990
.findAllByTestId('model-catalog-label-group')

frontend/src/__tests__/cypress/cypress/pages/modelCatalog/modelDetailsPage.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ class ModelDetailsPage {
4747
return cy.findByTestId('source-image-location');
4848
}
4949

50+
getModelSourceImageLocation() {
51+
return cy.get('@modelSourceImageLocation');
52+
}
53+
5054
findModelCardMarkdown() {
5155
return cy.findByTestId('model-card-markdown');
5256
}

frontend/src/__tests__/cypress/cypress/pages/modelServing.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,20 @@ class ModelServingWizard extends Wizard {
959959
return cy.findByTestId('serving-runtime-template-selection-toggle');
960960
}
961961

962+
findServingRuntimeSelectRadio() {
963+
return cy.findByLabelText('Select from a list of serving runtimes, including custom ones');
964+
}
965+
966+
findFirstServingRuntimeTemplateOption() {
967+
// Open the menu
968+
this.findServingRuntimeTemplateSearchSelector().click();
969+
// Grab the first runtime whose testid starts with "servingRuntime"
970+
return cy
971+
.findByTestId('global-scoped-serving-runtimes')
972+
.find('[data-testid^="servingRuntime"]')
973+
.first();
974+
}
975+
962976
findServingRuntimeTemplateSearchInput() {
963977
return cy.findByTestId('serving-runtime-template-selection-search').find('input');
964978
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {
2+
modelServingGlobal,
3+
modelServingWizard,
4+
} from '#~/__tests__/cypress/cypress/pages/modelServing';
5+
import { modelDetailsPage } from '#~/__tests__/cypress/cypress/pages/modelCatalog/modelDetailsPage';
6+
import type { DataScienceProjectData } from '#~/__tests__/cypress/cypress/types';
7+
import { retryableBefore } from '#~/__tests__/cypress/cypress/utils/retryableHooks';
8+
import { loadDSPFixture } from '#~/__tests__/cypress/cypress/utils/dataLoader';
9+
import { generateTestUUID } from '#~/__tests__/cypress/cypress/utils/uuidGenerator';
10+
import { deleteOpenShiftProject } from '#~/__tests__/cypress/cypress/utils/oc_commands/project';
11+
import { provisionProjectForModelServing } from '#~/__tests__/cypress/cypress/utils/oc_commands/modelServing';
12+
import { HTPASSWD_CLUSTER_ADMIN_USER } from '#~/__tests__/cypress/cypress/utils/e2eUsers';
13+
import { modelCatalog } from '#~/__tests__/cypress/cypress/pages/modelCatalog/modelCatalog';
14+
15+
let testData: DataScienceProjectData;
16+
let projectName: string;
17+
let modelName: string;
18+
const awsBucket = 'BUCKET_1' as const;
19+
const uuid = generateTestUUID();
20+
21+
// TODO: Update this to check for model readiness once vLLM CPU works: https://issues.redhat.com/browse/RHAIRFE-28
22+
// and make it RHOAI-specific, unless the vLLM CPU image works on ODH at that time
23+
describe('Verify a model can be deployed from model catalog', () => {
24+
retryableBefore(() =>
25+
// Setup: Load test data and ensure clean state
26+
loadDSPFixture('e2e/modelCatalog/testModelCatalog.yaml').then(
27+
(fixtureData: DataScienceProjectData) => {
28+
testData = fixtureData;
29+
projectName = `${testData.projectResourceName}-${uuid}`;
30+
modelName = testData.singleModelName;
31+
32+
if (!projectName) {
33+
throw new Error('Project name is undefined or empty in the loaded fixture');
34+
}
35+
cy.log(`Loaded project name: ${projectName}`);
36+
// Create a Project for pipelines
37+
provisionProjectForModelServing(
38+
projectName,
39+
awsBucket,
40+
'resources/yaml/data_connection_model_serving.yaml',
41+
);
42+
},
43+
),
44+
);
45+
after(() => {
46+
deleteOpenShiftProject(projectName, { wait: false, ignoreNotFound: true, timeout: 300000 });
47+
});
48+
it(
49+
'Verify a model can be deployed from model catalog',
50+
{ tags: ['@Dashboard', '@ModelServing', '@Smoke', '@SmokeSet3'] },
51+
() => {
52+
cy.visitWithLogin('/', HTPASSWD_CLUSTER_ADMIN_USER);
53+
// Enable model catalog
54+
cy.window().then((win) => {
55+
win.sessionStorage.setItem('odh-feature-flags', '{"disableModelCatalog":false}');
56+
});
57+
cy.reload();
58+
59+
cy.step('Navigate to Model Catalog');
60+
modelCatalog.visit();
61+
62+
// Find the first model card and click on the detail link
63+
modelCatalog.findFirstModelCatalogCard().should('exist');
64+
modelCatalog.findFirstModelCatalogCardLink().should('exist').click();
65+
modelDetailsPage.findModelSourceImageLocation().should('exist');
66+
modelDetailsPage
67+
.findModelSourceImageLocation()
68+
.invoke('text')
69+
.then((text) => {
70+
cy.wrap(text.trim()).as('modelSourceImageLocation');
71+
});
72+
73+
modelCatalog.clickDeployModelButtonWithRetry();
74+
75+
cy.step('Verify model location gets prefilled');
76+
modelServingWizard.findModelSourceStep().click();
77+
modelServingWizard.findModelLocationSelect().should('contain.text', 'URI');
78+
modelDetailsPage.getModelSourceImageLocation().then((modelSourceImageLocation) => {
79+
modelServingWizard.findUrilocationInput().should('have.value', modelSourceImageLocation);
80+
});
81+
modelServingWizard.findNextButton().should('be.enabled').click();
82+
83+
cy.step('Model deployment step');
84+
modelServingWizard.findModelDeploymentNameInput().clear().type(modelName);
85+
modelServingWizard.findModelDeploymentProjectSelector().should('exist');
86+
modelServingWizard.findModelDeploymentProjectSelector().click();
87+
modelServingWizard
88+
.findModelDeploymentProjectSelectorOption(projectName)
89+
.should('exist')
90+
.click();
91+
92+
modelServingWizard.findServingRuntimeSelectRadio().click();
93+
modelServingWizard.findFirstServingRuntimeTemplateOption().should('exist').click();
94+
95+
cy.step('Advanced options step');
96+
modelServingWizard.findNextButton().should('be.enabled').click();
97+
modelServingWizard.findTokenAuthenticationCheckbox().should('be.enabled');
98+
modelServingWizard.findTokenAuthenticationCheckbox().click();
99+
modelServingWizard.findNextButton().should('be.enabled').click();
100+
101+
cy.step('Summary step');
102+
modelServingWizard.findSubmitButton().should('be.enabled').click();
103+
104+
cy.step('Verify redirection to the global page');
105+
cy.location('pathname').should('eq', `/ai-hub/deployments/${projectName}`);
106+
modelServingGlobal.getInferenceServiceRow(modelName);
107+
},
108+
);
109+
});

packages/model-registry/upstream/frontend/src/odh/components/ModelCatalogDeployWrapper.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,22 @@ const ModelCatalogDeployWrapper: React.FC<ModelCatalogDeployWrapperProps> = ({ m
6060
return navigateExtensionsLoaded && navigateExtensions.length > 0;
6161
}, [navigateExtensions, navigateExtensionsLoaded]);
6262

63-
const buttonState =
63+
const canInitializeWizardNavigation =
6464
navigateExtensionsLoaded &&
6565
navigateExtensions.length > 0 &&
6666
artifactsLoaded &&
67-
!artifactsLoadError
67+
!artifactsLoadError &&
68+
!!uri;
69+
70+
const buttonState =
71+
canInitializeWizardNavigation && navigateToWizard !== null
6872
? { enabled: true }
6973
: { enabled: false, tooltip: 'Deployment wizard is not available' };
7074

7175
return (
7276
<>
73-
{/* Get navigation function */}
74-
{navigateExtensionsLoaded &&
75-
navigateExtensions.length > 0 &&
77+
{/* Get navigation function only when we have all the prefill data */}
78+
{canInitializeWizardNavigation &&
7679
navigateExtensions.map((extension) => {
7780
if (!extension.properties.useNavigateToDeploymentWizardWithData) {
7881
return null;

packages/model-serving/modelRegistry/useNavigateToDeploymentWizardWithData.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const useNavigateToDeploymentWizardWithData = (
1515
): ((projectName?: string) => void) => {
1616
const resourceName = translateDisplayNameForK8s(deployPrefillData.modelName);
1717

18-
const [connectionTypes] = useWatchConnectionTypes(true);
18+
const [connectionTypes, connectionTypesLoaded] = useWatchConnectionTypes(true);
1919
const uri = deployPrefillData.modelUri;
2020
let connectionTypeName = ConnectionTypeRefs.URI;
2121

@@ -64,11 +64,21 @@ export const useNavigateToDeploymentWizardWithData = (
6464
}),
6565
[deployPrefillData, connectionTypeObject, resourceName],
6666
);
67-
const navigationFunction = useNavigateToDeploymentWizard(
67+
const navigateToWizardInner = useNavigateToDeploymentWizard(
6868
null,
6969
prefillInfo,
7070
deployPrefillData.returnRouteValue,
7171
deployPrefillData.cancelReturnRouteValue,
7272
);
73-
return navigationFunction;
73+
74+
return React.useCallback(
75+
(projectName?: string) => {
76+
if (!uri || !connectionTypesLoaded || !connectionTypeObject) {
77+
// If we don't have all the prefill data, don't navigate to the wizard
78+
return;
79+
}
80+
navigateToWizardInner(projectName);
81+
},
82+
[uri, connectionTypesLoaded, connectionTypeObject, navigateToWizardInner],
83+
);
7484
};

packages/model-serving/src/components/deploymentWizard/fields/ProjectSection.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22
import { FormGroup, Content, Popover, Button } from '@patternfly/react-core';
33
import { HelpIcon } from '@patternfly/react-icons';
44
import ProjectSelector from '@odh-dashboard/internal/concepts/projects/ProjectSelector';
5+
import { ProjectsContext, byName } from '@odh-dashboard/internal/concepts/projects/ProjectsContext';
56

67
type ProjectSectionType = {
78
initialProjectName?: string;
@@ -14,7 +15,20 @@ export const isValidProjectName = (projectName?: string): boolean => {
1415
};
1516

1617
export const useProjectSection = (initialProjectName?: string): ProjectSectionType => {
17-
const [projectName, setProjectName] = React.useState<string | undefined>(initialProjectName);
18+
const [projectName, setProjectNameState] = React.useState<string | undefined>(initialProjectName);
19+
const { projects, updatePreferredProject } = React.useContext(ProjectsContext);
20+
21+
const setProjectName = React.useCallback(
22+
(newProjectName?: string) => {
23+
setProjectNameState(newProjectName);
24+
25+
const project = projects.find(byName(newProjectName));
26+
if (project) {
27+
updatePreferredProject(project);
28+
}
29+
},
30+
[projects, updatePreferredProject],
31+
);
1832
return { initialProjectName, projectName, setProjectName };
1933
};
2034

0 commit comments

Comments
 (0)