Skip to content

Commit 9e4a22c

Browse files
committed
Add namespace selector to register form
Signed-off-by: ppadti <ppadti@redhat.com>
1 parent 5791ac3 commit 9e4a22c

File tree

12 files changed

+486
-62
lines changed

12 files changed

+486
-62
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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()
55+
.find('span.pf-v6-c-menu-toggle__text')
56+
.should('contain.text', placeholder);
57+
return this;
58+
}
59+
60+
shouldHaveNamespaceOptions(namespaces: string[]) {
61+
this.findNamespaceSelector().click();
62+
namespaces.forEach((namespace) => {
63+
cy.findByRole('option', { name: namespace }).should('exist');
64+
});
65+
this.findNamespaceSelector().click();
66+
return this;
67+
}
68+
69+
shouldShowSelectedNamespace(name: string) {
70+
this.findNamespaceSelector().find('span.pf-v6-c-menu-toggle__text').should('have.text', name);
71+
return this;
72+
}
73+
74+
shouldHideOriginLocationSection() {
75+
this.findOriginLocationSection().should('not.exist');
76+
return this;
77+
}
78+
79+
shouldHideDestinationLocationSection() {
80+
this.findDestinationLocationSection().should('not.exist');
81+
return this;
82+
}
83+
84+
shouldShowOriginLocationSection() {
85+
this.findOriginLocationSection().should('exist');
86+
return this;
87+
}
88+
89+
shouldShowDestinationLocationSection() {
90+
this.findDestinationLocationSection().should('exist');
91+
return this;
92+
}
93+
94+
shouldHaveRegistrationModeToggle() {
95+
this.findRegistrationModeToggleGroup().should('exist');
96+
return this;
97+
}
98+
99+
shouldHaveRegisterModeSelected() {
100+
this.findRegisterToggleButton().find('button').should('have.attr', 'aria-pressed', 'true');
101+
return this;
102+
}
103+
104+
shouldHaveRegisterAndStoreModeSelected() {
105+
this.findRegisterAndStoreToggleButton()
106+
.find('button')
107+
.should('have.attr', 'aria-pressed', 'true');
108+
return this;
109+
}
110+
}
111+
112+
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)