Skip to content

Commit 202bfb3

Browse files
authored
Merge pull request #812 from grafana/fix-800-provider-switching-ui-consistency
2 parents b390e40 + d45a0d2 commit 202bfb3

File tree

3 files changed

+212
-11
lines changed

3 files changed

+212
-11
lines changed

packages/grafana-llm-app/src/components/AppConfig/LLMConfig.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ export function LLMConfig({
327327
secretsSet={secretsSet}
328328
onChangeSecrets={onChangeSecrets}
329329
allowCustomPath={false}
330+
parentProvider={settings.provider}
330331
/>
331332
<ModelConfig
332333
provider={settings.provider ?? 'openai'}
@@ -382,6 +383,7 @@ export function LLMConfig({
382383
secretsSet={secretsSet}
383384
onChangeSecrets={onChangeSecrets}
384385
allowCustomPath={true}
386+
parentProvider={settings.provider}
385387
/>
386388
<ModelConfig
387389
provider={settings.provider ?? 'custom'}

packages/grafana-llm-app/src/components/AppConfig/OpenAI.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,21 @@ export function OpenAIConfig({
3434
onChange,
3535
onChangeSecrets,
3636
allowCustomPath = false,
37+
parentProvider,
3738
}: {
3839
settings: OpenAISettings;
3940
onChange: (settings: OpenAISettings) => void;
4041
secrets: Secrets;
4142
secretsSet: SecretsSet;
4243
onChangeSecrets: (secrets: Secrets) => void;
4344
allowCustomPath: boolean;
45+
parentProvider?: ProviderType;
4446
}) {
4547
const s = useStyles2(getStyles);
48+
49+
// Get the effective provider - use the stored provider, or default to 'openai' if undefined
50+
const effectiveProvider = settings.provider ?? 'openai';
51+
4652
// Helper to update settings using the name of the HTML event.
4753
const onChangeField = (event: ChangeEvent<HTMLInputElement>) => {
4854
onChange({
@@ -80,7 +86,7 @@ export function OpenAIConfig({
8086

8187
return (
8288
<FieldSet>
83-
{settings.provider !== 'custom' && (
89+
{parentProvider !== 'custom' && (
8490
<Field label="Provider">
8591
<Select
8692
data-testid={testIds.appConfig.provider}
@@ -90,31 +96,31 @@ export function OpenAIConfig({
9096
{ label: 'Azure OpenAI', value: 'azure' },
9197
] as Array<SelectableValue<ProviderType>>
9298
}
93-
value={settings.provider ?? 'openai'}
99+
value={effectiveProvider}
94100
onChange={(e) => onChangeProvider(e.value as ProviderType)}
95101
width={60}
96102
/>
97103
</Field>
98104
)}
99105

100106
<Field
101-
label={settings.provider === 'azure' ? 'Azure OpenAI Language API Endpoint' : 'API URL'}
107+
label={effectiveProvider === 'azure' ? 'Azure OpenAI Language API Endpoint' : 'API URL'}
102108
className={s.marginTop}
103109
>
104110
<Input
105111
width={60}
106112
name="url"
107113
data-testid={testIds.appConfig.openAIUrl}
108-
value={settings.provider === 'openai' ? OPENAI_API_URL : settings.url}
114+
value={effectiveProvider === 'openai' ? OPENAI_API_URL : settings.url}
109115
placeholder={
110-
settings.provider === 'azure'
116+
effectiveProvider === 'azure'
111117
? AZURE_OPENAI_URL_TEMPLATE
112-
: settings.provider === 'openai'
118+
: effectiveProvider === 'openai'
113119
? OPENAI_API_URL
114120
: `https://llm.domain.com`
115121
}
116122
onChange={onChangeField}
117-
disabled={settings.provider === 'openai'}
123+
disabled={effectiveProvider === 'openai' && parentProvider !== 'custom'}
118124
/>
119125
</Field>
120126

@@ -145,20 +151,20 @@ export function OpenAIConfig({
145151
</Field>
146152
)}
147153

148-
<Field label={settings.provider === 'azure' ? 'Azure OpenAI Key' : 'API Key'}>
154+
<Field label={effectiveProvider === 'azure' ? 'Azure OpenAI Key' : 'API Key'}>
149155
<SecretInput
150156
width={60}
151157
data-testid={testIds.appConfig.openAIKey}
152158
name="openAIKey"
153159
value={secrets.openAIKey}
154160
isConfigured={secretsSet.openAIKey ?? false}
155-
placeholder={settings.provider === 'azure' ? '' : 'sk-...'}
161+
placeholder={effectiveProvider === 'azure' ? '' : 'sk-...'}
156162
onChange={(e) => onChangeSecrets({ ...secrets, openAIKey: e.currentTarget.value })}
157163
onReset={() => onChangeSecrets({ ...secrets, openAIKey: '' })}
158164
/>
159165
</Field>
160166

161-
{settings.provider === 'openai' && (
167+
{effectiveProvider === 'openai' && (
162168
<Field label="API Organization ID">
163169
<Input
164170
width={60}
@@ -171,7 +177,7 @@ export function OpenAIConfig({
171177
</Field>
172178
)}
173179

174-
{settings.provider === 'azure' && (
180+
{effectiveProvider === 'azure' && (
175181
<Field label="Azure OpenAI Model Mapping" description="Mapping from model name to Azure deployment name.">
176182
<AzureModelDeploymentConfig
177183
modelMapping={settings.azureModelMapping ?? []}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/**
2+
* End-to-end tests for provider switching UI consistency.
3+
*
4+
* This test suite verifies that when switching between LLM providers (OpenAI, Azure, Custom, Anthropic),
5+
* the UI maintains consistent state and properly shows/hides fields based on the selected provider.
6+
*
7+
* Key behaviors tested:
8+
* - Provider dropdown visibility (hidden for custom, visible for openai/azure)
9+
* - URL field enabled/disabled state (disabled for openai, enabled for azure/custom)
10+
* - Organization ID field visibility (only for openai, not for azure/custom)
11+
* - Custom API path option (only available for custom provider)
12+
* - Field consistency when switching between providers
13+
*
14+
* This addresses a bug where fields would have inconsistent state depending on whether
15+
* settings.openAI.provider was set, particularly when switching to 'custom' provider.
16+
*/
17+
import { test, expect } from '@grafana/plugin-e2e';
18+
import { testIds } from '../src/components/testIds';
19+
20+
test.describe('Provider Switching UI Consistency', () => {
21+
test.beforeEach(async ({ page }) => {
22+
// Navigate to the plugin configuration page
23+
await page.goto('/plugins/grafana-llm-app');
24+
25+
// Wait for the configuration form to load
26+
await page.waitForSelector(`[data-testid="${testIds.appConfig.container}"]`, {
27+
state: 'visible',
28+
timeout: 10000,
29+
});
30+
});
31+
32+
test('should show consistent UI when first switching to custom provider', async ({ page }) => {
33+
// Click on the Custom API card
34+
const customCard = page.locator('text=Use a Custom API').locator('..');
35+
await customCard.click();
36+
37+
// Wait for the card to be selected
38+
await page.waitForTimeout(500);
39+
40+
// Verify provider dropdown is NOT visible (it should be hidden for custom)
41+
const providerDropdown = page.getByTestId(testIds.appConfig.provider);
42+
await expect(providerDropdown).not.toBeVisible();
43+
44+
// Verify URL field is visible and enabled
45+
const urlField = page.getByTestId(testIds.appConfig.openAIUrl);
46+
await expect(urlField).toBeVisible();
47+
await expect(urlField).toBeEnabled();
48+
49+
// Verify Organization ID field is visible (should default to openai mode)
50+
const orgIdField = page.getByTestId(testIds.appConfig.openAIOrganizationID);
51+
await expect(orgIdField).toBeVisible();
52+
53+
// Verify API Key field is visible
54+
const apiKeyField = page.getByTestId(testIds.appConfig.openAIKey);
55+
await expect(apiKeyField).toBeVisible();
56+
57+
// Verify custom API path checkbox is visible
58+
const customPathCheckbox = page.getByTestId(testIds.appConfig.customizeOpenAIApiPath);
59+
await expect(customPathCheckbox).toBeVisible();
60+
});
61+
62+
test('should maintain consistent UI when switching from openai to custom and back', async ({ page }) => {
63+
// First, select OpenAI
64+
const openaiCard = page.locator('text=Use OpenAI-compatible API').locator('..');
65+
await openaiCard.click();
66+
await page.waitForTimeout(500);
67+
68+
// Verify provider dropdown IS visible for OpenAI
69+
let providerDropdown = page.getByTestId(testIds.appConfig.provider);
70+
await expect(providerDropdown).toBeVisible();
71+
72+
// Verify URL field is disabled for OpenAI (always uses api.openai.com)
73+
let urlField = page.getByTestId(testIds.appConfig.openAIUrl);
74+
await expect(urlField).toBeVisible();
75+
await expect(urlField).toBeDisabled();
76+
77+
// Now switch to Custom
78+
const customCard = page.locator('text=Use a Custom API').locator('..');
79+
await customCard.click();
80+
await page.waitForTimeout(500);
81+
82+
// Verify provider dropdown is now hidden
83+
providerDropdown = page.getByTestId(testIds.appConfig.provider);
84+
await expect(providerDropdown).not.toBeVisible();
85+
86+
// Verify URL field is now enabled
87+
urlField = page.getByTestId(testIds.appConfig.openAIUrl);
88+
await expect(urlField).toBeEnabled();
89+
90+
// Verify Organization ID field is still visible
91+
const orgIdField = page.getByTestId(testIds.appConfig.openAIOrganizationID);
92+
await expect(orgIdField).toBeVisible();
93+
94+
// Switch back to OpenAI
95+
await openaiCard.click();
96+
await page.waitForTimeout(500);
97+
98+
// Verify provider dropdown is visible again
99+
providerDropdown = page.getByTestId(testIds.appConfig.provider);
100+
await expect(providerDropdown).toBeVisible();
101+
102+
// Verify URL field is disabled again
103+
urlField = page.getByTestId(testIds.appConfig.openAIUrl);
104+
await expect(urlField).toBeDisabled();
105+
});
106+
107+
test('should hide provider dropdown when in custom mode', async ({ page }) => {
108+
// Select OpenAI first
109+
const openaiCard = page.locator('text=Use OpenAI-compatible API').locator('..');
110+
await openaiCard.click();
111+
await page.waitForTimeout(500);
112+
113+
// Provider dropdown should be visible
114+
const providerDropdown = page.getByTestId(testIds.appConfig.provider);
115+
await expect(providerDropdown).toBeVisible();
116+
117+
// Select Custom
118+
const customCard = page.locator('text=Use a Custom API').locator('..');
119+
await customCard.click();
120+
await page.waitForTimeout(500);
121+
122+
// Provider dropdown should be hidden
123+
await expect(providerDropdown).not.toBeVisible();
124+
});
125+
126+
test('should switch to Anthropic provider correctly', async ({ page }) => {
127+
// Select Anthropic card
128+
const anthropicCard = page.locator('text=Use Anthropic API').locator('..');
129+
await anthropicCard.click();
130+
await page.waitForTimeout(500);
131+
132+
// Verify Anthropic-specific fields are visible
133+
const anthropicUrl = page.getByTestId(testIds.appConfig.anthropicUrl);
134+
const anthropicKey = page.getByTestId(testIds.appConfig.anthropicKey);
135+
136+
await expect(anthropicUrl).toBeVisible();
137+
await expect(anthropicKey).toBeVisible();
138+
139+
// OpenAI-specific fields should not be visible
140+
const openaiUrl = page.getByTestId(testIds.appConfig.openAIUrl);
141+
await expect(openaiUrl).not.toBeVisible();
142+
});
143+
144+
test('should disable LLM features correctly', async ({ page }) => {
145+
// Select the disable card
146+
const disableCard = page.locator('text=Disable all LLM features in Grafana').locator('..');
147+
await disableCard.click();
148+
await page.waitForTimeout(500);
149+
150+
// Configuration fields should not be visible
151+
const openaiUrl = page.getByTestId(testIds.appConfig.openAIUrl);
152+
const anthropicUrl = page.getByTestId(testIds.appConfig.anthropicUrl);
153+
154+
await expect(openaiUrl).not.toBeVisible();
155+
await expect(anthropicUrl).not.toBeVisible();
156+
157+
// Re-enable by selecting OpenAI
158+
const openaiCard = page.locator('text=Use OpenAI-compatible API').locator('..');
159+
await openaiCard.click();
160+
await page.waitForTimeout(500);
161+
162+
// OpenAI fields should be visible again
163+
await expect(openaiUrl).toBeVisible();
164+
});
165+
166+
test('should show confirmation dialog when switching providers with existing model configs', async ({ page }) => {
167+
// This test assumes there might be existing model configurations
168+
// First, select OpenAI and configure it
169+
const openaiCard = page.locator('text=Use OpenAI-compatible API').locator('..');
170+
await openaiCard.click();
171+
await page.waitForTimeout(500);
172+
173+
// Try switching to Anthropic
174+
const anthropicCard = page.locator('text=Use Anthropic API').locator('..');
175+
await anthropicCard.click();
176+
177+
// Check if confirmation dialog appears (it may not if there are no model configs)
178+
const confirmDialog = page.locator('text=Switch LLM Provider?');
179+
180+
// If dialog appears, handle it
181+
const dialogVisible = await confirmDialog.isVisible().catch(() => false);
182+
if (dialogVisible) {
183+
// Click "Switch Provider" to confirm
184+
const confirmButton = page.locator('button', { hasText: 'Switch Provider' });
185+
await confirmButton.click();
186+
await page.waitForTimeout(500);
187+
188+
// Verify we switched to Anthropic
189+
const anthropicUrl = page.getByTestId(testIds.appConfig.anthropicUrl);
190+
await expect(anthropicUrl).toBeVisible();
191+
}
192+
});
193+
});

0 commit comments

Comments
 (0)