Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/gui/0_gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ Click "New Target" to open the creation dialog. Fill in:

- **Target Type** (required): Select from `OpenAIChatTarget`, `OpenAICompletionTarget`, `OpenAIImageTarget`, `OpenAIVideoTarget`, `OpenAITTSTarget`, `OpenAIResponseTarget`, or `AzureMLChatTarget`
- **Endpoint URL** (required): Your Azure OpenAI, OpenAI API, or Azure ML endpoint
- **Model / Deployment Name** (optional): e.g., `gpt-4o`, `dall-e-3`, `Llama-3.2-3B-Instruct`
- **Model / Deployment Name** (required): e.g., `gpt-4o`, `dall-e-3`, `Llama-3.2-3B-Instruct`
- **API Key** (optional): Stored in memory only (not persisted to disk)

For `AzureMLChatTarget`, additional fields are available: **Max New Tokens**, **Temperature**, **Top P**, and **Repetition Penalty**.
Expand Down
53 changes: 40 additions & 13 deletions frontend/e2e/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,8 +456,30 @@ test.describe("Create Target Dialog", () => {
await expect(page.getByText("OpenAIChatTarget")).toBeVisible();
});

test("should show validation errors for empty required fields", async ({ page }) => {
await page.route(/\/api\/targets/, async (route) => {
test("should require an endpoint and validate the identity host", async ({ page }) => {
await page.route(/\/api\/targets\/types(?:\?.*)?$/, async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
items: [
{
target_type: "OpenAIChatTarget",
parameters: [
{
name: "endpoint",
type_name: "str",
required: true,
default: null,
},
],
supported_auth_modes: ["api_key", "identity"],
},
],
}),
});
});
await page.route(/\/api\/targets(?:\?.*)?$/, async (route) => {
await route.fulfill(mockTargetsList([]));
});

Expand All @@ -467,25 +489,30 @@ test.describe("Create Target Dialog", () => {
await page.getByRole("button", { name: /new target/i }).click();
await expect(page.getByText("Create New Target")).toBeVisible();

// The Create Target button should be disabled when fields are empty
const createBtn = page.locator('[role="dialog"]').getByRole("button", { name: "Create Target" });
await expect(createBtn).toBeDisabled();

// Fill only endpoint (no target type) — button should still be disabled
await page.locator('[role="dialog"]').getByPlaceholder("https://your-resource.openai.azure.com/").fill("https://test.com");
const dialog = page.locator('[role="dialog"]');
const createBtn = dialog.getByRole("button", { name: "Create Target" });
await expect(createBtn).toBeDisabled();

// Clear endpoint, select type — button should still be disabled
await page.locator('[role="dialog"]').getByPlaceholder("https://your-resource.openai.azure.com/").fill("");
await selectTargetType(
page,
page.locator('[role="dialog"]'),
dialog,
"OpenAIChatTarget"
);

// Every rendered endpoint field is required.
await expect(createBtn).toBeDisabled();
await dialog.getByPlaceholder("https://your-resource.openai.azure.com/").fill(
"https://api.openai.com"
);
await expect(createBtn).toBeEnabled();

// Identity authentication additionally requires a recognized Azure host.
await dialog.getByRole("radio", { name: /Identity-based/ }).click();
await expect(createBtn).toBeDisabled();

// Fill both — button should be enabled
await page.locator('[role="dialog"]').getByPlaceholder("https://your-resource.openai.azure.com/").fill("https://test.com");
await dialog.getByPlaceholder("https://your-resource.openai.azure.com/").fill(
"https://test.openai.azure.com"
);
await expect(createBtn).toBeEnabled();
});
});
Expand Down
35 changes: 27 additions & 8 deletions frontend/e2e/numeric-controls.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,23 @@ async function mockNumericControlApis(page: Page): Promise<void> {
pagination: { limit: 200, has_more: false },
},
'/api/targets/types': {
items: ['AzureMLChatTarget', 'RoundRobinTarget'].map((targetType) => ({
target_type: targetType,
parameters: [],
supported_auth_modes: ['api_key'],
})),
items: [
{
target_type: 'AzureMLChatTarget',
parameters: [
{ name: 'max_new_tokens', type_name: 'int', required: false, default: '400' },
{ name: 'temperature', type_name: 'float', required: false, default: '1.0' },
{ name: 'top_p', type_name: 'float', required: false, default: '1.0' },
{ name: 'repetition_penalty', type_name: 'float', required: false, default: '1.0' },
],
supported_auth_modes: ['api_key'],
},
{
target_type: 'RoundRobinTarget',
parameters: [],
supported_auth_modes: ['api_key'],
},
],
},
'/api/labels': { source: 'attacks', labels: {} },
[`/api/scenarios/catalog/${SCENARIO.scenario_name}`]: SCENARIO,
Expand Down Expand Up @@ -169,22 +181,29 @@ test('native scenario dataset and shared dynamic parameter controls step once',
await expect(datasetSize).toHaveValue('')
})

test('native Azure ML numeric controls retain their configured steps and reset defaults', async ({ page }) => {
test('native Azure ML advanced controls retain steps and reset to default guidance', async ({ page }) => {
await page.goto('/registry/targets')
await page.getByRole('button', { name: /new target/i }).click()
const dialog = page.getByRole('dialog')
await dialog.getByRole('combobox', { name: 'Target Type' }).click()
await page.getByRole('option', { name: /Implementation: AzureMLChatTarget/ }).click()
await dialog.getByText('Advanced settings').click()

for (const [label, initial] of [['Max New Tokens', 400], ['Temperature', 1], ['Top P', 1], ['Repetition Penalty', 1]] as const) {
await expectNativeSteps(dialog.getByRole('spinbutton', { name: label, exact: true }), initial)
const input = dialog.getByRole('spinbutton', { name: label, exact: true })
await expect(input).toHaveValue('')
await expect(input).toHaveAttribute('placeholder', `Defaults to ${initial === 1 ? '1.0' : initial}`)
await expectNativeSteps(input, initial)
}
await dialog.getByRole('spinbutton', { name: 'Temperature', exact: true }).fill('0.7')
await dialog.getByRole('button', { name: 'Cancel', exact: true }).click()
await page.getByRole('button', { name: /new target/i }).click()
await dialog.getByRole('combobox', { name: 'Target Type' }).click()
await page.getByRole('option', { name: /Implementation: AzureMLChatTarget/ }).click()
await expect(dialog.getByRole('spinbutton', { name: 'Temperature', exact: true })).toHaveValue('1.0')
await dialog.getByText('Advanced settings').click()
const resetTemperature = dialog.getByRole('spinbutton', { name: 'Temperature', exact: true })
await expect(resetTemperature).toHaveValue('')
await expect(resetTemperature).toHaveAttribute('placeholder', 'Defaults to 1.0')
})

test('native round-robin weights step once and preserve bounds and integer validation', async ({ page }) => {
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/components/Config/CreateTargetDialog.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,25 @@ export const useCreateTargetDialogStyles = makeStyles({
overflowWrap: 'anywhere',
wordBreak: 'break-word',
},
advancedSettings: {
minWidth: 0,
border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke2}`,
borderRadius: tokens.borderRadiusMedium,
backgroundColor: tokens.colorNeutralBackground2,
},
advancedSettingsSummary: {
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
color: tokens.colorNeutralForeground1,
fontWeight: tokens.fontWeightSemibold,
cursor: 'pointer',
},
advancedSettingsFields: {
display: 'flex',
flexDirection: 'column',
minWidth: 0,
gap: tokens.spacingVerticalL,
padding: `0 ${tokens.spacingHorizontalM} ${tokens.spacingVerticalM}`,
},
selectedTargetsSection: {
minWidth: 0,
maxWidth: '100%',
Expand Down
Loading
Loading