Bedrock settings: one auth method at a time

'Connect with' segmented choice (API key / profile / IAM keys) shows only that
method's fields; non-selected fields are dropped at build so stale values can't leak.
This commit is contained in:
Rohit C Prasad
2026-07-25 22:55:28 -07:00
parent 2a882c09d3
commit b719227a9a
6 changed files with 305 additions and 26 deletions
+2
View File
@@ -1265,6 +1265,8 @@ export interface ProviderField {
help: string;
placeholder: string;
default?: string; // pre-filled editable value (e.g. an OpenAI-compatible vendor's endpoint)
choices?: { value: string; label: string }[]; // non-empty → segmented choice, not a text input
show_when?: Record<string, string> | null; // render only while these fields hold these values
}
export interface ProviderInfo {
@@ -0,0 +1,96 @@
// Auth-method segmented choice + show_when field visibility (Bedrock's "Connect with"):
// only the selected method's fields render, and clicking a segment switches them.
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { ProviderForm, type ProviderSetupState } from "./ProviderSetup";
import type { ProviderInfo } from "../api";
vi.mock("../tauri", () => ({ openExternal: vi.fn() }));
afterEach(cleanup);
const BEDROCK: ProviderInfo = {
name: "bedrock",
title: "AWS Bedrock",
needs_key: true,
configured: false,
values: {},
suggested_models: [],
recommended_model: null,
fields: [
{ key: "region", label: "AWS region", secret: false, required: true, help: "", placeholder: "us-east-1" },
{
key: "auth_method",
label: "Connect with",
secret: false,
required: false,
help: "",
placeholder: "",
default: "api_key",
choices: [
{ value: "api_key", label: "Bedrock API key" },
{ value: "profile", label: "AWS profile" },
{ value: "iam", label: "IAM keys" },
],
},
{ key: "bedrock_api_key", label: "Bedrock API key", secret: true, required: false, help: "", placeholder: "ABSK…", show_when: { auth_method: "api_key" } },
{ key: "aws_profile", label: "AWS profile", secret: false, required: false, help: "", placeholder: "default", show_when: { auth_method: "profile" } },
{ key: "aws_secret_access_key", label: "Secret access key", secret: true, required: false, help: "", placeholder: "", show_when: { auth_method: "iam" } },
],
};
function makePs(fields: Record<string, string>, setFieldValue = vi.fn()): ProviderSetupState {
return {
providers: [BEDROCK],
ordered: [BEDROCK],
refreshProviders: async () => {},
sel: "bedrock",
info: BEDROCK,
fields,
setFieldValue,
dirty: false,
verify: { state: "idle" },
showEndpoint: false,
setShowEndpoint: () => {},
keylessOk: new Set(),
credentialed: false,
savedState: false,
secretFilled: true,
openProvider: () => {},
backToGallery: () => {},
runTestAndSave: async () => true,
removeKey: async () => {},
cancelBackTimer: () => {},
statusFor: () => null,
saveField: async () => {},
fieldSaved: null,
};
}
describe("ProviderForm auth-method choice", () => {
it("renders only the selected method's fields", () => {
render(<ProviderForm ps={makePs({ auth_method: "api_key" })} tp="t" />);
expect(screen.getByTestId("t-field-bedrock_api_key")).toBeTruthy();
expect(screen.queryByTestId("t-field-aws_profile")).toBeNull();
expect(screen.queryByTestId("t-field-aws_secret_access_key")).toBeNull();
expect(screen.getByTestId("t-choice-auth_method-api_key").getAttribute("aria-checked")).toBe("true");
});
it("switching the segment swaps the visible fields", () => {
const setFieldValue = vi.fn();
const { rerender } = render(
<ProviderForm ps={makePs({ auth_method: "api_key" }, setFieldValue)} tp="t" />,
);
fireEvent.click(screen.getByTestId("t-choice-auth_method-profile"));
expect(setFieldValue).toHaveBeenCalledWith("auth_method", "profile");
rerender(<ProviderForm ps={makePs({ auth_method: "profile" }, setFieldValue)} tp="t" />);
expect(screen.getByTestId("t-field-aws_profile")).toBeTruthy();
expect(screen.queryByTestId("t-field-bedrock_api_key")).toBeNull();
});
it("iam segment shows the key-pair fields", () => {
render(<ProviderForm ps={makePs({ auth_method: "iam" })} tp="t" />);
expect(screen.getByTestId("t-field-aws_secret_access_key")).toBeTruthy();
expect(screen.queryByTestId("t-field-bedrock_api_key")).toBeNull();
});
});
@@ -341,6 +341,41 @@ export function ProviderForm({
// A keyed provider's base_url is an expert option — it renders BELOW the key-help
// line as its own advanced section (owner nit 2026-07-19), not inside the loop.
if (f.key === "base_url" && keyed) return null;
// Conditional fields (Bedrock's per-auth-method inputs) render only while their
// controlling field holds the matching value.
if (f.show_when && !Object.entries(f.show_when).every(([k, v]) => (ps.fields[k] || "") === v))
return null;
// Segmented choice (e.g. Bedrock's "Connect with"): a button per option, stored
// like any field value. Switching methods requires a fresh Test to save.
if (f.choices?.length)
return (
<div key={f.key}>
<label className={label}>{f.label}</label>
<div className="flex gap-1.5" role="radiogroup" aria-label={f.label}>
{f.choices.map((c) => {
const active = (ps.fields[f.key] || "") === c.value;
return (
<button
key={c.value}
role="radio"
aria-checked={active}
className={
"px-3 py-1.5 rounded-lg border text-[12.5px] transition-colors " +
(active
? "border-accent text-ink font-medium"
: "border-line text-muted hover:border-lineStrong")
}
data-testid={`${tp}-choice-${f.key}-${c.value}`}
onClick={() => ps.setFieldValue(f.key, c.value)}
>
{c.label}
</button>
);
})}
</div>
{f.help && <p className="text-[11.5px] text-faint mt-1">{f.help}</p>}
</div>
);
// Test lives next to the required secret (the API key) when there is one; cloud
// providers whose secrets are all optional (Bedrock, Vertex) test from the first
// field instead — their credentials may be ambient (~/.aws, ADC).