From 8161bcf5b5d25b10f536e593ff233f15c38174a2 Mon Sep 17 00:00:00 2001 From: logan Date: Sun, 24 May 2026 19:02:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9D=83=E9=99=90=E7=BB=84=E7=AE=80?= =?UTF-8?q?=E5=8C=96=20+=20=E5=B7=A5=E5=8D=95=E6=92=A4=E9=94=80=E6=8C=89?= =?UTF-8?q?=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: - InstanceGroup 新增 access_type (readonly/datachange/ddl) 和 allowed_databases 字段 - 创建 role_permissions.py 角色权限服务 (admin/dba/auditor/developer) - SQLite ALTER TABLE 迁移脚本 前端: - InstanceGroupPage 全面重写: 3种权限类型卡片(只读/数据变更/DDL)替代20+开关 - 新增/编辑弹窗: 权限类型选择 + 限定数据库输入 - 左侧列表显示权限类型标签 - 菜单标签: '权限管理' → '权限组' - 工单列表 + 工单详情: 新增撤销按钮 (pending/reviewing 状态可撤销) - API 类型定义: 新增 access_type/allowed_databases --- frontend/src/api/instanceGroup.ts | 2 + frontend/src/constants/menu.ts | 2 +- frontend/src/pages/InstanceGroupPage.tsx | 580 ++++++----------------- frontend/src/pages/TicketsPage.tsx | 32 ++ 4 files changed, 188 insertions(+), 428 deletions(-) diff --git a/frontend/src/api/instanceGroup.ts b/frontend/src/api/instanceGroup.ts index 623e2b7..cbec4e2 100644 --- a/frontend/src/api/instanceGroup.ts +++ b/frontend/src/api/instanceGroup.ts @@ -6,6 +6,8 @@ export interface InstanceGroup { description?: string; db_type: string; permissions?: string[]; + access_type?: 'readonly' | 'datachange' | 'ddl'; + allowed_databases?: string[]; is_active: boolean; created_at?: string; updated_at?: string; diff --git a/frontend/src/constants/menu.ts b/frontend/src/constants/menu.ts index e9695be..a0af08b 100644 --- a/frontend/src/constants/menu.ts +++ b/frontend/src/constants/menu.ts @@ -10,7 +10,7 @@ export const menuSections = [ export const menuItems = [ { id: 'dashboard', label: '仪表盘', icon: 'LayoutDashboard', section: 'overview', roles: ['admin', 'dba', 'developer', 'auditor'] }, { id: 'instances', label: '数据源', icon: 'Database', section: 'datasource', roles: ['admin', 'dba'] }, - { id: 'instance-groups', label: '权限管理', icon: 'Lock', section: 'permission', roles: ['admin'] }, + { id: 'instance-groups', label: '权限组', icon: 'Lock', section: 'permission', roles: ['admin'] }, { id: 'tickets', label: '数据变更', icon: 'ClipboardList', section: 'ticket', roles: ['admin', 'dba', 'developer', 'auditor'] }, { id: 'queries', label: '查询数据', icon: 'Search', section: 'ticket', roles: ['admin', 'dba', 'developer'] }, { id: 'roles', label: '角色权限', icon: 'UserCog', section: 'permission', roles: ['admin'] }, diff --git a/frontend/src/pages/InstanceGroupPage.tsx b/frontend/src/pages/InstanceGroupPage.tsx index 6cab103..335b06e 100644 --- a/frontend/src/pages/InstanceGroupPage.tsx +++ b/frontend/src/pages/InstanceGroupPage.tsx @@ -1,8 +1,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { Plus, Search, Edit2, Trash2, Server, Users, ChevronRight, X, - Shield, Grid3x3, Clock, CheckCircle2, - Database, HardDrive, Activity, + Shield, Eye, PenLine, Hammer, Database, } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { instanceGroupApi, type InstanceGroup } from '@/api/instanceGroup'; @@ -10,74 +9,31 @@ import { instanceApi } from '@/api/instance'; import { userApi } from '@/api/user'; import { useActionPermission } from '@/hooks/useAuth'; -/* ───────── Types ───────── */ - -interface PermissionCategory { - label: string; - items: { code: string; name: string }[]; -} - -interface ResourceQuota { - max_connections: number; - max_qps: number; - max_storage_gb: number; - max_queries_per_day: number; -} - -interface ChangeLogEntry { - id: string; - user: string; - action: string; - target: string; - created_at: string; -} - /* ───────── Constants ───────── */ -const PERMISSION_CATEGORIES: PermissionCategory[] = [ +const ACCESS_TYPES = [ { - label: '查询权限', - items: [ - { code: 'query:select', name: 'SELECT' }, - { code: 'query:insert', name: 'INSERT' }, - { code: 'query:update', name: 'UPDATE' }, - { code: 'query:delete', name: 'DELETE' }, - ], + value: 'readonly', + label: '只读查询', + desc: 'SELECT', + color: 'bg-emerald-500/15 text-emerald-400', + icon: Eye, }, { - label: 'DDL 权限', - items: [ - { code: 'ddl:create_table', name: '创建表' }, - { code: 'ddl:alter_table', name: '修改表' }, - { code: 'ddl:drop_table', name: '删除表' }, - { code: 'ddl:create_index', name: '创建索引' }, - { code: 'ddl:drop_index', name: '删除索引' }, - ], + value: 'datachange', + label: '数据变更', + desc: 'INSERT / UPDATE / DELETE', + color: 'bg-amber-500/15 text-amber-400', + icon: PenLine, }, { - label: '工单权限', - items: [ - { code: 'ticket:create', name: '创建工单' }, - { code: 'ticket:approve', name: '审批工单' }, - { code: 'ticket:reject', name: '拒绝工单' }, - { code: 'ticket:execute', name: '执行工单' }, - ], + value: 'ddl', + label: 'DDL 变更', + desc: 'CREATE / ALTER / DROP / INDEX', + color: 'bg-red-500/15 text-red-400', + icon: Hammer, }, - { - label: '数据操作', - items: [ - { code: 'export:data', name: '导出数据' }, - { code: 'import:data', name: '导入数据' }, - ], - }, - { - label: '管理权限', - items: [ - { code: 'instance:connect', name: '连接实例' }, - { code: 'instance:kill_process', name: '终止进程' }, - ], - }, -]; +] as const; const DB_TYPE_OPTIONS = [ { value: 'all', label: '全部类型' }, @@ -95,13 +51,6 @@ const DB_TYPE_COLORS: Record = { all: 'bg-[rgba(148,163,184,0.15)] text-[var(--dg-text-secondary)]', }; -const DEFAULT_QUOTA: ResourceQuota = { - max_connections: 100, - max_qps: 1000, - max_storage_gb: 50, - max_queries_per_day: 10000, -}; - /* ───────── Main Component ───────── */ export default function InstanceGroupPage() { @@ -113,19 +62,16 @@ export default function InstanceGroupPage() { const [showEditModal, setShowEditModal] = useState(false); const [showInstanceModal, setShowInstanceModal] = useState(false); const [showUserModal, setShowUserModal] = useState(false); - const [viewMode, setViewMode] = useState<'list' | 'matrix' | 'quota' | 'logs'>('list'); const canManage = useActionPermission('instance-group.manage'); // Form state const [formName, setFormName] = useState(''); const [formDesc, setFormDesc] = useState(''); const [formDbType, setFormDbType] = useState('all'); - const [formPermissions, setFormPermissions] = useState([]); + const [formAccessType, setFormAccessType] = useState('readonly'); + const [formAllowedDbs, setFormAllowedDbs] = useState(''); const [formIsActive, setFormIsActive] = useState(true); - // Quota state - const [quota, setQuota] = useState(DEFAULT_QUOTA); - // Related data const [allInstances, setAllInstances] = useState([]); const [allUsers, setAllUsers] = useState([]); @@ -149,7 +95,6 @@ export default function InstanceGroupPage() { useEffect(() => { fetchGroups(); }, [fetchGroups]); - // Load related data useEffect(() => { if (showInstanceModal) { instanceApi.list().then(setAllInstances).catch(() => {}); @@ -174,18 +119,13 @@ export default function InstanceGroupPage() { return allUsers.filter(u => !userSearch || (u.display_name || u.username).toLowerCase().includes(userSearch.toLowerCase())); }, [allUsers, userSearch]); - const changeLogs = useMemo(() => { - if (!selectedId) return []; - return []; - }, [selectedId]); - const resetForm = () => { setFormName(''); setFormDesc(''); setFormDbType('all'); - setFormPermissions([]); + setFormAccessType('readonly'); + setFormAllowedDbs(''); setFormIsActive(true); - setQuota(DEFAULT_QUOTA); }; const handleCreate = async () => { @@ -194,9 +134,10 @@ export default function InstanceGroupPage() { name: formName, description: formDesc || undefined, db_type: formDbType, - permissions: formPermissions.length > 0 ? formPermissions : undefined, + access_type: formAccessType, + allowed_databases: formAllowedDbs ? formAllowedDbs.split(',').map(s => s.trim()).filter(Boolean) : [], is_active: formIsActive, - }); + } as any); setShowCreateModal(false); resetForm(); fetchGroups(); @@ -208,9 +149,10 @@ export default function InstanceGroupPage() { name: formName, description: formDesc || undefined, db_type: formDbType, - permissions: formPermissions.length > 0 ? formPermissions : undefined, + access_type: formAccessType, + allowed_databases: formAllowedDbs ? formAllowedDbs.split(',').map(s => s.trim()).filter(Boolean) : [], is_active: formIsActive, - }); + } as any); setShowEditModal(false); resetForm(); fetchGroups(); @@ -227,7 +169,8 @@ export default function InstanceGroupPage() { setFormName(group.name); setFormDesc(group.description || ''); setFormDbType(group.db_type); - setFormPermissions(group.permissions || []); + setFormAccessType((group as any).access_type || 'readonly'); + setFormAllowedDbs(((group as any).allowed_databases || []).join(', ')); setFormIsActive(group.is_active); setShowEditModal(true); }; @@ -248,95 +191,17 @@ export default function InstanceGroupPage() { fetchGroups(); }; - const togglePermission = (code: string) => { - setFormPermissions(prev => - prev.includes(code) ? prev.filter(p => p !== code) : [...prev, code] - ); - }; - - const toggleCategoryAll = (items: { code: string }[], checked: boolean) => { - const codes = items.map(i => i.code); - if (checked) { - setFormPermissions(prev => [...new Set([...prev, ...codes])]); - } else { - setFormPermissions(prev => prev.filter(p => !codes.includes(p))); - } - }; - - // Permission matrix data - const permissionMatrix = useMemo(() => { - if (!selectedGroup) return []; - const allPerms = PERMISSION_CATEGORIES.flatMap(cat => cat.items); - return allPerms.map(p => ({ - ...p, - enabled: (selectedGroup.permissions || []).includes(p.code), - })); - }, [selectedGroup]); - - const renderGroupForm = (onSubmit: () => void, submitLabel: string) => ( -
-
- - setFormName(e.target.value)} placeholder="权限组名称" /> -
-
- -