From 40868c61bf2f0bd3dc0f6c4dd4f124ab7d7c0c0f Mon Sep 17 00:00:00 2001 From: logan Date: Sun, 24 May 2026 19:43:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9D=83=E9=99=90=E7=BB=84=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E6=94=B9=E4=B8=BA=E5=8F=8CTransfer=E5=90=8C=E6=97=B6?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=8F=AA=E8=AF=BB+=E5=86=99=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新建时不再二选一,上下两列Transfer同时配置查询权限和写入权限 - 自动生成「名称-只读」「名称-写入」两个权限组 - 编辑模式保持单组编辑不变 - 按钮显示将创建几个权限组 --- frontend/src/pages/InstanceGroupPage.tsx | 358 ++++++++++------------- 1 file changed, 158 insertions(+), 200 deletions(-) diff --git a/frontend/src/pages/InstanceGroupPage.tsx b/frontend/src/pages/InstanceGroupPage.tsx index 7b1dc4b..262d0a5 100644 --- a/frontend/src/pages/InstanceGroupPage.tsx +++ b/frontend/src/pages/InstanceGroupPage.tsx @@ -2,7 +2,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { Plus, Search, Edit2, Trash2, Server, Users, ChevronRight, X, Eye, PenLine, ChevronLeft, ChevronRight as ChevRight, - Database, UserPlus, } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { instanceGroupApi, type InstanceGroup } from '@/api/instanceGroup'; @@ -13,22 +12,8 @@ import { useActionPermission } from '@/hooks/useAuth'; /* ───────── Constants ───────── */ const ACCESS_TYPES = [ - { - value: 'readonly', - label: '只读查看', - desc: '关联只读/从库,用于查询数据', - color: 'text-emerald-400', - bg: 'bg-emerald-500/15', - icon: Eye, - }, - { - value: 'write', - label: '写入变更', - desc: '关联读写/主库,用于数据变更工单', - color: 'text-amber-400', - bg: 'bg-amber-500/15', - icon: PenLine, - }, + { value: 'readonly', label: '只读查看', desc: '关联只读/从库,用于查询数据', color: 'text-emerald-400', bg: 'bg-emerald-500/15', icon: Eye }, + { value: 'write', label: '写入变更', desc: '关联读写/主库,用于数据变更工单', color: 'text-amber-400', bg: 'bg-amber-500/15', icon: PenLine }, ] as const; const DB_TYPE_COLORS: Record = { @@ -41,18 +26,13 @@ const DB_TYPE_COLORS: Record = { /* ───────── Transfer Component ───────── */ -interface TransferItem { - label: string; - value: string; - tag?: string; - tagColor?: string; -} +interface TransferItem { label: string; value: string; tag?: string; tagColor?: string } interface TransferProps { title: string; items: TransferItem[]; selected: string[]; - onChange: (selected: string[]) => void; + onChange: (v: string[]) => void; } function Transfer({ title, items, selected, onChange }: TransferProps) { @@ -60,45 +40,36 @@ function Transfer({ title, items, selected, onChange }: TransferProps) { const [pendingAdd, setPendingAdd] = useState>(new Set()); const [pendingRemove, setPendingRemove] = useState>(new Set()); - const unselected = items.filter( - i => !selected.includes(i.value) && - (i.label.toLowerCase().includes(search.toLowerCase()) || i.value.toLowerCase().includes(search.toLowerCase())) + const unselected = items.filter(i => + !selected.includes(i.value) && (i.label.toLowerCase().includes(search.toLowerCase()) || i.value.includes(search)) ); - const selectedFiltered = items.filter( - i => selected.includes(i.value) && - (i.label.toLowerCase().includes(search.toLowerCase()) || i.value.toLowerCase().includes(search.toLowerCase())) + const selectedFiltered = items.filter(i => + selected.includes(i.value) && (i.label.toLowerCase().includes(search.toLowerCase()) || i.value.includes(search)) ); - const addSelected = () => { - onChange([...selected, ...pendingAdd]); - setPendingAdd(new Set()); - }; - const removeSelected = () => { - onChange(selected.filter(v => !pendingRemove.has(v))); - setPendingRemove(new Set()); - }; - const toggleAdd = (v: string) => setPendingAdd(prev => { const n = new Set(prev); n.has(v) ? n.delete(v) : n.add(v); return n; }); - const toggleRemove = (v: string) => setPendingRemove(prev => { const n = new Set(prev); n.has(v) ? n.delete(v) : n.add(v); return n; }); + const addSelected = () => { onChange([...selected, ...pendingAdd]); setPendingAdd(new Set()); }; + const removeSelected = () => { onChange(selected.filter(v => !pendingRemove.has(v))); setPendingRemove(new Set()); }; + const toggleAdd = (v: string) => setPendingAdd(p => { const n = new Set(p); n.has(v) ? n.delete(v) : n.add(v); return n; }); + const toggleRemove = (v: string) => setPendingRemove(p => { const n = new Set(p); n.has(v) ? n.delete(v) : n.add(v); return n; }); - const rowCls = (active: boolean, color: string) => + const row = (active: boolean, color: string) => `flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer text-xs transition-colors ${active ? color : 'hover:bg-[var(--dg-bg-hover)]'}`; return (
- {/* Left: Available */}
-
+
可选实例 {unselected.length} 项
- setSearch(e.target.value)} />
-
- {unselected.length === 0 &&
无可用项
} +
+ {unselected.length === 0 &&
无可用项
} {unselected.map(item => ( -
- - {/* Arrows */} -
- - +
+ +
- - {/* Right: Selected */}
-
+
{title} {selectedFiltered.length} 项
-
- {selectedFiltered.length === 0 &&
请从左侧选择实例
} +
+ {selectedFiltered.length === 0 &&
请从左侧选择实例
} {selectedFiltered.map(item => ( -
- {/* 2 permission type cards */} + {/* Summary cards */}
{ACCESS_TYPES.map(at => { const count = groups.filter(g => (g as any).access_type === at.value).length; @@ -300,13 +253,8 @@ export default function InstanceGroupPage() { return ( -
- -
-
-
{at.label}
-
{at.desc}
-
+
+
{at.label}
{at.desc}
{count}
@@ -324,13 +272,13 @@ export default function InstanceGroupPage() {
{filteredGroups.length === 0 &&
暂无权限组
} {filteredGroups.map(group => { - const at = getAtConfig((group as any).access_type || 'readonly'); + const at = getAt((group as any).access_type || 'readonly'); const Icon = at.icon; return (
setSelectedId(group.id)} className={`p-3 rounded-lg cursor-pointer transition-colors ${selectedId === group.id ? 'bg-blue-500/10 border border-blue-500/30' : 'bg-[var(--dg-bg-secondary)] border border-transparent hover:bg-[var(--dg-bg-hover)]'}`}>
- {group.name} + {group.name}
{at.label} @@ -347,7 +295,7 @@ export default function InstanceGroupPage() {
{selectedGroup ? (() => { const sg = selectedGroup; - const at = getAtConfig((sg as any).access_type || 'readonly'); + const at = getAt((sg as any).access_type || 'readonly'); const Icon = at.icon; return (
@@ -372,29 +320,23 @@ export default function InstanceGroupPage() {
- - {/* Related Instances */}

关联实例 ({sg.instances?.length || 0})

{sg.instances && sg.instances.length > 0 ? ( -
- {sg.instances.map(inst => ( -
-
- - {inst.name} - {inst.host}:{inst.port} -
- {inst.db_type} +
{sg.instances.map(inst => ( +
+
+ + {inst.name} + {inst.host}:{inst.port}
- ))} -
+ {inst.db_type} +
+ ))}
) :
暂未关联实例
} - - {/* Related Users */}
@@ -402,15 +344,13 @@ export default function InstanceGroupPage() { {canManage && }
{sg.users && sg.users.length > 0 ? ( -
- {sg.users.map(u => ( -
-
{(u.display_name || u.username).charAt(0)}
- {u.display_name || u.username} - {u.role} -
- ))} -
+
{sg.users.map(u => ( +
+
{(u.display_name || u.username).charAt(0)}
+ {u.display_name || u.username} + {u.role} +
+ ))}
) :
暂未关联用户
}
@@ -425,69 +365,87 @@ export default function InstanceGroupPage() {
- {/* Create/Edit Modal */} - {(showCreateModal || showEditModal) && ( + {/* ═══════ Create Modal: dual transfer ═══════ */} + {showCreateModal && (
-
e.stopPropagation()}> -

{showCreateModal ? '新建权限组' : '编辑权限组'}

+
e.stopPropagation()}> +

新建权限策略

-
- - setFormName(e.target.value)} placeholder="如:生产只读、开发写入" /> -
+
+ + setFormName(e.target.value)} placeholder="如:生产环境、开发环境" /> +

自动生成「名称-只读」和「名称-写入」两个权限组

+
setFormDesc(e.target.value)} placeholder="可选" />
-
- - -
- {/* Access Type */} -
- -
- {ACCESS_TYPES.map(at => { - const Icon = at.icon; - return ( - - ); - })} + {/* Row 1: Readonly */} +
+
+ + 只读查看 + — 查询数据的实例
+
- {/* Transfer: select instances */} -
- - + {/* Row 2: Write */} +
+
+ + 写入变更 + — 数据变更工单的实例 +
+
- - + +
)} - {/* User Modal */} + {/* ═══════ Edit Modal: single group ═══════ */} + {showEditModal && ( +
+
e.stopPropagation()}> +

编辑权限组

+
+
+
+ + setFormName(e.target.value)} /> +
+
+ +
+ {getAt(editAccessType).label} +
+
+
+
+ + +
+
+ + +
+
+
+
+ )} + + {/* ═══════ User Modal ═══════ */} {showUserModal && (
e.stopPropagation()}>