coai/app/src/components/admin/assemblies/ChannelTable.tsx

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;