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 cf59a76 feat(channel): create a channel by picking its source
cf59a76 is described below
commit cf59a76b5b8f4f4466835ebad6721cf2fb6944b7
Author: Yang Luo <[email protected]>
AuthorDate: Sat Aug 22 16:37:38 2026 +0800
feat(channel): create a channel by picking its source
---
web/src/components/ChannelSourcePicker.tsx | 82 +++++++++
web/src/lib/channels.ts | 62 +++++--
web/src/locales/en/data.json | 15 +-
web/src/locales/zh/data.json | 15 +-
web/src/pages/ChannelListPage.tsx | 279 +++++++++++++++--------------
5 files changed, 288 insertions(+), 165 deletions(-)
diff --git a/web/src/components/ChannelSourcePicker.tsx
b/web/src/components/ChannelSourcePicker.tsx
new file mode 100644
index 0000000..54e442c
--- /dev/null
+++ b/web/src/components/ChannelSourcePicker.tsx
@@ -0,0 +1,82 @@
+// Copyright 2026 The casbin Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {KeyRound, LogIn, Settings2} from "lucide-react";
+import i18next from "i18next";
+
+import {channelSources, customSource, subscriptionSource, type ChannelSource}
from "@/lib/channels";
+
+/**
+ * The title and the line under it. A vendor card is named after the vendor and
+ * says where its requests go; the two cards that are not a vendor say what
they
+ * are instead, because their base URL would not.
+ */
+export function sourceTitle(source: ChannelSource) {
+ if (source.key === subscriptionSource) {
+ return i18next.t("channel:Claude subscription");
+ }
+ if (source.key === customSource) {
+ return i18next.t("channel:Custom vendor");
+ }
+ return source.label;
+}
+
+function sourceDetail(source: ChannelSource) {
+ if (source.key === subscriptionSource) {
+ return i18next.t("channel:Claude subscription detail");
+ }
+ if (source.key === customSource) {
+ return i18next.t("channel:Custom vendor detail");
+ }
+ return source.channel.baseUrl ?? "";
+}
+
+function sourceIcon(source: ChannelSource) {
+ if (source.key === subscriptionSource) {
+ return LogIn;
+ }
+ if (source.key === customSource) {
+ return Settings2;
+ }
+ return KeyRound;
+}
+
+/**
+ * The first step of creating a channel: where its credentials come from.
Picking
+ * a card fills in the type, base URL, models and authentication mode, so a
+ * subscription needs nothing typed and a vendor needs only its key.
+ */
+export function ChannelSourcePicker({onPick}: {onPick: (source: ChannelSource)
=> void}) {
+ return (
+ <div className="grid gap-3 sm:grid-cols-2">
+ {channelSources.map(source => {
+ const Icon = sourceIcon(source);
+ return (
+ <button
+ key={source.key}
+ type="button"
+ onClick={() => onPick(source)}
+ 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 shrink-0" />
+ {sourceTitle(source)}
+ </span>
+ <span className="text-muted-foreground break-all
text-xs">{sourceDetail(source)}</span>
+ </button>
+ );
+ })}
+ </div>
+ );
+}
diff --git a/web/src/lib/channels.ts b/web/src/lib/channels.ts
index 3fc8b1d..ed9788b 100644
--- a/web/src/lib/channels.ts
+++ b/web/src/lib/channels.ts
@@ -13,13 +13,15 @@
// limitations under the License.
import * as Setting from "@/Setting";
+import type {Channel} from "@/types";
/** Mirrors object.ChannelProtocol on the server. */
export function channelProtocol(type: string) {
return type === "anthropic" ? "anthropic" : "openai";
}
-/** Mirrors object.ChannelAuthClient on the server. */
+/** Mirrors object.ChannelAuthChannel and object.ChannelAuthClient on the
server. */
+export const authChannel = "channel";
export const authClient = "client";
/** Mirrors object.UsesClientAuth: whose credentials reach the upstream. */
@@ -122,21 +124,53 @@ export const channelPresets: ChannelPreset[] = [
},
];
+/** Where a new channel gets its credentials: one card of the picker. */
+export interface ChannelSource {
+ /** Stable id. The two cards that are not a vendor are titled by the picker.
*/
+ key: string;
+ /** The vendor's own name, empty when the picker titles the card itself. */
+ label: string;
+ /** What the form starts from once the card is picked. */
+ channel: Partial<Channel>;
+}
+
+export const subscriptionSource = "subscription";
+export const customSource = "custom";
+
/**
- * 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.
+ * The sign-in comes first: it is the only source that needs nothing filled in,
+ * and the one people with a subscription and no API key are here for.
Anthropic
+ * is the vendor of the clients that mode works with; 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,
- };
-}
+export const channelSources: ChannelSource[] = [
+ {
+ key: subscriptionSource,
+ label: "",
+ channel: {
+ type: "anthropic",
+ baseUrl: channelPresets.find(preset => preset.type ===
"anthropic")?.baseUrl ?? "",
+ models: [],
+ apiKey: "",
+ authMode: authClient,
+ },
+ },
+ ...channelPresets.map(preset => ({
+ key: preset.label.toLowerCase(),
+ label: preset.label,
+ channel: {
+ type: preset.type,
+ baseUrl: preset.baseUrl,
+ models: preset.models,
+ authMode: authChannel,
+ },
+ })),
+ {
+ key: customSource,
+ label: "",
+ channel: {type: "custom", baseUrl: "", models: [], authMode: authChannel},
+ },
+];
/** The base URLs and models offered for a channel type, from the vendors of
it. */
export function baseUrlPresets(type: string) {
diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json
index 4a88a95..cef1bb3 100644
--- a/web/src/locales/en/data.json
+++ b/web/src/locales/en/data.json
@@ -281,18 +281,23 @@
"Base URL": "Base URL",
"Base URL hint": "Requests will be sent to",
"Caller's own login": "The caller's own login",
+ "Change source": "Change",
"Channel": "Channel",
"Channel added successfully": "Channel added successfully",
"Channel deleted successfully": "Channel deleted successfully",
"Channel not found": "Channel not found",
"Channel saved": "Channel saved",
"Channels": "Channels",
+ "Claude subscription": "Claude subscription",
+ "Claude subscription detail": "The sign-in Claude Code already has,
forwarded upstream. No API key, nothing to fill in. Codex is the exception: it
signs in against an API Gateway does not relay.",
"Clear": "Clear",
"Client auth hint": "Requests are forwarded with the credentials the
client sent, so an agent already signed in with a subscription keeps its own
login and no key is stored here. Codex is the exception: its ChatGPT sign-in
speaks a different API.",
"Client auth usage hint": "Only the base URL is set: a token here would
replace the sign-in the client already has.",
"Connection Failed": "Connection Failed",
"Connection Successful": "Connection Successful",
"Cooling down": "Cooling down",
+ "Custom vendor": "Custom",
+ "Custom vendor detail": "Any OpenAI- or Anthropic-compatible API. You give
it a base URL and a key.",
"Disabled": "Disabled",
"Edit Channel": "Edit Channel",
"Enabled": "Enabled",
@@ -321,18 +326,14 @@
"Recovering": "Recovering",
"Retried at": "retried at",
"Select all": "Select all",
- "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.",
+ "Source": "Source",
+ "Source hint": "Where this channel gets its credentials. Picking one fills
in the type, base URL and models, so a subscription needs nothing typed and a
vendor needs only its key.",
"Status": "Status",
"Stored API key": "Stored API key",
"Test Connectivity": "Test Connectivity",
"Type": "Type",
"Usage": "Usage",
- "Usage hint": "Point any client that speaks this channel's API at Gateway
with these variables.",
- "Vendor": "Vendor",
- "Vendor hint": "Fills in the type, base URL and models. Everything stays
editable."
+ "Usage hint": "Point any client that speaks this channel's API at Gateway
with these variables."
},
"general": {
"Action": "Action",
diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json
index 66955b0..38941f6 100644
--- a/web/src/locales/zh/data.json
+++ b/web/src/locales/zh/data.json
@@ -281,18 +281,23 @@
"Base URL": "基础 URL",
"Base URL hint": "实际请求将发往",
"Caller's own login": "调用方自己的登录",
+ "Change source": "更换",
"Channel": "渠道",
"Channel added successfully": "渠道添加成功",
"Channel deleted successfully": "渠道删除成功",
"Channel not found": "渠道不存在",
"Channel saved": "渠道已保存",
"Channels": "渠道",
+ "Claude subscription": "Claude 订阅登录",
+ "Claude subscription detail": "直接用 Claude Code 已有的登录,原样转发给上游。不需要 API
Key,什么都不用填。Codex 除外:它的登录所对的 API 不在 Gateway 的转发范围内。",
"Clear": "清空",
"Client auth hint": "请求会带着客户端自己的凭据转发上游,已经用订阅登录过的 Agent 可以继续用自己的登录,这里不保存任何
Key。Codex 除外:它的 ChatGPT 登录走的是另一套 API。",
"Client auth usage hint": "只设置 base URL:这里再设一个 token 会覆盖客户端已有的登录。",
"Connection Failed": "连接失败",
"Connection Successful": "连接成功",
"Cooling down": "冷却中",
+ "Custom vendor": "自定义",
+ "Custom vendor detail": "任何兼容 OpenAI 或 Anthropic 的 API,由你填写 base URL 和
Key。",
"Disabled": "已禁用",
"Edit Channel": "编辑渠道",
"Enabled": "已启用",
@@ -321,18 +326,14 @@
"Recovering": "恢复中",
"Retried at": "将于",
"Select all": "全选",
- "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
拿不到。",
+ "Source": "来源",
+ "Source hint": "这个 Channel 的凭据从哪里来。选一个就会自动填好类型、base URL
和模型:订阅登录什么都不用填,厂商只用填一个 Key。",
"Status": "状态",
"Stored API key": "Gateway 保存的 API Key",
"Test Connectivity": "测试连接",
"Type": "类型",
"Usage": "使用方法",
- "Usage hint": "任何说这个渠道所用 API 的客户端,配上下面的变量就能走网关。",
- "Vendor": "厂商",
- "Vendor hint": "一键填好类型、base URL 和模型,填完还可以改。"
+ "Usage hint": "任何说这个渠道所用 API 的客户端,配上下面的变量就能走网关。"
},
"general": {
"Action": "操作",
diff --git a/web/src/pages/ChannelListPage.tsx
b/web/src/pages/ChannelListPage.tsx
index eef2e78..8aaf31c 100644
--- a/web/src/pages/ChannelListPage.tsx
+++ b/web/src/pages/ChannelListPage.tsx
@@ -14,12 +14,13 @@
import * as React from "react";
import {Link, useNavigate} from "react-router-dom";
-import {CircleCheck, CircleX, KeyRound, LogIn, Pencil, Plug, Plus, RefreshCw,
Trash2} from "lucide-react";
+import {ChevronDown, CircleCheck, CircleX, Pencil, Plug, Plus, RefreshCw,
Trash2} from "lucide-react";
import i18next from "i18next";
import * as ChannelBackend from "@/backend/ChannelBackend";
import * as Setting from "@/Setting";
import {ChannelModelsField} from "@/components/ChannelModelsField";
+import {ChannelSourcePicker, sourceTitle} from
"@/components/ChannelSourcePicker";
import {ConfirmDialog} from "@/components/shared/confirm-dialog";
import {DataTable, type Column, type SortOrder} from
"@/components/shared/data-table";
import {Field, FormDialog} from "@/components/shared/form-dialog";
@@ -33,56 +34,58 @@ import {Card, CardContent, CardDescription, CardHeader,
CardTitle} from "@/compo
import {Input} from "@/components/ui/input";
import {SimpleTooltip} from "@/components/ui/tooltip";
import {
+ authChannel,
authClient,
baseUrlPlaceholder,
baseUrlPresets,
- channelPresets,
- clientAuthDefaults,
+ customSource,
usesClientAuth,
- type ChannelPreset,
+ type ChannelSource,
} from "@/lib/channels";
+import {cn} from "@/lib/utils";
import type {Account, Channel, ChannelHealth} from "@/types";
-function newChannel(owner: string): Channel {
+function newChannel(owner: string, label = "New Channel"): Channel {
const randomName = Setting.getRandomName();
return {
owner: owner,
name: `channel_${randomName}`,
- displayName: `New Channel - ${randomName}`,
+ displayName: `${label} - ${randomName}`,
type: "openai",
status: "enabled",
models: [],
priority: 0,
baseUrl: "",
apiKey: "",
- authMode: "channel",
+ authMode: authChannel,
};
}
-/** 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;
-}) {
+/** What a picked source leaves to fill in, which for a subscription is
nothing. */
+function channelFromSource(owner: string, source: ChannelSource): Channel {
+ return {...newChannel(owner, sourceTitle(source)), ...source.channel};
+}
+
+/**
+ * The fields the source already answered. They stay reachable — a preset is a
+ * starting point, not a lock — but out of the way of the one field, if any,
+ * that is actually left to fill in.
+ */
+function Advanced({defaultOpen, children}: {defaultOpen: boolean; children:
React.ReactNode}) {
+ const [open, setOpen] = React.useState(defaultOpen);
+
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>
+ <div className="grid gap-4">
+ <button
+ type="button"
+ onClick={() => setOpen(!open)}
+ className="text-muted-foreground hover:text-foreground flex
items-center gap-1 text-sm"
+ >
+ <ChevronDown className={cn("size-3.5 transition-transform", open &&
"rotate-180")} />
+ {i18next.t("general:Advanced")}
+ </button>
+ {open ? children : null}
+ </div>
);
}
@@ -101,6 +104,8 @@ export default function ChannelListPage({account}:
{account: Account}) {
const [addOpen, setAddOpen] = React.useState(false);
const [adding, setAdding] = React.useState(false);
const [form, setForm] = React.useState<Channel>(() =>
newChannel(account.name));
+ // null while the dialog is still asking where the credentials come from.
+ const [source, setSource] = React.useState<ChannelSource | null>(null);
const [nameError, setNameError] = React.useState("");
const [health, setHealth] = React.useState<ChannelHealth[]>([]);
@@ -155,8 +160,9 @@ export default function ChannelListPage({account}:
{account: Account}) {
return () => clearInterval(interval);
}, []);
- const openAddDialog = (start?: Partial<Channel>) => {
- setForm({...newChannel(account.name), ...start});
+ const openAddDialog = (start?: ChannelSource) => {
+ setForm(start ? channelFromSource(account.name, start) :
newChannel(account.name));
+ setSource(start ?? null);
setNameError("");
setAddOpen(true);
};
@@ -165,15 +171,10 @@ export default function ChannelListPage({account}:
{account: Account}) {
setForm(prev => ({...prev, [key]: value}));
};
- const applyPreset = (preset: ChannelPreset) => {
- 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 pickSource = (picked: ChannelSource) => {
+ setForm(channelFromSource(account.name, picked));
+ setNameError("");
+ setSource(picked);
};
const addChannel = () => {
@@ -394,19 +395,8 @@ export default function ChannelListPage({account}:
{account: Account}) {
</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>
+ <ChannelSourcePicker onPick={openAddDialog} />
</CardContent>
</Card>
) : (
@@ -439,95 +429,110 @@ export default function ChannelListPage({account}:
{account: Account}) {
open={addOpen}
onOpenChange={setAddOpen}
title={i18next.t("channel:New Channel")}
+ description={source === null ? i18next.t("channel:Source hint") :
undefined}
+ size={source === null ? "lg" : "default"}
submitting={adding}
onSubmit={addChannel}
+ // Nothing is filled in yet while the source is still being picked, so
+ // there is nothing to submit.
+ footer={
+ source === null ? (
+ <Button type="button" variant="outline" onClick={() =>
setAddOpen(false)}>
+ {i18next.t("general:Cancel")}
+ </Button>
+ ) : undefined
+ }
>
- <Field label={i18next.t("general:Name")} htmlFor="channel-name"
required error={nameError}>
- <Input
- id="channel-name"
- value={form.name}
- onChange={event => {
- setFormField("name", event.target.value);
- setNameError("");
- }}
- />
- </Field>
- <Field label={i18next.t("general:Display name")}
htmlFor="channel-display-name">
- <Input
- id="channel-display-name"
- value={form.displayName}
- onChange={event => setFormField("displayName", event.target.value)}
- />
- </Field>
- <Field label={i18next.t("channel:Vendor")}
hint={i18next.t("channel:Vendor hint")}>
- <div className="flex flex-wrap gap-2">
- {channelPresets.map(preset => (
- <Button
- key={preset.label}
- type="button"
- size="sm"
- variant={form.baseUrl === preset.baseUrl ? "default" :
"outline"}
- onClick={() => applyPreset(preset)}
+ {source === null ? (
+ <ChannelSourcePicker onPick={pickSource} />
+ ) : (
+ <>
+ <Field label={i18next.t("channel:Source")}>
+ <div className="flex flex-wrap items-center gap-2">
+ <Badge variant="info">{sourceTitle(source)}</Badge>
+ {usesClientAuth(form) ? (
+ <Badge variant="muted">{i18next.t("channel:Caller's own
login")}</Badge>
+ ) : null}
+ <Button type="button" size="xs" variant="ghost" onClick={() =>
setSource(null)}>
+ {i18next.t("channel:Change source")}
+ </Button>
+ </div>
+ </Field>
+ <Field label={i18next.t("general:Name")} htmlFor="channel-name"
required error={nameError}>
+ <Input
+ id="channel-name"
+ value={form.name}
+ onChange={event => {
+ setFormField("name", event.target.value);
+ setNameError("");
+ }}
+ />
+ </Field>
+ <Field label={i18next.t("general:Display name")}
htmlFor="channel-display-name">
+ <Input
+ id="channel-display-name"
+ value={form.displayName}
+ onChange={event => setFormField("displayName",
event.target.value)}
+ />
+ </Field>
+ {usesClientAuth(form) ? null : (
+ <Field
+ label={i18next.t("channel:API Key")}
+ htmlFor="channel-api-key"
+ hint={i18next.t("channel:API Key ownership hint")}
>
- {preset.label}
- </Button>
- ))}
- </div>
- </Field>
- <Field label={i18next.t("channel:Type")}>
- <SimpleSelect
- value={form.type}
- onChange={value => setFormField("type", value)}
- options={[
- {label: "OpenAI", value: "openai"},
- {label: "Anthropic", value: "anthropic"},
- {label: "Custom", value: "custom"},
- ]}
- />
- </Field>
- <Field label={i18next.t("channel:Base URL")}
htmlFor="channel-base-url">
- <SearchSelect
- allowCustomValue
- id="channel-base-url"
- value={form.baseUrl}
- placeholder={baseUrlPlaceholder(form.type)}
- options={baseUrlPresets(form.type)}
- onChange={value => setFormField("baseUrl", value)}
- />
- </Field>
- <Field
- label={i18next.t("channel:Authentication")}
- hint={usesClientAuth(form) ? i18next.t("channel:Client auth hint") :
undefined}
- >
- <SimpleSelect
- value={form.authMode}
- // The stored key is meaningless once the caller's own is
forwarded.
- onChange={value => setForm(prev => ({...prev, authMode: value,
apiKey: ""}))}
- options={[
- {label: i18next.t("channel:Stored API key"), value: "channel"},
- {label: i18next.t("channel:Caller's own login"), value:
authClient},
- ]}
- />
- </Field>
- {usesClientAuth(form) ? null : (
- <Field
- label={i18next.t("channel:API Key")}
- htmlFor="channel-api-key"
- hint={i18next.t("channel:API Key ownership hint")}
- >
- <PasswordInput
- id="channel-api-key"
- placeholder="sk-..."
- value={form.apiKey}
- onChange={event => setFormField("apiKey", event.target.value)}
+ <PasswordInput
+ id="channel-api-key"
+ placeholder="sk-..."
+ value={form.apiKey}
+ onChange={event => setFormField("apiKey",
event.target.value)}
+ />
+ </Field>
+ )}
+ <ChannelModelsField
+ channel={form}
+ hint={usesClientAuth(form) ? i18next.t("channel:Any model hint")
: i18next.t("channel:Models hint")}
+ onChange={value => setFormField("models", value)}
/>
- </Field>
+ <Advanced key={source.key} defaultOpen={source.key ===
customSource}>
+ <Field label={i18next.t("channel:Type")}>
+ <SimpleSelect
+ value={form.type}
+ onChange={value => setFormField("type", value)}
+ options={[
+ {label: "OpenAI", value: "openai"},
+ {label: "Anthropic", value: "anthropic"},
+ {label: "Custom", value: "custom"},
+ ]}
+ />
+ </Field>
+ <Field label={i18next.t("channel:Base URL")}
htmlFor="channel-base-url">
+ <SearchSelect
+ allowCustomValue
+ id="channel-base-url"
+ value={form.baseUrl}
+ placeholder={baseUrlPlaceholder(form.type)}
+ options={baseUrlPresets(form.type)}
+ onChange={value => setFormField("baseUrl", value)}
+ />
+ </Field>
+ <Field
+ label={i18next.t("channel:Authentication")}
+ hint={usesClientAuth(form) ? i18next.t("channel:Client auth
hint") : undefined}
+ >
+ <SimpleSelect
+ value={form.authMode}
+ // The stored key is meaningless once the caller's own is
forwarded.
+ onChange={value => setForm(prev => ({...prev, authMode:
value, apiKey: ""}))}
+ options={[
+ {label: i18next.t("channel:Stored API key"), value:
authChannel},
+ {label: i18next.t("channel:Caller's own login"), value:
authClient},
+ ]}
+ />
+ </Field>
+ </Advanced>
+ </>
)}
- <ChannelModelsField
- channel={form}
- hint={usesClientAuth(form) ? i18next.t("channel:Any model hint") :
i18next.t("channel:Models hint")}
- onChange={value => setFormField("models", value)}
- />
</FormDialog>
</PageContainer>
);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]