Skip to content

Commit 2019dcb

Browse files
authored
feat(mlflow): add BFF status endpoint and DSPA integration (opendatahub-io#7119)
* feat(mlflow): add BFF status endpoint and DSPA integration Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> * Code review Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> * Code review 2 Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> * Code review 3 Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> * Code review 4 Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> * Fixes after rebase Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> * Code review 5 Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> * Code review 6 Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> * Fixes after rebase Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> * Code review 7 Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> * Code review 8 Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> --------- Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com>
1 parent 16da977 commit 2019dcb

File tree

55 files changed

+2452
-267
lines changed

Some content is hidden

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

55 files changed

+2452
-267
lines changed

frontend/src/__mocks__/mockDataSciencePipelinesApplicationK8sResource.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DSPipelineAPIServerStore, DSPipelineKind } from '#~/k8sTypes';
1+
import { DSPAMlflowIntegrationMode, DSPipelineAPIServerStore, DSPipelineKind } from '#~/k8sTypes';
22

33
type MockResourceConfigType = {
44
name?: string;
@@ -10,6 +10,7 @@ type MockResourceConfigType = {
1010
dspaSecretName?: string;
1111
pipelineStore?: DSPipelineAPIServerStore;
1212
cacheEnabled?: boolean;
13+
mlflowIntegrationMode?: DSPAMlflowIntegrationMode;
1314
};
1415

1516
export const mockDataSciencePipelineApplicationK8sResource = ({
@@ -21,6 +22,7 @@ export const mockDataSciencePipelineApplicationK8sResource = ({
2122
dspaSecretName = 'aws-connection-testdb',
2223
pipelineStore,
2324
cacheEnabled = true,
25+
mlflowIntegrationMode,
2426
}: MockResourceConfigType): DSPipelineKind => ({
2527
apiVersion: 'datasciencepipelinesapplications.opendatahub.io/v1',
2628
kind: 'DataSciencePipelinesApplication',
@@ -42,6 +44,9 @@ export const mockDataSciencePipelineApplicationK8sResource = ({
4244
username: 'mlpipeline',
4345
},
4446
},
47+
...(mlflowIntegrationMode !== undefined && {
48+
mlflow: { integrationMode: mlflowIntegrationMode },
49+
}),
4550
objectStorage: {
4651
externalStorage: {
4752
region: 'us-east-2',

frontend/src/concepts/areas/const.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export const techPreviewFlags = {
88
modelAsService: true,
99
maasAuthPolicies: true,
1010
aiAssetCustomEndpoints: false,
11-
mlflowPipelines: false,
1211
mcpCatalog: false,
1312
projectRBAC: true,
1413
observabilityDashboard: false,
@@ -22,6 +21,7 @@ export const techPreviewFlags = {
2221
export const devTemporaryFeatureFlags = {
2322
disableKueue: true,
2423
disableProjectScoped: true,
24+
mlflowPipelines: false,
2525
} satisfies Partial<DashboardCommonConfig>;
2626

2727
// Group 1: Core Dashboard Features

frontend/src/concepts/mlflow/MlflowExperimentSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
import useTableColumnSort from '#~/components/table/useTableColumnSort';
1717
import { MlflowExperiment, MlflowSelectorStatus } from './types';
1818
import { mlflowExperimentColumns } from './columns';
19-
import useMlflowExperiments from './useMlflowExperiments';
19+
import useMlflowExperiments from './hooks/useMlflowExperiments';
2020
import MlflowExperimentTable from './MlflowExperimentTable';
2121

2222
type MlflowExperimentSelectorProps = {

frontend/src/concepts/mlflow/__tests__/MlflowExperimentSelector.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import * as React from 'react';
22
import { render, screen } from '@testing-library/react';
33
import '@testing-library/jest-dom';
44
import MlflowExperimentSelector from '#~/concepts/mlflow/MlflowExperimentSelector';
5-
import useMlflowExperiments from '#~/concepts/mlflow/useMlflowExperiments';
5+
import useMlflowExperiments from '#~/concepts/mlflow/hooks/useMlflowExperiments';
66
import useTableColumnSort from '#~/components/table/useTableColumnSort';
77

8-
jest.mock('#~/concepts/mlflow/useMlflowExperiments');
8+
jest.mock('#~/concepts/mlflow/hooks/useMlflowExperiments');
99
jest.mock('#~/components/table/useTableColumnSort');
1010
jest.mock('#~/concepts/mlflow/MlflowExperimentTable', () => {
1111
const MockMlflowExperimentTable = (props: { data: unknown[] }) => (

frontend/src/concepts/mlflow/const.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
export const EXPERIMENTS_ENDPOINT = '/_bff/mlflow/api/v1/experiments';
1+
export const BFF_API_PREFIX = '/_bff/mlflow/api/v1';
2+
export const STATUS_ENDPOINT = `${BFF_API_PREFIX}/status`;
3+
export const EXPERIMENTS_ENDPOINT = `${BFF_API_PREFIX}/experiments`;
24
export const FILTER_PARAM_KEY = 'filter';
35

46
export const EXPERIMENT_NAME_COLUMN_WIDTH = 60;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { testHook } from '@odh-dashboard/jest-config/hooks';
2+
import { SupportedArea, useIsAreaAvailable } from '#~/concepts/areas';
3+
import { useMLflowStatus } from '#~/concepts/mlflow/hooks/useMLflowStatus';
4+
import useIsMlflowCRAvailable from '#~/concepts/mlflow/hooks/useIsMlflowCRAvailable';
5+
6+
jest.mock('#~/concepts/areas', () => ({
7+
...jest.requireActual('#~/concepts/areas'),
8+
useIsAreaAvailable: jest.fn(),
9+
}));
10+
11+
jest.mock('#~/concepts/mlflow/hooks/useMLflowStatus', () => ({
12+
useMLflowStatus: jest.fn(),
13+
}));
14+
15+
const mockUseIsAreaAvailable = jest.mocked(useIsAreaAvailable);
16+
const mockUseMLflowStatus = jest.mocked(useMLflowStatus);
17+
18+
const mockAreaAvailable = (status: boolean) => {
19+
mockUseIsAreaAvailable.mockReturnValue({
20+
status,
21+
devFlags: null,
22+
featureFlags: null,
23+
reliantAreas: null,
24+
requiredComponents: null,
25+
requiredCapabilities: null,
26+
customCondition: () => false,
27+
});
28+
};
29+
30+
describe('useIsMlflowCRAvailable', () => {
31+
beforeEach(() => {
32+
jest.clearAllMocks();
33+
mockAreaAvailable(true);
34+
mockUseMLflowStatus.mockReturnValue({ configured: true, loaded: true });
35+
});
36+
37+
it('should use SupportedArea.MLFLOW', () => {
38+
testHook(useIsMlflowCRAvailable)();
39+
expect(mockUseIsAreaAvailable).toHaveBeenCalledWith(SupportedArea.MLFLOW);
40+
});
41+
42+
it('should pass area availability as shouldFetch to useMLflowStatus', () => {
43+
testHook(useIsMlflowCRAvailable)();
44+
expect(mockUseMLflowStatus).toHaveBeenCalledWith(true);
45+
46+
jest.clearAllMocks();
47+
mockAreaAvailable(false);
48+
mockUseMLflowStatus.mockReturnValue({ configured: false, loaded: true });
49+
50+
testHook(useIsMlflowCRAvailable)();
51+
expect(mockUseMLflowStatus).toHaveBeenCalledWith(false);
52+
});
53+
54+
it('should return available=false when area is not available', () => {
55+
mockAreaAvailable(false);
56+
mockUseMLflowStatus.mockReturnValue({ configured: false, loaded: true });
57+
58+
const { result } = testHook(useIsMlflowCRAvailable)();
59+
expect(result.current).toStrictEqual({ available: false, loaded: true });
60+
});
61+
62+
it('should return available=true when BFF reports configured', () => {
63+
const { result } = testHook(useIsMlflowCRAvailable)();
64+
expect(result.current).toStrictEqual({ available: true, loaded: true });
65+
});
66+
67+
it('should return available=false and loaded=false when BFF is not loaded yet', () => {
68+
mockUseMLflowStatus.mockReturnValue({ configured: false, loaded: false });
69+
70+
const { result } = testHook(useIsMlflowCRAvailable)();
71+
expect(result.current).toStrictEqual({ available: false, loaded: false });
72+
});
73+
74+
it('should return available=false when MLflow is not configured', () => {
75+
mockUseMLflowStatus.mockReturnValue({ configured: false, loaded: true });
76+
77+
const { result } = testHook(useIsMlflowCRAvailable)();
78+
expect(result.current).toStrictEqual({ available: false, loaded: true });
79+
});
80+
81+
it('should return loaded=true when area is disabled (no BFF poll needed)', () => {
82+
mockAreaAvailable(false);
83+
mockUseMLflowStatus.mockReturnValue({ configured: false, loaded: false });
84+
85+
const { result } = testHook(useIsMlflowCRAvailable)();
86+
expect(result.current).toStrictEqual({ available: false, loaded: true });
87+
});
88+
});
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { testHook } from '@odh-dashboard/jest-config/hooks';
2+
import { usePipelinesAPI } from '#~/concepts/pipelines/context';
3+
import { DSPAMlflowIntegrationMode, ProjectKind } from '#~/k8sTypes';
4+
import useIsMlflowDSPAEnabled from '#~/concepts/mlflow/hooks/useIsMlflowDSPAEnabled';
5+
6+
jest.mock('#~/concepts/pipelines/context', () => ({
7+
usePipelinesAPI: jest.fn(),
8+
}));
9+
10+
const mockUsePipelinesAPI = jest.mocked(usePipelinesAPI);
11+
12+
const basePipelinesAPI = {
13+
namespace: 'test-ns',
14+
project: {} as ProjectKind,
15+
refreshAllAPI: jest.fn(),
16+
getRecurringRunInformation: jest.fn(),
17+
metadataStoreServiceClient: {} as never,
18+
refreshState: jest.fn(),
19+
managedPipelines: undefined,
20+
mlflowIntegrationMode: undefined,
21+
apiAvailable: false,
22+
api: {} as never,
23+
pipelineLoadError: undefined,
24+
};
25+
26+
describe('useIsMlflowDSPAEnabled', () => {
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
});
30+
31+
it('should return enabled=false, loaded=false while DSPA is initializing', () => {
32+
mockUsePipelinesAPI.mockReturnValue({
33+
...basePipelinesAPI,
34+
pipelinesServer: {
35+
initializing: true,
36+
installed: false,
37+
compatible: false,
38+
timedOut: false,
39+
name: '',
40+
crStatus: undefined,
41+
isStarting: false,
42+
},
43+
mlflowIntegrationMode: undefined,
44+
});
45+
46+
const renderResult = testHook(useIsMlflowDSPAEnabled)();
47+
expect(renderResult.result.current).toStrictEqual({ enabled: false, loaded: false });
48+
});
49+
50+
it('should return enabled=false, loaded=true when pipelines server is not installed', () => {
51+
mockUsePipelinesAPI.mockReturnValue({
52+
...basePipelinesAPI,
53+
pipelinesServer: {
54+
initializing: false,
55+
installed: false,
56+
compatible: false,
57+
timedOut: false,
58+
name: '',
59+
crStatus: undefined,
60+
isStarting: false,
61+
},
62+
mlflowIntegrationMode: undefined,
63+
});
64+
65+
const renderResult = testHook(useIsMlflowDSPAEnabled)();
66+
expect(renderResult.result.current).toStrictEqual({ enabled: false, loaded: true });
67+
});
68+
69+
it('should return enabled=false, loaded=true when pipelines server is not compatible', () => {
70+
mockUsePipelinesAPI.mockReturnValue({
71+
...basePipelinesAPI,
72+
pipelinesServer: {
73+
initializing: false,
74+
installed: true,
75+
compatible: false,
76+
timedOut: false,
77+
name: 'dspa',
78+
crStatus: undefined,
79+
isStarting: false,
80+
},
81+
mlflowIntegrationMode: undefined,
82+
});
83+
84+
const renderResult = testHook(useIsMlflowDSPAEnabled)();
85+
expect(renderResult.result.current).toStrictEqual({ enabled: false, loaded: true });
86+
});
87+
88+
it('should return enabled=true when integrationMode is undefined (omitted)', () => {
89+
mockUsePipelinesAPI.mockReturnValue({
90+
...basePipelinesAPI,
91+
pipelinesServer: {
92+
initializing: false,
93+
installed: true,
94+
compatible: true,
95+
timedOut: false,
96+
name: 'dspa',
97+
crStatus: undefined,
98+
isStarting: false,
99+
},
100+
mlflowIntegrationMode: undefined,
101+
});
102+
103+
const renderResult = testHook(useIsMlflowDSPAEnabled)();
104+
expect(renderResult.result.current).toStrictEqual({ enabled: true, loaded: true });
105+
});
106+
107+
it('should return enabled=true when integrationMode is AUTODETECT', () => {
108+
mockUsePipelinesAPI.mockReturnValue({
109+
...basePipelinesAPI,
110+
pipelinesServer: {
111+
initializing: false,
112+
installed: true,
113+
compatible: true,
114+
timedOut: false,
115+
name: 'dspa',
116+
crStatus: undefined,
117+
isStarting: false,
118+
},
119+
mlflowIntegrationMode: DSPAMlflowIntegrationMode.AUTODETECT,
120+
});
121+
122+
const renderResult = testHook(useIsMlflowDSPAEnabled)();
123+
expect(renderResult.result.current).toStrictEqual({ enabled: true, loaded: true });
124+
});
125+
126+
it('should return enabled=false when integrationMode is DISABLED', () => {
127+
mockUsePipelinesAPI.mockReturnValue({
128+
...basePipelinesAPI,
129+
pipelinesServer: {
130+
initializing: false,
131+
installed: true,
132+
compatible: true,
133+
timedOut: false,
134+
name: 'dspa',
135+
crStatus: undefined,
136+
isStarting: false,
137+
},
138+
mlflowIntegrationMode: DSPAMlflowIntegrationMode.DISABLED,
139+
});
140+
141+
const renderResult = testHook(useIsMlflowDSPAEnabled)();
142+
expect(renderResult.result.current).toStrictEqual({ enabled: false, loaded: true });
143+
});
144+
145+
it('should return enabled=false for unknown integrationMode values', () => {
146+
mockUsePipelinesAPI.mockReturnValue({
147+
...basePipelinesAPI,
148+
pipelinesServer: {
149+
initializing: false,
150+
installed: true,
151+
compatible: true,
152+
timedOut: false,
153+
name: 'dspa',
154+
crStatus: undefined,
155+
isStarting: false,
156+
},
157+
mlflowIntegrationMode: 'SomeFutureValue' as DSPAMlflowIntegrationMode,
158+
});
159+
160+
const renderResult = testHook(useIsMlflowDSPAEnabled)();
161+
expect(renderResult.result.current).toStrictEqual({ enabled: false, loaded: true });
162+
});
163+
});

0 commit comments

Comments
 (0)