easyai-ai-gateway/apps/web/src/pages/AdminPage.tsx
chengcheng e0a356ee0c feat(web): 增加统一认证接入与运维页面
系统设置新增统一认证配对、验证、激活、重验证、回滚与禁用流程,安全事件改为统一能力状态。前端登录入口运行时读取公开配置,不再依赖 VITE_OIDC 构建变量。\n\n验证:web 31 项测试;web typecheck;pnpm lint;web production build
2026-07-17 12:18:26 +08:00

333 lines
16 KiB
TypeScript

import type { ReactNode } from 'react';
import { Boxes, Building2, Gauge, History, KeyRound, Route, ServerCog, Settings, ShieldCheck, UsersRound, Workflow } from 'lucide-react';
import type {
BaseModelUpsertRequest,
CatalogProviderUpsertRequest,
ClientCustomizationSettingsUpdateRequest,
FileStorageChannelUpsertRequest,
FileStorageSettingsUpdateRequest,
GatewayAccessRuleBatchRequest,
GatewayAccessRuleUpsertRequest,
GatewayTenantUpsertRequest,
GatewayRunnerPolicyUpsertRequest,
GatewayUserUpsertRequest,
IntegrationPlatform,
PlatformDynamicPriorityUpdateRequest,
PricingRuleSetUpsertRequest,
RuntimePolicySetUpsertRequest,
UserGroupUpsertRequest,
WalletRechargeRequest,
} from '@easyai-ai-gateway/contracts';
import type { ConsoleData, StatItem } from '../app-state';
import { EntityTable } from '../components/EntityTable';
import { StatGrid } from '../components/StatGrid';
import { Badge, Card, CardContent, CardHeader, CardTitle, ScreenMessage, Tabs } from '../components/ui';
import type { AdminSection, LoadState, PlatformWithModelsInput } from '../types';
import { AccessRulesPanel } from './admin/AccessRulesPanel';
import { AuditLogsPanel } from './admin/AuditLogsPanel';
import { BaseModelCatalogPanel } from './admin/BaseModelCatalogPanel';
import { TenantsPanel, UserGroupsPanel, UsersPanel } from './admin/IdentityManagementPanels';
import { PlatformManagementPanel } from './admin/PlatformManagementPanel';
import { PricingRulesPanel } from './admin/PricingRulesPanel';
import { ProviderManagementPanel } from './admin/ProviderManagementPanel';
import { RealtimeLoadPanel } from './admin/RealtimeLoadPanel';
import { RuntimePoliciesPanel } from './admin/RuntimePoliciesPanel';
import { SystemSettingsPanel } from './admin/SystemSettingsPanel';
const tabs = [
{ value: 'overview', label: '总览', icon: <Workflow size={15} /> },
{ value: 'globalModels', label: 'Provider', icon: <Boxes size={15} /> },
{ value: 'pricing', label: '定价规则', icon: <Gauge size={15} /> },
{ value: 'runtime', label: '运行策略', icon: <ShieldCheck size={15} /> },
{ value: 'baseModels', label: '基准模型库', icon: <Boxes size={15} /> },
{ value: 'platforms', label: '平台管理', icon: <ServerCog size={15} /> },
{ value: 'realtimeLoad', label: '实时负载', icon: <Gauge size={15} /> },
{ value: 'tenants', label: '租户', icon: <Building2 size={15} /> },
{ value: 'users', label: '用户', icon: <UsersRound size={15} /> },
{ value: 'userGroups', label: '用户组', icon: <UsersRound size={15} /> },
{ value: 'accessRules', label: '模型权限', icon: <KeyRound size={15} /> },
{ value: 'systemSettings', label: '系统设置', icon: <Settings size={15} /> },
{ value: 'auditLogs', label: '审计日志', icon: <History size={15} /> },
] satisfies Array<{ value: AdminSection; label: string; icon: ReactNode }>;
export function AdminPage(props: {
token: string;
data: ConsoleData;
operationMessage: string;
section: AdminSection;
stats: StatItem[];
state: LoadState;
onDeleteBaseModel: (baseModelId: string) => Promise<void>;
onDeletePlatform: (platformId: string) => Promise<void>;
onDeleteProvider: (providerId: string) => Promise<void>;
onDeletePricingRuleSet: (ruleSetId: string) => Promise<void>;
onDeleteRuntimePolicySet: (policySetId: string) => Promise<void>;
onDeleteAccessRule: (ruleId: string) => Promise<void>;
onDeleteFileStorageChannel: (channelId: string) => Promise<void>;
onDeleteTenant: (tenantId: string) => Promise<void>;
onDeleteUser: (userId: string) => Promise<void>;
onDeleteUserGroup: (groupId: string) => Promise<void>;
onSaveBaseModel: (input: BaseModelUpsertRequest, baseModelId?: string) => Promise<void>;
onResetAllBaseModels: () => Promise<void>;
onResetBaseModel: (baseModelId: string) => Promise<void>;
onBatchAccessRules: (input: GatewayAccessRuleBatchRequest) => Promise<void>;
onSavePlatform: (input: PlatformWithModelsInput) => Promise<void>;
onSavePlatformDynamicPriority: (platformId: string, input: PlatformDynamicPriorityUpdateRequest) => Promise<void>;
onRestoreRuntimeModel: (platformModelId: string) => Promise<void>;
onTogglePlatformStatus: (platform: IntegrationPlatform, status: 'enabled' | 'disabled') => Promise<void>;
onSaveProvider: (input: CatalogProviderUpsertRequest, providerId?: string) => Promise<void>;
onSavePricingRuleSet: (input: PricingRuleSetUpsertRequest, ruleSetId?: string) => Promise<void>;
onSaveRunnerPolicy: (input: GatewayRunnerPolicyUpsertRequest) => Promise<void>;
onSaveRuntimePolicySet: (input: RuntimePolicySetUpsertRequest, policySetId?: string) => Promise<void>;
onSaveAccessRule: (input: GatewayAccessRuleUpsertRequest, ruleId?: string) => Promise<void>;
onSaveClientCustomizationSettings: (input: ClientCustomizationSettingsUpdateRequest) => Promise<void>;
onSaveFileStorageChannel: (input: FileStorageChannelUpsertRequest, channelId?: string) => Promise<void>;
onSaveFileStorageSettings: (input: FileStorageSettingsUpdateRequest) => Promise<void>;
onConnectSecurityEvents: (transmitterIssuer: string) => Promise<void>;
onDisconnectSecurityEvents: () => Promise<void>;
onRefreshSecurityEvents: () => Promise<void>;
onRotateSecurityEventsCredential: () => Promise<void>;
onVerifySecurityEvents: () => Promise<void>;
onSaveTenant: (input: GatewayTenantUpsertRequest, tenantId?: string) => Promise<void>;
onSaveUser: (input: GatewayUserUpsertRequest, userId?: string) => Promise<void>;
onRechargeUserWalletBalance: (userId: string, input: WalletRechargeRequest) => Promise<void>;
onSaveUserGroup: (input: UserGroupUpsertRequest, groupId?: string) => Promise<void>;
onClearOperationMessage: () => void;
onSectionChange: (value: AdminSection) => void;
}) {
return (
<div className="pageStack">
<ScreenMessage
duration={props.state === 'error' ? 0 : undefined}
message={props.operationMessage}
variant={props.state === 'error' ? 'error' : 'success'}
onClose={props.onClearOperationMessage}
/>
<div className="subPageLayout">
<Tabs className="sideTabs" value={props.section} tabs={tabs} onValueChange={props.onSectionChange} />
<div className="subPageContent">
{props.section === 'overview' && <OverviewPanel data={props.data} stats={props.stats} />}
{props.section === 'globalModels' && (
<ProviderManagementPanel
message={props.operationMessage}
providers={props.data.providers}
state={props.state}
onDeleteProvider={props.onDeleteProvider}
onSaveProvider={props.onSaveProvider}
/>
)}
{props.section === 'baseModels' && (
<BaseModelCatalogPanel
baseModels={props.data.baseModels}
message={props.operationMessage}
pricingRuleSets={props.data.pricingRuleSets}
providers={props.data.providers}
runtimePolicySets={props.data.runtimePolicySets}
state={props.state}
onDeleteBaseModel={props.onDeleteBaseModel}
onResetAllBaseModels={props.onResetAllBaseModels}
onResetBaseModel={props.onResetBaseModel}
onSaveBaseModel={props.onSaveBaseModel}
/>
)}
{props.section === 'runtime' && (
<RuntimePoliciesPanel
message={props.operationMessage}
runnerPolicy={props.data.runnerPolicy}
runtimePolicySets={props.data.runtimePolicySets}
state={props.state}
onDeleteRuntimePolicySet={props.onDeleteRuntimePolicySet}
onSaveRunnerPolicy={props.onSaveRunnerPolicy}
onSaveRuntimePolicySet={props.onSaveRuntimePolicySet}
/>
)}
{props.section === 'accessRules' && (
<AccessRulesPanel
accessRules={props.data.accessRules}
baseModels={props.data.baseModels}
message={props.operationMessage}
platformModels={props.data.models}
platforms={props.data.platforms}
state={props.state}
userGroups={props.data.userGroups}
onBatchAccessRules={props.onBatchAccessRules}
/>
)}
{props.section === 'pricing' && (
<PricingRulesPanel
message={props.operationMessage}
pricingRuleSets={props.data.pricingRuleSets}
state={props.state}
onDeletePricingRuleSet={props.onDeletePricingRuleSet}
onSavePricingRuleSet={props.onSavePricingRuleSet}
/>
)}
{props.section === 'platforms' && (
<PlatformManagementPanel
baseModels={props.data.baseModels}
message={props.operationMessage}
networkProxyConfig={props.data.networkProxyConfig}
platformModels={props.data.models}
platforms={props.data.platforms}
pricingRuleSets={props.data.pricingRuleSets}
providers={props.data.providers}
state={props.state}
onDeletePlatform={props.onDeletePlatform}
onSavePlatform={props.onSavePlatform}
onTogglePlatformStatus={props.onTogglePlatformStatus}
/>
)}
{props.section === 'realtimeLoad' && (
<RealtimeLoadPanel
modelRateLimits={props.data.modelRateLimits}
modelRateLimitsUpdatedAt={props.data.modelRateLimitsUpdatedAt}
platforms={props.data.platforms}
onSavePlatformDynamicPriority={props.onSavePlatformDynamicPriority}
onRestoreRuntimeModel={props.onRestoreRuntimeModel}
/>
)}
{props.section === 'tenants' && <TenantsPanel {...identityPanelProps(props)} />}
{props.section === 'users' && <UsersPanel {...identityPanelProps(props)} />}
{props.section === 'userGroups' && <UserGroupsPanel {...identityPanelProps(props)} />}
{props.section === 'auditLogs' && <AuditLogsPanel auditLogs={props.data.auditLogs} message={props.operationMessage} />}
{props.section === 'systemSettings' && (
<SystemSettingsPanel
token={props.token}
channels={props.data.fileStorageChannels}
clientCustomizationSettings={props.data.clientCustomizationSettings}
settings={props.data.fileStorageSettings}
message={props.operationMessage}
state={props.state}
onDeleteFileStorageChannel={props.onDeleteFileStorageChannel}
onSaveFileStorageChannel={props.onSaveFileStorageChannel}
onSaveFileStorageSettings={props.onSaveFileStorageSettings}
onSaveClientCustomizationSettings={props.onSaveClientCustomizationSettings}
/>
)}
</div>
</div>
</div>
);
}
function identityPanelProps(props: {
data: ConsoleData;
operationMessage: string;
state: LoadState;
onDeleteTenant: (tenantId: string) => Promise<void>;
onDeleteUser: (userId: string) => Promise<void>;
onDeleteUserGroup: (groupId: string) => Promise<void>;
onSaveTenant: (input: GatewayTenantUpsertRequest, tenantId?: string) => Promise<void>;
onSaveUser: (input: GatewayUserUpsertRequest, userId?: string) => Promise<void>;
onRechargeUserWalletBalance: (userId: string, input: WalletRechargeRequest) => Promise<void>;
onSaveUserGroup: (input: UserGroupUpsertRequest, groupId?: string) => Promise<void>;
}) {
return {
data: props.data,
operationMessage: props.operationMessage,
state: props.state,
onDeleteTenant: props.onDeleteTenant,
onDeleteUser: props.onDeleteUser,
onDeleteUserGroup: props.onDeleteUserGroup,
onSaveTenant: props.onSaveTenant,
onSaveUser: props.onSaveUser,
onRechargeUserWalletBalance: props.onRechargeUserWalletBalance,
onSaveUserGroup: props.onSaveUserGroup,
};
}
function OverviewPanel(props: { data: ConsoleData; stats: StatItem[] }) {
const enabledPlatforms = props.data.platforms.filter((item) => item.status === 'enabled');
const chatModels = props.data.models.filter((item) => item.modelType.includes('text_generate') && item.enabled);
const imageModels = props.data.models.filter((item) => item.modelType.some((type) => type.includes('image')) && item.enabled);
return (
<div className="pageStack">
<StatGrid items={props.stats} />
<section className="contentGrid three">
<ManagementCard icon={<Route size={18} />} label="路由候选" value={enabledPlatforms.length} detail="平台优先级 / 失败切换" />
<ManagementCard icon={<ShieldCheck size={18} />} label="策略对象" value={props.data.userGroups.length} detail="用户组 / 折扣 / 并发" />
<ManagementCard icon={<Workflow size={18} />} label="运行窗口" value={props.data.rateLimitWindows.length} detail="TPM / RPM / 并发" />
</section>
<section className="contentGrid two">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<EntityTable
columns={['Provider', '平台', '状态', '优先级']}
empty="暂无平台"
rows={props.data.platforms.slice(0, 6).map((item) => [item.provider, item.internalName || item.name, item.status, item.priority])}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
{props.data.taskResult ? (
<div className="taskPreview">
<Badge variant={props.data.taskResult.status === 'succeeded' ? 'success' : 'secondary'}>{props.data.taskResult.status}</Badge>
<strong>{props.data.taskResult.model}</strong>
<pre>{JSON.stringify(props.data.taskResult.result ?? {}, null, 2)}</pre>
</div>
) : (
<div className="emptyState">
<KeyRound size={18} />
<strong></strong>
</div>
)}
</CardContent>
</Card>
</section>
<section className="contentGrid two">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<EntityTable
columns={['能力', '数量', '阶段', '入口']}
empty="暂无模型"
rows={[
['对话', chatModels.length, 'Phase 1', '/v1/chat/completions'],
['图像', imageModels.length, 'Phase 1', '/v1/images/*'],
['视频', props.data.models.filter((item) => item.modelType.some((type) => type.includes('video'))).length, 'Next', '/v1/videos/*'],
]}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<EntityTable
columns={['Scope', '指标', '使用', '限制']}
empty="暂无限流窗口"
rows={props.data.rateLimitWindows.slice(0, 6).map((item) => [item.scopeKey, item.metric, item.usedValue, item.limitValue])}
/>
</CardContent>
</Card>
</section>
</div>
);
}
function ManagementCard(props: { detail: string; icon: ReactNode; label: string; value: number }) {
return (
<Card>
<CardContent className="capabilityCard">
<div className="iconBox">{props.icon}</div>
<div>
<span>{props.label}</span>
<strong>{props.value}</strong>
<small>{props.detail}</small>
</div>
</CardContent>
</Card>
);
}