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
214 lines
7.4 KiB
TypeScript
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>
|
|
);
|
|
}
|