This is an automated email from the ASF dual-hosted git repository.
hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git
The following commit(s) were added to refs/heads/master by this push:
new e9e81d3 feat(channel): offer two ways to create the first channel
e9e81d3 is described below
commit e9e81d38e252e5e894f71df00eedde89c35f1268
Author: Yang Luo <[email protected]>
AuthorDate: Fri Aug 21 22:24:16 2026 +0800
feat(channel): offer two ways to create the first channel
---
web/src/lib/channels.ts | 16 ++++++++
web/src/locales/en/data.json | 4 ++
web/src/locales/zh/data.json | 4 ++
web/src/pages/ChannelListPage.tsx | 82 ++++++++++++++++++++++++++++++---------
4 files changed, 87 insertions(+), 19 deletions(-)
diff --git a/web/src/lib/channels.ts b/web/src/lib/channels.ts
index 0e9d8eb..3fc8b1d 100644
--- a/web/src/lib/channels.ts
+++ b/web/src/lib/channels.ts
@@ -122,6 +122,22 @@ export const channelPresets: ChannelPreset[] = [
},
];
+/**
+ * What a channel starts from when it is meant to forward the caller's own
+ * login. Anthropic is the vendor of the clients that mode exists for; Codex
+ * signs in against an API Gateway does not relay.
+ */
+export function clientAuthDefaults() {
+ const preset = channelPresets.find(item => item.type === "anthropic");
+ return {
+ type: "anthropic",
+ baseUrl: preset?.baseUrl ?? "",
+ models: [] as string[],
+ apiKey: "",
+ authMode: authClient,
+ };
+}
+
/** The base URLs and models offered for a channel type, from the vendors of
it. */
export function baseUrlPresets(type: string) {
return channelPresets.filter(preset => preset.type === type).map(preset =>
preset.baseUrl);
diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json
index a2b4759..bd22b0c 100644
--- a/web/src/locales/en/data.json
+++ b/web/src/locales/en/data.json
@@ -315,6 +315,10 @@
"Priority hint": "Smaller number means higher priority, 0 is the highest;
when multiple channels serve the same model, the one with the smaller value is
used first.",
"Recovering": "Recovering",
"Retried at": "retried at",
+ "Start from a sign-in": "Use the sign-in my agent has",
+ "Start from a sign-in detail": "No API key. Every request keeps the
credentials the agent sent, so a ChatGPT or Claude subscription goes on
working. Codex is the exception: it signs in against an API Gateway does not
relay.",
+ "Start from an API key": "Paste a vendor API key",
+ "Start from an API key detail": "Pick a vendor, paste its key, and Gateway
holds it from then on instead of the agent.",
"Status": "Status",
"Stored API key": "Stored API key",
"Test Connectivity": "Test Connectivity",
diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json
index 05f342a..0ffd49a 100644
--- a/web/src/locales/zh/data.json
+++ b/web/src/locales/zh/data.json
@@ -315,6 +315,10 @@
"Priority hint": "数字越小优先级越高,0 为最高;同模型多渠道时优先使用数值小的渠道。",
"Recovering": "恢复中",
"Retried at": "将于",
+ "Start from a sign-in": "沿用 Agent 已有的登录",
+ "Start from a sign-in detail": "不需要 API Key。每个请求都带着 Agent 自己发来的凭据,ChatGPT
或 Claude 订阅照常可用。Codex 除外:它登录的那套 API 不在 Gateway 的转发范围内。",
+ "Start from an API key": "填一个厂商的 API Key",
+ "Start from an API key detail": "选厂商、填 Key,之后这把 Key 由 Gateway 持有,Agent
拿不到。",
"Status": "状态",
"Stored API key": "Gateway 保存的 API Key",
"Test Connectivity": "测试连接",
diff --git a/web/src/pages/ChannelListPage.tsx
b/web/src/pages/ChannelListPage.tsx
index 06b10b0..8727a18 100644
--- a/web/src/pages/ChannelListPage.tsx
+++ b/web/src/pages/ChannelListPage.tsx
@@ -14,14 +14,13 @@
import * as React from "react";
import {Link, useNavigate} from "react-router-dom";
-import {CircleCheck, CircleX, Pencil, Plug, Plus, RefreshCw, Trash2} from
"lucide-react";
+import {CircleCheck, CircleX, KeyRound, LogIn, Pencil, Plug, Plus, RefreshCw,
Trash2} from "lucide-react";
import i18next from "i18next";
import * as ChannelBackend from "@/backend/ChannelBackend";
import * as Setting from "@/Setting";
import {ConfirmDialog} from "@/components/shared/confirm-dialog";
import {DataTable, type Column, type SortOrder} from
"@/components/shared/data-table";
-import {EmptyState} from "@/components/shared/empty-state";
import {Field, FormDialog} from "@/components/shared/form-dialog";
import {CodeText} from "@/components/shared/misc";
import {PageContainer, PageHeader} from "@/components/shared/page-header";
@@ -29,7 +28,7 @@ import {PasswordInput} from
"@/components/shared/password-input";
import {SearchSelect, SimpleSelect} from "@/components/shared/simple-select";
import {Badge} from "@/components/ui/badge";
import {Button} from "@/components/ui/button";
-import {Card} from "@/components/ui/card";
+import {Card, CardContent, CardDescription, CardHeader, CardTitle} from
"@/components/ui/card";
import {Input} from "@/components/ui/input";
import {TagsInput} from "@/components/ui/tags-input";
import {SimpleTooltip} from "@/components/ui/tooltip";
@@ -38,6 +37,7 @@ import {
baseUrlPlaceholder,
baseUrlPresets,
channelPresets,
+ clientAuthDefaults,
modelPresets,
modelsPlaceholder,
usesClientAuth,
@@ -61,6 +61,33 @@ function newChannel(owner: string): Channel {
};
}
+/** One of the ways to get a first channel, offered while the list is empty. */
+function StartOption({
+ icon: Icon,
+ title,
+ description,
+ onClick,
+}: {
+ icon: React.ComponentType<{className?: string}>;
+ title: string;
+ description: string;
+ onClick: () => void;
+}) {
+ return (
+ <button
+ type="button"
+ onClick={onClick}
+ className="hover:border-primary hover:bg-accent/40 flex flex-col
items-start gap-1 rounded-lg border p-4 text-left transition-colors"
+ >
+ <span className="flex items-center gap-2 text-sm font-medium">
+ <Icon className="size-4" />
+ {title}
+ </span>
+ <span className="text-muted-foreground text-xs">{description}</span>
+ </button>
+ );
+}
+
export default function ChannelListPage({account}: {account: Account}) {
const navigate = useNavigate();
const [data, setData] = React.useState<Channel[]>([]);
@@ -130,8 +157,8 @@ export default function ChannelListPage({account}:
{account: Account}) {
return () => clearInterval(interval);
}, []);
- const openAddDialog = () => {
- setForm(newChannel(account.name));
+ const openAddDialog = (start?: Partial<Channel>) => {
+ setForm({...newChannel(account.name), ...start});
setNameError("");
setAddOpen(true);
};
@@ -141,7 +168,14 @@ export default function ChannelListPage({account}:
{account: Account}) {
};
const applyPreset = (preset: ChannelPreset) => {
- setForm(prev => ({...prev, type: preset.type, baseUrl: preset.baseUrl,
models: preset.models}));
+ setForm(prev => ({
+ ...prev,
+ type: preset.type,
+ baseUrl: preset.baseUrl,
+ // A client-auth channel takes any model, so the vendor's list would only
+ // narrow it.
+ models: usesClientAuth(prev) ? prev.models : preset.models,
+ }));
};
const addChannel = () => {
@@ -346,7 +380,7 @@ export default function ChannelListPage({account}:
{account: Account}) {
title={i18next.t("channel:Channels")}
description={i18next.t("channel:Page description")}
actions={
- <Button onClick={openAddDialog}>
+ <Button onClick={() => openAddDialog()}>
<Plus />
{i18next.t("channel:New Channel")}
</Button>
@@ -354,18 +388,28 @@ export default function ChannelListPage({account}:
{account: Account}) {
/>
{loaded && data.length === 0 ? (
- <Card className="py-0">
- <EmptyState
- icon={Plug}
- title={i18next.t("channel:No channels yet")}
- description={i18next.t("channel:No channels yet detail")}
- action={
- <Button onClick={openAddDialog}>
- <Plus />
- {i18next.t("channel:New Channel")}
- </Button>
- }
- />
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Plug className="size-4" />
+ {i18next.t("channel:No channels yet")}
+ </CardTitle>
+ <CardDescription>{i18next.t("channel:No channels yet
detail")}</CardDescription>
+ </CardHeader>
+ <CardContent className="grid gap-3 sm:grid-cols-2">
+ <StartOption
+ icon={LogIn}
+ title={i18next.t("channel:Start from a sign-in")}
+ description={i18next.t("channel:Start from a sign-in detail")}
+ onClick={() => openAddDialog(clientAuthDefaults())}
+ />
+ <StartOption
+ icon={KeyRound}
+ title={i18next.t("channel:Start from an API key")}
+ description={i18next.t("channel:Start from an API key detail")}
+ onClick={() => openAddDialog()}
+ />
+ </CardContent>
</Card>
) : (
<DataTable
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]