Skip to content

Commit c92a00b

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 6b63bf2 + 8329bce commit c92a00b

83 files changed

Lines changed: 15011 additions & 1153 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/contract-tests/scripts/run-go-bff-consumer.sh

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,12 @@ fi
198198

199199
log_info "Starting Mock BFF server on port $PORT..."
200200

201-
log_info "Starting Mock BFF server with go run"
201+
BFF_BINARY="$(mktemp -d)/bff-test"
202+
log_info "Building Mock BFF binary..."
203+
go build -o "$BFF_BINARY" ./cmd
202204

203-
go run ./cmd $BFF_MOCK_FLAGS --port "$PORT" --allowed-origins="*" > "$BFF_LOG_FILE" 2>&1 &
205+
log_info "Starting Mock BFF server"
206+
"$BFF_BINARY" $BFF_MOCK_FLAGS --port "$PORT" --allowed-origins="*" > "$BFF_LOG_FILE" 2>&1 &
204207

205208
BFF_PID=$!
206209
echo "$BFF_PID" > "$RESULTS_DIR/bff.pid"
@@ -215,6 +218,7 @@ cleanup() {
215218
sleep 2
216219
kill -9 "$BFF_PID" 2>/dev/null || true
217220
fi
221+
rm -f "$BFF_BINARY"
218222
}
219223
trap cleanup EXIT INT TERM
220224

packages/cypress/cypress/pages/modelsAsAService.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,57 @@ class DeleteSubscriptionModal extends DeleteModal {
657657
return this.find().findByRole('button', { name: /Delete/, hidden: true });
658658
}
659659
}
660+
class ViewSubscriptionPage {
661+
visit(name: string): void {
662+
cy.visitWithLogin(`/maas/subscriptions/view/${name}`);
663+
this.wait();
664+
}
665+
666+
private wait(): void {
667+
cy.findByTestId('app-page-title').should('exist');
668+
cy.testA11y();
669+
}
670+
671+
findTitle(): Cypress.Chainable<JQuery<HTMLElement>> {
672+
return cy.findByTestId('app-page-title');
673+
}
674+
675+
findBreadcrumb(): Cypress.Chainable<JQuery<HTMLElement>> {
676+
return cy.findByTestId('app-page-breadcrumb');
677+
}
678+
679+
findBreadcrumbSubscriptionsLink(): Cypress.Chainable<JQuery<HTMLElement>> {
680+
return cy.findByTestId('breadcrumb-subscriptions-link');
681+
}
682+
683+
findDetailsSection(): Cypress.Chainable<JQuery<HTMLElement>> {
684+
return cy.findByTestId('subscription-details-section');
685+
}
686+
687+
findGroupsSection(): Cypress.Chainable<JQuery<HTMLElement>> {
688+
return cy.findByTestId('subscription-groups-section');
689+
}
690+
691+
findGroupsTable(): Cypress.Chainable<JQuery<HTMLElement>> {
692+
return cy.findByTestId('subscription-groups-table');
693+
}
694+
695+
findModelsSection(): Cypress.Chainable<JQuery<HTMLElement>> {
696+
return cy.findByTestId('subscription-models-section');
697+
}
698+
699+
findModelsTable(): Cypress.Chainable<JQuery<HTMLElement>> {
700+
return cy.findByTestId('subscription-models-table');
701+
}
702+
703+
findPageError(): Cypress.Chainable<JQuery<HTMLElement>> {
704+
return cy.findByTestId('error-empty-state-body');
705+
}
706+
707+
findDetailsTab(): Cypress.Chainable<JQuery<HTMLElement>> {
708+
return cy.findByTestId('subscription-details-tab');
709+
}
710+
}
660711

661712
export const tiersPage = new TiersPage();
662713
export const createTierPage = new CreateTierPage();
@@ -670,3 +721,4 @@ export const createApiKeyModal = new CreateApiKeyModal();
670721
export const copyApiKeyModal = new CopyApiKeyModal();
671722
export const subscriptionsPage = new SubscriptionsPage();
672723
export const deleteSubscriptionModal = new DeleteSubscriptionModal();
724+
export const viewSubscriptionPage = new ViewSubscriptionPage();

packages/cypress/cypress/support/commands/odh.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ import type {
9797
BulkRevokeResponse,
9898
CreateAPIKeyResponse,
9999
} from '@odh-dashboard/maas/types/api-key';
100-
import type { MaaSSubscription } from '@odh-dashboard/maas/types/subscriptions';
100+
import type {
101+
MaaSSubscription,
102+
SubscriptionInfoResponse,
103+
} from '@odh-dashboard/maas/types/subscriptions';
101104

102105
type SuccessErrorResponse = {
103106
success: boolean;
@@ -1151,6 +1154,11 @@ declare global {
11511154
type: 'DELETE /maas/api/v1/subscription/:name',
11521155
options: { path: { name: string } },
11531156
response: OdhResponse<{ message: string }>,
1157+
) => Cypress.Chainable<null>) &
1158+
((
1159+
type: 'GET /maas/api/v1/subscription-info/:name',
1160+
options: { path: { name: string } },
1161+
response: OdhResponse<SubscriptionInfoResponse>,
11541162
) => Cypress.Chainable<null>);
11551163
}
11561164
}

packages/cypress/cypress/tests/mocked/modelsAsAService/maasSubscriptions.cy.ts

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,32 @@
11
import { mockDashboardConfig, mockDscStatus } from '@odh-dashboard/internal/__mocks__';
22
import { DataScienceStackComponent } from '@odh-dashboard/internal/concepts/areas/types';
33
import { asProductAdminUser } from '../../../utils/mockUsers';
4-
import { deleteSubscriptionModal, subscriptionsPage } from '../../../pages/modelsAsAService';
5-
import { mockSubscriptions } from '../../../utils/maasUtils';
4+
import {
5+
deleteSubscriptionModal,
6+
subscriptionsPage,
7+
viewSubscriptionPage,
8+
} from '../../../pages/modelsAsAService';
9+
import { mockSubscriptions, mockSubscriptionInfo } from '../../../utils/maasUtils';
10+
11+
const setupCommonIntercepts = () => {
12+
asProductAdminUser();
13+
cy.interceptOdh('GET /api/config', mockDashboardConfig({ modelAsService: true }));
14+
cy.interceptOdh('GET /maas/api/v1/user', { data: { userId: 'test-user', clusterAdmin: false } });
15+
cy.interceptOdh('GET /maas/api/v1/namespaces', { data: [] });
16+
cy.interceptOdh(
17+
'GET /api/dsc/status',
18+
mockDscStatus({
19+
components: {
20+
[DataScienceStackComponent.LLAMA_STACK_OPERATOR]: { managementState: 'Managed' },
21+
},
22+
}),
23+
);
24+
};
625

726
describe('Subscriptions Page', () => {
827
beforeEach(() => {
9-
asProductAdminUser();
10-
cy.interceptOdh(
11-
'GET /api/config',
12-
mockDashboardConfig({
13-
modelAsService: true,
14-
}),
15-
);
16-
cy.interceptOdh('GET /maas/api/v1/user', {
17-
data: { userId: 'test-user', clusterAdmin: false },
18-
});
19-
cy.interceptOdh('GET /maas/api/v1/namespaces', { data: [] });
20-
cy.interceptOdh(
21-
'GET /api/dsc/status',
22-
mockDscStatus({
23-
components: {
24-
[DataScienceStackComponent.LLAMA_STACK_OPERATOR]: { managementState: 'Managed' },
25-
},
26-
}),
27-
);
28-
cy.interceptOdh('GET /maas/api/v1/all-subscriptions', {
29-
data: mockSubscriptions(),
30-
});
28+
setupCommonIntercepts();
29+
cy.interceptOdh('GET /maas/api/v1/all-subscriptions', { data: mockSubscriptions() });
3130
subscriptionsPage.visit();
3231
});
3332

@@ -96,3 +95,55 @@ describe('Subscriptions Page', () => {
9695
subscriptionsPage.findTable().should('not.contain', 'premium-team-sub');
9796
});
9897
});
98+
99+
describe('View Subscription Page', () => {
100+
const subscriptionName = 'premium-team-sub';
101+
102+
beforeEach(() => {
103+
setupCommonIntercepts();
104+
cy.interceptOdh(
105+
'GET /maas/api/v1/subscription-info/:name',
106+
{ path: { name: subscriptionName } },
107+
mockSubscriptionInfo(subscriptionName),
108+
);
109+
});
110+
111+
it('should display the page content with title, breadcrumb, details, groups, and models', () => {
112+
cy.interceptOdh('GET /maas/api/v1/all-subscriptions', { data: mockSubscriptions() });
113+
subscriptionsPage.visit();
114+
subscriptionsPage.getRow(subscriptionName).findKebabAction('View details').click();
115+
cy.url().should('include', `/maas/subscriptions/view/${subscriptionName}`);
116+
117+
viewSubscriptionPage.findTitle().should('contain.text', subscriptionName);
118+
119+
viewSubscriptionPage
120+
.findDetailsSection()
121+
.should('contain.text', subscriptionName)
122+
.and('contain.text', 'Name')
123+
.and('contain.text', 'Date created');
124+
125+
viewSubscriptionPage.findGroupsSection().should('exist');
126+
viewSubscriptionPage.findGroupsTable().should('contain.text', 'premium-users');
127+
128+
viewSubscriptionPage.findModelsSection().should('exist');
129+
viewSubscriptionPage
130+
.findModelsTable()
131+
.should('contain.text', 'granite-3-8b-instruct Display')
132+
.and('contain.text', 'granite-3-8b-instruct')
133+
.and('contain.text', 'maas-models')
134+
.and('contain.text', '100,000');
135+
136+
viewSubscriptionPage.findBreadcrumbSubscriptionsLink().click();
137+
cy.url().should('include', '/maas/subscriptions');
138+
});
139+
140+
it('should show error state when the subscription-info API fails', () => {
141+
cy.interceptOdh(
142+
'GET /maas/api/v1/subscription-info/:name',
143+
{ path: { name: subscriptionName } },
144+
{ forceNetworkError: true } as never,
145+
);
146+
viewSubscriptionPage.visit(subscriptionName);
147+
viewSubscriptionPage.findPageError().should('exist');
148+
});
149+
});

packages/cypress/cypress/utils/maasUtils.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import type {
44
CreateAPIKeyResponse,
55
CreateAPIKeyRequest,
66
} from '@odh-dashboard/maas/types/api-key';
7-
import type { MaaSSubscription } from '@odh-dashboard/maas/types/subscriptions';
7+
import type {
8+
MaaSSubscription,
9+
SubscriptionInfoResponse,
10+
} from '@odh-dashboard/maas/types/subscriptions';
811

912
// Standardized tier templates - use these directly or as building blocks
1013
export const MOCK_TIERS: Record<'free' | 'premium' | 'enterprise', Tier> = {
@@ -177,3 +180,32 @@ export const mockTier = ({
177180
limits,
178181
};
179182
};
183+
184+
export const mockSubscriptionInfo = (name = 'premium-team-sub'): SubscriptionInfoResponse => {
185+
const subscription = mockSubscriptions().find((s) => s.name === name) ?? mockSubscriptions()[0];
186+
return {
187+
subscription,
188+
modelRefs: subscription.modelRefs.map((ref) => ({
189+
name: ref.name,
190+
namespace: ref.namespace,
191+
displayName: `${ref.name} Display`,
192+
modelRef: { kind: 'LLMInferenceService', name: ref.name },
193+
phase: 'Ready',
194+
endpoint: `https://${ref.name}.example.com`,
195+
})),
196+
authPolicies: [
197+
{
198+
name: `${name}-policy`,
199+
namespace: subscription.namespace,
200+
phase: 'Active',
201+
modelRefs: subscription.modelRefs.map((ref) => ({
202+
name: ref.name,
203+
namespace: ref.namespace,
204+
})),
205+
subjects: {
206+
groups: subscription.owner.groups,
207+
},
208+
},
209+
],
210+
};
211+
};

packages/gen-ai/bff/.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ __debug*
66

77
# Local MLflow data (created by mock mode)
88
.mlflow/
9-
.mlflow-*/
9+
.mlflow-*/
10+
11+
# Local Llama Stack runtime data (created by mock/test mode)
12+
testdata/llamastack/.data/

packages/gen-ai/bff/Makefile

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,27 @@ MLFLOW_VERSION ?= 3.9.0
2222
MLFLOW_PORT ?= 5001
2323
MLFLOW_DATA ?= $(shell pwd)/.mlflow
2424

25+
# Llama Stack test server configuration (used by mock/test mode only)
26+
TEST_LLAMA_STACK_VERSION ?= 0.6.0
27+
TEST_LLAMA_STACK_PORT ?= 18321
28+
TEST_LLAMA_STACK_DIR ?= testdata/llamastack
29+
TEST_LLAMA_STACK_DATA ?= $(shell pwd)/$(TEST_LLAMA_STACK_DIR)/.data
30+
TEST_LLAMA_STACK_RECORDINGS ?= $(shell pwd)/$(TEST_LLAMA_STACK_DIR)
31+
TEST_LLAMA_STACK_CONFIG ?= $(TEST_LLAMA_STACK_DIR)/config.yaml
32+
TEST_LLAMA_STACK_TEST_ID ?= bff/$(TEST_LLAMA_STACK_DIR)/test.py::record
33+
34+
# Provider and bare model IDs (source of truth, used by config yaml)
35+
TEST_LLAMA_STACK_PROVIDER ?= gemini
36+
TEST_LLAMA_STACK_MODEL_ID ?= models/gemini-2.5-flash
37+
TEST_LLAMA_STACK_EMBEDDING_MODEL_ID ?= models/gemini-embedding-001
38+
TEST_LLAMA_STACK_EMBEDDING_DIMENSION ?= 128
39+
TEST_LLAMA_STACK_SHIELD_MODEL_ID ?= models/gemini-2.5-flash
40+
41+
# Prefixed model refs (derived from above, used by Go tests and shield)
42+
TEST_LLAMA_STACK_MODEL ?= $(TEST_LLAMA_STACK_PROVIDER)/$(TEST_LLAMA_STACK_MODEL_ID)
43+
TEST_LLAMA_STACK_EMBEDDING_MODEL ?= $(TEST_LLAMA_STACK_PROVIDER)/$(TEST_LLAMA_STACK_EMBEDDING_MODEL_ID)
44+
TEST_LLAMA_STACK_SHIELD_MODEL ?= $(TEST_LLAMA_STACK_PROVIDER)/$(TEST_LLAMA_STACK_SHIELD_MODEL_ID)
45+
2546
.PHONY: all
2647
all: build
2748

@@ -53,6 +74,15 @@ vet: . ## Runs static analysis tools on source files and reports suspicious con
5374
test: fmt vet envtest uv ## Runs the full test suite.
5475
ENVTEST_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" \
5576
MLFLOW_PORT=$(MLFLOW_PORT) MLFLOW_VERSION=$(MLFLOW_VERSION) \
77+
LLAMA_STACK_TEST_ID=$(TEST_LLAMA_STACK_TEST_ID) \
78+
TEST_LLAMA_STACK_PORT=$(TEST_LLAMA_STACK_PORT) TEST_LLAMA_STACK_VERSION=$(TEST_LLAMA_STACK_VERSION) \
79+
TEST_LLAMA_STACK_PROVIDER=$(TEST_LLAMA_STACK_PROVIDER) \
80+
TEST_LLAMA_STACK_MODEL=$(TEST_LLAMA_STACK_MODEL) \
81+
TEST_LLAMA_STACK_EMBEDDING_MODEL=$(TEST_LLAMA_STACK_EMBEDDING_MODEL) \
82+
TEST_LLAMA_STACK_MODEL_ID=$(TEST_LLAMA_STACK_MODEL_ID) \
83+
TEST_LLAMA_STACK_EMBEDDING_MODEL_ID=$(TEST_LLAMA_STACK_EMBEDDING_MODEL_ID) \
84+
TEST_LLAMA_STACK_EMBEDDING_DIMENSION=$(TEST_LLAMA_STACK_EMBEDDING_DIMENSION) \
85+
TEST_LLAMA_STACK_SHIELD_MODEL=$(TEST_LLAMA_STACK_SHIELD_MODEL) \
5686
go test ./...
5787

5888
.PHONY: build
@@ -96,6 +126,49 @@ mlflow-clean: mlflow-down ## Remove local MLflow data.
96126
rm -rf $(MLFLOW_DATA) $(MLFLOW_DATA)-test
97127
@echo "MLflow data cleaned"
98128

129+
##@ Llama Stack
130+
131+
.PHONY: llamastack-up
132+
llamastack-up: uv ## Start local Llama Stack server in replay mode (Ctrl+C to stop).
133+
@rm -rf $(TEST_LLAMA_STACK_DATA)
134+
@mkdir -p $(TEST_LLAMA_STACK_DATA)
135+
@echo "Starting Llama Stack in REPLAY mode on port $(TEST_LLAMA_STACK_PORT)..."
136+
SQLITE_STORE_DIR=$(TEST_LLAMA_STACK_DATA) \
137+
GEMINI_API_KEY=dummy-key-for-replay \
138+
LLAMA_STACK_TEST_INFERENCE_MODE=replay \
139+
LLAMA_STACK_TEST_RECORDING_DIR=$(TEST_LLAMA_STACK_RECORDINGS) \
140+
LLAMA_STACK_TEST_STACK_CONFIG_TYPE=server \
141+
LLAMA_STACK_TEST_ID=$(TEST_LLAMA_STACK_TEST_ID) \
142+
TEST_LLAMA_STACK_PROVIDER=$(TEST_LLAMA_STACK_PROVIDER) \
143+
TEST_LLAMA_STACK_MODEL_ID=$(TEST_LLAMA_STACK_MODEL_ID) \
144+
TEST_LLAMA_STACK_EMBEDDING_MODEL_ID=$(TEST_LLAMA_STACK_EMBEDDING_MODEL_ID) \
145+
TEST_LLAMA_STACK_EMBEDDING_DIMENSION=$(TEST_LLAMA_STACK_EMBEDDING_DIMENSION) \
146+
TEST_LLAMA_STACK_EMBEDDING_MODEL=$(TEST_LLAMA_STACK_EMBEDDING_MODEL) \
147+
TEST_LLAMA_STACK_SHIELD_MODEL=$(TEST_LLAMA_STACK_SHIELD_MODEL) \
148+
$(UV) run --with llama-stack==$(TEST_LLAMA_STACK_VERSION) --with-requirements $(TEST_LLAMA_STACK_DIR)/requirements.txt \
149+
llama stack run $(TEST_LLAMA_STACK_CONFIG) --port $(TEST_LLAMA_STACK_PORT)
150+
151+
.PHONY: llamastack-down
152+
llamastack-down: ## Stop local Llama Stack server and remove ephemeral data.
153+
@-lsof -t -i :$(TEST_LLAMA_STACK_PORT) | xargs kill 2>/dev/null || true
154+
@sleep 2
155+
@-lsof -t -i :$(TEST_LLAMA_STACK_PORT) | xargs kill -9 2>/dev/null || true
156+
@rm -rf $(TEST_LLAMA_STACK_DATA)
157+
@echo "Llama Stack server stopped (runtime data cleaned)"
158+
159+
.PHONY: llamastack-record
160+
llamastack-record: ## Record Llama Stack fixtures by running Go tests against real Gemini (needs GEMINI_API_KEY).
161+
@if [ -z "$${GEMINI_API_KEY:-}" ]; then \
162+
echo "ERROR: GEMINI_API_KEY must be set for recording."; \
163+
echo "Usage: GEMINI_API_KEY=<key> make llamastack-record"; \
164+
exit 1; \
165+
fi
166+
@rm -rf $(TEST_LLAMA_STACK_RECORDINGS)/recordings
167+
@echo "Cleared previous recordings"
168+
LLAMA_STACK_TEST_INFERENCE_MODE=record $(MAKE) test
169+
@COUNT=$$(find $(TEST_LLAMA_STACK_RECORDINGS)/recordings -name "*.json" 2>/dev/null | wc -l | tr -d ' '); \
170+
echo " Recording complete! $${COUNT} JSON files";
171+
99172
##@ Dependencies
100173

101174
## Location to install dependencies to

packages/gen-ai/bff/cmd/main_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ func TestGeneralBffConfiguration(t *testing.T) {
5151
envVar: "LLAMA_STACK_URL",
5252
varType: "string",
5353
defaultValue: "",
54-
testValue: testutil.TestLlamaStackURL,
54+
testValue: testutil.GetTestLlamaStackURL(),
5555
expectedDefault: "",
56-
expectedSet: testutil.TestLlamaStackURL,
56+
expectedSet: testutil.GetTestLlamaStackURL(),
5757
},
5858
{
5959
name: "LOG_LEVEL environment variable",

0 commit comments

Comments
 (0)