Skip to content

Commit 02a527f

Browse files
committed
Merge branch 'main' of https://github.com/opendatahub-io/odh-dashboard into fix-aae-page-embedding-models-and-configure-playground
2 parents c92a00b + 698b5ce commit 02a527f

22 files changed

Lines changed: 607 additions & 233 deletions

packages/gen-ai/frontend/src/__tests__/cypress/cypress/pages/aiAssetsPage.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
class AIAssetsPage {
2-
visit(namespace?: string): void {
2+
visit(namespace?: string, queryParams?: Record<string, string>): void {
3+
const qs = queryParams ? `?${new URLSearchParams(queryParams).toString()}` : '';
34
if (namespace) {
4-
cy.visit(`/gen-ai-studio/assets/${namespace}`);
5+
cy.visit(`/gen-ai-studio/assets/${namespace}${qs}`);
56
} else {
6-
cy.visit('/gen-ai-studio/assets');
7+
cy.visit(`/gen-ai-studio/assets${qs}`);
78
}
89
this.waitForPageLoad();
910
}

packages/gen-ai/frontend/src/__tests__/cypress/cypress/pages/chatbotPage.ts

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -405,16 +405,11 @@ class ChatbotPage {
405405
return cy.get('[data-testid^="chatbot-pane-"][role="region"]');
406406
}
407407

408-
// Find a specific chatbot pane by index (0 = Model 1, 1 = Model 2)
408+
// Find a specific chatbot pane by index (0 = Chat 1, 1 = Chat 2)
409409
findChatbotPaneByIndex(index: number): Cypress.Chainable<JQuery<HTMLElement>> {
410410
return this.findAllChatbotPanes().eq(index);
411411
}
412412

413-
// Find pane settings button by pane index
414-
findPaneSettingsButton(index: number): Cypress.Chainable<JQuery<HTMLElement>> {
415-
return this.findChatbotPaneByIndex(index).find('[data-testid$="-settings-button"]');
416-
}
417-
418413
// Find pane close button by pane index
419414
findPaneCloseButton(index: number): Cypress.Chainable<JQuery<HTMLElement>> {
420415
return this.findChatbotPaneByIndex(index).find('[data-testid$="-close-button"]');
@@ -425,12 +420,28 @@ class ChatbotPage {
425420
return this.findChatbotPaneByIndex(index).findByTestId('chatbot-model-selector-toggle');
426421
}
427422

428-
// Find pane label text (e.g., "Model 1", "Model 2")
423+
// Find pane label text (e.g., "Chat 1", "Chat 2")
429424
findPaneLabel(index: number): Cypress.Chainable<JQuery<HTMLElement>> {
430-
const labelText = `Model ${index + 1}`;
425+
const labelText = `Chat ${index + 1}`;
431426
return this.findChatbotPaneByIndex(index).contains(labelText);
432427
}
433428

429+
// Find the global Settings button in the page header
430+
findSettingsButton(): Cypress.Chainable<JQuery<HTMLElement>> {
431+
return cy.findByTestId('settings-button');
432+
}
433+
434+
// Find the config switcher toggle group inside the settings panel
435+
findConfigSwitcher(): Cypress.Chainable<JQuery<HTMLElement>> {
436+
return cy.findByTestId('chatbot-config-switcher');
437+
}
438+
439+
// Find the button inside a config tab in the settings panel switcher (1-based)
440+
// aria-pressed is on the inner button, not the wrapper div
441+
findConfigTab(chatNumber: number): Cypress.Chainable<JQuery<HTMLButtonElement>> {
442+
return cy.findByTestId(`chatbot-config-tab-${chatNumber}`).find('button');
443+
}
444+
434445
// Verify we are in compare mode (two panes visible)
435446
verifyInCompareMode(): void {
436447
this.findAllChatbotPanes().should('have.length', 2);
@@ -451,22 +462,27 @@ class ChatbotPage {
451462
this.findCompareChatButton().click();
452463
}
453464

454-
// Exit compare mode by closing a pane
465+
// Exit compare mode by closing a pane (confirms the modal)
455466
closePaneByIndex(index: number): void {
456467
this.findPaneCloseButton(index).click();
468+
cy.findByTestId('close-compare-modal').findByTestId('modal-submit-button').click();
457469
}
458470

459-
// Open settings panel for a specific pane
460-
openPaneSettings(index: number): void {
461-
// First ensure any existing settings panel is closed
462-
this.closeSettingsPanel();
463-
// Then click the settings button for the specified pane
464-
this.findPaneSettingsButton(index).click({ force: true });
471+
// Open the settings panel via the header Settings button and switch to the given pane (1-based)
472+
openPaneSettings(chatNumber: number): void {
473+
// Only click Settings button if the panel isn't already open
474+
cy.get('body').then(($body) => {
475+
if (!$body.find('[data-testid="chatbot-config-switcher"]').is(':visible')) {
476+
this.findSettingsButton().click();
477+
}
478+
});
479+
this.findConfigSwitcher().should('be.visible');
480+
// Use force:true to avoid detachment during drawer open animation
481+
this.findConfigTab(chatNumber).click({ force: true });
465482
}
466483

467-
// Find settings panel header (shows "Configure - 1" or "Configure - 2")
484+
// Find settings panel header (single mode only — shows "Configure")
468485
findSettingsPanelHeader(): Cypress.Chainable<JQuery<HTMLElement>> {
469-
// Wait for the drawer to be visible before finding the header
470486
return cy.findByTestId('chatbot-settings-panel-header', { timeout: 10000 });
471487
}
472488

@@ -489,8 +505,6 @@ class ChatbotPage {
489505
const closeButton = $body.find('[aria-label="Close settings panel"]');
490506
if (closeButton.length > 0 && closeButton.is(':visible')) {
491507
cy.get('[aria-label="Close settings panel"]').click({ force: true });
492-
// Wait for panel to close
493-
cy.get('[data-testid="chatbot-settings-panel-header"]').should('not.exist');
494508
}
495509
});
496510
}

packages/gen-ai/frontend/src/__tests__/cypress/cypress/pages/modelsTabPage.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ class ModelsTabPage {
5252
openEndpointModal(modelName: string): void {
5353
this.getRow(modelName).findEndpointCell().findByTestId('endpoint-view-button').click();
5454
}
55+
56+
findWarningAlert(): Cypress.Chainable<JQuery<HTMLElement>> {
57+
return cy.findByTestId('models-tab-warning-alert');
58+
}
59+
60+
findEmptyState(): Cypress.Chainable<JQuery<HTMLElement>> {
61+
return cy.findByTestId('empty-state');
62+
}
5563
}
5664

5765
class EndpointModalPage {

packages/gen-ai/frontend/src/__tests__/cypress/cypress/support/helpers/modelsTab/modelsTabTestHelpers.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface ModelsTabTestOptions {
2525
namespace?: string;
2626
aiModels?: Partial<AAModelResponse>[];
2727
maasModels?: Partial<MaaSModel>[];
28+
maasError?: boolean;
2829
lsdStatus?: 'Ready' | 'NotReady';
2930
}
3031

@@ -37,13 +38,19 @@ export const setupModelsTabIntercepts = (options: ModelsTabTestOptions = {}): vo
3738
];
3839
cy.interceptGenAi('GET /api/v1/namespaces', { data: namespacesData });
3940

40-
// mockAAModels() returns a default model when called without args
4141
cy.interceptGenAi('GET /api/v1/aaa/models', mockAAModels(options.aiModels)).as('aaModels');
4242

43-
cy.interceptGenAi(
44-
'GET /api/v1/maas/models',
45-
options.maasModels ? mockMaaSModels(options.maasModels) : mockEmptyList(),
46-
).as('maasModels');
43+
if (options.maasError) {
44+
cy.interceptGenAi('GET /api/v1/maas/models', {
45+
statusCode: 500,
46+
body: { error: 'MaaS service unavailable' },
47+
}).as('maasModels');
48+
} else {
49+
cy.interceptGenAi(
50+
'GET /api/v1/maas/models',
51+
options.maasModels ? mockMaaSModels(options.maasModels) : mockEmptyList(),
52+
).as('maasModels');
53+
}
4754

4855
cy.interceptGenAi('GET /api/v1/lsd/status', mockStatus(options.lsdStatus ?? 'Ready'));
4956

packages/gen-ai/frontend/src/__tests__/cypress/cypress/tests/mocked/aiAssets/modelsTab.cy.ts

Lines changed: 131 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,56 +5,60 @@ import { setupModelsTabIntercepts } from '~/__tests__/cypress/cypress/support/he
55

66
const TEST_NAMESPACE = 'test-namespace';
77

8+
const AI_MODELS = [
9+
{
10+
model_name: 'Llama-3B-Internal',
11+
model_id: 'llama-3b-internal',
12+
display_name: 'Llama 3B Internal',
13+
description: 'Locally deployed Llama model',
14+
usecase: 'text-generation',
15+
status: 'Running',
16+
model_source_type: 'namespace',
17+
model_type: 'llm',
18+
endpoints: ['internal: http://llama-3b.test-namespace.svc.cluster.local:8080'],
19+
},
20+
{
21+
model_name: 'GPT-4-External',
22+
model_id: 'gpt-4-external',
23+
display_name: 'GPT-4 External',
24+
description: 'External GPT-4 model',
25+
usecase: 'text-generation',
26+
status: 'Running',
27+
model_source_type: 'custom_endpoint',
28+
model_type: 'llm',
29+
endpoints: ['https://api.openai.com/v1/models/gpt-4'],
30+
},
31+
{
32+
model_name: 'Embedding-Model',
33+
model_id: 'embedding-model',
34+
display_name: 'Embedding Model',
35+
description: 'Custom endpoint embedding model',
36+
usecase: 'embedding',
37+
status: 'Stop',
38+
model_source_type: 'custom_endpoint',
39+
model_type: 'embedding',
40+
endpoints: ['http://embedding.cluster.local:8080'],
41+
},
42+
];
43+
44+
const MAAS_MODELS = [
45+
{
46+
id: 'maas-llama-70b',
47+
display_name: 'Llama 70B MaaS',
48+
description: 'Llama 70B via Models as a Service',
49+
usecase: 'text-generation',
50+
ready: true,
51+
url: 'https://maas.example.com/v1/models/llama-70b',
52+
model_type: 'llm',
53+
},
54+
];
55+
856
describe('AI Assets - Models Tab', () => {
957
beforeEach(() => {
1058
setupModelsTabIntercepts({
1159
namespace: TEST_NAMESPACE,
12-
aiModels: [
13-
{
14-
model_name: 'Llama-3B-Internal',
15-
model_id: 'llama-3b-internal',
16-
display_name: 'Llama 3B Internal',
17-
description: 'Locally deployed Llama model',
18-
usecase: 'text-generation',
19-
status: 'Running',
20-
model_source_type: 'namespace',
21-
model_type: 'llm',
22-
endpoints: ['internal: http://llama-3b.test-namespace.svc.cluster.local:8080'],
23-
},
24-
{
25-
model_name: 'GPT-4-External',
26-
model_id: 'gpt-4-external',
27-
display_name: 'GPT-4 External',
28-
description: 'External GPT-4 model',
29-
usecase: 'text-generation',
30-
status: 'Running',
31-
model_source_type: 'custom_endpoint',
32-
model_type: 'llm',
33-
endpoints: ['https://api.openai.com/v1/models/gpt-4'],
34-
},
35-
{
36-
model_name: 'Embedding-Model',
37-
model_id: 'embedding-model',
38-
display_name: 'Embedding Model',
39-
description: 'Custom endpoint embedding model',
40-
usecase: 'embedding',
41-
status: 'Stop',
42-
model_source_type: 'custom_endpoint',
43-
model_type: 'embedding',
44-
endpoints: ['http://embedding.cluster.local:8080'],
45-
},
46-
],
47-
maasModels: [
48-
{
49-
id: 'maas-llama-70b',
50-
display_name: 'Llama 70B MaaS',
51-
description: 'Llama 70B via Models as a Service',
52-
usecase: 'text-generation',
53-
ready: true,
54-
url: 'https://maas.example.com/v1/models/llama-70b',
55-
model_type: 'llm',
56-
},
57-
],
60+
aiModels: AI_MODELS,
61+
maasModels: MAAS_MODELS,
5862
});
5963

6064
aiAssetsPage.visit(TEST_NAMESPACE);
@@ -95,3 +99,84 @@ describe('AI Assets - Models Tab', () => {
9599
},
96100
);
97101
});
102+
103+
describe('AI Assets - Models Tab (MaaS disabled)', () => {
104+
beforeEach(() => {
105+
setupModelsTabIntercepts({
106+
namespace: TEST_NAMESPACE,
107+
aiModels: AI_MODELS,
108+
maasModels: MAAS_MODELS,
109+
});
110+
});
111+
112+
it(
113+
'should only show AI models when modelAsService flag is disabled',
114+
{ tags: ['@GenAI', '@ModelsTab', '@AIAssets', '@FeatureFlag'] },
115+
() => {
116+
cy.step('Visit with modelAsService feature flag disabled');
117+
aiAssetsPage.visit(TEST_NAMESPACE, { modelAsService: 'false' });
118+
119+
cy.step('Verify the table shows only AI models (no MaaS)');
120+
modelsTabPage.findTable().should('be.visible');
121+
modelsTabPage.findTableRows().should('have.length', 3);
122+
123+
cy.step('Verify MaaS model is not present');
124+
modelsTabPage.findTable().should('not.contain.text', 'Llama 70B MaaS');
125+
126+
cy.step('Verify AI models are still present');
127+
modelsTabPage.findTable().should('contain.text', 'Llama 3B Internal');
128+
modelsTabPage.findTable().should('contain.text', 'GPT-4 External');
129+
modelsTabPage.findTable().should('contain.text', 'Embedding Model');
130+
131+
cy.step('Verify no warning alert is shown');
132+
modelsTabPage.findWarningAlert().should('not.exist');
133+
},
134+
);
135+
});
136+
137+
describe('AI Assets - Models Tab (MaaS error)', () => {
138+
beforeEach(() => {
139+
setupModelsTabIntercepts({
140+
namespace: TEST_NAMESPACE,
141+
aiModels: AI_MODELS,
142+
maasError: true,
143+
});
144+
145+
aiAssetsPage.visit(TEST_NAMESPACE);
146+
});
147+
148+
it(
149+
'should show warning alert when MaaS models fail to load',
150+
{ tags: ['@GenAI', '@ModelsTab', '@AIAssets'] },
151+
() => {
152+
cy.step('Verify warning alert is displayed');
153+
modelsTabPage.findWarningAlert().should('be.visible');
154+
modelsTabPage
155+
.findWarningAlert()
156+
.should('contain.text', 'Models as a Service could not be loaded');
157+
158+
cy.step('Verify AI models still display');
159+
modelsTabPage.findTable().should('be.visible');
160+
modelsTabPage.findTableRows().should('have.length', 3);
161+
},
162+
);
163+
164+
it(
165+
'should allow dismissing the warning alert',
166+
{ tags: ['@GenAI', '@ModelsTab', '@AIAssets'] },
167+
() => {
168+
cy.step('Verify warning alert is displayed');
169+
modelsTabPage.findWarningAlert().should('be.visible');
170+
171+
cy.step('Click the close button on the alert');
172+
modelsTabPage.findWarningAlert().find('button[aria-label*="Close"]').click();
173+
174+
cy.step('Verify the alert is dismissed');
175+
modelsTabPage.findWarningAlert().should('not.exist');
176+
177+
cy.step('Verify models table is still visible');
178+
modelsTabPage.findTable().should('be.visible');
179+
modelsTabPage.findTableRows().should('have.length', 3);
180+
},
181+
);
182+
});

0 commit comments

Comments
 (0)