Files
CrossRelay/src/pages/Providers.tsx
T

149 lines
5.5 KiB
TypeScript

import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Plus, Trash2, Pencil } from "lucide-react";
import type { Provider } from "@/lib/types";
import { useProviders, useProviderMutations } from "@/lib/queries";
import { useToast } from "@/components/toast";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ProviderForm, type ProviderSubmitInput } from "@/components/ProviderForm";
export default function Providers() {
const { t } = useTranslation();
const { data, isLoading, isError } = useProviders("claude");
const mutations = useProviderMutations();
const { show } = useToast();
const [editing, setEditing] = useState<{ provider: Provider | null } | null>(null);
const [lastAppliedEnv, setLastAppliedEnv] = useState<Record<string, string> | null>(null);
const providers = data?.items ?? [];
const submitForm = (input: ProviderSubmitInput) => {
const target = editing?.provider ?? null;
const onSuccess = () => {
show(target ? t("providers.updated") : t("providers.created"));
setEditing(null);
};
const onError = (err: Error) => show(err.message, "error");
if (target) {
mutations.update.mutate({ id: target.id, input }, { onSuccess, onError });
} else {
mutations.create.mutate(input, { onSuccess, onError });
}
};
const apply = (provider: Provider) => {
mutations.apply.mutate(
{ id: provider.id, target: "claude" },
{
onSuccess: (result) => {
setLastAppliedEnv(result.env ?? null);
show(`${t("providers.activated")}: ${provider.name}`);
},
onError: (err: Error) => show(err.message, "error"),
},
);
};
if (isLoading) return <div className="text-sm text-zinc-500">{t("common.loading")}</div>;
if (isError) return <div className="text-sm text-red-600">{t("common.error")}</div>;
return (
<div className="flex flex-col gap-5">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">{t("providers.title")}</h2>
<Button size="sm" onClick={() => setEditing({ provider: null })}>
<Plus size={14} aria-hidden />
{t("providers.add")}
</Button>
</div>
{editing && (
<ProviderForm
target="claude"
editing={editing.provider}
pending={mutations.create.isPending || mutations.update.isPending}
onSubmit={submitForm}
onCancel={() => setEditing(null)}
/>
)}
{lastAppliedEnv && (
<Card>
<CardHeader>
<CardTitle>{t("providers.appliedEnv")}</CardTitle>
</CardHeader>
<CardContent>
<ul className="grid grid-cols-2 gap-1 font-mono text-xs text-zinc-600">
{Object.entries(lastAppliedEnv)
.filter(([key]) => key.startsWith("ANTHROPIC_"))
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => (
<li key={key}>
<span className="text-zinc-400">{key}</span> = {value}
</li>
))}
</ul>
</CardContent>
</Card>
)}
{providers.length === 0 ? (
<Card>
<CardContent className="py-8 text-center text-sm text-zinc-500">
{t("providers.empty")}
</CardContent>
</Card>
) : (
<div className="flex flex-col gap-3">
{providers.map((provider) => (
<Card key={provider.id}>
<CardContent className="flex items-center gap-4 py-4">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{provider.name}</span>
{provider.enabled_for_claude && (
<Badge tone="success">{t("providers.activated")}</Badge>
)}
{provider.preset && <Badge>{provider.preset}</Badge>}
</div>
<div className="truncate text-xs text-zinc-500">{provider.base_url}</div>
</div>
<div className="flex shrink-0 gap-2">
<Button variant="outline" size="sm" onClick={() => apply(provider)}>
{t("providers.activate")}
</Button>
<Button
variant="ghost"
size="icon"
aria-label={t("common.edit")}
onClick={() => setEditing({ provider })}
>
<Pencil size={14} aria-hidden />
</Button>
<Button
variant="ghost"
size="icon"
aria-label={t("common.delete")}
onClick={() => {
if (window.confirm(t("providers.deleteConfirm"))) {
mutations.remove.mutate(provider.id, {
onSuccess: () => show(t("providers.deleted")),
onError: (err: Error) => show(err.message, "error"),
});
}
}}
>
<Trash2 size={14} aria-hidden />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
}