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]

Reply via email to