mirror of
https://github.com/coaidev/coai.git
synced 2025-05-20 13:30:13 +09:00
313 lines
9.4 KiB
TypeScript
313 lines
9.4 KiB
TypeScript
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table.tsx";
|
|
import { Badge } from "@/components/ui/badge.tsx";
|
|
import {
|
|
Activity,
|
|
Check,
|
|
Plus,
|
|
RotateCw,
|
|
Settings2,
|
|
Trash,
|
|
X,
|
|
} from "lucide-react";
|
|
import { Button } from "@/components/ui/button.tsx";
|
|
import OperationAction from "@/components/OperationAction.tsx";
|
|
import { Dispatch, useEffect, useMemo, useState } from "react";
|
|
import { Channel, getShortChannelType } from "@/admin/channel.ts";
|
|
import { toastState } from "@/api/common.ts";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useEffectAsync } from "@/utils/hook.ts";
|
|
import {
|
|
activateChannel,
|
|
deactivateChannel,
|
|
deleteChannel,
|
|
listChannel,
|
|
} from "@/admin/api/channel.ts";
|
|
import { useToast } from "@/components/ui/use-toast.ts";
|
|
import { cn } from "@/components/ui/lib/utils.ts";
|
|
import { getApiModels } from "@/api/v1.ts";
|
|
import { getHostName } from "@/utils/base.ts";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog.tsx";
|
|
import { Label } from "@/components/ui/label.tsx";
|
|
import { Input } from "@/components/ui/input.tsx";
|
|
import { DialogClose } from "@radix-ui/react-dialog";
|
|
|
|
type ChannelTableProps = {
|
|
display: boolean;
|
|
dispatch: Dispatch<any>;
|
|
setId: (id: number) => void;
|
|
setEnabled: (enabled: boolean) => void;
|
|
};
|
|
|
|
type TypeBadgeProps = {
|
|
type: string;
|
|
};
|
|
|
|
function TypeBadge({ type }: TypeBadgeProps) {
|
|
const content = useMemo(() => getShortChannelType(type), [type]);
|
|
|
|
return (
|
|
<Badge className={`select-none w-max cursor-pointer`}>
|
|
{content || type}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
type SyncDialogProps = {
|
|
dispatch: Dispatch<any>;
|
|
open: boolean;
|
|
setOpen: (open: boolean) => void;
|
|
};
|
|
|
|
function SyncDialog({ dispatch, open, setOpen }: SyncDialogProps) {
|
|
const { t } = useTranslation();
|
|
const { toast } = useToast();
|
|
|
|
const [endpoint, setEndpoint] = useState<string>("https://api.openai.com");
|
|
const [secret, setSecret] = useState<string>("");
|
|
|
|
const submit = async (endpoint: string): Promise<boolean> => {
|
|
endpoint = endpoint.trim();
|
|
endpoint.endsWith("/") && (endpoint = endpoint.slice(0, -1));
|
|
|
|
const resp = await getApiModels(secret, { endpoint });
|
|
toastState(toast, t, resp, true);
|
|
|
|
if (!resp.status) return false;
|
|
|
|
const name = getHostName(endpoint).replace(/\./g, "-");
|
|
const data: Channel = {
|
|
id: -1,
|
|
name,
|
|
type: "openai",
|
|
models: resp.data,
|
|
priority: 0,
|
|
weight: 1,
|
|
retry: 1,
|
|
secret,
|
|
endpoint,
|
|
mapper: "",
|
|
state: true,
|
|
group: [],
|
|
proxy: { proxy: "", proxy_type: 0, username: "", password: "" },
|
|
};
|
|
|
|
dispatch({ type: "set", value: data });
|
|
return true;
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{t("admin.channels.joint")}</DialogTitle>
|
|
</DialogHeader>
|
|
<div className={`pt-2 flex flex-col`}>
|
|
<div className={`flex flex-row items-center mb-4`}>
|
|
<Label className={`mr-2 whitespace-nowrap`}>
|
|
{t("admin.channels.joint-endpoint")}
|
|
</Label>
|
|
<Input
|
|
value={endpoint}
|
|
onChange={(e) => setEndpoint(e.target.value)}
|
|
placeholder={t("admin.channels.upstream-endpoint-placeholder")}
|
|
/>
|
|
</div>
|
|
<div className={`flex flex-row items-center`}>
|
|
<Label className={`mr-2 whitespace-nowrap`}>
|
|
{t("admin.channels.secret")}
|
|
</Label>
|
|
<Input
|
|
value={secret}
|
|
onChange={(e) => setSecret(e.target.value)}
|
|
placeholder={t("admin.channels.sync-secret-placeholder")}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<DialogClose asChild>
|
|
<Button variant={`outline`}>{t("cancel")}</Button>
|
|
</DialogClose>
|
|
<Button
|
|
className={`mb-1`}
|
|
onClick={async () => {
|
|
const status = await submit(endpoint);
|
|
status && setOpen(false);
|
|
}}
|
|
>
|
|
{t("confirm")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function ChannelTable({
|
|
display,
|
|
dispatch,
|
|
setId,
|
|
setEnabled,
|
|
}: ChannelTableProps) {
|
|
const { t } = useTranslation();
|
|
const { toast } = useToast();
|
|
const [data, setData] = useState<Channel[]>([]);
|
|
const [loading, setLoading] = useState<boolean>(false);
|
|
const [open, setOpen] = useState<boolean>(false);
|
|
|
|
const refresh = async () => {
|
|
setLoading(true);
|
|
const resp = await listChannel();
|
|
setLoading(false);
|
|
if (!resp.status) toastState(toast, t, resp);
|
|
else setData(resp.data);
|
|
};
|
|
useEffectAsync(refresh, []);
|
|
useEffectAsync(refresh, [display]);
|
|
|
|
useEffect(() => {
|
|
if (display) setId(-1);
|
|
}, [display]);
|
|
|
|
return (
|
|
display && (
|
|
<div>
|
|
<SyncDialog
|
|
open={open}
|
|
setOpen={setOpen}
|
|
dispatch={(action) => {
|
|
dispatch(action);
|
|
setEnabled(true);
|
|
setId(-1);
|
|
}}
|
|
/>
|
|
<div className={`flex flex-row w-full h-max`}>
|
|
<Button
|
|
className={`mr-2`}
|
|
onClick={() => {
|
|
setEnabled(true);
|
|
setId(-1);
|
|
}}
|
|
>
|
|
<Plus className={`h-4 w-4 mr-1`} />
|
|
{t("admin.channels.create")}
|
|
</Button>
|
|
<Button
|
|
className={`mr-2`}
|
|
variant={`outline`}
|
|
onClick={() => setOpen(true)}
|
|
>
|
|
<Activity className={`h-4 w-4 mr-1`} />
|
|
{t("admin.channels.joint")}
|
|
</Button>
|
|
<div className={`grow`} />
|
|
<Button
|
|
variant={`outline`}
|
|
size={`icon`}
|
|
className={`mr-2`}
|
|
onClick={refresh}
|
|
>
|
|
<RotateCw className={cn(`h-4 w-4`, loading && `animate-spin`)} />
|
|
</Button>
|
|
</div>
|
|
<Table className={`channel-table mt-4`}>
|
|
<TableHeader>
|
|
<TableRow className={`select-none whitespace-nowrap`}>
|
|
<TableCell>{t("admin.channels.id")}</TableCell>
|
|
<TableCell>{t("admin.channels.name")}</TableCell>
|
|
<TableCell>{t("admin.channels.type")}</TableCell>
|
|
<TableCell>{t("admin.channels.priority")}</TableCell>
|
|
<TableCell>{t("admin.channels.weight")}</TableCell>
|
|
<TableCell>{t("admin.channels.state")}</TableCell>
|
|
<TableCell>{t("admin.channels.action")}</TableCell>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{(data || []).map((chan, idx) => (
|
|
<TableRow key={idx}>
|
|
<TableCell className={`channel-id select-none`}>
|
|
#{chan.id}
|
|
</TableCell>
|
|
<TableCell>{chan.name}</TableCell>
|
|
<TableCell>
|
|
<TypeBadge type={chan.type} />
|
|
</TableCell>
|
|
<TableCell>{chan.priority}</TableCell>
|
|
<TableCell>{chan.weight}</TableCell>
|
|
<TableCell>
|
|
{chan.state ? (
|
|
<Check className={`h-4 w-4 text-green-500`} />
|
|
) : (
|
|
<X className={`h-4 w-4 text-destructive`} />
|
|
)}
|
|
</TableCell>
|
|
<TableCell className={`flex flex-row flex-wrap gap-2`}>
|
|
<OperationAction
|
|
tooltip={t("admin.channels.edit")}
|
|
onClick={() => {
|
|
setEnabled(true);
|
|
setId(chan.id);
|
|
}}
|
|
>
|
|
<Settings2 className={`h-4 w-4`} />
|
|
</OperationAction>
|
|
{chan.state ? (
|
|
<OperationAction
|
|
tooltip={t("admin.channels.disable")}
|
|
variant={`destructive`}
|
|
onClick={async () => {
|
|
const resp = await deactivateChannel(chan.id);
|
|
toastState(toast, t, resp, true);
|
|
await refresh();
|
|
}}
|
|
>
|
|
<X className={`h-4 w-4`} />
|
|
</OperationAction>
|
|
) : (
|
|
<OperationAction
|
|
tooltip={t("admin.channels.enable")}
|
|
onClick={async () => {
|
|
const resp = await activateChannel(chan.id);
|
|
toastState(toast, t, resp, true);
|
|
await refresh();
|
|
}}
|
|
>
|
|
<Check className={`h-4 w-4`} />
|
|
</OperationAction>
|
|
)}
|
|
<OperationAction
|
|
tooltip={t("admin.channels.delete")}
|
|
variant={`destructive`}
|
|
onClick={async () => {
|
|
const resp = await deleteChannel(chan.id);
|
|
toastState(toast, t, resp, true);
|
|
await refresh();
|
|
}}
|
|
>
|
|
<Trash className={`h-4 w-4`} />
|
|
</OperationAction>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)
|
|
);
|
|
}
|
|
|
|
export default ChannelTable;
|