Show admin save feedback and preserve custom failover actions

This commit is contained in:
2026-05-12 21:57:10 +08:00
parent a76ab941bc
commit e778fad6ed
3 changed files with 22 additions and 7 deletions
+1
View File
@@ -1058,6 +1058,7 @@ export function App() {
onSaveUser={saveUser} onSaveUser={saveUser}
onSetUserWalletBalance={saveUserWalletBalance} onSetUserWalletBalance={saveUserWalletBalance}
onSaveUserGroup={saveUserGroup} onSaveUserGroup={saveUserGroup}
onClearOperationMessage={() => setCoreMessage('')}
onSectionChange={navigateAdminSection} onSectionChange={navigateAdminSection}
/> />
) : ( ) : (
+7 -1
View File
@@ -18,7 +18,7 @@ import type {
import type { ConsoleData, StatItem } from '../app-state'; import type { ConsoleData, StatItem } from '../app-state';
import { EntityTable } from '../components/EntityTable'; import { EntityTable } from '../components/EntityTable';
import { StatGrid } from '../components/StatGrid'; import { StatGrid } from '../components/StatGrid';
import { Badge, Card, CardContent, CardHeader, CardTitle, Tabs } from '../components/ui'; import { Badge, Card, CardContent, CardHeader, CardTitle, ScreenMessage, Tabs } from '../components/ui';
import type { AdminSection, LoadState, PlatformWithModelsInput } from '../types'; import type { AdminSection, LoadState, PlatformWithModelsInput } from '../types';
import { AccessRulesPanel } from './admin/AccessRulesPanel'; import { AccessRulesPanel } from './admin/AccessRulesPanel';
import { AuditLogsPanel } from './admin/AuditLogsPanel'; import { AuditLogsPanel } from './admin/AuditLogsPanel';
@@ -76,10 +76,16 @@ export function AdminPage(props: {
onSaveUser: (input: GatewayUserUpsertRequest, userId?: string) => Promise<void>; onSaveUser: (input: GatewayUserUpsertRequest, userId?: string) => Promise<void>;
onSetUserWalletBalance: (userId: string, input: WalletBalanceAdjustmentRequest) => Promise<void>; onSetUserWalletBalance: (userId: string, input: WalletBalanceAdjustmentRequest) => Promise<void>;
onSaveUserGroup: (input: UserGroupUpsertRequest, groupId?: string) => Promise<void>; onSaveUserGroup: (input: UserGroupUpsertRequest, groupId?: string) => Promise<void>;
onClearOperationMessage: () => void;
onSectionChange: (value: AdminSection) => void; onSectionChange: (value: AdminSection) => void;
}) { }) {
return ( return (
<div className="pageStack"> <div className="pageStack">
<ScreenMessage
message={props.operationMessage}
variant={props.state === 'error' ? 'error' : 'success'}
onClose={props.onClearOperationMessage}
/>
<div className="subPageLayout"> <div className="subPageLayout">
<Tabs className="sideTabs" value={props.section} tabs={tabs} onValueChange={props.onSectionChange} /> <Tabs className="sideTabs" value={props.section} tabs={tabs} onValueChange={props.onSectionChange} />
<div className="subPageContent"> <div className="subPageContent">
@@ -76,6 +76,8 @@ const failoverActionDefinitions = [
}, },
] as const; ] as const;
const failoverActionValues = new Set<string>(failoverActionDefinitions.map((item) => item.value));
const failoverCategoryOptions = [ const failoverCategoryOptions = [
'network', 'network',
'timeout', 'timeout',
@@ -653,19 +655,18 @@ function defaultFailoverActions(): Record<string, unknown> {
} }
function failoverCategoryRoutingGroups(allowCategories: string[], denyCategories: string[], actions: Record<string, unknown>) { function failoverCategoryRoutingGroups(allowCategories: string[], denyCategories: string[], actions: Record<string, unknown>) {
const actionSet = new Set(failoverActionDefinitions.map((item) => item.value));
const assignments = new Map<string, string>(); const assignments = new Map<string, string>();
for (const category of cleanTags(allowCategories)) { for (const category of cleanTags(allowCategories)) {
const rawAction = actions[category]; const rawAction = actions[category];
const action = typeof rawAction === 'string' ? rawAction : ''; const action = typeof rawAction === 'string' ? rawAction : '';
assignments.set(category, actionSet.has(action as (typeof failoverActionDefinitions)[number]['value']) ? action : 'next'); assignments.set(category, failoverActionValues.has(action) ? action : 'next');
} }
for (const [category, rawAction] of Object.entries(actions)) { for (const [category, rawAction] of Object.entries(actions)) {
if (assignments.has(category)) continue; if (assignments.has(category)) continue;
const action = typeof rawAction === 'string' ? rawAction : ''; const action = typeof rawAction === 'string' ? rawAction : '';
if (action === 'stop') { if (action === 'stop') {
assignments.set(category, 'deny'); assignments.set(category, 'deny');
} else if (actionSet.has(action as (typeof failoverActionDefinitions)[number]['value'])) { } else if (failoverActionValues.has(action)) {
assignments.set(category, action); assignments.set(category, action);
} }
} }
@@ -708,9 +709,16 @@ function updateFailoverCategoryRouting(
knownCategories.add(category); knownCategories.add(category);
} }
for (const [category, rawAction] of Object.entries(actions)) { for (const [category, rawAction] of Object.entries(actions)) {
if (!knownCategories.has(category) && typeof rawAction === 'string' && rawAction !== 'next' && rawAction !== 'stop') { const shouldPreserveCustomAction = (
nextActions[category] = rawAction; !knownCategories.has(category)
} && typeof rawAction === 'string'
&& rawAction !== 'next'
&& rawAction !== 'stop'
&& !failoverActionValues.has(rawAction)
);
if (!shouldPreserveCustomAction) continue;
// Keep forward-compatible custom actions that this editor cannot render.
nextActions[category] = rawAction;
} }
return { return {
allowCategories: cleanTags(nextAllowCategories), allowCategories: cleanTags(nextAllowCategories),