feat: 完善告警规则配置页面 — 增删改查弹窗 + 修复白屏bug + UpdateRule notify_channels序列化修复
- AlertManager: 新建/编辑告警规则弹窗(规则名称/描述/类型/数据源/表达式/级别/持续时间/通知渠道Switch)
- AlertManager: 修复白屏bug — getRules返回{list,total}未解包导致rules.map崩溃(Frontend Pitfall #1)
- AlertManager: 修复规则列表字段显示 — 原引用不存在的rule.metric/operator/threshold字段,改为rule.source/expression
- AlertManager: 新增规则删除按钮(带确认)
- alert.go: 修复UpdateRule notify_channels序列化bug — 原直接赋值[]string到text列导致500,改用toJSON()
- 同步提交前序未提交改动(WebShell/K8s/Server/Login等组件改进)
This commit is contained in:
1 parent
170818c141
commit
00d5510366
13 files changed
+2170
-496
No files matched your search
@@ -1,13 +1,93 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { User, Lock, Palette, Check, Loader2, AlertCircle } from 'lucide-react';
|
||||
import { useUserStore } from '@/store';
|
||||
import { userApi } from '@/services/api';
|
||||
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
/* ---------- Floating background orbs (themed light spots) ---------- */
|
||||
const ORBS = [
|
||||
{
|
||||
top: '-10%',
|
||||
left: '-5%',
|
||||
size: 520,
|
||||
color: 'var(--dg-blue)',
|
||||
opacity: 0.22,
|
||||
blur: 90,
|
||||
animation: 'login-orb-1 18s ease-in-out infinite',
|
||||
},
|
||||
{
|
||||
bottom: '-15%',
|
||||
right: '-8%',
|
||||
size: 480,
|
||||
color: 'var(--dg-purple)',
|
||||
opacity: 0.2,
|
||||
blur: 100,
|
||||
animation: 'login-orb-2 22s ease-in-out infinite',
|
||||
},
|
||||
{
|
||||
top: '30%',
|
||||
right: '10%',
|
||||
size: 320,
|
||||
color: 'var(--dg-cyan)',
|
||||
opacity: 0.16,
|
||||
blur: 70,
|
||||
animation: 'login-orb-3 16s ease-in-out infinite',
|
||||
},
|
||||
{
|
||||
bottom: '10%',
|
||||
left: '8%',
|
||||
size: 380,
|
||||
color: 'var(--dg-gradient-start)',
|
||||
opacity: 0.18,
|
||||
blur: 80,
|
||||
animation: 'login-orb-4 20s ease-in-out infinite',
|
||||
},
|
||||
];
|
||||
|
||||
const ANIMATIONS = `
|
||||
@keyframes login-orb-1 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
33% { transform: translate(40px, -50px) scale(1.12); }
|
||||
66% { transform: translate(-30px, 35px) scale(0.92); }
|
||||
}
|
||||
@keyframes login-orb-2 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(-50px, 25px) scale(1.18); }
|
||||
}
|
||||
@keyframes login-orb-3 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(30px, 45px) scale(0.88); }
|
||||
}
|
||||
@keyframes login-orb-4 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(25px, -30px) scale(1.1); }
|
||||
}
|
||||
@keyframes login-card-enter {
|
||||
from { opacity: 0; transform: translateY(24px) scale(0.97); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
@keyframes login-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
`;
|
||||
|
||||
export function Login() {
|
||||
const navigate = useNavigate();
|
||||
const login = useUserStore((s) => s.login);
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [username, setUsername] = useState('admin');
|
||||
const [password, setPassword] = useState('123456');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
@@ -17,70 +97,278 @@ export function Login() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await userApi.login(username, password);
|
||||
login(result.user, result.token);
|
||||
useUserStore.getState().login(result.user, result.token);
|
||||
navigate('/', { replace: true });
|
||||
} catch (err: any) {
|
||||
setError(err.message || '登录失败');
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '登录失败,请检查用户名和密码';
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 flex items-center justify-center px-4">
|
||||
<div className="w-full max-w-sm">
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-blue-600 mb-4">
|
||||
<svg className="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 17v-2a4 4 0 014-4h2m4-2H5a2 2 0 00-2 2v10a2 2 0 002 2h14a2 2 0 002-2V7a2 2 0 00-2-2z M7 7h.01M11 7h.01M15 7h.01" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white">AIOps 智能运维平台</h1>
|
||||
<p className="text-slate-400 mt-2 text-sm">EZOps Platform</p>
|
||||
<>
|
||||
<style>{ANIMATIONS}</style>
|
||||
|
||||
<div
|
||||
className="relative min-h-screen flex items-center justify-center px-4 overflow-hidden"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(135deg, var(--dg-bg-primary) 0%, var(--dg-bg-secondary) 100%)',
|
||||
}}
|
||||
>
|
||||
{/* -------- Floating themed light spots -------- */}
|
||||
{ORBS.map((orb, i) => (
|
||||
<div
|
||||
key={`orb-${i}`}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute"
|
||||
style={{
|
||||
top: orb.top,
|
||||
left: orb.left,
|
||||
bottom: orb.bottom,
|
||||
right: orb.right,
|
||||
width: orb.size,
|
||||
height: orb.size,
|
||||
borderRadius: '50%',
|
||||
background: `radial-gradient(circle, ${orb.color} 0%, transparent 70%)`,
|
||||
opacity: orb.opacity,
|
||||
filter: `blur(${orb.blur}px)`,
|
||||
animation: orb.animation,
|
||||
willChange: 'transform',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* -------- Theme switcher (top-right) -------- */}
|
||||
<div
|
||||
className="fixed top-4 right-4 z-50"
|
||||
style={{ animation: 'login-fade-in 0.6s ease-out' }}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="切换主题"
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center size-10 rounded-full',
|
||||
'border transition-all duration-200 hover:scale-105 active:scale-95'
|
||||
)}
|
||||
style={{
|
||||
background: 'color-mix(in srgb, var(--dg-bg-secondary) 70%, transparent)',
|
||||
borderColor: 'var(--dg-border)',
|
||||
color: 'var(--dg-text-secondary)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
WebkitBackdropFilter: 'blur(12px)',
|
||||
}}
|
||||
>
|
||||
<Palette className="size-5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-60">
|
||||
<DropdownMenuLabel>主题切换</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{THEME_OPTIONS.map((option) => (
|
||||
<DropdownMenuItem
|
||||
key={option.id}
|
||||
className="flex items-center gap-2.5 cursor-pointer"
|
||||
onClick={() => setTheme(option.id)}
|
||||
>
|
||||
<span
|
||||
className="size-3.5 rounded-full shrink-0 ring-2"
|
||||
style={{
|
||||
backgroundColor: option.primaryColor,
|
||||
// @ts-expect-error css var in style string
|
||||
'--tw-ring-color': 'var(--dg-bg-primary)',
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-medium block">{option.name}</span>
|
||||
<span className="text-xs opacity-60 block truncate">
|
||||
{option.description}
|
||||
</span>
|
||||
</div>
|
||||
{theme === option.id && <Check className="size-4 shrink-0" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="bg-slate-900/60 rounded-xl p-6 space-y-4">
|
||||
{error && (
|
||||
<div className="bg-red-500/10 text-red-400 text-sm rounded-lg px-4 py-3">
|
||||
{error}
|
||||
{/* -------- Login card -------- */}
|
||||
<div
|
||||
className="relative z-10 w-full max-w-md"
|
||||
style={{
|
||||
background:
|
||||
'color-mix(in srgb, var(--dg-bg-secondary) 75%, transparent)',
|
||||
backdropFilter: 'blur(24px)',
|
||||
WebkitBackdropFilter: 'blur(24px)',
|
||||
border: '1px solid var(--dg-border)',
|
||||
borderRadius: 24,
|
||||
boxShadow:
|
||||
'0 8px 48px rgba(0,0,0,0.28), 0 0 0 1px rgba(255,255,255,0.02)',
|
||||
padding: '40px 36px',
|
||||
animation: 'login-card-enter 0.6s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
}}
|
||||
>
|
||||
{/* Brand */}
|
||||
<div className="text-center mb-8">
|
||||
<div
|
||||
className="inline-flex items-center justify-center size-14 rounded-2xl mb-4 shadow-lg"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(135deg, var(--dg-gradient-start), var(--dg-gradient-end))',
|
||||
boxShadow: '0 8px 24px color-mix(in srgb, var(--dg-gradient-start) 35%, transparent)',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className="size-7 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.8}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M3 12h4l3-8 4 16 3-8h4" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300 mb-1.5">用户名</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full bg-slate-800 text-white rounded-lg px-4 py-2.5 border border-slate-700 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 transition-colors"
|
||||
placeholder="请输入用户名"
|
||||
autoFocus
|
||||
/>
|
||||
<h1 className="text-3xl font-bold dg-gradient-text tracking-tight">
|
||||
EZOps AIOps
|
||||
</h1>
|
||||
<p
|
||||
className="mt-2 text-sm"
|
||||
style={{ color: 'var(--dg-text-secondary)' }}
|
||||
>
|
||||
智能运维 · 全栈可观测平台
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-300 mb-1.5">密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full bg-slate-800 text-white rounded-lg px-4 py-2.5 border border-slate-700 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 transition-colors"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white font-medium rounded-lg px-4 py-2.5 transition-colors"
|
||||
>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="text-center text-slate-500 text-xs mt-6">
|
||||
默认账号 admin / 123456
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div
|
||||
className="flex items-start gap-2 rounded-lg px-4 py-3 text-sm"
|
||||
style={{
|
||||
background: 'color-mix(in srgb, var(--dg-red) 12%, transparent)',
|
||||
border: '1px solid color-mix(in srgb, var(--dg-red) 30%, transparent)',
|
||||
color: 'var(--dg-red)',
|
||||
animation: 'login-fade-in 0.25s ease-out',
|
||||
}}
|
||||
>
|
||||
<AlertCircle className="size-4 shrink-0 mt-0.5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Username */}
|
||||
<div>
|
||||
<label
|
||||
className="block text-sm font-medium mb-1.5"
|
||||
style={{ color: 'var(--dg-text-secondary)' }}
|
||||
>
|
||||
用户名
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User
|
||||
className="absolute left-3.5 top-1/2 -translate-y-1/2 size-4 pointer-events-none"
|
||||
style={{ color: 'var(--dg-text-muted)' }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="请输入用户名"
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
className="dg-input w-full rounded-lg py-2.5 pl-10 pr-4 text-sm transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label
|
||||
className="block text-sm font-medium mb-1.5"
|
||||
style={{ color: 'var(--dg-text-secondary)' }}
|
||||
>
|
||||
密码
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock
|
||||
className="absolute left-3.5 top-1/2 -translate-y-1/2 size-4 pointer-events-none"
|
||||
style={{ color: 'var(--dg-text-muted)' }}
|
||||
/>
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="请输入密码"
|
||||
autoComplete="current-password"
|
||||
className="dg-input w-full rounded-lg py-2.5 pl-10 pr-12 text-sm transition-colors"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword((v) => !v)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-medium transition-colors"
|
||||
style={{ color: 'var(--dg-text-muted)' }}
|
||||
>
|
||||
{showPassword ? '隐藏' : '显示'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !username || !password}
|
||||
className={cn(
|
||||
'dg-gradient-bg w-full rounded-lg py-3 mt-2 text-white font-semibold text-[15px]',
|
||||
'transition-all duration-200 active:scale-[0.98]',
|
||||
'disabled:cursor-not-allowed'
|
||||
)}
|
||||
style={{
|
||||
opacity: loading || !username || !password ? 0.6 : 1,
|
||||
boxShadow:
|
||||
'0 6px 24px color-mix(in srgb, var(--dg-gradient-start) 30%, transparent)',
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<span className="inline-flex items-center justify-center gap-2">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
登录中…
|
||||
</span>
|
||||
) : (
|
||||
'登 录'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Hints */}
|
||||
<div
|
||||
className="mt-6 pt-5 text-center text-xs space-y-1"
|
||||
style={{
|
||||
borderTop: '1px solid var(--dg-border-light)',
|
||||
color: 'var(--dg-text-muted)',
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
默认管理员账号 <span className="font-medium" style={{ color: 'var(--dg-text-secondary)' }}>admin / 123456</span>
|
||||
</p>
|
||||
<p>LDAP 用户可直接使用域账号登录</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<p
|
||||
className="absolute bottom-4 left-0 right-0 text-center text-xs"
|
||||
style={{ color: 'var(--dg-text-muted)', opacity: 0.7 }}
|
||||
>
|
||||
© EZOps AIOps · 智能运维平台
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user