"use client";
import { useState } from "react";
import { Trash2 } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type {
CredentialBody,
CredentialType,
PublicCredential,
} from "@/lib/api";
// Shared, scope-agnostic credential UI components. The same form/row
// renders both the global Credentials page and the per-agent overrides
// section — the only difference is which API the parent wires into
// `onSubmit`.
export type CredentialRowProps = {
cred: PublicCredential;
onEdit: () => void;
onDelete: () => void;
/** Optional badge string ("inherited", "override", etc.) shown next to the type. */
scopeLabel?: string;
};
export function CredentialRow({ cred, onEdit, onDelete, scopeLabel }: CredentialRowProps) {
return (
{cred.name}
{cred.type}
{scopeLabel && {scopeLabel} }
Edit
);
}
export function CredentialSummary({ cred }: { cred: PublicCredential }) {
if (cred.type === "aws") {
return (
region: {cred.awsRegion}
account: {cred.awsAccountId}
role: {cred.awsCrossAccountRoleArn}
);
}
if (cred.type === "gcp") {
return (
project: {cred.gcpProjectId}
{cred.gcpLocation &&
location: {cred.gcpLocation}
}
SA JSON: {cred.hasServiceAccount ? "set" : "not set"}
);
}
if (cred.type === "kv") {
return (
keys: {(cred.kvKeys ?? []).join(", ") || "(none)"}
);
}
return null;
}
export type CredentialFormProps = {
initial: PublicCredential | null;
/** Called with the full request body (sealed server-side). Throws on failure. */
onSubmit: (body: CredentialBody) => Promise;
onCancel: () => void;
onError: (msg: string | null) => void;
};
export function CredentialForm({ initial, onSubmit, onCancel, onError }: CredentialFormProps) {
const [name, setName] = useState(initial?.name ?? "");
const [type, setType] = useState(initial?.type ?? "aws");
const [awsRegion, setAwsRegion] = useState(initial?.awsRegion ?? "");
const [awsAccountId, setAwsAccountId] = useState(initial?.awsAccountId ?? "");
const [awsRoleArn, setAwsRoleArn] = useState(initial?.awsCrossAccountRoleArn ?? "");
const [gcpProjectId, setGcpProjectId] = useState(initial?.gcpProjectId ?? "");
const [gcpLocation, setGcpLocation] = useState(initial?.gcpLocation ?? "us-central1");
const [gcpSaJson, setGcpSaJson] = useState("");
const seedKv: [string, string][] =
initial?.type === "kv" && initial.kvKeys
? initial.kvKeys.map((k) => [k, ""])
: [["", ""]];
const [kvEntries, setKvEntries] = useState<[string, string][]>(seedKv);
const [saving, setSaving] = useState(false);
const isEdit = !!initial;
async function handleSave() {
onError(null);
setSaving(true);
try {
const trimmedName = name.trim();
if (!trimmedName) throw new Error("name is required");
const body: CredentialBody = { name: trimmedName, type };
if (type === "aws") {
body.awsRegion = awsRegion.trim();
body.awsAccountId = awsAccountId.trim();
body.awsCrossAccountRoleArn = awsRoleArn.trim();
} else if (type === "gcp") {
body.gcpProjectId = gcpProjectId.trim();
body.gcpLocation = gcpLocation.trim() || undefined;
if (gcpSaJson.trim()) body.gcpServiceAccountJson = gcpSaJson;
} else if (type === "kv") {
const kv: Record = {};
for (const [k, v] of kvEntries) {
const key = k.trim();
if (key) kv[key] = v;
}
body.kv = kv;
}
await onSubmit(body);
} catch (e) {
onError(e instanceof Error ? e.message : "save failed");
} finally {
setSaving(false);
}
}
return (
Name
setName(e.target.value)}
placeholder="prod-aws"
disabled={isEdit}
className="font-mono text-xs"
/>
{isEdit && (
Name is immutable. Delete and re-create to rename.
)}
Type
setType(e.target.value as CredentialType)}
disabled={isEdit}
className="h-9 w-full rounded-md border border-input bg-transparent px-2 text-sm"
>
AWS
GCP
Key/value
{type === "aws" && (
)}
{type === "gcp" && (
Service account JSON {isEdit && "(leave empty to keep existing)"}
)}
{type === "kv" && (
)}
Cancel
{saving ? "Saving…" : isEdit ? "Save changes" : "Add credential"}
);
}