Skip to content

Commit 3f0648c

Browse files
Taj010Ambient Code Botclaude
authored
fix(model-serving): gate AAA checkbox behind genAiStudio feature flag (#6926)
* fix(model-serving): gate AAA checkbox behind genAiStudio feature flag The Available AI Assets (AAA) checkbox in the model deployment wizard was showing for all generative models, even when Gen AI Studio was not available on the cluster. This created a confusing UX where users could enable a feature that wouldn't work. Changes: - Added useIsAreaAvailable hook check for 'plugin-gen-ai' area - Wrapped AAA checkbox in conditional rendering (isGenAiEnabled check) - Added same conditional to use case input field to prevent orphaned UI elements - Updated tests to mock useIsAreaAvailable and cover both enabled/disabled states The fix follows the existing pattern from ModelTypeSelectField.tsx which uses the same approach for conditional feature rendering. Testing: - Updated ModelAvailabilityFields.test.tsx with mock for useIsAreaAvailable - Added test coverage for both area available and unavailable states - Verified checkbox and use case input are hidden when genAiStudio is disabled Fixes https://redhat.atlassian.net/browse/RHOAIENG-37896 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * adjust some test and fix lint errors * adress coderabbit commnet and test failing * adjust the title showing when feature flag is off * fix * test(cypress): mock LLAMA_STACK_OPERATOR for PLUGIN_GEN_AI in deploy wizard DSC status must include llamastackoperator when gen-ai extension is loaded, so save-as-ai-asset checkbox appears and modelServingDeploy tests pass in CI. Made-with: Cursor * address comments * fix lint * fix type check --------- Co-authored-by: Ambient Code Bot <bot@ambient-code.local> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 5b22aa3 commit 3f0648c

9 files changed

Lines changed: 298 additions & 68 deletions

File tree

frontend/src/concepts/areas/const.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ export const SupportedAreasStateMap: SupportedAreasState = {
239239
featureFlags: ['llmGatewayField'],
240240
reliantAreas: [SupportedArea.LLMD_SERVING],
241241
},
242+
[SupportedArea.PLUGIN_GEN_AI]: {
243+
featureFlags: ['genAiStudio'],
244+
},
242245
[SupportedArea.MAAS_AUTH_POLICIES]: {
243246
featureFlags: ['maasAuthPolicies'],
244247
},

frontend/src/concepts/areas/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export enum SupportedArea {
8080

8181
/* Plugins */
8282
PLUGIN_MODEL_SERVING = 'plugin-model-serving',
83+
PLUGIN_GEN_AI = 'plugin-gen-ai',
8384

8485
/* RAG & Agentic */
8586
LLAMA_STACK_CHAT_BOT = 'llama-stack-chat-bot',

packages/cypress/cypress/tests/mocked/modelServing/modelServingDeploy.cy.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ const initIntercepts = ({
6969
mockDscStatus({
7070
components: {
7171
[DataScienceStackComponent.K_SERVE]: { managementState: 'Managed' },
72+
// Gen AI plugin registers PLUGIN_GEN_AI with this required component; without it the
73+
// save-as-ai-asset UI stays hidden while genAiStudio is still true in dashboard config.
74+
[DataScienceStackComponent.LLAMA_STACK_OPERATOR]: { managementState: 'Managed' },
7275
},
7376
}),
7477
);
@@ -79,6 +82,7 @@ const initIntercepts = ({
7982
disableKServe: false,
8083
deploymentWizardYAMLViewer: true,
8184
vLLMDeploymentOnMaaS,
85+
genAiStudio: true,
8286
}),
8387
);
8488
// used by addSupportServingPlatformProject

packages/model-serving/src/__tests__/mockUtils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ export const mockDeploymentWizardState = (
216216
useCase: '',
217217
},
218218
setData: jest.fn(),
219+
isGenAiEnabled: true,
219220
},
220221
modelServer: {
221222
data: undefined,

packages/model-serving/src/components/deploymentWizard/fields/ModelAvailabilityFields.tsx

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '@patternfly/react-core';
1313
import { z } from 'zod';
1414
import { ServingRuntimeModelType } from '@odh-dashboard/internal/types';
15+
import { SupportedArea, useIsAreaAvailable } from '@odh-dashboard/internal/concepts/areas';
1516
import { ModelTypeFieldData } from './ModelTypeSelectField';
1617
import { GenericFieldRenderer } from './GenericFieldRenderer';
1718
import type { UseModelDeploymentWizardState } from '../useDeploymentWizard';
@@ -25,6 +26,7 @@ export type ModelAvailabilityFieldsData = {
2526
export type ModelAvailabilityFields = {
2627
data: ModelAvailabilityFieldsData;
2728
setData: (data: ModelAvailabilityFieldsData) => void;
29+
isGenAiEnabled: boolean;
2830
showField?: boolean;
2931
showSaveAsMaaS?: boolean;
3032
};
@@ -42,6 +44,8 @@ export const useModelAvailabilityFields = (
4244
existingData?: ModelAvailabilityFieldsData,
4345
modelType?: ModelTypeFieldData,
4446
): ModelAvailabilityFields => {
47+
const isGenAiEnabled = useIsAreaAvailable(SupportedArea.PLUGIN_GEN_AI).status;
48+
4549
const [data, setData] = React.useState<ModelAvailabilityFieldsData>(
4650
existingData ?? {
4751
saveAsAiAsset: false,
@@ -56,26 +60,32 @@ export const useModelAvailabilityFields = (
5660
useCase: '',
5761
};
5862
}
63+
if (!isGenAiEnabled) {
64+
return { ...data, saveAsAiAsset: false, useCase: '' };
65+
}
5966
return data;
60-
}, [data, modelType]);
67+
}, [data, modelType, isGenAiEnabled]);
6168

6269
return {
6370
data: AiAssetData,
6471
setData,
72+
isGenAiEnabled,
6573
showField: modelType?.type === ServingRuntimeModelType.GENERATIVE,
6674
};
6775
};
6876

6977
type AvailableAiAssetsFieldsComponentProps = {
7078
data: ModelAvailabilityFieldsData;
7179
setData: (data: ModelAvailabilityFieldsData) => void;
80+
isGenAiEnabled: boolean;
7281
wizardState: UseModelDeploymentWizardState;
7382
externalData?: ExternalDataMap;
7483
};
7584

7685
export const AvailableAiAssetsFieldsComponent: React.FC<AvailableAiAssetsFieldsComponentProps> = ({
7786
data,
7887
setData,
88+
isGenAiEnabled,
7989
wizardState,
8090
externalData,
8191
}) => {
@@ -93,30 +103,34 @@ export const AvailableAiAssetsFieldsComponent: React.FC<AvailableAiAssetsFieldsC
93103
return (
94104
<StackItem>
95105
<Stack hasGutter>
96-
<StackItem>
97-
<Checkbox
98-
id="save-as-ai-asset-checkbox"
99-
data-testid="save-as-ai-asset-checkbox"
100-
label={
101-
<>
102-
<div className="pf-v6-c-form__label-text">Publish as AI asset endpoint</div>
103-
<Flex>
104-
<FlexItem>
105-
Publishing as an AI asset endpoint allows users with access to your project to
106-
test the model in the{' '}
107-
<span className="pf-v6-c-form__label-text">Playground</span>.
108-
</FlexItem>
109-
<Label isCompact color="yellow" variant="outline">
110-
Tech preview
111-
</Label>
112-
</Flex>
113-
</>
114-
}
115-
isChecked={data.saveAsAiAsset}
116-
onChange={(_, checked) => setDataWithClearUseCase({ ...data, saveAsAiAsset: checked })}
117-
/>
118-
</StackItem>
119-
{data.saveAsAiAsset && (
106+
{isGenAiEnabled && (
107+
<StackItem>
108+
<Checkbox
109+
id="save-as-ai-asset-checkbox"
110+
data-testid="save-as-ai-asset-checkbox"
111+
label={
112+
<>
113+
<div className="pf-v6-c-form__label-text">Add as AI asset endpoint</div>
114+
<Flex>
115+
<FlexItem>
116+
Publishing as an AI asset endpoint allows users with access to your project to
117+
test the model in the{' '}
118+
<span className="pf-v6-c-form__label-text">Playground</span>.
119+
</FlexItem>
120+
<Label isCompact color="yellow" variant="outline">
121+
Tech preview
122+
</Label>
123+
</Flex>
124+
</>
125+
}
126+
isChecked={data.saveAsAiAsset}
127+
onChange={(_, checked) =>
128+
setDataWithClearUseCase({ ...data, saveAsAiAsset: checked })
129+
}
130+
/>
131+
</StackItem>
132+
)}
133+
{isGenAiEnabled && data.saveAsAiAsset && (
120134
<StackItem>
121135
<div style={{ marginLeft: 'var(--pf-t--global--spacer--lg)' }}>
122136
<FormGroup label="Use case">

packages/model-serving/src/components/deploymentWizard/fields/__tests__/ModelAvailabilityFields.test.tsx

Lines changed: 127 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,42 @@
11
import React, { act } from 'react';
22
import { render, screen, renderHook } from '@testing-library/react';
3+
import { useIsAreaAvailable } from '@odh-dashboard/internal/concepts/areas';
4+
import type { IsAreaAvailableStatus } from '@odh-dashboard/internal/concepts/areas/types';
35
import { ServingRuntimeModelType } from '@odh-dashboard/internal/types';
6+
import { mockExtensions } from '../../../../__tests__/mockUtils';
7+
import type { UseModelDeploymentWizardState } from '../../useDeploymentWizard';
48
import {
59
modelAvailabilityFieldsSchema,
610
AvailableAiAssetsFieldsComponent,
711
isValidModelAvailabilityFieldsData,
812
useModelAvailabilityFields,
913
} from '../ModelAvailabilityFields';
10-
import { mockExtensions } from '../../../../__tests__/mockUtils';
11-
import type { UseModelDeploymentWizardState } from '../../useDeploymentWizard';
1214

1315
jest.mock('@odh-dashboard/plugin-core');
16+
jest.mock('@odh-dashboard/internal/concepts/areas', () => ({
17+
...jest.requireActual('@odh-dashboard/internal/concepts/areas'),
18+
useIsAreaAvailable: jest.fn(),
19+
}));
20+
21+
const mockUseIsAreaAvailable = jest.mocked(useIsAreaAvailable);
22+
23+
const mockAreaAvailabilityStatus = (status: boolean): IsAreaAvailableStatus => ({
24+
status,
25+
devFlags: null,
26+
featureFlags: null,
27+
reliantAreas: null,
28+
requiredCapabilities: null,
29+
requiredComponents: null,
30+
customCondition: () => false,
31+
});
1432

1533
const mockWizardState: UseModelDeploymentWizardState = {
1634
fields: [],
1735
} as unknown as UseModelDeploymentWizardState;
1836

1937
describe('AvailableAiAssetsFields', () => {
2038
beforeEach(() => {
39+
jest.clearAllMocks();
2140
mockExtensions([]);
2241
});
2342

@@ -51,10 +70,15 @@ describe('AvailableAiAssetsFields', () => {
5170
});
5271
});
5372
describe('useAvailableAiAssetsFields', () => {
73+
beforeEach(() => {
74+
mockUseIsAreaAvailable.mockReturnValue(mockAreaAvailabilityStatus(true));
75+
});
76+
5477
it('should initialize with false by default', () => {
5578
const { result } = renderHook(() => useModelAvailabilityFields());
5679
expect(result.current.data.saveAsAiAsset).toBe(false);
5780
expect(result.current.data.useCase).toBe('');
81+
expect(result.current.isGenAiEnabled).toBe(true);
5882
});
5983
it('should initialize with existing data', () => {
6084
const { result } = renderHook(() =>
@@ -71,45 +95,112 @@ describe('AvailableAiAssetsFields', () => {
7195
expect(result.current.data.saveAsAiAsset).toBe(true);
7296
expect(result.current.data.useCase).toBe('test');
7397
});
98+
it('should force-clear saveAsAiAsset and useCase when Gen AI is disabled', () => {
99+
mockUseIsAreaAvailable.mockReturnValue(mockAreaAvailabilityStatus(false));
100+
const { result } = renderHook(() =>
101+
useModelAvailabilityFields({ saveAsAiAsset: true, useCase: 'chat' }),
102+
);
103+
expect(result.current.data.saveAsAiAsset).toBe(false);
104+
expect(result.current.data.useCase).toBe('');
105+
expect(result.current.isGenAiEnabled).toBe(false);
106+
});
74107
});
75108
describe('AvailableAiAssetsFieldsComponent', () => {
76-
it('should render with default props', () => {
77-
render(
78-
<AvailableAiAssetsFieldsComponent
79-
data={{ saveAsAiAsset: false, useCase: '' }}
80-
setData={jest.fn()}
81-
wizardState={mockWizardState}
82-
/>,
83-
);
84-
expect(screen.getByTestId('save-as-ai-asset-checkbox')).toBeInTheDocument();
85-
expect(screen.getByTestId('save-as-ai-asset-checkbox')).not.toBeChecked();
86-
});
87-
it('should render with saveAsAiAsset true', () => {
88-
render(
89-
<AvailableAiAssetsFieldsComponent
90-
data={{ saveAsAiAsset: true, useCase: '' }}
91-
setData={jest.fn()}
92-
wizardState={mockWizardState}
93-
/>,
94-
);
95-
expect(screen.getByTestId('save-as-ai-asset-checkbox')).toBeInTheDocument();
96-
expect(screen.getByTestId('save-as-ai-asset-checkbox')).toBeChecked();
97-
});
98-
it('should render with useCase input', () => {
99-
render(
100-
<AvailableAiAssetsFieldsComponent
101-
data={{ saveAsAiAsset: true, useCase: 'test' }}
102-
setData={jest.fn()}
103-
wizardState={mockWizardState}
104-
/>,
105-
);
106-
expect(screen.getByTestId('save-as-ai-asset-checkbox')).toBeInTheDocument();
107-
expect(screen.getByTestId('save-as-ai-asset-checkbox')).toBeChecked();
108-
expect(screen.getByTestId('use-case-input')).toBeInTheDocument();
109-
expect(screen.getByTestId('use-case-input')).toHaveValue('test');
109+
describe('when Gen AI Studio area is available', () => {
110+
beforeEach(() => {
111+
mockUseIsAreaAvailable.mockReturnValue(mockAreaAvailabilityStatus(true));
112+
});
113+
114+
it('should render with default props', () => {
115+
render(
116+
<AvailableAiAssetsFieldsComponent
117+
data={{ saveAsAiAsset: false, useCase: '' }}
118+
setData={jest.fn()}
119+
isGenAiEnabled
120+
wizardState={mockWizardState}
121+
/>,
122+
);
123+
expect(screen.getByTestId('save-as-ai-asset-checkbox')).toBeInTheDocument();
124+
expect(screen.getByTestId('save-as-ai-asset-checkbox')).not.toBeChecked();
125+
});
126+
127+
it('should render with saveAsAiAsset true', () => {
128+
render(
129+
<AvailableAiAssetsFieldsComponent
130+
data={{ saveAsAiAsset: true, useCase: '' }}
131+
setData={jest.fn()}
132+
isGenAiEnabled
133+
wizardState={mockWizardState}
134+
/>,
135+
);
136+
expect(screen.getByTestId('save-as-ai-asset-checkbox')).toBeInTheDocument();
137+
expect(screen.getByTestId('save-as-ai-asset-checkbox')).toBeChecked();
138+
});
139+
140+
it('should render with useCase input', () => {
141+
render(
142+
<AvailableAiAssetsFieldsComponent
143+
data={{ saveAsAiAsset: true, useCase: 'test' }}
144+
setData={jest.fn()}
145+
isGenAiEnabled
146+
wizardState={mockWizardState}
147+
/>,
148+
);
149+
expect(screen.getByTestId('save-as-ai-asset-checkbox')).toBeInTheDocument();
150+
expect(screen.getByTestId('save-as-ai-asset-checkbox')).toBeChecked();
151+
expect(screen.getByTestId('use-case-input')).toBeInTheDocument();
152+
expect(screen.getByTestId('use-case-input')).toHaveValue('test');
153+
});
154+
});
155+
156+
describe('when Gen AI Studio area is NOT available', () => {
157+
beforeEach(() => {
158+
mockUseIsAreaAvailable.mockReturnValue(mockAreaAvailabilityStatus(false));
159+
});
160+
161+
it('should not render checkbox when area is disabled', () => {
162+
render(
163+
<AvailableAiAssetsFieldsComponent
164+
data={{ saveAsAiAsset: false, useCase: '' }}
165+
setData={jest.fn()}
166+
isGenAiEnabled={false}
167+
wizardState={mockWizardState}
168+
/>,
169+
);
170+
expect(screen.queryByTestId('save-as-ai-asset-checkbox')).not.toBeInTheDocument();
171+
});
172+
173+
it('should not render checkbox even if saveAsAiAsset is true', () => {
174+
render(
175+
<AvailableAiAssetsFieldsComponent
176+
data={{ saveAsAiAsset: true, useCase: 'test' }}
177+
setData={jest.fn()}
178+
isGenAiEnabled={false}
179+
wizardState={mockWizardState}
180+
/>,
181+
);
182+
expect(screen.queryByTestId('save-as-ai-asset-checkbox')).not.toBeInTheDocument();
183+
expect(screen.queryByTestId('use-case-input')).not.toBeInTheDocument();
184+
});
185+
186+
it('should not render use case input when area is disabled', () => {
187+
render(
188+
<AvailableAiAssetsFieldsComponent
189+
data={{ saveAsAiAsset: true, useCase: 'existing data' }}
190+
setData={jest.fn()}
191+
isGenAiEnabled={false}
192+
wizardState={mockWizardState}
193+
/>,
194+
);
195+
expect(screen.queryByTestId('use-case-input')).not.toBeInTheDocument();
196+
});
110197
});
111198
});
112199
describe('useModelAvailabilityFields hook visibility logic', () => {
200+
beforeEach(() => {
201+
mockUseIsAreaAvailable.mockReturnValue(mockAreaAvailabilityStatus(true));
202+
});
203+
113204
it('should show field when model type is generative', () => {
114205
const { result } = renderHook(() =>
115206
useModelAvailabilityFields(undefined, {

0 commit comments

Comments
 (0)