Files
AgentCompany/src/renderer/components/settings/providers/ProviderRow.tsx
T
ZacharyZhang-NY 9a805ae270 Package Boss build 0.4.182 from the current workspace snapshot
Constraint: Boss Windows artifacts are built on a native GitHub runner from a temporary snapshot branch
Rejected: Build Boss Windows locally on Apple Silicon | docker/wine packaging is not stable on this host
Confidence: medium
Scope-risk: narrow
Directive: This snapshot branch is transport-only and should be deleted after the Boss build is recovered locally
Tested: local Boss mac build stages before snapshot creation
Not-tested: remote Boss Windows packaging before workflow execution
2026-05-14 15:55:47 -04:00

214 lines
7.4 KiB
TypeScript

import { useState } from "react";
import type { CredentialSummary } from "@shared/contracts/schemas-credentials";
import type { ConnectorCatalogEntry } from "@shared/connector-catalog";
import { ApiKeyForm } from "../forms/ApiKeyForm";
import { AzureForm } from "../forms/AzureForm";
import { AwsForm } from "../forms/AwsForm";
import { VertexForm } from "../forms/VertexForm";
import { LocalForm } from "../forms/LocalForm";
import { ActionButton } from "../../ui";
import { useT, type TranslationFn } from "../../../i18n";
interface Props {
entry: ConnectorCatalogEntry;
credential: CredentialSummary | null;
}
function kindLabel(
auth: ConnectorCatalogEntry["auth"]["type"],
t: TranslationFn,
): string {
switch (auth) {
case "oauth":
return t("providers.kindOAuth");
case "api_key":
return t("providers.kindApiKey");
case "azure":
return t("providers.kindAzure");
case "aws":
return t("providers.kindAws");
case "gcp_adc":
return t("providers.kindGcp");
case "local":
return t("providers.kindLocal");
}
}
const STATUS_STYLE: Record<string, { dot: string; text: string }> = {
active: { dot: "bg-success", text: "text-success" },
expired: { dot: "bg-warning", text: "text-warning" },
error: { dot: "bg-destructive", text: "text-destructive" },
not_configured: { dot: "bg-muted-foreground/30", text: "text-muted-foreground" },
revoked: { dot: "bg-muted-foreground/30", text: "text-muted-foreground" },
};
function statusLabel(status: string, t: TranslationFn): string {
switch (status) {
case "active":
return t("providers.statusConnected");
case "expired":
return t("providers.statusExpired");
case "error":
return t("providers.statusError");
case "not_configured":
return t("providers.statusNotConfigured");
case "revoked":
return t("providers.statusRevoked");
default:
return status;
}
}
export function ProviderRow({ entry, credential }: Props) {
const t = useT();
const [busy, setBusy] = useState<"login" | "remove" | "test" | null>(null);
const [feedback, setFeedback] = useState<string | null>(null);
const [editing, setEditing] = useState(false);
const status = credential?.status ?? "not_configured";
const statusStyle = STATUS_STYLE[status] ?? STATUS_STYLE.not_configured;
const statusLabelText = statusLabel(status, t);
const hasCredential = credential !== null;
const handleOAuthLogin = async () => {
const desktop = window.agentCompany;
if (!desktop) return;
setBusy("login");
setFeedback(null);
const result = await desktop.startOAuth({ connectorId: entry.id });
setBusy(null);
if (!result.ok) setFeedback(result.error.message);
};
const handleRemove = async () => {
const desktop = window.agentCompany;
if (!desktop) return;
setBusy("remove");
const result = await desktop.removeCredential({ connectorId: entry.id });
setBusy(null);
if (!result.ok) setFeedback(result.error.message);
};
const handleTest = async () => {
const desktop = window.agentCompany;
if (!desktop) return;
setBusy("test");
setFeedback(null);
const result = await desktop.testCredential({ connectorId: entry.id });
setBusy(null);
if (!result.ok) {
setFeedback(result.error.message);
return;
}
setFeedback(
result.data.ok
? t("providers.testOk").replace("{message}", result.data.message)
: t("providers.testFailed").replace("{message}", result.data.message),
);
};
const renderForm = () => {
if (!editing || entry.auth.type === "oauth") return null;
const onSaved = () => {
setEditing(false);
setFeedback(t("providers.saved"));
};
const apiKeyEnv = entry.auth.type === "api_key" ? entry.auth.envKey : undefined;
if (entry.auth.type === "api_key")
return <ApiKeyForm connectorId={entry.id} defaultEnvVar={apiKeyEnv} onSaved={onSaved} />;
if (entry.auth.type === "azure")
return <AzureForm connectorId={entry.id} onSaved={onSaved} />;
if (entry.auth.type === "aws")
return <AwsForm connectorId={entry.id} onSaved={onSaved} />;
if (entry.auth.type === "gcp_adc")
return <VertexForm connectorId={entry.id} onSaved={onSaved} />;
if (entry.auth.type === "local")
return <LocalForm connectorId={entry.id} defaultBaseUrl={entry.auth.defaultBaseUrl} onSaved={onSaved} />;
return null;
};
const disabled = busy !== null;
const isOAuth = entry.auth.type === "oauth";
return (
<li className="px-4 py-3 transition-colors hover:bg-muted/30">
<div className="flex items-center gap-4">
<span
aria-label={t("providers.statusAria").replace("{label}", statusLabelText)}
className={`block h-2 w-2 flex-shrink-0 rounded-full ${statusStyle.dot}`}
/>
<div className="min-w-0 flex-1">
<div className="flex items-baseline gap-2">
<div className="truncate text-[13px] font-semibold text-foreground">
{entry.label}
</div>
<div className="text-[11px] uppercase tracking-[0.08em] text-muted-foreground/80">
{kindLabel(entry.auth.type, t)}
</div>
</div>
<div className="flex items-center gap-2 text-[11px] text-muted-foreground">
<span className="truncate">
{hasCredential && credential.displayHint ? credential.displayHint : statusLabelText}
</span>
{hasCredential && status !== "active" ? (
<span className={`shrink-0 font-medium ${statusStyle.text}`}>· {statusLabelText}</span>
) : null}
</div>
</div>
<div className="flex flex-shrink-0 items-center gap-1">
{isOAuth ? (
<ActionButton
label={
busy === "login"
? t("providers.opening")
: hasCredential
? t("providers.reLogin")
: t("providers.login")
}
disabled={disabled}
onClick={() => void handleOAuthLogin()}
tone={hasCredential ? "default" : "accent"}
/>
) : (
<ActionButton
label={
editing
? t("providers.close")
: hasCredential
? t("providers.edit")
: t("providers.configure")
}
disabled={disabled}
onClick={() => setEditing((current) => !current)}
tone={hasCredential || editing ? "default" : "accent"}
/>
)}
{hasCredential ? (
<ActionButton
label={busy === "test" ? t("providers.testing") : t("providers.test")}
disabled={disabled}
onClick={() => void handleTest()}
/>
) : null}
{hasCredential ? (
<ActionButton
label={busy === "remove" ? t("providers.removing") : t("providers.remove")}
disabled={disabled}
onClick={() => void handleRemove()}
/>
) : null}
</div>
</div>
{feedback ? (
<div className="mt-2 pl-6 text-[11px] text-muted-foreground">{feedback}</div>
) : null}
{editing && !isOAuth ? (
<div className="mt-3 rounded-md border border-border bg-background/60 p-3">
{renderForm()}
</div>
) : null}
</li>
);
}