diff --git a/frontend/src/pages/InstanceGroupPage.tsx b/frontend/src/pages/InstanceGroupPage.tsx
index 33d44d1..7bcac4e 100644
--- a/frontend/src/pages/InstanceGroupPage.tsx
+++ b/frontend/src/pages/InstanceGroupPage.tsx
@@ -1,7 +1,17 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import {
- Plus, Search, Edit2, Trash2, Server, Users, ChevronRight,
- Eye, PenLine, ChevronLeft, ChevronRight as ChevRight, X,
+ Plus,
+ Search,
+ Edit2,
+ Trash2,
+ Server,
+ Users,
+ ChevronRight,
+ Eye,
+ PenLine,
+ ChevronLeft,
+ ChevronRight as ChevRight,
+ X,
} from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { instanceGroupApi, type InstanceGroup } from '@/api/instanceGroup';
@@ -24,7 +34,13 @@ const DB_TYPE_COLORS: Record = {
/* ───────── Transfer Component ───────── */
-interface TransferItem { label: string; value: string; tag?: string; tagColor?: string; accessMode?: string }
+interface TransferItem {
+ label: string;
+ value: string;
+ tag?: string;
+ tagColor?: string;
+ accessMode?: string;
+}
interface TransferProps {
title: string;
@@ -39,17 +55,35 @@ function Transfer({ title, items, selected, onChange, emptyText = '请从左侧
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.includes(search))
+ 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.includes(search))
+ 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(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 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 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)]'}`;
@@ -62,24 +96,49 @@ function Transfer({ title, items, selected, onChange, emptyText = '请从左侧
{unselected.length}
- setSearch(e.target.value)} />
+ setSearch(e.target.value)}
+ />
- {unselected.length === 0 &&
无可用项
}
- {unselected.map(item => (
+ {unselected.length === 0 && (
+
无可用项
+ )}
+ {unselected.map((item) => (
))}
@@ -87,13 +146,22 @@ function Transfer({ title, items, selected, onChange, emptyText = '请从左侧
{selectedFiltered.length}
- {selectedFiltered.length === 0 &&
{emptyText}
}
- {selectedFiltered.map(item => (
+ {selectedFiltered.length === 0 && (
+
{emptyText}
+ )}
+ {selectedFiltered.map((item) => (
))}
@@ -112,7 +180,7 @@ export default function InstanceGroupPage() {
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showUserModal, setShowUserModal] = useState(false);
- const [deleteConfirm, setDeleteConfirm] = useState<{msg: string; onConfirm: () => void} | null>(null);
+ const [deleteConfirm, setDeleteConfirm] = useState<{ msg: string; onConfirm: () => void } | null>(null);
const canManage = useActionPermission('instance-group.manage');
// Form state — shared by create & edit
@@ -128,56 +196,79 @@ export default function InstanceGroupPage() {
const [userSearch, setUserSearch] = useState('');
const fetchGroups = useCallback(async () => {
- try { setGroups(await instanceGroupApi.list()); } catch {} finally { setLoading(false); }
+ try {
+ setGroups(await instanceGroupApi.list());
+ } catch {
+ } finally {
+ setLoading(false);
+ }
}, []);
- useEffect(() => { fetchGroups(); }, [fetchGroups]);
+ useEffect(() => {
+ fetchGroups();
+ }, [fetchGroups]);
useEffect(() => {
if (showCreateModal || showEditModal) {
- instanceApi.list().then((res: any) => {
- setAllInstances(Array.isArray(res) ? res : (res?.items || []));
- }).catch(() => {});
+ instanceApi
+ .list()
+ .then((res: any) => {
+ setAllInstances(Array.isArray(res) ? res : res?.items || []);
+ })
+ .catch(() => {});
}
}, [showCreateModal, showEditModal]);
useEffect(() => {
if (showUserModal) {
- userApi.list().then((d: any) => {
- setAllUsers(Array.isArray(d?.items || d) ? (d?.items || d) : []);
- }).catch(() => {});
+ userApi
+ .list()
+ .then((d: any) => {
+ setAllUsers(Array.isArray(d?.items || d) ? d?.items || d : []);
+ })
+ .catch(() => {});
}
}, [showUserModal]);
- const transferItems = useMemo(() =>
- allInstances.map((i: any) => ({
- label: `${i.name} (${i.host}:${i.port})`,
- value: String(i.id),
- tag: i.db_type,
- tagColor: DB_TYPE_COLORS[i.db_type] || '',
- accessMode: i.access_mode || 'readwrite',
- })),
- [allInstances]
+ const transferItems = useMemo(
+ () =>
+ allInstances.map((i: any) => ({
+ label: `${i.name} (${i.host}:${i.port})`,
+ value: String(i.id),
+ tag: i.db_type,
+ tagColor: DB_TYPE_COLORS[i.db_type] || '',
+ accessMode: i.access_mode || 'readwrite',
+ })),
+ [allInstances],
);
// 查询授权:只匹配 read + readwrite 实例
- const readonlyTransferItems = useMemo(() =>
- transferItems.filter(i => i.accessMode === 'read' || i.accessMode === 'readwrite'),
- [transferItems]
+ const readonlyTransferItems = useMemo(
+ () => transferItems.filter((i) => i.accessMode === 'read' || i.accessMode === 'readwrite'),
+ [transferItems],
);
// 变更授权:只匹配 write + readwrite 实例
- const writeTransferItems = useMemo(() =>
- transferItems.filter(i => i.accessMode === 'write' || i.accessMode === 'readwrite'),
- [transferItems]
+ const writeTransferItems = useMemo(
+ () => transferItems.filter((i) => i.accessMode === 'write' || i.accessMode === 'readwrite'),
+ [transferItems],
);
- const filteredGroups = useMemo(() => groups.filter(g => !search || g.name.includes(search)), [groups, search]);
- const filteredUsers = useMemo(() => allUsers.filter(u => !userSearch || (u.display_name || u.username).toLowerCase().includes(userSearch.toLowerCase())), [allUsers, userSearch]);
+ const filteredGroups = useMemo(() => groups.filter((g) => !search || g.name.includes(search)), [groups, search]);
+ const filteredUsers = useMemo(
+ () =>
+ allUsers.filter(
+ (u) => !userSearch || (u.display_name || u.username).toLowerCase().includes(userSearch.toLowerCase()),
+ ),
+ [allUsers, userSearch],
+ );
const resetForm = () => {
- setFormName(''); setFormDesc(''); setFormIsActive(true);
- setReadonlyIds([]); setWriteIds([]);
+ setFormName('');
+ setFormDesc('');
+ setFormIsActive(true);
+ setReadonlyIds([]);
+ setWriteIds([]);
};
/* ── Create ── */
@@ -191,7 +282,9 @@ export default function InstanceGroupPage() {
readonly_instance_ids: readonlyIds.map(Number),
write_instance_ids: writeIds.map(Number),
});
- setShowCreateModal(false); resetForm(); fetchGroups();
+ setShowCreateModal(false);
+ resetForm();
+ fetchGroups();
};
/* ── Edit ── */
@@ -214,42 +307,64 @@ export default function InstanceGroupPage() {
readonly_instance_ids: readonlyIds.map(Number),
write_instance_ids: writeIds.map(Number),
});
- setShowEditModal(false); resetForm(); fetchGroups();
+ setShowEditModal(false);
+ resetForm();
+ fetchGroups();
};
/* ── Delete ── */
const handleDelete = async (id: number) => {
- setDeleteConfirm({msg: '确定删除该权限组?', onConfirm: async () => {
- await instanceGroupApi.delete(id);
- if (selectedId === id) setSelectedId(null);
- fetchGroups();
- }});
+ setDeleteConfirm({
+ msg: '确定删除该权限组?',
+ onConfirm: async () => {
+ await instanceGroupApi.delete(id);
+ if (selectedId === id) setSelectedId(null);
+ fetchGroups();
+ },
+ });
};
/* ── Assign Users ── */
const handleAssignUsers = async () => {
if (!selectedId || selectedUserIds.length === 0) return;
await instanceGroupApi.assignUsers(selectedId, selectedUserIds);
- setShowUserModal(false); setSelectedUserIds([]); fetchGroups();
+ setShowUserModal(false);
+ setSelectedUserIds([]);
+ fetchGroups();
};
- const selectedGroup = groups.find(g => g.id === selectedId);
+ const selectedGroup = groups.find((g) => g.id === selectedId);
- if (loading) return
;
+ if (loading)
+ return (
+
+ );
/* ── Instance Row ── */
const InstanceList = ({ instances }: { instances: any[] }) => {
if (instances.length === 0) return
暂无
;
return (
- {instances.map(inst => (
-
+ {instances.map((inst) => (
+
-
+
{inst.name}
- {inst.host}:{inst.port}
+
+ {inst.host}:{inst.port}
+
-
{inst.db_type}
+
+ {inst.db_type}
+
))}
@@ -257,8 +372,12 @@ export default function InstanceGroupPage() {
};
/* ── Form Modal shared close/submit ── */
- const formClose = () => { setShowCreateModal(false); setShowEditModal(false); resetForm(); };
- const formMode = showCreateModal ? 'create' as const : showEditModal ? 'edit' as const : null;
+ const formClose = () => {
+ setShowCreateModal(false);
+ setShowEditModal(false);
+ resetForm();
+ };
+ const formMode = showCreateModal ? ('create' as const) : showEditModal ? ('edit' as const) : null;
const formSubmit = formMode === 'create' ? handleCreate : handleEdit;
return (
@@ -270,7 +389,13 @@ export default function InstanceGroupPage() {
每个策略包含「查询授权」和「变更授权」两类实例
{canManage && (
-