Skip to content

Commit 674cb84

Browse files
authored
Add namespace selector to register form (kubeflow#2165)
* Add namespace selector to register form Signed-off-by: ppadti <ppadti@redhat.com> * Fix the width of selector in navbar Signed-off-by: ppadti <ppadti@redhat.com> * Fix the page object Signed-off-by: ppadti <ppadti@redhat.com> * Fix naming conventions and props Signed-off-by: ppadti <ppadti@redhat.com> --------- Signed-off-by: ppadti <ppadti@redhat.com>
1 parent 599ced0 commit 674cb84

12 files changed

Lines changed: 471 additions & 62 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { TempDevFeature } from '~/app/hooks/useTempDevFeatureAvailable';
2+
3+
class RegisterAndStoreFields {
4+
visit(enableRegistryStorageFeature = true) {
5+
if (enableRegistryStorageFeature) {
6+
window.localStorage.setItem(TempDevFeature.RegistryStorage, 'true');
7+
}
8+
const preferredModelRegistry = 'modelregistry-sample';
9+
cy.visit(`/model-registry/${preferredModelRegistry}/register/model`);
10+
}
11+
12+
findNamespaceFormGroup() {
13+
return cy.findByTestId('namespace-form-group');
14+
}
15+
16+
findNamespaceSelector() {
17+
return cy.findByTestId('form-namespace-selector');
18+
}
19+
20+
findOriginLocationSection() {
21+
return cy.findByTestId('model-origin-location-section');
22+
}
23+
24+
findDestinationLocationSection() {
25+
return cy.findByTestId('model-destination-location-section');
26+
}
27+
28+
findRegistrationModeToggleGroup() {
29+
return cy.findByTestId('registration-mode-toggle-group');
30+
}
31+
32+
findRegisterToggleButton() {
33+
return cy.findByTestId('registration-mode-register');
34+
}
35+
36+
findRegisterAndStoreToggleButton() {
37+
return cy.findByTestId('registration-mode-register-and-store');
38+
}
39+
40+
selectNamespace(name: string) {
41+
this.findNamespaceSelector().click();
42+
cy.findByRole('option', { name }).click();
43+
}
44+
45+
selectRegisterMode() {
46+
this.findRegisterToggleButton().click();
47+
}
48+
49+
selectRegisterAndStoreMode() {
50+
this.findRegisterAndStoreToggleButton().click();
51+
}
52+
53+
shouldShowPlaceholder(placeholder = 'Select a namespace') {
54+
this.findNamespaceSelector().findByText(placeholder).should('contain.text', placeholder);
55+
return this;
56+
}
57+
58+
shouldHaveNamespaceOptions(namespaces: string[]) {
59+
this.findNamespaceSelector().click();
60+
namespaces.forEach((namespace) => {
61+
cy.findByRole('option', { name: namespace }).should('exist');
62+
});
63+
this.findNamespaceSelector().click();
64+
return this;
65+
}
66+
67+
shouldShowSelectedNamespace(name: string) {
68+
this.findNamespaceSelector().findByText(name).should('have.text', name);
69+
return this;
70+
}
71+
72+
shouldHideOriginLocationSection() {
73+
this.findOriginLocationSection().should('not.exist');
74+
return this;
75+
}
76+
77+
shouldHideDestinationLocationSection() {
78+
this.findDestinationLocationSection().should('not.exist');
79+
return this;
80+
}
81+
82+
shouldShowOriginLocationSection() {
83+
this.findOriginLocationSection().should('exist');
84+
return this;
85+
}
86+
87+
shouldShowDestinationLocationSection() {
88+
this.findDestinationLocationSection().should('exist');
89+
return this;
90+
}
91+
92+
shouldHaveRegistrationModeToggle() {
93+
this.findRegistrationModeToggleGroup().should('exist');
94+
return this;
95+
}
96+
97+
shouldHaveRegisterModeSelected() {
98+
this.findRegisterToggleButton().find('button').should('have.attr', 'aria-pressed', 'true');
99+
return this;
100+
}
101+
102+
shouldHaveRegisterAndStoreModeSelected() {
103+
this.findRegisterAndStoreToggleButton()
104+
.find('button')
105+
.should('have.attr', 'aria-pressed', 'true');
106+
return this;
107+
}
108+
}
109+
110+
export const registerAndStoreFields = new RegisterAndStoreFields();

clients/ui/frontend/src/__tests__/cypress/cypress/pages/navBar.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ class NavBar {
2828
this.findNamespaceSelector().click();
2929
cy.findByRole('option').should('not.exist');
3030
}
31+
32+
shouldNamespaceSelectorShow(name: string) {
33+
this.findNamespaceSelector().findByText(name).should('exist');
34+
}
3135
}
3236

3337
export const navBar = new NavBar();

clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/kubeflowStandalone/navBar.cy.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,48 @@ describe('NavBar', () => {
7979
navBar.findNamespaceSelector().findByText('namespace-2').should('exist');
8080
});
8181
});
82+
83+
describe('NavBar - NamespaceSelector', () => {
84+
beforeEach(() => {
85+
cy.intercept('/logout').as('logout');
86+
});
87+
88+
it('Should display empty state when no namespaces are returned', () => {
89+
initIntercepts({ namespaces: [] });
90+
appChrome.visit();
91+
navBar.shouldNamespaceSelectorHaveNoItems();
92+
});
93+
94+
it('Should auto-select first namespace on initial load', () => {
95+
initIntercepts({
96+
namespaces: [
97+
mockNamespace({ name: 'namespace-1' }),
98+
mockNamespace({ name: 'namespace-2' }),
99+
mockNamespace({ name: 'namespace-3' }),
100+
],
101+
});
102+
appChrome.visit();
103+
navBar.shouldNamespaceSelectorShow('namespace-1');
104+
});
105+
106+
it('Should select and update namespace', () => {
107+
initIntercepts({});
108+
appChrome.visit();
109+
110+
navBar.findNamespaceSelector().findByText('namespace-1').should('exist');
111+
navBar.selectNamespace('namespace-2');
112+
navBar.findNamespaceSelector().findByText('namespace-2').should('exist');
113+
});
114+
115+
it('Should maintain namespace selection across navigation', () => {
116+
initIntercepts({});
117+
appChrome.visit();
118+
119+
navBar.selectNamespace('namespace-2');
120+
navBar.shouldNamespaceSelectorShow('namespace-2');
121+
appChrome.findNavItem('Model Catalog').click();
122+
navBar.shouldNamespaceSelectorShow('namespace-2');
123+
appChrome.findNavItem('Model Registry').click();
124+
navBar.shouldNamespaceSelectorShow('namespace-2');
125+
});
126+
});
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/* eslint-disable camelcase */
2+
import type { Namespace } from 'mod-arch-core';
3+
import { mockNamespace } from '~/__mocks__/mockNamespace';
4+
import { mockModelRegistry } from '~/__mocks__/mockModelRegistry';
5+
import { registerAndStoreFields } from '~/__tests__/cypress/cypress/pages/modelRegistryView/registerAndStoreFields';
6+
import { MODEL_REGISTRY_API_VERSION } from '~/__tests__/cypress/cypress/support/commands/api';
7+
import type { ModelRegistry, RegisteredModel } from '~/app/types';
8+
import { mockRegisteredModelList } from '~/__mocks__/mockRegisteredModelsList';
9+
10+
type HandlersProps = {
11+
modelRegistries?: ModelRegistry[];
12+
namespaces?: Namespace[];
13+
registeredModels?: RegisteredModel[];
14+
};
15+
16+
const initIntercepts = ({
17+
modelRegistries = [mockModelRegistry({ name: 'modelregistry-sample' })],
18+
namespaces = [
19+
mockNamespace({ name: 'namespace-1' }),
20+
mockNamespace({ name: 'namespace-2' }),
21+
mockNamespace({ name: 'namespace-3' }),
22+
],
23+
registeredModels = [],
24+
}: HandlersProps = {}) => {
25+
cy.interceptApi(
26+
'GET /api/:apiVersion/namespaces',
27+
{
28+
path: { apiVersion: MODEL_REGISTRY_API_VERSION },
29+
},
30+
namespaces,
31+
);
32+
33+
cy.interceptApi(
34+
`GET /api/:apiVersion/model_registry`,
35+
{
36+
path: { apiVersion: MODEL_REGISTRY_API_VERSION },
37+
},
38+
modelRegistries,
39+
);
40+
41+
cy.interceptApi(
42+
'GET /api/:apiVersion/model_registry/:modelRegistryName/registered_models',
43+
{
44+
path: {
45+
apiVersion: MODEL_REGISTRY_API_VERSION,
46+
modelRegistryName: 'modelregistry-sample',
47+
},
48+
},
49+
mockRegisteredModelList({ items: registeredModels }),
50+
);
51+
};
52+
53+
describe('Register and Store Fields - Toggle Behavior', () => {
54+
beforeEach(() => {
55+
initIntercepts({});
56+
registerAndStoreFields.visit();
57+
});
58+
59+
it('Should display registration mode toggle when feature is enabled', () => {
60+
registerAndStoreFields.shouldHaveRegistrationModeToggle();
61+
});
62+
63+
it('Should have "Register" mode selected by default', () => {
64+
registerAndStoreFields.shouldHaveRegisterModeSelected();
65+
});
66+
67+
it('Should switch to "Register and store" mode', () => {
68+
registerAndStoreFields.selectRegisterAndStoreMode();
69+
registerAndStoreFields.shouldHaveRegisterAndStoreModeSelected();
70+
});
71+
72+
it('Should show namespace selector only in "Register and store" mode', () => {
73+
registerAndStoreFields.findNamespaceFormGroup().should('not.exist');
74+
registerAndStoreFields.selectRegisterAndStoreMode();
75+
registerAndStoreFields.findNamespaceSelector().should('exist');
76+
});
77+
78+
it('Should switch back to "Register" mode and hide namespace selector', () => {
79+
registerAndStoreFields.selectRegisterAndStoreMode();
80+
registerAndStoreFields.findNamespaceSelector().should('exist');
81+
registerAndStoreFields.selectRegisterMode();
82+
registerAndStoreFields.shouldHaveRegisterModeSelected();
83+
registerAndStoreFields.findNamespaceSelector().should('not.exist');
84+
});
85+
86+
it('Should reset namespace selection when switching modes', () => {
87+
registerAndStoreFields.selectRegisterAndStoreMode();
88+
registerAndStoreFields.selectNamespace('namespace-1');
89+
registerAndStoreFields.shouldShowSelectedNamespace('namespace-1');
90+
registerAndStoreFields.selectRegisterMode();
91+
registerAndStoreFields.selectRegisterAndStoreMode();
92+
registerAndStoreFields.shouldShowPlaceholder('Select a namespace');
93+
});
94+
});
95+
96+
describe('Register and Store Fields - NamespaceSelector', () => {
97+
beforeEach(() => {
98+
initIntercepts({});
99+
registerAndStoreFields.visit();
100+
registerAndStoreFields.selectRegisterAndStoreMode();
101+
});
102+
103+
it('Should show placeholder text instead of auto-selecting', () => {
104+
registerAndStoreFields.shouldShowPlaceholder('Select a namespace');
105+
});
106+
107+
it('Should display all available namespaces in dropdown', () => {
108+
registerAndStoreFields.shouldHaveNamespaceOptions([
109+
'namespace-1',
110+
'namespace-2',
111+
'namespace-3',
112+
]);
113+
});
114+
115+
it('Should hide form sections until namespace is selected', () => {
116+
registerAndStoreFields.shouldHideOriginLocationSection().shouldHideDestinationLocationSection();
117+
});
118+
119+
it('Should show form sections after namespace selection', () => {
120+
registerAndStoreFields.selectNamespace('namespace-1');
121+
122+
registerAndStoreFields.shouldShowOriginLocationSection();
123+
registerAndStoreFields.shouldShowDestinationLocationSection();
124+
});
125+
126+
it('Should update selected namespace in dropdown', () => {
127+
registerAndStoreFields.selectNamespace('namespace-2');
128+
registerAndStoreFields.shouldShowSelectedNamespace('namespace-2');
129+
});
130+
131+
it('Should handle empty namespace list gracefully', () => {
132+
initIntercepts({ namespaces: [] });
133+
registerAndStoreFields.visit();
134+
registerAndStoreFields.selectRegisterAndStoreMode();
135+
136+
registerAndStoreFields.findNamespaceSelector().should('exist');
137+
registerAndStoreFields.findNamespaceSelector().should('be.disabled');
138+
139+
registerAndStoreFields.shouldShowPlaceholder('Select a namespace');
140+
});
141+
});

clients/ui/frontend/src/app/pages/modelCatalog/screens/RegisterCatalogModelForm.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const RegisterCatalogModelForm: React.FC<RegisterCatalogModelFormProps> = ({
6969
modelLocationType: ModelLocationType.URI,
7070
modelLocationURI: uri || '',
7171
modelRegistry: preferredModelRegistry.name,
72+
namespace: '',
7273
modelCustomProperties: { ...getLabelsFromCustomProperties(model?.customProperties), ...tasks },
7374
versionCustomProperties: {
7475
...model?.customProperties,

0 commit comments

Comments
 (0)