refactor: 弹框架构重构 + 审核规则seed API + LDAP默认配置 + 全量初始数据

弹框架构:
- Layout header/sidebar z-[200] 隔离弹框层,弹框不再遮挡面包屑
- dialog.tsx 简化: 去掉 border/backdrop-blur,统一暗色主题变量
- ModalOverlay z-[100] 对齐 Radix Dialog overlay 层级
- EditInstanceDialog 同步 z-index 修复
- 修复 InstanceGroupPage/InstancePage/LdapConfigPage 缺失 import
- 修复 InstanceGroupPage 缺失 X icon import
- RedisDetailPage ModalOverlay 迁移为 FormDialog

审核规则:
- POST /audit-rules/seed/:dbType 初始化默认规则 (mysql 11/pg 7/redis 4/mongo 4)
- POST /audit-rules/:id/toggle 开关规则
- PUT /audit-rules/:id 编辑规则
- seed.go 启动时自动创建 26 条默认审核规则

LDAP 默认配置:
- seed.go 自动写入 9 条 LDAP 配置 (8.217.205.92:389, dc=opsx,dc=vip)
- 全新 DB 启动即就位,无需手动配置
This commit is contained in:
logan committed 2026-05-29 22:48:45 +08:00
1 parent 47431f127c
commit 889cce140b
24 files changed
+494 -252

No files matched your search

@@ -137,7 +137,7 @@ export default function EditInstanceDialog({ instance, open, onClose, onSaved, m
const labelCls = 'block text-xs font-medium text-[var(--dg-text-secondary)] mb-1';
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/40">
<div
className="bg-[var(--dg-bg-secondary)] rounded-xl shadow-2xl w-full max-w-lg max-h-[85vh] overflow-y-auto"
onClick={e => e.stopPropagation()}
+4 -4
View File
@@ -52,8 +52,8 @@ export default function Layout() {
return (
<div className="flex h-screen bg-[var(--dg-bg-primary)] text-[var(--dg-text-primary)] overflow-hidden">
{/* 侧边栏 */}
<aside className="flex flex-col bg-[var(--dg-bg-secondary)] border-r border-[var(--dg-border)] w-[220px] min-w-[220px]">
{/* 侧边栏 — z-[200] 永远在弹框之上 */}
<aside className="relative z-[200] flex flex-col bg-[var(--dg-bg-secondary)] border-r border-[var(--dg-border)] w-[220px] min-w-[220px]">
{/* Logo */}
<div className="h-14 flex items-center px-4">
<div className="flex items-center gap-2.5">
@@ -123,8 +123,8 @@ export default function Layout() {
{/* 主内容区 */}
<main id="app-main" className="relative flex-1 flex flex-col min-w-0 overflow-hidden">
{/* 顶部栏 */}
<header className="h-14 bg-[var(--dg-bg-secondary)] border-b border-[var(--dg-border)] flex items-center justify-between px-5 flex-shrink-0">
{/* 顶部栏 — z-[200] 永远在弹框之上 */}
<header className="relative z-[200] h-14 bg-[var(--dg-bg-secondary)] border-b border-[var(--dg-border)] flex items-center justify-between px-5 flex-shrink-0">
{/* Breadcrumb area */}
<div className="flex items-center gap-1.5 text-sm">
<span className="text-[var(--dg-text-muted)] font-medium">OpsxSQL</span>
+7 -1
View File
@@ -8,6 +8,12 @@ interface ModalOverlayProps {
children: React.ReactNode;
}
/**
* 通用弹框组件
* - overlay z-[100],低于 header/sidebar 的 z-[200],不遮挡面包屑
* - 点击遮罩层不关闭
* - ESC 键关闭
*/
export function ModalOverlay({ open, onClose, width = 'max-w-lg', className = '', children }: ModalOverlayProps) {
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
@@ -28,7 +34,7 @@ export function ModalOverlay({ open, onClose, width = 'max-w-lg', className = ''
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/40">
<div
className={`bg-[var(--dg-bg-secondary)] rounded-xl shadow-2xl w-full ${width} max-h-[85vh] overflow-y-auto ${className}`}
onClick={(e) => e.stopPropagation()}
+3 -3
View File
@@ -1,4 +1,4 @@
import React, { useRef, useEffect, useCallback, useImperativeHandle, forwardRef } from 'react';
import { useRef, useEffect, useCallback, useImperativeHandle, forwardRef } from 'react';
import { EditorState, Compartment } from '@codemirror/state';
import type { Extension } from '@codemirror/state';
import { EditorView, keymap, lineNumbers, placeholder as cmPlaceholder } from '@codemirror/view';
@@ -125,8 +125,8 @@ function getSQLExtension(language: SqlEditorProps['language'], schema?: SqlEdito
const dialect = SQLDialect.define({
keywords: 'select from where order by group having limit offset join inner left right full outer cross on and or not in is null like between as asc desc distinct all case when then else end exists union intersect except insert into values update set delete create alter drop table index view primary key foreign references unique check default grant revoke commit rollback begin transaction count sum avg min max show describe use database explain analyze',
schema: tables,
});
...(tables ? { schema: tables } : {}),
} as any);
return sql({ dialect });
}
+1 -1
View File
@@ -68,7 +68,7 @@ const STARS = [
{ left: '60%', top: '30%', size: 1, delay: 2.2, duration: 4.2 },
];
export default function AuroraView(): JSX.Element {
export default function AuroraView() {
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
return (
+1 -1
View File
@@ -66,7 +66,7 @@ interface Particle {
}
/* ── 组件 ── */
export default function CosmosView(): JSX.Element {
export default function CosmosView() {
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
/* 生成静态星星数据 */
@@ -43,7 +43,7 @@ const KEYFRAMES = `
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
export default function InkWashView(): JSX.Element {
export default function InkWashView() {
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
return (
@@ -31,7 +31,7 @@ const KEYFRAMES = `
}
`;
export default function NeonCityView(): JSX.Element {
export default function NeonCityView() {
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
const [focus, setFocus] = useState<string | null>(null);
const isDisabled = !username || !password || loading;
+1 -1
View File
@@ -26,7 +26,7 @@ export default function SidePanel() {
onClick={toggleTheme}
className="absolute top-5 right-5 z-20 w-10 h-10 rounded-xl bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] flex items-center justify-center text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] transition-colors"
>
{theme === 'dark' ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
{(theme as string) === 'dark' ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
</button>
{/* 左侧 */}
+26 -59
View File
@@ -1,42 +1,30 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 z-[100] bg-black/50",
"fixed inset-0 z-[100] bg-black/40",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
@@ -51,28 +39,24 @@ function DialogContent({
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
}: React.ComponentProps<typeof DialogPrimitive.Content> & { showCloseButton?: boolean }) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
onPointerDownOutside={(e) => {
e.preventDefault()
}}
onInteractOutside={(e) => {
e.preventDefault()
}}
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out",
"bg-[var(--dg-bg-secondary)] text-[var(--dg-text-primary)]",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
"fixed z-[101] grid w-full max-w-[calc(100%-2rem)]",
"top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]",
"gap-4 rounded-lg border p-6 shadow-lg duration-200",
"outline-none sm:max-w-lg",
"fixed z-[101] top-[50%] left-[50%] -translate-x-1/2 -translate-y-1/2",
"w-full max-w-[calc(100%-2rem)] sm:max-w-lg",
"rounded-xl shadow-2xl",
"max-h-[85vh] flex flex-col",
"outline-none duration-200",
className
)}
{...props}
@@ -81,9 +65,9 @@ function DialogContent({
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
className="absolute top-3.5 right-3.5 rounded-md p-1 text-[var(--dg-text-muted)] opacity-70 transition-opacity hover:opacity-100 focus:outline-none"
>
<XIcon />
<XIcon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
@@ -94,48 +78,31 @@ function DialogContent({
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
<div data-slot="dialog-header" className={cn("flex items-center justify-between", className)} {...props} />
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
<div data-slot="dialog-footer" className={cn("flex justify-end gap-2 pt-2", className)} {...props} />
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
className={cn("text-base font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
className={cn("text-sm text-[var(--dg-text-muted)]", className)}
{...props}
/>
)
@@ -148,8 +115,8 @@ export {
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
DialogOverlay,
DialogPortal,
}
+1 -1
View File
@@ -32,7 +32,7 @@ export function useLogin(): LoginState {
if (ok) {
// Apply user preferences from backend (cross-browser sync)
try {
const prefs = await api.get<{ theme?: string; login_style?: string }>('/user/preferences');
const { data: prefs } = await api.get<{ theme?: string; login_style?: string }>('/user/preferences');
if (prefs?.theme) {
const t = prefs.theme;
if (t === 'dark' || t === 'light') {
+75 -86
View File
@@ -1,8 +1,8 @@
import { useState, useEffect, useCallback } from 'react';
import request from '@/api/request';
import { Megaphone, Plus, Edit2, Trash2, Eye, Clock, CheckCircle, X, Save } from 'lucide-react';
import { Megaphone, Plus, Edit2, Trash2, Eye, Clock, CheckCircle, Save } from 'lucide-react';
import { ConfirmDialog } from '@/components/ConfirmDialog';
import { ModalOverlay } from '@/components/ModalOverlay';
import { FormDialog } from '@/components/FormDialog';
/* ── types ── */
interface Announcement {
@@ -231,94 +231,35 @@ export default function AnnouncementPage() {
{/* ── View Preview Modal ── */}
{viewAnn && (
<ModalOverlay open={!!viewAnn} onClose={() => setViewAnn(null)} width="max-w-2xl">
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold text-[var(--dg-text-primary)]">{viewAnn.title}</h3>
<span
className={`text-[11px] px-2 py-0.5 rounded-full ${
viewAnn.is_published ? 'bg-green-500/15 text-green-400' : 'bg-yellow-500/15 text-yellow-400'
}`}
>
{viewAnn.is_published ? '已发布' : '草稿'}
</span>
</div>
<button onClick={() => setViewAnn(null)} className="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-[var(--dg-bg-hover)]">
<X className="w-5 h-5" />
</button>
</div>
<div className="text-xs text-[var(--dg-text-muted)] flex items-center gap-3 mb-4 pb-3 border-b border-[var(--dg-border)]">
<span className="flex items-center gap-1"><Clock className="w-3 h-3" /> {viewAnn.created_at}</span>
</div>
<div
className="text-sm text-[var(--dg-text-secondary)] leading-relaxed prose-sm"
dangerouslySetInnerHTML={{ __html: simpleMarkdown(viewAnn.content) }}
/>
<FormDialog open={!!viewAnn} onClose={() => setViewAnn(null)} title={viewAnn.title} width="max-w-2xl">
<div className="flex items-center gap-2 mb-3">
<span
className={`text-[11px] px-2 py-0.5 rounded-full ${
viewAnn.is_published ? 'bg-green-500/15 text-green-400' : 'bg-yellow-500/15 text-yellow-400'
}`}
>
{viewAnn.is_published ? '已发布' : '草稿'}
</span>
</div>
</ModalOverlay>
<div className="text-xs text-[var(--dg-text-muted)] flex items-center gap-3 mb-4 pb-3 border-b border-[var(--dg-border)]">
<span className="flex items-center gap-1"><Clock className="w-3 h-3" /> {viewAnn.created_at}</span>
</div>
<div
className="text-sm text-[var(--dg-text-secondary)] leading-relaxed prose-sm"
dangerouslySetInnerHTML={{ __html: simpleMarkdown(viewAnn.content) }}
/>
</FormDialog>
)}
{/* ── Create / Edit Modal ── */}
{showModal && (
<ModalOverlay open={showModal} onClose={() => setShowModal(false)} width="max-w-3xl">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-[var(--dg-text-primary)]">{editingId ? '编辑公告' : '创建公告'}</h3>
<button onClick={() => setShowModal(false)} className="w-8 h-8 rounded-lg flex items-center justify-center hover:bg-[var(--dg-bg-hover)]">
<X className="w-5 h-5" />
</button>
</div>
<div className="space-y-4">
<div>
<label className="text-xs text-[var(--dg-text-muted)] mb-1.5 block">
标题 <span className="text-red-400">*</span>
</label>
<input
type="text"
value={formTitle}
onChange={e => setFormTitle(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] focus:outline-none focus:border-[var(--dg-blue)]"
placeholder="输入公告标题"
/>
</div>
<div>
<div className="flex items-center justify-between mb-1.5">
<label className="text-xs text-[var(--dg-text-muted)]">
内容 <span className="text-red-400">*</span> <span className="text-[var(--dg-text-muted)]">(支持 Markdown)</span>
</label>
<button
onClick={() => setShowPreview(!showPreview)}
className={`text-xs px-2 py-1 rounded transition-colors ${
showPreview
? 'bg-[var(--dg-blue)] text-white'
: 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]'
}`}
>
{showPreview ? '编辑' : '预览'}
</button>
</div>
{showPreview ? (
<div
className="w-full min-h-[200px] px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-secondary)] leading-relaxed overflow-auto"
dangerouslySetInnerHTML={{ __html: simpleMarkdown(formContent) }}
/>
) : (
<textarea
value={formContent}
onChange={e => setFormContent(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] focus:outline-none focus:border-[var(--dg-blue)] resize-none font-mono"
rows={12}
placeholder="输入公告内容,支持 Markdown 语法...&#10;&#10;**粗体** *斜体* `代码`&#10;## 二级标题&#10;- 列表项&#10;1. 有序列表"
/>
)}
</div>
</div>
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-[var(--dg-border)]">
<FormDialog
open={showModal}
onClose={() => setShowModal(false)}
title={editingId ? '编辑公告' : '创建公告'}
width="max-w-3xl"
footer={
<>
<button
onClick={() => setShowModal(false)}
className="px-5 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] transition-colors"
@@ -332,9 +273,57 @@ export default function AnnouncementPage() {
>
<Save className="w-4 h-4" /> {saving ? '保存中…' : editingId ? '保存修改' : '创建'}
</button>
</>
}
>
<div className="space-y-4 pt-2">
<div>
<label className="text-xs text-[var(--dg-text-muted)] mb-1.5 block">
标题 <span className="text-red-400">*</span>
</label>
<input
type="text"
value={formTitle}
onChange={e => setFormTitle(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] focus:outline-none focus:border-[var(--dg-blue)]"
placeholder="输入公告标题"
/>
</div>
<div>
<div className="flex items-center justify-between mb-1.5">
<label className="text-xs text-[var(--dg-text-muted)]">
内容 <span className="text-red-400">*</span> <span className="text-[var(--dg-text-muted)]">(支持 Markdown)</span>
</label>
<button
onClick={() => setShowPreview(!showPreview)}
className={`text-xs px-2 py-1 rounded transition-colors ${
showPreview
? 'bg-[var(--dg-blue)] text-white'
: 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]'
}`}
>
{showPreview ? '编辑' : '预览'}
</button>
</div>
{showPreview ? (
<div
className="w-full min-h-[200px] px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-secondary)] leading-relaxed overflow-auto"
dangerouslySetInnerHTML={{ __html: simpleMarkdown(formContent) }}
/>
) : (
<textarea
value={formContent}
onChange={e => setFormContent(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] focus:outline-none focus:border-[var(--dg-blue)] resize-none font-mono"
rows={12}
placeholder="输入公告内容,支持 Markdown 语法...&#10;&#10;**粗体** *斜体* `代码`&#10;## 二级标题&#10;- 列表项&#10;1. 有序列表"
/>
)}
</div>
</div>
</ModalOverlay>
</FormDialog>
)}
<ConfirmDialog
open={!!deleteConfirm}
+2 -1
View File
@@ -28,7 +28,8 @@ export default function AppearanceSettings() {
useEffect(() => {
api.get<{ theme?: string; login_style?: string }>('/user/preferences')
.then(prefs => {
.then(res => {
const prefs = res.data;
if (prefs?.theme) {
const t = prefs.theme;
if (t === 'dark' || t === 'light') {
+1 -1
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import {
Clock, XCircle, CheckCircle, AlertTriangle,
Clock, AlertTriangle,
Database, Zap, Loader2, Calendar, Ban,
} from 'lucide-react';
import api from '@/api/request';
+2 -9
View File
@@ -16,13 +16,6 @@ import type { DashboardStats } from '@/types';
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
interface QuickAction {
label: string;
icon: any;
onClick: () => void;
color: string;
bg: string;
}
/* ------------------------------------------------------------------ */
/* Helpers */
@@ -75,8 +68,8 @@ function StatCardSkeleton() {
/* Main Dashboard */
/* ------------------------------------------------------------------ */
export default function Dashboard() {
const theme = useTheme();
const { user } = useAuth();
useTheme();
useAuth();
const navigate = useNavigate();
const [dismissedIds, setDismissedIds] = useState<Set<string>>(new Set());
const [expandedId, setExpandedId] = useState<string | null>(null);
+1 -1
View File
@@ -241,7 +241,7 @@ export default function DataDictionaryPage() {
return tables.filter(t => t.toLowerCase().includes(searchFilter.toLowerCase()));
}, [tables, searchFilter]);
const relations = useMemo(() => {
const relations = useMemo<TableRelation[]>(() => {
if (!selectedTable) return [];
return [];
}, [selectedTable]);
+1
View File
@@ -9,6 +9,7 @@ import { instanceApi } from '@/api/instance';
import { userApi } from '@/api/user';
import { useActionPermission } from '@/hooks/useAuth';
import { ConfirmDialog } from '@/components/ConfirmDialog';
import { FormDialog } from '@/components/FormDialog';
import { ModalOverlay } from '@/components/ModalOverlay';
/* ───────── Constants ───────── */
+1
View File
@@ -13,6 +13,7 @@ import EmptyState from '@/components/EmptyState';
import { instanceApi } from '@/api/instance';
import EditInstanceDialog from '@/components/EditInstanceDialog';
import { ConfirmDialog } from '@/components/ConfirmDialog';
import { FormDialog } from '@/components/FormDialog';
import { ModalOverlay } from '@/components/ModalOverlay';
/* ------------------------------------------------------------------ */
+90 -48
View File
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import api from '@/api/request';
import { FormDialog } from '@/components/FormDialog';
import { ModalOverlay } from '@/components/ModalOverlay';
import { toast } from '@/hooks/useToast';
import {
@@ -743,66 +744,107 @@ export default function LdapConfigPage() {
{/* ── 账号测试弹框 ── */}
{showAccountTest && (
<ModalOverlay open={true} onClose={() => setShowAccountTest(false)} width="w-[420px]">
<div className="bg-[var(--dg-bg-secondary)] rounded-xl shadow-2xl">
<div className="flex items-center justify-between px-5 py-4">
<h3 className="text-sm font-semibold">LDAP 账号验证测试</h3>
<button onClick={() => setShowAccountTest(false)} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">
✕
</button>
<ModalOverlay open={true} onClose={() => { setShowAccountTest(false); setAccountResult(null); }} width="w-[440px]">
{/* 标题 */}
<div className="flex items-center justify-between px-5 py-3.5">
<div className="flex items-center gap-2">
<KeyRound className="w-4 h-4 text-blue-400" />
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)]">账号验证测试</h3>
</div>
<div className="px-5 space-y-4">
<div>
<label className="text-xs text-[var(--dg-text-muted)] mb-1 block">测试用户名</label>
<input
value={testUser}
onChange={e => setTestUser(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400"
placeholder="LDAP 用户名"
/>
</div>
<div>
<label className="text-xs text-[var(--dg-text-muted)] mb-1 block">测试密码</label>
<input
type="password"
value={testPass}
onChange={e => setTestPass(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400"
placeholder="密码"
/>
</div>
{accountResult && (
<div className={`px-3 py-2 rounded-lg text-sm ${accountResult.ok ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'}`}>
{accountResult.ok ? '✅ ' : '❌ '}{accountResult.msg}
<button onClick={() => { setShowAccountTest(false); setAccountResult(null); }} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)] transition-colors">
✕
</button>
</div>
{/* 表单区 */}
<div className="px-5 pb-4 space-y-3">
<div>
<label className="text-xs text-[var(--dg-text-muted)] mb-1 block">用户名</label>
<input
value={testUser}
onChange={e => { setTestUser(e.target.value); setAccountResult(null); }}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400 transition-colors"
placeholder="LDAP 用户名"
/>
</div>
<div>
<label className="text-xs text-[var(--dg-text-muted)] mb-1 block">密码</label>
<input
type="password"
value={testPass}
onChange={e => { setTestPass(e.target.value); setAccountResult(null); }}
className="w-full px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm focus:outline-none focus:border-blue-400 transition-colors"
placeholder="密码"
onKeyDown={e => { if (e.key === 'Enter' && testUser && testPass) handleTestAccount(); }}
/>
</div>
</div>
{/* 结果区 — 独立视觉层级 */}
{accountResult && (
<div className="mx-5 mb-4">
{accountResult.ok ? (
/* 成功 */
<div className="rounded-lg bg-emerald-500/12 p-4">
<div className="flex items-center gap-2 mb-3">
<div className="w-6 h-6 rounded-full bg-emerald-500/25 flex items-center justify-center">
<CheckCircle className="w-4 h-4 text-emerald-400" />
</div>
<span className="text-sm font-medium text-emerald-400">验证通过</span>
</div>
{accountResult.info && (
<div className="mt-2 text-xs space-y-1 font-mono">
{Object.entries(accountResult.info).map(([k, v]) => (
<div key={k}>
<span className="text-[var(--dg-text-muted)]">{k}:</span>{' '}
<div className="space-y-1.5 pl-8">
{Object.entries(accountResult.info)
.filter(([, v]) => v && String(v).length > 0)
.map(([k, v]) => (
<div key={k} className="flex text-xs gap-2">
<span className="text-emerald-400/60 min-w-[48px] shrink-0">{
k === 'Display Name' ? '姓名' :
k === 'Email' ? '邮箱' :
k === 'User DN' ? 'DN' :
k === 'User ID' ? '用户名' : k
}</span>
{Array.isArray(v) ? (
v.length > 0 ? (
<span className="block ml-4 space-y-0.5">
{v.map((g, i) => <div key={i}>- {g}</div>)}
<span className="text-emerald-300">
{v.join(', ')}
</span>
) : <span>(none)</span>
) : String(v)}
) : null
) : <span className="text-emerald-300">{String(v)}</span>}
</div>
))}
</div>
)}
</div>
) : (
/* 失败 */
<div className="rounded-lg bg-red-500/12 p-4">
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-red-500/25 flex items-center justify-center">
<XCircle className="w-4 h-4 text-red-400" />
</div>
<span className="text-sm font-medium text-red-400">验证失败</span>
</div>
<p className="text-xs text-red-300/80 mt-2 pl-8">{accountResult.msg}</p>
</div>
)}
</div>
<div className="flex justify-end gap-3 px-5 py-4">
<button onClick={() => setShowAccountTest(false)} className="px-4 py-2 text-sm text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">取消</button>
<button
onClick={handleTestAccount}
disabled={testingAccount || !testUser || !testPass}
className="px-4 py-2 text-sm font-medium rounded-lg bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50"
>
{testingAccount ? '测试中…' : '测试验证'}
</button>
</div>
)}
{/* 操作栏 */}
<div className="flex justify-end gap-3 px-5 py-3.5">
<button onClick={() => { setShowAccountTest(false); setAccountResult(null); }} className="px-4 py-2 text-sm text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)] transition-colors">关闭</button>
<button
onClick={handleTestAccount}
disabled={testingAccount || !testUser || !testPass}
className="flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-lg bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50 transition-colors"
>
{testingAccount ? (
<><RefreshCw className="w-3.5 h-3.5 animate-spin" />验证中…</>
) : (
<><Shield className="w-3.5 h-3.5" />测试验证</>
)}
</button>
</div>
</ModalOverlay>
)}
+2 -1
View File
@@ -1,7 +1,8 @@
import { useState, useEffect, useCallback } from 'react';
import request from '@/api/request';
import { Plus, Trash2, Send, Bell, Mail, Settings, X } from 'lucide-react';
import { Plus, Trash2, Send, Bell, Mail, Settings } from 'lucide-react';
import { ConfirmDialog } from '@/components/ConfirmDialog';
import { FormDialog } from '@/components/FormDialog';
import { ModalOverlay } from '@/components/ModalOverlay';
/* ── types ── */
+17 -31
View File
@@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, Key, Activity, Terminal, Gauge, Plus, RefreshCw, Search } from 'lucide-react';
import { redisApi } from '@/api/redis';
import { ConfirmDialog } from '@/components/ConfirmDialog';
import { ModalOverlay } from '@/components/ModalOverlay';
import { FormDialog } from '@/components/FormDialog';
type TabKey = 'keys' | 'monitor' | 'cli' | 'perf';
@@ -266,12 +266,7 @@ export default function RedisDetailPage() {
{/* 键详情弹窗 */}
{selectedKey && (
<ModalOverlay open={!!selectedKey} onClose={function () { setSelectedKey(null); }} width="max-w-lg">
<div className="bg-[var(--dg-bg-secondary)] rounded-xl shadow-2xl w-full max-h-[70vh] overflow-y-auto p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold">键详情</h3>
<button onClick={function () { setSelectedKey(null); }} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">✕</button>
</div>
<FormDialog open={!!selectedKey} onClose={function () { setSelectedKey(null); }} title="键详情" width="max-w-lg">
<div className="space-y-3">
<div className="flex gap-2 text-sm"><span className="text-[var(--dg-text-muted)]">键名:</span><span className="font-mono">{selectedKey.key || selectedKey.name}</span></div>
<div className="flex gap-2 text-sm"><span className="text-[var(--dg-text-muted)]">类型:</span><span>{selectedKey.type}</span></div>
@@ -282,33 +277,24 @@ export default function RedisDetailPage() {
</pre>
</div>
</div>
</div>
</ModalOverlay>
</FormDialog>
)}
{/* 新增键弹窗 */}
{showAddKey && (
<ModalOverlay open={showAddKey} onClose={function () { setShowAddKey(false); }} width="max-w-md">
<div className="bg-[var(--dg-bg-secondary)] rounded-xl shadow-2xl w-full p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold">新增键</h3>
<button onClick={function () { setShowAddKey(false); }} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">✕</button>
</div>
<div className="space-y-3">
<div><label className="text-sm text-[var(--dg-text-muted)]">类型</label><select value={newKey.type} onChange={function (e) { setNewKey(Object.assign({}, newKey, { type: e.target.value })); }} className="w-full px-3 py-2 mt-1 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm">
<option value="string">String</option><option value="hash">Hash</option><option value="list">List</option><option value="set">Set</option><option value="zset">ZSet</option>
</select></div>
<div><label className="text-sm text-[var(--dg-text-muted)]">键名</label><input value={newKey.key} onChange={function (e) { setNewKey(Object.assign({}, newKey, { key: e.target.value })); }} className="w-full px-3 py-2 mt-1 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm" placeholder="key" /></div>
<div><label className="text-sm text-[var(--dg-text-muted)]">值</label><textarea value={newKey.value} onChange={function (e) { setNewKey(Object.assign({}, newKey, { value: e.target.value })); }} className="w-full px-3 py-2 mt-1 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm font-mono min-h-[80px]" placeholder="value" /></div>
<div><label className="text-sm text-[var(--dg-text-muted)]">TTL (秒, -1 永不过期)</label><input type="number" value={newKey.ttl} onChange={function (e) { setNewKey(Object.assign({}, newKey, { ttl: Number(e.target.value) })); }} className="w-full px-3 py-2 mt-1 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm" /></div>
</div>
<div className="flex justify-end gap-3 mt-4">
<button onClick={function () { setShowAddKey(false); }} className="px-4 py-2 bg-[var(--dg-bg-tertiary)] rounded-lg text-sm">取消</button>
<button onClick={handleAddKey} className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm">添加</button>
</div>
</div>
</ModalOverlay>
)}
<FormDialog open={showAddKey} onClose={() => setShowAddKey(false)} title="新增键" width="max-w-md"
footer={<>
<button onClick={() => setShowAddKey(false)} className="px-4 py-2 bg-[var(--dg-bg-tertiary)] rounded-lg text-sm">取消</button>
<button onClick={handleAddKey} className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm">添加</button>
</>}>
<div className="space-y-3">
<div><label className="text-sm text-[var(--dg-text-muted)]">类型</label><select value={newKey.type} onChange={function (e) { setNewKey(Object.assign({}, newKey, { type: e.target.value })); }} className="w-full px-3 py-2 mt-1 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm">
<option value="string">String</option><option value="hash">Hash</option><option value="list">List</option><option value="set">Set</option><option value="zset">ZSet</option>
</select></div>
<div><label className="text-sm text-[var(--dg-text-muted)]">键名</label><input value={newKey.key} onChange={function (e) { setNewKey(Object.assign({}, newKey, { key: e.target.value })); }} className="w-full px-3 py-2 mt-1 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm" placeholder="key" /></div>
<div><label className="text-sm text-[var(--dg-text-muted)]">值</label><textarea value={newKey.value} onChange={function (e) { setNewKey(Object.assign({}, newKey, { value: e.target.value })); }} className="w-full px-3 py-2 mt-1 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm font-mono min-h-[80px]" placeholder="value" /></div>
<div><label className="text-sm text-[var(--dg-text-muted)]">TTL (秒, -1 永不过期)</label><input type="number" value={newKey.ttl} onChange={function (e) { setNewKey(Object.assign({}, newKey, { ttl: Number(e.target.value) })); }} className="w-full px-3 py-2 mt-1 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm" /></div>
</div>
</FormDialog>
<ConfirmDialog
open={!!deleteConfirm}
title={deleteConfirm?.msg || '确认操作'}