feat: 全站 PageShell 设计系统与专业 UI 统一

- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架
- index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩)
- 全站路由页接入 PageShell,云账单三 Tab 为标杆
- 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化
- 登录页/侧栏/顶栏去渐变光球,企业克制风
- 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
Logan committed 2026-07-01 12:45:38 +08:00
1 parent 1acb0c49af
commit e363ded445
213 files changed
+7148 -6818

No files matched your search

@@ -118,10 +118,10 @@ export function AlertChannelDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-slate-900 border-slate-700 text-slate-100 max-w-2xl">
<DialogContent className="dg-input max-w-2xl">
<DialogHeader>
<DialogTitle className="text-slate-100">通知渠道配置</DialogTitle>
<DialogDescription className="text-slate-500">
<DialogTitle className="text-[var(--dg-text-primary)]">通知渠道配置</DialogTitle>
<DialogDescription className="dg-text-muted">
管理告警通知渠道(钉钉/企业微信/邮件/短信/Webhook),配置后可在告警规则中引用
</DialogDescription>
</DialogHeader>
@@ -130,11 +130,11 @@ export function AlertChannelDialog({
{/* 渠道列表 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-slate-300 text-sm">已配置渠道</Label>
<Label className="text-[var(--dg-text-secondary)] text-sm">已配置渠道</Label>
<Button
size="sm"
variant="outline"
className="h-7 border-slate-700 text-slate-300 hover:bg-slate-800 text-xs"
className="h-7 border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] text-xs"
onClick={resetChannelForm}
>
<Plus className="h-3 w-3 mr-1" />
@@ -142,7 +142,7 @@ export function AlertChannelDialog({
</Button>
</div>
{notifyChannels.length === 0 ? (
<div className="text-center py-8 text-sm text-slate-600 border border-dashed border-slate-800 rounded-lg">
<div className="text-center py-8 text-sm dg-text-muted border border-dashed border-[var(--dg-border)] rounded-lg">
暂无通知渠道,请在右侧添加
</div>
) : (
@@ -153,14 +153,14 @@ export function AlertChannelDialog({
className={`p-3 rounded-lg border cursor-pointer transition-colors ${
editingChannel && editingChannel.id === ch.id
? 'border-blue-500/50 bg-blue-500/5'
: 'border-slate-800 bg-slate-800/30 hover:border-slate-700'
: 'border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/30 hover:border-[var(--dg-border)]'
}`}
onClick={() => openEditChannel(ch)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 min-w-0">
<span className="text-sm text-slate-200 truncate">{ch.name}</span>
<Badge variant="outline" className="border-slate-600 text-slate-400 text-[10px] py-0">
<span className="text-sm text-[var(--dg-text-primary)] truncate">{ch.name}</span>
<Badge variant="outline" className="border-[var(--dg-border-hover)] dg-text-secondary text-[10px] py-0">
{getChannelTypeLabel(ch.type)}
</Badge>
</div>
@@ -173,15 +173,15 @@ export function AlertChannelDialog({
</div>
<div className="flex items-center justify-between mt-2">
{ch.test_status === 'success' ? (
<span className="text-[11px] text-emerald-400">测试通过</span>
<span className="text-[11px] dg-text-green">测试通过</span>
) : (
<span className="text-[11px] text-slate-600">未测试</span>
<span className="text-[11px] dg-text-muted">未测试</span>
)}
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
<Button
size="sm"
variant="ghost"
className="h-6 px-2 text-slate-400 hover:text-slate-200 hover:bg-slate-700 text-xs"
className="h-6 px-2 dg-text-secondary hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] text-xs"
disabled={channelTesting === ch.id}
onClick={() => handleTestChannel(ch.id)}
>
@@ -190,7 +190,7 @@ export function AlertChannelDialog({
<Button
size="sm"
variant="ghost"
className="h-6 px-2 text-red-400/70 hover:text-red-400 hover:bg-red-500/10 text-xs"
className="h-6 px-2 dg-text-red/70 hover:dg-text-red hover:bg-red-500/10 text-xs"
onClick={() => handleDeleteChannel(ch.id)}
>
<Trash2 className="h-3 w-3" />
@@ -205,24 +205,24 @@ export function AlertChannelDialog({
{/* 渠道表单 */}
<div className="space-y-4">
<Label className="text-slate-300 text-sm">
<Label className="text-[var(--dg-text-secondary)] text-sm">
{editingChannel ? '编辑渠道' : '新建渠道'}
</Label>
<div className="space-y-1.5">
<Label className="text-slate-400 text-xs">渠道名称 <span className="text-red-400">*</span></Label>
<Label className="dg-text-secondary text-xs">渠道名称 <span className="dg-text-red">*</span></Label>
<Input
value={channelForm.name}
onChange={(e) => setChannelForm({ ...channelForm, name: e.target.value })}
placeholder="如:运维告警群"
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600 h-9"
className="dg-input h-9"
/>
</div>
<div className="space-y-1.5">
<Label className="text-slate-400 text-xs">渠道类型</Label>
<Label className="dg-text-secondary text-xs">渠道类型</Label>
<select
value={channelForm.type}
onChange={(e) => setChannelForm({ ...channelForm, type: e.target.value })}
className="w-full h-9 px-3 rounded-md bg-slate-800/50 border border-slate-700 text-slate-200 text-sm"
className="w-full h-9 px-3 rounded-md bg-[var(--dg-bg-tertiary)]/50 border border-[var(--dg-border)] text-[var(--dg-text-primary)] text-sm"
>
{CHANNEL_TYPES.map(({ key, label }) => (
<option key={key} value={key}>{label}</option>
@@ -230,13 +230,13 @@ export function AlertChannelDialog({
</select>
</div>
<div className="space-y-1.5">
<Label className="text-slate-400 text-xs">配置(JSON)</Label>
<Label className="dg-text-secondary text-xs">配置(JSON)</Label>
<Textarea
value={channelForm.config}
onChange={(e) => setChannelForm({ ...channelForm, config: e.target.value })}
placeholder={CHANNEL_TYPES.find((t) => t.key === channelForm.type)?.placeholder || ''}
rows={5}
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600 font-mono text-xs resize-none"
className="dg-input font-mono text-xs resize-none"
/>
</div>
{editingChannel && (
@@ -246,7 +246,7 @@ export function AlertChannelDialog({
onCheckedChange={(v) => setChannelForm({ ...channelForm, enabled: v })}
className="data-[state=checked]:bg-emerald-600"
/>
<Label className="text-slate-400 text-xs">启用此渠道</Label>
<Label className="dg-text-secondary text-xs">启用此渠道</Label>
</div>
)}
</div>
@@ -255,7 +255,7 @@ export function AlertChannelDialog({
<DialogFooter>
<Button
variant="outline"
className="border-slate-700 text-slate-300 hover:bg-slate-800"
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]"
onClick={() => onOpenChange(false)}
>
关闭
@@ -43,21 +43,21 @@ export function AlertEventsTab({
switch (severity) {
case 'critical':
return (
<Badge variant="outline" className="border-red-500/30 text-red-400 bg-red-500/10">
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10">
<AlertTriangle className="h-3 w-3 mr-1" />
严重
</Badge>
);
case 'warning':
return (
<Badge variant="outline" className="border-amber-500/30 text-amber-400 bg-amber-500/10">
<Badge variant="outline" className="border-amber-500/30 dg-text-yellow bg-amber-500/10">
<AlertTriangle className="h-3 w-3 mr-1" />
警告
</Badge>
);
case 'info':
return (
<Badge variant="outline" className="border-blue-500/30 text-blue-400 bg-blue-500/10">
<Badge variant="outline" className="border-blue-500/30 dg-text-blue bg-blue-500/10">
<Bell className="h-3 w-3 mr-1" />
提示
</Badge>
@@ -71,13 +71,13 @@ export function AlertEventsTab({
switch (status) {
case 'firing':
return (
<Badge variant="outline" className="border-red-500/30 text-red-400 bg-red-500/10 animate-pulse">
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10 animate-pulse">
触发中
</Badge>
);
case 'resolved':
return (
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10">
<Badge variant="outline" className="border-emerald-500/30 dg-text-green bg-emerald-500/10">
已恢复
</Badge>
);
@@ -100,18 +100,18 @@ export function AlertEventsTab({
{/* Filters */}
<div className="flex items-center gap-3 flex-wrap">
<div className="relative w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
<Input
placeholder="搜索告警名称或目标..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
className="pl-9 h-9 dg-input"
/>
</div>
<select
value={severityFilter}
onChange={(e) => setSeverityFilter(e.target.value)}
className="h-9 px-3 rounded-md bg-slate-800/50 border border-slate-700 text-slate-200 text-sm"
className="h-9 px-3 rounded-md bg-[var(--dg-bg-tertiary)]/50 border border-[var(--dg-border)] text-[var(--dg-text-primary)] text-sm"
>
<option value="all">全部级别</option>
<option value="critical">严重</option>
@@ -121,7 +121,7 @@ export function AlertEventsTab({
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="h-9 px-3 rounded-md bg-slate-800/50 border border-slate-700 text-slate-200 text-sm"
className="h-9 px-3 rounded-md bg-[var(--dg-bg-tertiary)]/50 border border-[var(--dg-border)] text-[var(--dg-text-primary)] text-sm"
>
<option value="all">全部状态</option>
<option value="firing">触发中</option>
@@ -130,50 +130,50 @@ export function AlertEventsTab({
</div>
{/* Events Table */}
<Card className="bg-slate-900 border-slate-800">
<Card className="dg-card">
<CardContent className="p-0">
<div className="border-t border-slate-800">
<div className="border-t border-[var(--dg-border)]">
<Table>
<TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent">
<TableHead className="text-slate-400">级别</TableHead>
<TableHead className="text-slate-400">告警名称</TableHead>
<TableHead className="text-slate-400">状态</TableHead>
<TableHead className="text-slate-400">目标</TableHead>
<TableHead className="text-slate-400">当前值</TableHead>
<TableHead className="text-slate-400">阈值</TableHead>
<TableHead className="text-slate-400">开始时间</TableHead>
<TableHead className="text-slate-400 text-right">操作</TableHead>
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
<TableHead className="dg-text-secondary">级别</TableHead>
<TableHead className="dg-text-secondary">告警名称</TableHead>
<TableHead className="dg-text-secondary">状态</TableHead>
<TableHead className="dg-text-secondary">目标</TableHead>
<TableHead className="dg-text-secondary">当前值</TableHead>
<TableHead className="dg-text-secondary">阈值</TableHead>
<TableHead className="dg-text-secondary">开始时间</TableHead>
<TableHead className="dg-text-secondary text-right">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow className="border-slate-800">
<TableCell colSpan={8} className="text-center text-slate-500 py-8">
<TableRow className="border-[var(--dg-border)]">
<TableCell colSpan={8} className="text-center dg-text-muted py-8">
<Loader2 className="h-5 w-5 animate-spin inline mr-2" />
加载中...
</TableCell>
</TableRow>
) : filteredEvents.map((event) => (
<TableRow key={event.id} className="border-slate-800 hover:bg-slate-800/50">
<TableRow key={event.id} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
<TableCell>{getSeverityBadge(event.severity)}</TableCell>
<TableCell>
<span className="text-sm font-medium text-slate-200">
<span className="text-sm font-medium text-[var(--dg-text-primary)]">
{(event.rule && event.rule.name) || event.message}
</span>
</TableCell>
<TableCell>{getStatusBadge(event.status)}</TableCell>
<TableCell className="max-w-[160px]">
<span className="text-sm text-slate-300 block truncate" title={event.target}>{event.target}</span>
<span className="text-sm text-[var(--dg-text-secondary)] block truncate" title={event.target}>{event.target}</span>
</TableCell>
<TableCell>
<span className="text-sm text-slate-300">{typeof event.value === 'number' ? event.value.toFixed(1) : event.value}</span>
<span className="text-sm text-[var(--dg-text-secondary)]">{typeof event.value === 'number' ? event.value.toFixed(1) : event.value}</span>
</TableCell>
<TableCell>
<span className="text-sm text-slate-500">{event.threshold}</span>
<span className="text-sm dg-text-muted">{event.threshold}</span>
</TableCell>
<TableCell>
<span className="text-sm text-slate-400">
<span className="text-sm dg-text-secondary">
{event.created_at ? new Date(event.created_at).toLocaleString() : '--'}
</span>
</TableCell>
@@ -183,7 +183,7 @@ export function AlertEventsTab({
<Button
variant="ghost"
size="sm"
className="h-8 text-slate-400 hover:text-emerald-400"
className="h-8 dg-text-secondary hover:dg-text-green"
onClick={() => handleAcknowledge(event.id)}
>
<CheckCircle className="h-4 w-4 mr-1" />
+49 -108
View File
@@ -1,8 +1,7 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Plus, Settings, Bell, AlertTriangle, Clock, CheckCircle } from 'lucide-react';
import { Plus, Settings, Bell } from 'lucide-react';
import {
ResponsiveContainer,
AreaChart,
@@ -17,6 +16,8 @@ import { AlertEventsTab } from './AlertEventsTab';
import { AlertRulesTab } from './AlertRulesTab';
import { AlertRuleDialog } from './AlertRuleDialog';
import { AlertChannelDialog } from './AlertChannelDialog';
import { PageShell } from '@/components/layout/PageShell';
import { StatStrip } from '@/components/ui/stat-strip';
import type { JsonData } from '@/types/common';
import type { AlertEventRecord, AlertRuleRecord, AlertChannel } from '@/types';
@@ -73,95 +74,51 @@ export function AlertManager() {
{ time: '24:00', critical: 2, warning: 4, info: 7 },
];
const pageTabs = [
{ id: 'events', label: '告警事件' },
{ id: 'rules', label: '告警规则' },
];
return (
<div className="space-y-6">
{/* Page Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-100">监控告警</h1>
<p className="text-sm text-slate-500 mt-1">告警规则管理与告警事件处理</p>
</div>
<div className="flex items-center gap-2">
<PageShell
variant="list"
title="监控告警"
subtitle="告警规则管理与告警事件处理"
icon={<Bell className="h-5 w-5" />}
tabs={pageTabs}
activeTab={activeTab}
onTabChange={setActiveTab}
metrics={(
<StatStrip
items={[
{ label: '告警规则', value: rules.length, color: 'blue' },
{ label: '触发中', value: alerts.filter((e) => e.status === 'firing').length, color: 'red' },
{ label: '今日告警', value: alerts.length, color: 'amber' },
{ label: '已恢复', value: alerts.filter((e) => e.status === 'resolved').length, color: 'emerald' },
]}
/>
)}
actions={(
<>
<Button
variant="outline"
className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800"
className="dg-btn-secondary"
onClick={() => setChannelDialogOpen(true)}
>
<Settings className="h-4 w-4 mr-2" />
通知配置
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={openCreateRule}>
<Button className="dg-btn-primary" onClick={openCreateRule}>
<Plus className="h-4 w-4 mr-2" />
新建规则
</Button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">告警规则</p>
<p className="text-2xl font-bold text-slate-100">{rules.length}</p>
</div>
<div className="p-3 rounded-lg bg-blue-500/10 text-blue-400">
<Bell className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">触发中</p>
<p className="text-2xl font-bold text-red-400">
{alerts.filter((e) => e.status === 'firing').length}
</p>
</div>
<div className="p-3 rounded-lg bg-red-500/10 text-red-400">
<AlertTriangle className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">今日告警</p>
<p className="text-2xl font-bold text-amber-400">{alerts.length}</p>
</div>
<div className="p-3 rounded-lg bg-amber-500/10 text-amber-400">
<Clock className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">已恢复</p>
<p className="text-2xl font-bold text-emerald-400">
{alerts.filter((e) => e.status === 'resolved').length}
</p>
</div>
<div className="p-3 rounded-lg bg-emerald-500/10 text-emerald-400">
<CheckCircle className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Alert Trend Chart */}
<Card className="bg-slate-900 border-slate-800">
</>
)}
>
<Card className="dg-card">
<CardHeader className="pb-2">
<CardTitle className="text-slate-100">告警趋势</CardTitle>
<CardDescription className="text-slate-500">过去24小时告警统计</CardDescription>
<CardTitle className="text-[var(--dg-text-primary)]">告警趋势</CardTitle>
<CardDescription className="dg-text-muted">过去24小时告警统计</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[200px]">
@@ -210,34 +167,18 @@ export function AlertManager() {
</CardContent>
</Card>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="bg-slate-800 border border-slate-700">
<TabsTrigger value="events" className="data-[state=active]:bg-slate-700">
告警事件
</TabsTrigger>
<TabsTrigger value="rules" className="data-[state=active]:bg-slate-700">
告警规则
</TabsTrigger>
{activeTab === 'events' && (
<AlertEventsTab alerts={alerts} loading={loading} onReload={loadData} />
)}
{activeTab === 'rules' && (
<AlertRulesTab
rules={rules}
notifyChannels={notifyChannels}
onReload={loadData}
onEditRule={openEditRule}
/>
)}
</TabsList>
<TabsContent value="events">
<AlertEventsTab alerts={alerts} loading={loading} onReload={loadData} />
</TabsContent>
<TabsContent value="rules">
<AlertRulesTab
rules={rules}
notifyChannels={notifyChannels}
onReload={loadData}
onEditRule={openEditRule}
/>
</TabsContent>
</Tabs>
{/* Dialogs */}
<AlertRuleDialog
open={ruleDialogOpen}
onOpenChange={setRuleDialogOpen}
@@ -251,6 +192,6 @@ export function AlertManager() {
notifyChannels={notifyChannels}
onChannelsChanged={loadData}
/>
</div>
</PageShell>
);
}
}
@@ -124,41 +124,41 @@ export function AlertRuleDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-slate-900 border-slate-700 text-slate-100 max-w-lg">
<DialogContent className="dg-input max-w-lg">
<DialogHeader>
<DialogTitle className="text-slate-100">
<DialogTitle className="text-[var(--dg-text-primary)]">
{editingRule ? '编辑告警规则' : '新建告警规则'}
</DialogTitle>
<DialogDescription className="text-slate-500">
<DialogDescription className="dg-text-muted">
{editingRule ? '修改告警规则的触发条件和通知方式' : '配置监控指标的告警触发条件'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-1">
<div className="space-y-1.5">
<Label className="text-slate-300">规则名称 <span className="text-red-400">*</span></Label>
<Label className="text-[var(--dg-text-secondary)]">规则名称 <span className="dg-text-red">*</span></Label>
<Input
value={ruleForm.name}
onChange={(e) => setRuleForm({ ...ruleForm, name: e.target.value })}
placeholder="如:CPU 使用率告警"
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600"
className="dg-input"
/>
</div>
<div className="space-y-1.5">
<Label className="text-slate-300">描述</Label>
<Label className="text-[var(--dg-text-secondary)]">描述</Label>
<Textarea
value={ruleForm.description}
onChange={(e) => setRuleForm({ ...ruleForm, description: e.target.value })}
placeholder="规则说明"
rows={2}
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600 resize-none"
className="dg-input resize-none"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-slate-300">规则类型</Label>
<Label className="text-[var(--dg-text-secondary)]">规则类型</Label>
<select
value={ruleForm.type}
onChange={(e) => {
@@ -166,7 +166,7 @@ export function AlertRuleDialog({
const source = type === 'log' ? 'loki' : 'prometheus';
setRuleForm({ ...ruleForm, type, source });
}}
className="w-full h-9 px-3 rounded-md bg-slate-800/50 border border-slate-700 text-slate-200 text-sm"
className="w-full h-9 px-3 rounded-md bg-[var(--dg-bg-tertiary)]/50 border border-[var(--dg-border)] text-[var(--dg-text-primary)] text-sm"
>
<option value="metric">指标监控</option>
<option value="log">日志告警</option>
@@ -174,11 +174,11 @@ export function AlertRuleDialog({
</select>
</div>
<div className="space-y-1.5">
<Label className="text-slate-300">数据源</Label>
<Label className="text-[var(--dg-text-secondary)]">数据源</Label>
<select
value={ruleForm.source}
onChange={(e) => setRuleForm({ ...ruleForm, source: e.target.value, datasource_id: undefined })}
className="w-full h-9 px-3 rounded-md bg-slate-800/50 border border-slate-700 text-slate-200 text-sm"
className="w-full h-9 px-3 rounded-md bg-[var(--dg-bg-tertiary)]/50 border border-[var(--dg-border)] text-[var(--dg-text-primary)] text-sm"
>
{ruleForm.type === 'log' ? (
<>
@@ -198,11 +198,11 @@ export function AlertRuleDialog({
{ruleForm.type === 'log' && (
<div className="space-y-1.5">
<Label className="text-slate-300">日志数据源 <span className="text-red-400">*</span></Label>
<Label className="text-[var(--dg-text-secondary)]">日志数据源 <span className="dg-text-red">*</span></Label>
<select
value={ruleForm.datasource_id || ''}
onChange={(e) => setRuleForm({ ...ruleForm, datasource_id: Number(e.target.value) || undefined })}
className="w-full h-9 px-3 rounded-md bg-slate-800/50 border border-slate-700 text-slate-200 text-sm"
className="w-full h-9 px-3 rounded-md bg-[var(--dg-bg-tertiary)]/50 border border-[var(--dg-border)] text-[var(--dg-text-primary)] text-sm"
>
<option value="">请选择数据源</option>
{logSources.map((s) => (
@@ -210,20 +210,20 @@ export function AlertRuleDialog({
))}
</select>
{logSources.length === 0 && (
<p className="text-xs text-amber-400/80">请先前往「日志库」添加 {ruleForm.source === 'loki' ? 'Loki' : 'Elasticsearch'} 数据源</p>
<p className="text-xs dg-text-yellow/80">请先前往「日志库」添加 {ruleForm.source === 'loki' ? 'Loki' : 'Elasticsearch'} 数据源</p>
)}
</div>
)}
<div className="space-y-1.5">
<Label className="text-slate-300">触发表达式 <span className="text-red-400">*</span></Label>
<Label className="text-[var(--dg-text-secondary)]">触发表达式 <span className="dg-text-red">*</span></Label>
<Input
value={ruleForm.expression}
onChange={(e) => setRuleForm({ ...ruleForm, expression: e.target.value })}
placeholder={ruleForm.type === 'log' ? '{app="api"} |= "error"' : 'cpu_usage > 80'}
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-600 font-mono text-sm"
className="dg-input font-mono text-sm"
/>
<p className="text-xs text-slate-600">
<p className="text-xs dg-text-muted">
{ruleForm.type === 'log'
? (ruleForm.source === 'loki' ? 'LogQL 查询或 count_over_time 表达式' : 'Elasticsearch Lucene 查询')
: 'PromQL 或指标表达式,例如 cpu_usage > 80'}
@@ -232,11 +232,11 @@ export function AlertRuleDialog({
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-slate-300">告警级别</Label>
<Label className="text-[var(--dg-text-secondary)]">告警级别</Label>
<select
value={ruleForm.severity}
onChange={(e) => setRuleForm({ ...ruleForm, severity: e.target.value })}
className="w-full h-9 px-3 rounded-md bg-slate-800/50 border border-slate-700 text-slate-200 text-sm"
className="w-full h-9 px-3 rounded-md bg-[var(--dg-bg-tertiary)]/50 border border-[var(--dg-border)] text-[var(--dg-text-primary)] text-sm"
>
<option value="info">提示</option>
<option value="warning">警告</option>
@@ -244,20 +244,20 @@ export function AlertRuleDialog({
</select>
</div>
<div className="space-y-1.5">
<Label className="text-slate-300">持续时间(秒)</Label>
<Label className="text-[var(--dg-text-secondary)]">持续时间(秒)</Label>
<Input
type="number"
value={ruleForm.duration}
onChange={(e) => setRuleForm({ ...ruleForm, duration: Number(e.target.value) })}
className="bg-slate-800/50 border-slate-700 text-slate-200"
className="dg-input"
/>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-slate-300">通知渠道</Label>
<Label className="text-[var(--dg-text-secondary)]">通知渠道</Label>
{notifyChannels.length === 0 ? (
<div className="text-xs text-amber-400/80 bg-amber-500/5 border border-amber-500/20 rounded-md px-3 py-2">
<div className="text-xs dg-text-yellow/80 bg-amber-500/5 border border-amber-500/20 rounded-md px-3 py-2">
暂无已配置的通知渠道,请先在「通知渠道配置」中添加
</div>
) : (
@@ -268,17 +268,17 @@ export function AlertRuleDialog({
return (
<label
key={ch.id}
className="flex items-center gap-2 cursor-pointer select-none text-sm text-slate-300"
className="flex items-center gap-2 cursor-pointer select-none text-sm text-[var(--dg-text-secondary)]"
>
<Switch
checked={checked}
onCheckedChange={() => toggleNotifyChannel(String(ch.id))}
className="data-[state=checked]:bg-blue-600"
/>
<Icon className="h-4 w-4 text-slate-400" />
<Icon className="h-4 w-4 dg-text-secondary" />
<span>
{ch.name}
<span className="text-xs text-slate-500 ml-1">({getChannelTypeLabel(ch.type)})</span>
<span className="text-xs dg-text-muted ml-1">({getChannelTypeLabel(ch.type)})</span>
</span>
</label>
);
@@ -291,7 +291,7 @@ export function AlertRuleDialog({
<DialogFooter>
<Button
variant="outline"
className="border-slate-700 text-slate-300 hover:bg-slate-800"
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]"
onClick={() => onOpenChange(false)}
>
取消
@@ -106,11 +106,11 @@ export function AlertRulesTab({
{/* 批量操作栏 // Logan.Li */}
{selectedIds.length > 0 && (
<div className="flex items-center gap-2 mb-3">
<span className="text-sm text-slate-400">已选择 {selectedIds.length} 项</span>
<span className="text-sm dg-text-secondary">已选择 {selectedIds.length} 项</span>
<Button
variant="outline"
size="sm"
className="border-red-500/30 text-red-400 hover:bg-red-500/10"
className="border-red-500/30 dg-text-red hover:bg-red-500/10"
onClick={handleBatchDelete}
>
<Trash2 className="h-4 w-4 mr-2" />
@@ -119,56 +119,56 @@ export function AlertRulesTab({
</div>
)}
<Card className="bg-slate-900 border-slate-800">
<Card className="dg-card">
<CardContent className="p-0">
<div className="border-t border-slate-800">
<div className="border-t border-[var(--dg-border)]">
<Table>
<TableHeader>
<TableRow className="border-slate-800 hover:bg-transparent">
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
<TableHead className="w-12">
<Checkbox
checked={allSelected}
onCheckedChange={toggleSelectAll}
className="border-slate-600"
className="border-[var(--dg-border-hover)]"
/>
</TableHead>
<TableHead className="text-slate-400">规则名称</TableHead>
<TableHead className="text-slate-400">数据源</TableHead>
<TableHead className="text-slate-400">触发条件</TableHead>
<TableHead className="text-slate-400">级别</TableHead>
<TableHead className="text-slate-400">持续时间</TableHead>
<TableHead className="text-slate-400">通知渠道</TableHead>
<TableHead className="text-slate-400">状态</TableHead>
<TableHead className="text-slate-400 text-right">操作</TableHead>
<TableHead className="dg-text-secondary">规则名称</TableHead>
<TableHead className="dg-text-secondary">数据源</TableHead>
<TableHead className="dg-text-secondary">触发条件</TableHead>
<TableHead className="dg-text-secondary">级别</TableHead>
<TableHead className="dg-text-secondary">持续时间</TableHead>
<TableHead className="dg-text-secondary">通知渠道</TableHead>
<TableHead className="dg-text-secondary">状态</TableHead>
<TableHead className="dg-text-secondary text-right">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rules.length === 0 ? (
<TableRow className="border-slate-800">
<TableCell colSpan={COL_SPAN} className="text-center text-slate-500 py-12">
<TableRow className="border-[var(--dg-border)]">
<TableCell colSpan={COL_SPAN} className="text-center dg-text-muted py-12">
暂无告警规则
</TableCell>
</TableRow>
) : (
rules.map((rule) => (
<TableRow key={rule.id} className="border-slate-800 hover:bg-slate-800/50">
<TableRow key={rule.id} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
<TableCell>
<Checkbox
checked={selectedIds.includes(rule.id)}
onCheckedChange={() => toggleSelect(rule.id)}
className="border-slate-600"
className="border-[var(--dg-border-hover)]"
/>
</TableCell>
<TableCell>
<div>
<span className="text-sm font-medium text-slate-200">{rule.name}</span>
<span className="text-sm font-medium text-[var(--dg-text-primary)]">{rule.name}</span>
{rule.description && (
<p className="text-xs text-slate-500">{rule.description}</p>
<p className="text-xs dg-text-muted">{rule.description}</p>
)}
</div>
</TableCell>
<TableCell>
<code className="text-xs text-slate-300 bg-slate-800 px-2 py-0.5 rounded">
<code className="text-xs text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)] px-2 py-0.5 rounded">
{rule.source || '--'}
</code>
</TableCell>
@@ -179,21 +179,21 @@ export function AlertRulesTab({
</TableCell>
<TableCell>
{rule.severity === 'critical' ? (
<Badge variant="outline" className="border-red-500/30 text-red-400 bg-red-500/10">
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10">
严重
</Badge>
) : rule.severity === 'warning' ? (
<Badge variant="outline" className="border-amber-500/30 text-amber-400 bg-amber-500/10">
<Badge variant="outline" className="border-amber-500/30 dg-text-yellow bg-amber-500/10">
警告
</Badge>
) : (
<Badge variant="outline" className="border-blue-500/30 text-blue-400 bg-blue-500/10">
<Badge variant="outline" className="border-blue-500/30 dg-text-blue bg-blue-500/10">
提示
</Badge>
)}
</TableCell>
<TableCell>
<span className="text-sm text-slate-300">{(rule.duration || 0) / 60}分钟</span>
<span className="text-sm text-[var(--dg-text-secondary)]">{(rule.duration || 0) / 60}分钟</span>
</TableCell>
<TableCell>
{renderRuleChannels(rule, notifyChannels)}
@@ -210,7 +210,7 @@ export function AlertRulesTab({
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-slate-100"
className="h-8 w-8 dg-text-secondary hover:text-[var(--dg-text-primary)]"
onClick={() => onEditRule(rule)}
>
<Settings className="h-4 w-4" />
@@ -218,7 +218,7 @@ export function AlertRulesTab({
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-red-400"
className="h-8 w-8 dg-text-secondary hover:dg-text-red"
onClick={() => handleDeleteRule(rule.id)}
>
<Trash2 className="h-4 w-4" />
@@ -12,9 +12,11 @@ import { StatStrip } from '@/components/ui/stat-strip';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Switch } from '@/components/ui/switch';
import { toast } from 'sonner';
import { PageShell } from '@/components/layout/PageShell';
import { PageEmpty } from '@/components/layout/PageEmpty';
import { PageLoading } from '@/components/layout/PageLoading';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { alertCenterApi, alertApi } from '@/api';
import { useAlerts, useAgentAlertRules, useAlertMutations } from '@/hooks/api';
@@ -178,30 +180,16 @@ const AlertCenter: React.FC = () => {
setPage(1); // 过滤条件变化时重置到第一页
};
return (
<div className="space-y-3">
{/* 统计 — 紧凑条 */}
<StatStrip
items={[
{ label: t('alert.stat.firing'), value: stats.firing, color: stats.firing > 0 ? 'red' : 'muted' },
{ label: t('alert.stat.critical'), value: stats.critical, color: 'red' },
{ label: t('alert.stat.warning'), value: stats.warning, color: 'amber' },
{ label: t('alert.stat.resolved'), value: stats.resolved, color: 'emerald' },
]}
/>
const pageTabs = [
{ id: 'alerts', label: t('alert.tab.alerts') },
{ id: 'rules', label: t('alert.tab.rules') },
{ id: 'silences', label: t('alert.tab.silences') },
{ id: 'contacts', label: t('alert.tab.contacts') },
{ id: 'groups', label: t('alert.tab.groups') },
];
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-5 lg:w-[600px] h-9">
<TabsTrigger value="alerts" className="text-xs">{t('alert.tab.alerts')}</TabsTrigger>
<TabsTrigger value="rules" className="text-xs">{t('alert.tab.rules')}</TabsTrigger>
<TabsTrigger value="silences" className="text-xs">{t('alert.tab.silences')}</TabsTrigger>
<TabsTrigger value="contacts" className="text-xs">{t('alert.tab.contacts')}</TabsTrigger>
<TabsTrigger value="groups" className="text-xs">{t('alert.tab.groups')}</TabsTrigger>
</TabsList>
{/* 告警列表 Tab */}
<TabsContent value="alerts" className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2">
const alertsToolbar = activeTab === 'alerts' ? (
<div className="flex flex-wrap items-center justify-between gap-2 w-full">
<div className="flex items-center gap-2 flex-1 min-w-[280px]">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" />
@@ -235,16 +223,65 @@ const AlertCenter: React.FC = () => {
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<span>{t('alert.totalItems', { count: total })}</span>
<Button variant="outline" size="sm" onClick={() => refetchAlerts()} className="h-7 text-xs px-2">
<RefreshCw className="w-3 h-3 mr-1" />{t('common.refresh')}
</Button>
</div>
</div>
<div className="flex items-center gap-1 text-xs dg-text-muted">
<span>{t('alert.totalItems', { count: total })}</span>
<Button variant="outline" size="sm" onClick={() => refetchAlerts()} className="h-7 text-xs px-2 dg-btn-secondary">
<RefreshCw className="w-3 h-3 mr-1" />{t('common.refresh')}
</Button>
</div>
</div>
) : undefined;
{/* 告警表格 — 紧凑 */}
<Card>
return (
<PageShell
variant="list"
title="告警中心"
subtitle="告警事件处理、规则与通知配置"
icon={<AlertTriangle className="h-5 w-5" />}
tabs={pageTabs}
activeTab={activeTab}
onTabChange={setActiveTab}
toolbar={alertsToolbar}
metrics={(
<StatStrip
items={[
{ label: t('alert.stat.firing'), value: stats.firing, color: stats.firing > 0 ? 'red' : 'muted' },
{ label: t('alert.stat.critical'), value: stats.critical, color: 'red' },
{ label: t('alert.stat.warning'), value: stats.warning, color: 'amber' },
{ label: t('alert.stat.resolved'), value: stats.resolved, color: 'emerald' },
]}
/>
)}
actions={activeTab === 'rules' ? (
<>
<Button className="dg-btn-primary" onClick={handleOpenAddRule}>
<Plus className="w-4 h-4 mr-1" />{t('alert.addRule')}
</Button>
<Button variant="secondary" className="dg-btn-secondary" onClick={handleQuickSetup} disabled={quickSetupMutation.isPending}>
<Rocket className="w-4 h-4 mr-1" />
{quickSetupMutation.isPending ? t('alert.quickSetupCreating') : t('alert.quickSetup')}
</Button>
<Button variant="outline" className="dg-btn-secondary" onClick={() => refetchRules()}>
<RefreshCw className="w-4 h-4 mr-2" />{t('common.refresh')}
</Button>
</>
) : activeTab === 'contacts' ? (
<Button size="sm" className="dg-btn-primary" onClick={() => { setEditingContact(null); setContactDialogOpen(true); }}>
<Plus className="w-4 h-4 mr-1" />添加联系人
</Button>
) : activeTab === 'groups' ? (
<>
<Button variant="outline" className="dg-btn-secondary" onClick={fetchContactGroups}>
<RefreshCw className="w-4 h-4 mr-2" />{t('common.refresh')}
</Button>
<Button className="dg-btn-primary" onClick={() => { setEditingGroup(null); setGroupDialogOpen(true); }}>
<Plus className="w-4 h-4 mr-2" />添加联系组
</Button>
</>
) : undefined}
>
{activeTab === 'alerts' && (
<Card>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full text-sm">
@@ -260,9 +297,9 @@ const AlertCenter: React.FC = () => {
</thead>
<tbody className="divide-y">
{loading ? (
<tr><td colSpan={6} className="px-3 py-6 text-center text-xs text-muted-foreground">{t('alert.loading')}</td></tr>
<tr><td colSpan={6} className="px-3 py-2"><PageLoading label={t('alert.loading')} /></td></tr>
) : alerts.length === 0 ? (
<tr><td colSpan={6} className="px-3 py-6 text-center text-xs text-muted-foreground">{t('alert.noAlerts')}</td></tr>
<tr><td colSpan={6} className="px-3 py-2"><PageEmpty title={t('alert.noAlerts')} className="py-6" /></td></tr>
) : (
alerts.filter(alert => {
// 客户端也保留关键词搜索过滤
@@ -302,7 +339,7 @@ const AlertCenter: React.FC = () => {
{alert.status === 'firing' && (
<Button variant="ghost" size="sm" className="h-7 w-7 p-0"
onClick={() => handleAcknowledge(alert.id)}>
<Check className="w-3.5 h-3.5 text-blue-600" />
<Check className="w-3.5 h-3.5 dg-text-blue" />
</Button>
)}
{(alert.status === 'firing' || alert.status === 'acknowledged') && (
@@ -353,31 +390,15 @@ const AlertCenter: React.FC = () => {
)}
</CardContent>
</Card>
</TabsContent>
)}
{/* 告警规则 Tab */}
<TabsContent value="rules" className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<Button onClick={handleOpenAddRule}>
<Plus className="w-4 h-4 mr-1" />{t('alert.addRule')}
</Button>
<Button variant="secondary" onClick={handleQuickSetup} disabled={quickSetupMutation.isPending}>
<Rocket className="w-4 h-4 mr-1" />
{quickSetupMutation.isPending ? t('alert.quickSetupCreating') : t('alert.quickSetup')}
</Button>
</div>
<Button variant="outline" onClick={() => refetchRules()}>
<RefreshCw className="w-4 h-4 mr-2" />
刷新
</Button>
</div>
{rules.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<AlertTriangle className="w-10 h-10 mx-auto mb-2 opacity-50" />
<p className="text-sm">{t('alert.noRules')}</p>
<p className="text-xs mt-1">{t('alert.noRulesHint')}</p>
</div>
{activeTab === 'rules' && (
rules.length === 0 ? (
<PageEmpty
icon={<AlertTriangle className="w-10 h-10" />}
title={t('alert.noRules')}
description={t('alert.noRulesHint')}
/>
) : (
<div className="grid grid-cols-1 gap-4">
{rules.map((rule) => (
@@ -417,7 +438,7 @@ const AlertCenter: React.FC = () => {
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={() => handleEditRule(rule)}>{t('common.edit')}</Button>
<Button variant="ghost" size="sm" className="text-red-400" onClick={() => handleDeleteRule(rule)}>{t('common.delete')}</Button>
<Button variant="ghost" size="sm" className="dg-text-red" onClick={() => handleDeleteRule(rule)}>{t('common.delete')}</Button>
<Switch
checked={rule.enabled}
onCheckedChange={(checked) => toggleRule(String(rule.id), checked)}
@@ -428,27 +449,14 @@ const AlertCenter: React.FC = () => {
</Card>
))}
</div>
)}
</TabsContent>
)
)}
{/* 静默规则 Tab */}
<TabsContent value="silences">
<SilenceRuleTab />
</TabsContent>
{activeTab === 'silences' && <SilenceRuleTab />}
{/* 报警联系人 Tab */}
<TabsContent value="contacts" className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-sm font-medium">{t('alert.contactsTitle')}</h3>
<Button size="sm" onClick={() => { setEditingContact(null); setContactDialogOpen(true); }}>
<Plus className="w-4 h-4 mr-1" />
添加联系人
</Button>
</div>
{contacts.length === 0 ? (
<div className="text-center py-8 text-muted-foreground border border-dashed rounded-lg text-xs">
暂无报警联系人,点击「添加联系人」创建
</div>
{activeTab === 'contacts' && (
contacts.length === 0 ? (
<PageEmpty title="暂无报警联系人" description="点击「添加联系人」创建" />
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{contacts.map((c) => (
@@ -492,25 +500,12 @@ const AlertCenter: React.FC = () => {
</Card>
))}
</div>
)}
</TabsContent>
)
)}
{/* 报警联系组 Tab */}
<TabsContent value="groups" className="space-y-4">
<div className="flex justify-between items-center">
<Button variant="outline" onClick={fetchContactGroups}>
<RefreshCw className="w-4 h-4 mr-2" />
刷新
</Button>
<Button onClick={() => { setEditingGroup(null); setGroupDialogOpen(true); }}>
<Plus className="w-4 h-4 mr-2" />
添加联系组
</Button>
</div>
{contactGroups.length === 0 ? (
<div className="text-center py-16 text-muted-foreground border border-dashed rounded-lg">
暂无报警联系组,点击「添加联系组」创建
</div>
{activeTab === 'groups' && (
contactGroups.length === 0 ? (
<PageEmpty title="暂无报警联系组" description="点击「添加联系组」创建" />
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{contactGroups.map((g) => {
@@ -554,9 +549,8 @@ const AlertCenter: React.FC = () => {
);
})}
</div>
)}
</TabsContent>
</Tabs>
)
)}
<AlertDetailDialog
alert={selectedAlert}
@@ -590,7 +584,7 @@ const AlertCenter: React.FC = () => {
contacts={contacts}
onSaved={() => { fetchContactGroups(); }}
/>
</div>
</PageShell>
);
};
@@ -73,7 +73,7 @@ export function AlertDetailDialog({
{alert.runbookUrl && (
<div>
<Label className="text-muted-foreground">处理手册</Label>
<a href={alert.runbookUrl} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline block mt-1">
<a href={alert.runbookUrl} target="_blank" rel="noopener noreferrer" className="dg-text-blue hover:underline block mt-1">
查看处理手册 →
</a>
</div>
@@ -120,7 +120,7 @@ export const AlertChannelsTab: React.FC<{ refreshKey: number }> = ({ refreshKey
<div className="text-sm text-muted-foreground mb-3">
类型: {getChannelTypeLabel(channel.type)}
{channel.test_status === 'success' && (
<span className="ml-2 text-green-500">测试通过</span>
<span className="ml-2 dg-text-green">测试通过</span>
)}
</div>
<div className="flex gap-2">
@@ -201,7 +201,7 @@ const SilenceRuleTab: React.FC = () => {
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => handleEdit(s)}>
<Edit className="w-3 h-3 mr-1" />编辑
</Button>
<Button variant="ghost" size="sm" className="h-7 text-xs text-red-400" onClick={() => handleDelete(s)}>
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-red" onClick={() => handleDelete(s)}>
<Trash2 className="w-3 h-3 mr-1" />删除
</Button>
</div>
@@ -137,9 +137,9 @@ export function getChannelIcon(type: string) {
case 'slack': return <MessageSquare className="w-4 h-4" />;
case 'sms': return <Phone className="w-4 h-4" />;
case 'webhook': return <Zap className="w-4 h-4" />;
case 'feishu': return <Bell className="w-4 h-4 text-blue-500" />;
case 'feishu': return <Bell className="w-4 h-4 dg-text-blue" />;
case 'dingtalk': return <Bell className="w-4 h-4 text-sky-500" />;
case 'wechat': return <MessageSquare className="w-4 h-4 text-green-500" />;
case 'wechat': return <MessageSquare className="w-4 h-4 dg-text-green" />;
default: return <Bell className="w-4 h-4" />;
}
}
@@ -74,7 +74,7 @@ export function getChannelIcon(type: string) {
// 渲染规则的通知渠道引用列表(兼容新格式=渠道ID 和 旧格式=类型字符串)
export function renderRuleChannels( rule: AlertRuleRecord, notifyChannels: AlertChannel[]) {
const refs = parseChannels(rule.notify_channels);
if (refs.length === 0) return <span className="text-xs text-slate-600">--</span>;
if (refs.length === 0) return <span className="text-xs dg-text-muted">--</span>;
return (
<div className="flex flex-wrap gap-1">
{refs.map((ref) => {
@@ -85,9 +85,9 @@ export function renderRuleChannels( rule: AlertRuleRecord, notifyChannels: Alert
<span
key={ref}
title={`${resolved.name} (${getChannelTypeLabel(resolved.type)})`}
className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-slate-800 text-slate-300"
className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)]"
>
<Icon className="h-3 w-3 text-slate-400" />
<Icon className="h-3 w-3 dg-text-secondary" />
{resolved.name}
</span>
);
@@ -46,6 +46,9 @@ import { Checkbox } from '@/components/ui/checkbox';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { useNavigate } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { PageShell } from '@/components/layout/PageShell';
import { PageLoading } from '@/components/layout/PageLoading';
import { PageEmpty } from '@/components/layout/PageEmpty';
// ═══════════ Tab 配置 ═══════════
type TabId = 'agent' | 'zabbix' | 'prometheus' | 'dashboard';
@@ -129,9 +132,9 @@ const AgentTab: React.FC = () => {
return sd === 'asc' ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va));
});
const paged = sorted.slice((page - 1) * pageSize, page * pageSize);
const SI = ({ f }: { f: SortField }) => sf !== f ? <ArrowUpDown className="w-3 h-3 text-slate-500" /> : sd === 'asc' ? <ArrowUp className="w-3 h-3 text-emerald-400" /> : <ArrowDown className="w-3 h-3 text-emerald-400" />;
const SI = ({ f }: { f: SortField }) => sf !== f ? <ArrowUpDown className="w-3 h-3 dg-text-muted" /> : sd === 'asc' ? <ArrowUp className="w-3 h-3 dg-text-green" /> : <ArrowDown className="w-3 h-3 dg-text-green" />;
const bar = (v: number, l: string) => (
<div className="flex items-center gap-2 min-w-[90px]"><div className="flex-1 h-2.5 rounded-full bg-[var(--dg-bg-tertiary)] overflow-hidden"><div className={`h-full rounded-full transition-all duration-500 ${v >= 80 ? 'bg-orange-500' : 'bg-emerald-500'}`} style={{ width: `${Math.min(v, 100)}%` }} /></div><span className={`w-14 text-right text-xs font-mono tabular-nums ${v >= 80 ? 'text-orange-400' : ''}`}>{l}</span></div>
<div className="flex items-center gap-2 min-w-[90px]"><div className="flex-1 h-2.5 rounded-full bg-[var(--dg-bg-tertiary)] overflow-hidden"><div className={`h-full rounded-full transition-all duration-500 ${v >= 80 ? 'bg-orange-500' : 'bg-emerald-500'}`} style={{ width: `${Math.min(v, 100)}%` }} /></div><span className={`w-14 text-right text-xs font-mono tabular-nums ${v >= 80 ? 'dg-text-orange' : ''}`}>{l}</span></div>
);
const selAll = selected.size === hosts.length && hosts.length > 0;
@@ -199,8 +202,14 @@ const AgentTab: React.FC = () => {
finally { setBatchSubmitting(false); }
};
if (loading) return <div className="text-center py-20 text-[var(--dg-text-muted)]"><Activity className="h-6 w-6 mx-auto mb-2 animate-spin" /><p className="text-xs">加载中…</p></div>;
if (!hosts.length) return <div className="text-center py-20 text-[var(--dg-text-muted)]"><Server className="h-8 w-8 mx-auto mb-2" /><p className="text-sm">暂无服务器资产</p><p className="text-xs mt-1">请先在「服务器管理」中添加主机,然后在此安装 Agent</p></div>;
if (loading) return <PageLoading label="加载 Agent 主机列表..." />;
if (!hosts.length) return (
<PageEmpty
icon={<Server className="h-8 w-8" />}
title="暂无服务器资产"
description="请先在「服务器管理」中添加主机,然后在此安装 Agent"
/>
);
return (
<div>
{/* 工具栏 */}
@@ -216,10 +225,10 @@ const AgentTab: React.FC = () => {
</Button>
{selAny && (
<>
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 border-blue-500/30 text-blue-400 hover:bg-blue-500/10" onClick={openBatchDialog}>
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 dg-btn-secondary dg-text-blue" onClick={openBatchDialog}>
<PackageOpen className="w-3 h-3" />批量部署 ({selected.size})
</Button>
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 border-red-500/30 text-red-400 hover:bg-red-500/10" onClick={handleBatchUninstall} disabled={batchUninstalling}>
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 border-red-500/30 dg-text-red hover:bg-red-500/10" onClick={handleBatchUninstall} disabled={batchUninstalling}>
<Trash2 className="w-3 h-3" />批量卸载 ({selected.size})
</Button>
</>
@@ -236,27 +245,27 @@ const AgentTab: React.FC = () => {
{/* ────────── 批量部署 Agent 对话框 ────────── */}
<Dialog open={batchOpen} onOpenChange={(v) => { if (!v) { setBatchOpen(false); setBatchAck(false); setBatchCreds({ username: 'root', password: '', sshKey: '', authType: 'password' }); } }}>
<DialogContent className="max-w-2xl max-h-[85vh] overflow-auto bg-slate-900 border-slate-700 text-slate-100">
<DialogContent className="max-w-2xl max-h-[85vh] overflow-auto dg-input">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-base">
<PackageOpen className="w-5 h-5 text-blue-400" />
<PackageOpen className="w-5 h-5 dg-text-blue" />
批量部署 Agent
</DialogTitle>
<DialogDescription className="text-slate-400 text-xs">
<DialogDescription className="dg-text-secondary text-xs">
选中 {selectedHosts.length} 台主机(需安装: {needInstall.length},待升级: {needUpgrade.length})
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 模式选择 — 类似阿里云安装/升级切换 */}
<div className="flex items-center gap-2 p-1 rounded-lg bg-slate-800 border border-slate-700 w-fit">
<div className="flex items-center gap-2 p-1 rounded-lg bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] w-fit">
{(['install', 'upgrade'] as const).map(m => (
<button
key={m}
disabled={m === 'install' ? needInstall.length === 0 : needUpgrade.length === 0}
onClick={() => setBatchMode(m)}
className={`px-4 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-30 disabled:cursor-not-allowed ${
batchMode === m ? 'bg-blue-600 text-white shadow-sm' : 'text-slate-400 hover:text-slate-200'
batchMode === m ? 'dg-btn-primary text-white shadow-sm' : 'dg-text-secondary hover:text-[var(--dg-text-primary)]'
}`}
>
{m === 'install' ? `⚡ 安装 (${needInstall.length})` : `🔄 升级到最新版 (${needUpgrade.length})`}
@@ -265,32 +274,32 @@ const AgentTab: React.FC = () => {
</div>
{/* 主机列表 */}
<div className="rounded-md border border-slate-700 overflow-hidden">
<div className="rounded-md border border-[var(--dg-border)] overflow-hidden">
<table className="w-full text-xs">
<thead className="bg-slate-800">
<thead className="bg-[var(--dg-bg-tertiary)]">
<tr>
<th className="px-3 py-2 text-left font-medium text-slate-400">主机名</th>
<th className="px-3 py-2 text-left font-medium text-slate-400">IP</th>
<th className="px-3 py-2 text-left font-medium text-slate-400">状态</th>
<th className="px-3 py-2 text-left font-medium text-slate-400">当前版本</th>
<th className="px-3 py-2 text-left font-medium text-slate-400">操作</th>
<th className="px-3 py-2 text-left font-medium dg-text-secondary">主机名</th>
<th className="px-3 py-2 text-left font-medium dg-text-secondary">IP</th>
<th className="px-3 py-2 text-left font-medium dg-text-secondary">状态</th>
<th className="px-3 py-2 text-left font-medium dg-text-secondary">当前版本</th>
<th className="px-3 py-2 text-left font-medium dg-text-secondary">操作</th>
</tr>
</thead>
<tbody>
{(batchMode === 'install' ? needInstall : needUpgrade).map(h => (
<tr key={h.server_id} className="border-t border-slate-800 hover:bg-slate-800/50">
<td className="px-3 py-2 font-medium text-slate-200">{h.hostname}</td>
<td className="px-3 py-2 font-mono text-slate-400">{h.ip}</td>
<tr key={h.server_id} className="border-t border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
<td className="px-3 py-2 font-medium text-[var(--dg-text-primary)]">{h.hostname}</td>
<td className="px-3 py-2 font-mono dg-text-secondary">{h.ip}</td>
<td className="px-3 py-2">
{h.agent_status === 'online'
? <Badge className="text-[10px] bg-emerald-500/20 text-emerald-400">在线</Badge>
: <Badge className="text-[10px] bg-slate-500/20 text-slate-400">
? <Badge className="text-[10px] bg-emerald-500/20 dg-text-green">在线</Badge>
: <Badge className="text-[10px] bg-[var(--dg-bg-active)]/20 dg-text-secondary">
{h.agent_status === 'not_installed' ? '未安装' : h.agent_status || '离线'}
</Badge>}
</td>
<td className="px-3 py-2 font-mono text-slate-400">{h.agent_version || '—'}</td>
<td className="px-3 py-2 font-mono dg-text-secondary">{h.agent_version || '—'}</td>
<td className="px-3 py-2">
<span className="text-blue-400 text-[11px]">
<span className="dg-text-blue text-[11px]">
{batchMode === 'install' ? '即将安装' : '即将升级'}
</span>
</td>
@@ -302,38 +311,38 @@ const AgentTab: React.FC = () => {
{/* 安装凭据(安装模式且存在未保存凭据的主机时显示)// Logan.Li */}
{batchMode === 'install' && needInstall.some(h => !h.has_auth) && (
<div className="rounded-md border border-slate-700 bg-slate-800/50 p-3 space-y-3">
<div className="rounded-md border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/50 p-3 space-y-3">
<div className="flex items-center gap-2">
<Info className="w-4 h-4 text-blue-400 shrink-0" />
<span className="text-xs text-slate-300">以下主机未保存 SSH 凭据,请提供临时凭据用于批量安装(仅本次使用):</span>
<Info className="w-4 h-4 dg-text-blue shrink-0" />
<span className="text-xs text-[var(--dg-text-secondary)]">以下主机未保存 SSH 凭据,请提供临时凭据用于批量安装(仅本次使用):</span>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-[11px] text-slate-400 mb-1 block">认证方式</Label>
<Label className="text-[11px] dg-text-secondary mb-1 block">认证方式</Label>
<Select value={batchCreds.authType} onValueChange={(v: 'password' | 'key') => setBatchCreds(c => ({ ...c, authType: v }))}>
<SelectTrigger className="h-8 text-xs bg-slate-900 border-slate-700 text-slate-200">
<SelectTrigger className="h-8 text-xs dg-input text-[var(--dg-text-primary)]">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-slate-900 border-slate-700 text-slate-200">
<SelectContent className="dg-input text-[var(--dg-text-primary)]">
<SelectItem value="password">密码</SelectItem>
<SelectItem value="key">SSH 私钥</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-[11px] text-slate-400 mb-1 block">用户名</Label>
<Input value={batchCreds.username} onChange={e => setBatchCreds(c => ({ ...c, username: e.target.value }))} placeholder="root" className="h-8 text-xs bg-slate-900 border-slate-700 text-slate-200" />
<Label className="text-[11px] dg-text-secondary mb-1 block">用户名</Label>
<Input value={batchCreds.username} onChange={e => setBatchCreds(c => ({ ...c, username: e.target.value }))} placeholder="root" className="h-8 text-xs dg-input text-[var(--dg-text-primary)]" />
</div>
</div>
{batchCreds.authType === 'password' ? (
<div>
<Label className="text-[11px] text-slate-400 mb-1 block">密码</Label>
<Input type="password" value={batchCreds.password} onChange={e => setBatchCreds(c => ({ ...c, password: e.target.value }))} placeholder="输入 SSH 密码" className="h-8 text-xs bg-slate-900 border-slate-700 text-slate-200" />
<Label className="text-[11px] dg-text-secondary mb-1 block">密码</Label>
<Input type="password" value={batchCreds.password} onChange={e => setBatchCreds(c => ({ ...c, password: e.target.value }))} placeholder="输入 SSH 密码" className="h-8 text-xs dg-input text-[var(--dg-text-primary)]" />
</div>
) : (
<div>
<Label className="text-[11px] text-slate-400 mb-1 block">SSH 私钥</Label>
<textarea value={batchCreds.sshKey} onChange={e => setBatchCreds(c => ({ ...c, sshKey: e.target.value }))} placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" className="w-full h-24 rounded-md border border-slate-700 bg-slate-900 text-slate-200 text-xs p-2 resize-none focus:outline-none focus:ring-1 focus:ring-blue-500" />
<Label className="text-[11px] dg-text-secondary mb-1 block">SSH 私钥</Label>
<textarea value={batchCreds.sshKey} onChange={e => setBatchCreds(c => ({ ...c, sshKey: e.target.value }))} placeholder="-----BEGIN OPENSSH PRIVATE KEY-----" className="w-full h-24 rounded-md border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] text-[var(--dg-text-primary)] text-xs p-2 resize-none focus:outline-none focus:ring-1 focus:ring-[var(--dg-blue)]" />
</div>
)}
</div>
@@ -342,16 +351,16 @@ const AgentTab: React.FC = () => {
{/* ⚠️ 警告文案 — 参考阿里云批量安装/升级风格 */}
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 p-3 space-y-2">
<div className="flex items-start gap-2">
<Info className="w-4 h-4 text-amber-400 mt-0.5 shrink-0" />
<Info className="w-4 h-4 dg-text-yellow mt-0.5 shrink-0" />
<div>
<p className="text-xs font-medium text-amber-300 mb-1">
{batchMode === 'install' ? '本次安装' : '本次升级'},主机监控探针将部署 AIOps Agent。AIOps Agent 是 AIOps 平台推出的新一代数据采集器,提供主机监控指标采集和可观测数据采集能力。
</p>
<p className="text-[11px] text-amber-400/80 leading-relaxed">
<p className="text-[11px] dg-text-yellow/80 leading-relaxed">
{batchMode === 'upgrade' && '升级前请注意以下事项:'}
{batchMode === 'install' && '安装前请注意以下事项:'}
</p>
<ul className="mt-1 space-y-0.5 text-[11px] text-slate-400 list-disc list-inside">
<ul className="mt-1 space-y-0.5 text-[11px] dg-text-secondary list-disc list-inside">
{batchMode === 'upgrade' && (
<>
<li>如果您的机器已经安装旧版 Agent,升级操作将停止旧版并替换为最新版本。</li>
@@ -378,16 +387,16 @@ const AgentTab: React.FC = () => {
{/* 我已知悉 勾选 */}
<div className="flex items-center gap-2">
<Checkbox id="batch-ack" checked={batchAck} onCheckedChange={(c) => setBatchAck(!!c)} />
<label htmlFor="batch-ack" className="text-xs text-slate-300 cursor-pointer select-none">我已知悉以上风险和注意事项</label>
<label htmlFor="batch-ack" className="text-xs text-[var(--dg-text-secondary)] cursor-pointer select-none">我已知悉以上风险和注意事项</label>
</div>
</div>
<DialogFooter className="gap-2">
<Button variant="outline" className="border-slate-700 text-slate-300 h-8 text-xs" onClick={() => { setBatchOpen(false); setBatchAck(false); }}>
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] h-8 text-xs" onClick={() => { setBatchOpen(false); setBatchAck(false); }}>
取消
</Button>
<Button
className="bg-blue-600 hover:bg-blue-700 text-white h-8 text-xs gap-1"
className="dg-btn-primary h-8 text-xs gap-1"
disabled={batchSubmitting || !batchAck || (batchMode === 'install' ? needInstall.length === 0 : needUpgrade.length === 0)}
onClick={handleBatchSubmit}
>
@@ -423,7 +432,7 @@ const AgentTab: React.FC = () => {
<td className="px-3 py-2.5 font-medium text-[var(--dg-text-primary)]">{h.hostname}</td>
<td className="px-3 py-2.5 font-mono text-[var(--dg-text-secondary)]">
{operating.has(h.server_id) ? (
<span className="inline-flex items-center gap-1 text-blue-400">
<span className="inline-flex items-center gap-1 dg-text-blue">
<Loader2 className="w-3 h-3 animate-spin" />
<span className="text-[10px]">部署中</span>
</span>
@@ -431,14 +440,14 @@ const AgentTab: React.FC = () => {
h.agent_version || '—'
)}
</td>
<td className="px-3 py-2.5">{h.agent_status==='online'?<Badge className="text-[10px] bg-emerald-500/20 text-emerald-400"><Wifi className="w-3 h-3 mr-1"/>在线</Badge>:h.agent_status==='not_installed'?<Badge className="text-[10px] bg-amber-500/20 text-amber-400">未安装</Badge>:<Badge className="text-[10px] bg-slate-500/10 text-slate-400"><WifiOff className="w-3 h-3 mr-1"/>离线</Badge>}</td>
<td className="px-3 py-2.5">{h.agent_status==='online'?<Badge className="text-[10px] bg-emerald-500/20 dg-text-green"><Wifi className="w-3 h-3 mr-1"/>在线</Badge>:h.agent_status==='not_installed'?<Badge className="text-[10px] bg-amber-500/20 dg-text-yellow">未安装</Badge>:<Badge className="text-[10px] bg-[var(--dg-bg-active)]/10 dg-text-secondary"><WifiOff className="w-3 h-3 mr-1"/>离线</Badge>}</td>
<td className="px-3 py-2.5">{bar(Number(h.cpu_usage||0), `${Number(h.cpu_usage||0).toFixed(1)}%`)}</td>
<td className="px-3 py-2.5">{bar(Number(h.mem_usage||0), `${Number(h.mem_usage||0).toFixed(1)}%`)}</td>
<td className="px-3 py-2.5">{bar(Number(h.disk_usage||0), `${Number(h.disk_usage||0).toFixed(1)}%`)}</td>
<td className="px-3 py-2.5 text-[var(--dg-text-secondary)]">{h.region||'—'}</td>
<td className="px-3 py-2.5 font-mono text-[var(--dg-text-secondary)]">{h.ip||'—'}</td>
<td className="px-3 py-2.5">
<Button variant="ghost" size="sm" className="h-7 text-xs text-blue-400 hover:text-blue-300 hover:bg-blue-500/10 px-2"
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-blue hover:bg-[var(--dg-bg-hover)] px-2"
onClick={() => navigate(`/host-monitor?host=${encodeURIComponent(h.hostname)}`)}>
<BarChart3 className="w-3.5 h-3.5 mr-1" />图表
</Button>
@@ -479,8 +488,14 @@ const ZabbixTab: React.FC = () => {
setDs(list.find(( d: any) => d.is_default) || list[0] || null);
}).catch(() => {}).finally(() => setLoading(false));
}, []);
if (loading) return <div className="text-center py-20 text-[var(--dg-text-muted)]"><Activity className="h-6 w-6 mx-auto mb-2 animate-spin" /><p className="text-xs">加载数据源…</p></div>;
if (!ds) return <div className="text-center py-20 text-[var(--dg-text-muted)]"><Database className="h-8 w-8 mx-auto mb-2" /><p className="text-sm">未配置 Zabbix 数据源</p><a href="/datasources" className="text-xs text-blue-400 hover:underline inline-flex items-center gap-1">前往配置 <ExternalLink className="w-3 h-3" /></a></div>;
if (loading) return <PageLoading label="加载 Zabbix 数据源..." />;
if (!ds) return (
<PageEmpty
icon={<Database className="h-8 w-8" />}
title="未配置 Zabbix 数据源"
action={<a href="/datasources" className="text-xs dg-text-blue hover:underline inline-flex items-center gap-1">前往配置 <ExternalLink className="w-3 h-3" /></a>}
/>
);
return <ZabbixDashboard dsUrl={ds.url} dsUsername={ds.username} dsPassword={ds.password} />;
};
@@ -493,8 +508,14 @@ const PrometheusTab: React.FC = () => {
setDs(list.find(( d: any) => d.is_default) || list[0] || null);
}).catch(() => {}).finally(() => setLoading(false));
}, []);
if (loading) return <div className="text-center py-20 text-[var(--dg-text-muted)]"><Activity className="h-6 w-6 mx-auto mb-2 animate-spin" /><p className="text-xs">加载数据源…</p></div>;
if (!ds) return <div className="text-center py-20 text-[var(--dg-text-muted)]"><Database className="h-8 w-8 mx-auto mb-2" /><p className="text-sm">未配置 Prometheus 数据源</p><a href="/datasources" className="text-xs text-blue-400 hover:underline inline-flex items-center gap-1">前往配置 <ExternalLink className="w-3 h-3" /></a></div>;
if (loading) return <PageLoading label="加载 Prometheus 数据源..." />;
if (!ds) return (
<PageEmpty
icon={<Database className="h-8 w-8" />}
title="未配置 Prometheus 数据源"
action={<a href="/datasources" className="text-xs dg-text-blue hover:underline inline-flex items-center gap-1">前往配置 <ExternalLink className="w-3 h-3" /></a>}
/>
);
return <PrometheusDashboard dsUrl={ds.url} />;
};
@@ -555,7 +576,7 @@ const DashboardTab: React.FC = () => {
{active ? (
active.panels.length === 0 ? (
<div className="text-center py-16 text-[var(--dg-text-muted)]"><BarChart3 className="w-10 h-10 mx-auto mb-2 opacity-50" /><p className="text-xs">暂无面板</p></div>
<PageEmpty icon={<BarChart3 className="w-10 h-10" />} title="暂无面板" className="py-16" />
) : (
<div className="grid grid-cols-12 gap-3">
{active.panels.map(p => (
@@ -567,7 +588,7 @@ const DashboardTab: React.FC = () => {
</div>
)
) : (
<div className="text-center py-16 text-[var(--dg-text-muted)]"><Monitor className="w-10 h-10 mx-auto mb-2 opacity-50" /><p className="text-xs">暂无仪表盘</p></div>
<PageEmpty icon={<Monitor className="w-10 h-10" />} title="暂无仪表盘" className="py-16" />
)}
<Dialog open={createOpen} onOpenChange={setCreateOpen}><DialogContent><DialogHeader><DialogTitle>新建仪表盘</DialogTitle></DialogHeader>
@@ -589,40 +610,30 @@ const GrafanaDashboard: React.FC = () => {
const [tab, setTab] = useState<TabId>('agent');
const [fullscreen, setFullscreen] = useState(false);
const pageTabs = TABS.map((item) => ({ id: item.id, label: item.label }));
return (
<div className={`space-y-4 ${fullscreen ? 'fixed inset-0 z-50 bg-background p-4 overflow-auto' : ''}`}>
{/* Tab 切换栏 */}
<div className="flex items-center gap-1 p-1 rounded-lg bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)]">
{TABS.map(t => (
<button
key={t.id}
onClick={() => setTab(t.id)}
className={`flex items-center gap-1.5 px-4 py-2 rounded-md text-xs font-medium transition-colors ${
tab === t.id
? 'bg-[var(--dg-bg-primary)] text-[var(--dg-text-primary)] shadow-sm'
: 'text-[var(--dg-text-muted)] hover:text-[var(--dg-text-secondary)]'
}`}
>
{t.icon}
{t.label}
</button>
))}
<div className="ml-auto">
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => setFullscreen(!fullscreen)}>
{fullscreen ? <Minimize2 className="w-3.5 h-3.5" /> : <Maximize2 className="w-3.5 h-3.5" />}
</Button>
</div>
</div>
{/* Tab 内容 */}
<div className="min-h-[50vh]">
{tab === 'agent' && <AgentTab />}
{tab === 'zabbix' && <ZabbixTab />}
{tab === 'prometheus' && <PrometheusTab />}
{tab === 'dashboard' && <DashboardTab />}
</div>
</div>
<PageShell
variant="workspace"
title="监控大屏"
subtitle="Agent 主机、Zabbix、Prometheus 与自定义仪表盘"
icon={<Monitor className="h-5 w-5" />}
tabs={pageTabs}
activeTab={tab}
onTabChange={(id) => setTab(id as TabId)}
className={fullscreen ? 'fixed inset-0 z-50 bg-background p-4 overflow-auto' : undefined}
contentClassName="min-h-[50vh]"
actions={(
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => setFullscreen(!fullscreen)}>
{fullscreen ? <Minimize2 className="w-3.5 h-3.5" /> : <Maximize2 className="w-3.5 h-3.5" />}
</Button>
)}
>
{tab === 'agent' && <AgentTab />}
{tab === 'zabbix' && <ZabbixTab />}
{tab === 'prometheus' && <PrometheusTab />}
{tab === 'dashboard' && <DashboardTab />}
</PageShell>
);
};
@@ -147,11 +147,11 @@ export function GrafanaPanelContent({ panel, datasourceUrl }: { panel: Panel; da
const unit = (panel.options as any)?.unit ?? '';
return (
<div className="flex flex-col items-center justify-center h-full min-h-[120px]">
<span className="text-4xl font-bold text-blue-500">
<span className="text-4xl font-bold dg-text-blue">
{val > 1000 ? (val / 1000).toFixed(1) + 'K' : val.toFixed(2)}
</span>
{unit && <span className="text-sm text-[var(--dg-text-muted)] mt-2">{unit}</span>}
<div className="flex items-center gap-1 mt-2 text-green-500 text-sm">
<div className="flex items-center gap-1 mt-2 dg-text-green text-sm">
<TrendingUp className="w-4 h-4" />
<span>实时</span>
</div>
@@ -205,7 +205,7 @@ export function GrafanaPanelContent({ panel, datasourceUrl }: { panel: Panel; da
{Object.entries(row.labels).slice(0, 3).map(([k, v]) => (
<td key={k} className="py-1 px-2 text-[var(--dg-text-secondary)]">{v}</td>
))}
<td className="py-1 px-2 text-right font-mono text-blue-400">{row.value.toFixed(2)}</td>
<td className="py-1 px-2 text-right font-mono dg-text-blue">{row.value.toFixed(2)}</td>
</tr>
))}
</tbody>
@@ -39,7 +39,7 @@ function getUsageBar(val: number) {
const color = v > 85 ? 'bg-red-500' : v > 60 ? 'bg-amber-500' : 'bg-emerald-500';
return (
<div className="flex items-center gap-1.5 min-w-[80px]">
<div className="flex-1 h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
<div className="flex-1 h-1.5 rounded-full bg-[var(--dg-bg-active)]/50 overflow-hidden">
<div className={`h-full rounded-full ${color}`} style={{ width: `${Math.min(v, 100)}%` }} />
</div>
<span className="text-xs font-mono w-10 text-right text-[var(--dg-text-primary)]">{v.toFixed(0)}%</span>
@@ -126,10 +126,10 @@ export default function HostMonitorDashboard() {
};
const SortIcon = ({ field }: { field: SortField }) => {
if (sortField !== field) return <ArrowUpDown className="w-3 h-3 text-slate-500" />;
if (sortField !== field) return <ArrowUpDown className="w-3 h-3 dg-text-muted" />;
return sortDir === 'asc'
? <ArrowUp className="w-3 h-3 text-blue-400" />
: <ArrowDown className="w-3 h-3 text-blue-400" />;
? <ArrowUp className="w-3 h-3 dg-text-blue" />
: <ArrowDown className="w-3 h-3 dg-text-blue" />;
};
const totalPages = Math.ceil(total / pageSize);
@@ -141,10 +141,10 @@ export default function HostMonitorDashboard() {
// 状态标签颜色
const statusBadge = (status: string) => {
if (status === 'online') return <Badge className="bg-emerald-500/20 text-emerald-400 border-emerald-500/30 text-[11px]">在线</Badge>;
if (status === 'running') return <Badge className="bg-blue-500/20 text-blue-400 border-blue-500/30 text-[11px]">运行中</Badge>;
if (status === 'stopped') return <Badge className="bg-slate-500/20 text-slate-400 border-slate-500/30 text-[11px]">已停止</Badge>;
return <Badge variant="outline" className="text-[11px] text-slate-500">{status || '--'}</Badge>;
if (status === 'online') return <Badge className="bg-emerald-500/20 dg-text-green border-emerald-500/30 text-[11px]">在线</Badge>;
if (status === 'running') return <Badge className="bg-blue-500/20 dg-text-blue border-blue-500/30 text-[11px]">运行中</Badge>;
if (status === 'stopped') return <Badge className="bg-[var(--dg-bg-active)]/20 dg-text-secondary border-[var(--dg-border)] text-[11px]">已停止</Badge>;
return <Badge variant="outline" className="text-[11px] dg-text-muted">{status || '--'}</Badge>;
};
return (
@@ -153,7 +153,7 @@ export default function HostMonitorDashboard() {
<div className="grid grid-cols-6 gap-3">
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-3 flex items-center gap-2.5">
<div className="p-1.5 rounded-lg bg-blue-500/10"><Server className="h-4 w-4 text-blue-400" /></div>
<div className="p-1.5 rounded-lg bg-blue-500/10"><Server className="h-4 w-4 dg-text-blue" /></div>
<div>
<div className="text-xl font-bold text-[var(--dg-text-primary)]">{stats.total}</div>
<div className="text-[10px] text-[var(--dg-text-muted)]">主机总数</div>
@@ -162,18 +162,18 @@ export default function HostMonitorDashboard() {
</Card>
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-3 flex items-center gap-2.5">
<div className="p-1.5 rounded-lg bg-emerald-500/10"><Wifi className="h-4 w-4 text-emerald-400" /></div>
<div className="p-1.5 rounded-lg bg-emerald-500/10"><Wifi className="h-4 w-4 dg-text-green" /></div>
<div>
<div className="text-xl font-bold text-emerald-400">{stats.online}</div>
<div className="text-xl font-bold dg-text-green">{stats.online}</div>
<div className="text-[10px] text-[var(--dg-text-muted)]">Agent 在线</div>
</div>
</CardContent>
</Card>
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-3 flex items-center gap-2.5">
<div className="p-1.5 rounded-lg bg-slate-500/10"><WifiOff className="h-4 w-4 text-slate-400" /></div>
<div className="p-1.5 rounded-lg bg-[var(--dg-bg-active)]/10"><WifiOff className="h-4 w-4 dg-text-secondary" /></div>
<div>
<div className="text-xl font-bold text-slate-400">{stats.offline}</div>
<div className="text-xl font-bold dg-text-secondary">{stats.offline}</div>
<div className="text-[10px] text-[var(--dg-text-muted)]">离线</div>
</div>
</CardContent>
@@ -182,12 +182,12 @@ export default function HostMonitorDashboard() {
const onlines = hosts.filter((h: JsonData) => h.agent_status === 'online');
const avgCpu = onlines.length > 0 ? onlines.reduce((s: number, h: JsonData) => s + (h.cpu_usage || 0), 0) / onlines.length : 0;
const avgMem = onlines.length > 0 ? onlines.reduce((s: number, h: JsonData) => s + (h.memory_usage || 0), 0) / onlines.length : 0;
const cpuColor = avgCpu > 85 ? 'text-red-400' : avgCpu > 60 ? 'text-amber-400' : 'text-emerald-400';
const memColor = avgMem > 85 ? 'text-red-400' : avgMem > 60 ? 'text-amber-400' : 'text-emerald-400';
const cpuColor = avgCpu > 85 ? 'dg-text-red' : avgCpu > 60 ? 'dg-text-yellow' : 'dg-text-green';
const memColor = avgMem > 85 ? 'dg-text-red' : avgMem > 60 ? 'dg-text-yellow' : 'dg-text-green';
return (<>
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-3 flex items-center gap-2.5">
<div className="p-1.5 rounded-lg bg-red-500/10"><Cpu className="h-4 w-4 text-red-400" /></div>
<div className="p-1.5 rounded-lg bg-red-500/10"><Cpu className="h-4 w-4 dg-text-red" /></div>
<div>
<div className={`text-xl font-bold ${cpuColor}`}>{avgCpu.toFixed(1)}<span className="text-sm">%</span></div>
<div className="text-[10px] text-[var(--dg-text-muted)]">平均 CPU</div>
@@ -196,7 +196,7 @@ export default function HostMonitorDashboard() {
</Card>
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-3 flex items-center gap-2.5">
<div className="p-1.5 rounded-lg bg-amber-500/10"><Server className="h-4 w-4 text-amber-400" /></div>
<div className="p-1.5 rounded-lg bg-amber-500/10"><Server className="h-4 w-4 dg-text-yellow" /></div>
<div>
<div className={`text-xl font-bold ${memColor}`}>{avgMem.toFixed(1)}<span className="text-sm">%</span></div>
<div className="text-[10px] text-[var(--dg-text-muted)]">平均内存</div>
@@ -214,9 +214,9 @@ export default function HostMonitorDashboard() {
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2 text-[var(--dg-text-primary)]">
<Cpu className="h-4 w-4 text-red-400" />
<Cpu className="h-4 w-4 dg-text-red" />
CPU 使用率 Top 5
<Badge variant="outline" className="text-[10px] bg-red-500/10 text-red-400 border-red-500/30">阈值: 60% / 85%</Badge>
<Badge variant="outline" className="text-[10px] bg-red-500/10 dg-text-red border-red-500/30">阈值: 60% / 85%</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
@@ -229,7 +229,7 @@ export default function HostMonitorDashboard() {
return (
<div key={h.id} className="flex items-center gap-2">
<span className="text-xs text-[var(--dg-text-secondary)] w-20 truncate" title={h.hostname}>{h.hostname}</span>
<div className="flex-1 h-4 rounded bg-slate-700/50 overflow-hidden">
<div className="flex-1 h-4 rounded bg-[var(--dg-bg-active)]/50 overflow-hidden">
<div
className="h-full rounded transition-all"
style={{
@@ -243,7 +243,7 @@ export default function HostMonitorDashboard() {
/>
</div>
<span className={`text-xs font-mono w-10 text-right ${
val >= 85 ? 'text-red-400' : val >= 60 ? 'text-amber-400' : 'text-emerald-400'
val >= 85 ? 'dg-text-red' : val >= 60 ? 'dg-text-yellow' : 'dg-text-green'
}`}>{val.toFixed(1)}%</span>
</div>
);
@@ -255,9 +255,9 @@ export default function HostMonitorDashboard() {
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2 text-[var(--dg-text-primary)]">
<Server className="h-4 w-4 text-amber-400" />
<Server className="h-4 w-4 dg-text-yellow" />
内存使用率 Top 5
<Badge variant="outline" className="text-[10px] bg-amber-500/10 text-amber-400 border-amber-500/30">阈值: 60% / 85%</Badge>
<Badge variant="outline" className="text-[10px] bg-amber-500/10 dg-text-yellow border-amber-500/30">阈值: 60% / 85%</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
@@ -270,7 +270,7 @@ export default function HostMonitorDashboard() {
return (
<div key={h.id} className="flex items-center gap-2">
<span className="text-xs text-[var(--dg-text-secondary)] w-20 truncate" title={h.hostname}>{h.hostname}</span>
<div className="flex-1 h-4 rounded bg-slate-700/50 overflow-hidden">
<div className="flex-1 h-4 rounded bg-[var(--dg-bg-active)]/50 overflow-hidden">
<div
className="h-full rounded transition-all"
style={{
@@ -284,7 +284,7 @@ export default function HostMonitorDashboard() {
/>
</div>
<span className={`text-xs font-mono w-10 text-right ${
val >= 85 ? 'text-red-400' : val >= 60 ? 'text-amber-400' : 'text-emerald-400'
val >= 85 ? 'dg-text-red' : val >= 60 ? 'dg-text-yellow' : 'dg-text-green'
}`}>{val.toFixed(1)}%</span>
</div>
);
@@ -299,13 +299,13 @@ export default function HostMonitorDashboard() {
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Activity className="h-5 w-5 text-emerald-400" />
<Activity className="h-5 w-5 dg-text-green" />
<CardTitle className="text-base text-[var(--dg-text-primary)]">主机监控列表</CardTitle>
<Badge variant="outline" className="text-xs bg-emerald-500/10 text-emerald-400 border-emerald-500/30">实时</Badge>
<Badge variant="outline" className="text-xs bg-emerald-500/10 dg-text-green border-emerald-500/30">实时</Badge>
</div>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
<Input
className="w-[200px] pl-8 h-8 text-sm bg-[var(--dg-bg-primary)]"
placeholder="搜索主机名/IP..."
@@ -324,12 +324,12 @@ export default function HostMonitorDashboard() {
<CardContent className="p-0">
{loading ? (
<div className="py-16 text-center text-[var(--dg-text-muted)]">
<Activity className="h-8 w-8 mx-auto mb-2 animate-spin text-emerald-400" />
<Activity className="h-8 w-8 mx-auto mb-2 animate-spin dg-text-green" />
<p className="text-sm">加载中...</p>
</div>
) : sorted.length === 0 ? (
<div className="py-16 text-center text-[var(--dg-text-muted)]">
<Server className="h-10 w-10 mx-auto mb-2 text-slate-600" />
<Server className="h-10 w-10 mx-auto mb-2 dg-text-muted" />
<p className="text-sm">暂无主机数据</p>
<p className="text-xs mt-1">请先添加服务器,或等待 Agent 上报数据</p>
</div>
@@ -369,7 +369,7 @@ export default function HostMonitorDashboard() {
{/* 主机名/ID */}
<td className="px-3 py-2">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${h.agent_status === 'online' ? 'bg-emerald-400' : h.status === 'running' ? 'bg-blue-400' : 'bg-slate-500'}`} />
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${h.agent_status === 'online' ? 'bg-emerald-400' : h.status === 'running' ? 'bg-blue-400' : 'bg-[var(--dg-bg-active)]'}`} />
<div>
<div className="text-sm font-medium text-[var(--dg-text-primary)]">{h.hostname}</div>
<div className="text-[10px] text-[var(--dg-text-muted)]">ID: {h.id}</div>
@@ -387,21 +387,21 @@ export default function HostMonitorDashboard() {
{/* CPU 使用率 */}
<td className="px-3 py-2">
<div className="flex items-center gap-1.5">
<Cpu className="h-3 w-3 text-red-400 flex-shrink-0" />
<Cpu className="h-3 w-3 dg-text-red flex-shrink-0" />
{getUsageBar(h.cpu_usage)}
</div>
</td>
{/* 内存使用率 */}
<td className="px-3 py-2">
<div className="flex items-center gap-1.5">
<Server className="h-3 w-3 text-amber-400 flex-shrink-0" />
<Server className="h-3 w-3 dg-text-yellow flex-shrink-0" />
{getUsageBar(h.memory_usage)}
</div>
</td>
{/* 磁盘使用率 */}
<td className="px-3 py-2">
<div className="flex items-center gap-1.5">
<HardDrive className="h-3 w-3 text-orange-400 flex-shrink-0" />
<HardDrive className="h-3 w-3 dg-text-orange flex-shrink-0" />
{getUsageBar(h.disk_usage)}
</div>
</td>
@@ -413,7 +413,7 @@ export default function HostMonitorDashboard() {
</td>
{/* IP 地址 */}
<td className="px-3 py-2">
<code className="text-xs text-slate-300 font-mono">{h.ip || '--'}</code>
<code className="text-xs text-[var(--dg-text-secondary)] font-mono">{h.ip || '--'}</code>
</td>
{/* 操作栏 */}
<td className="px-3 py-2">
@@ -421,7 +421,7 @@ export default function HostMonitorDashboard() {
<Button
variant="ghost"
size="sm"
className="h-7 text-xs text-blue-400 hover:text-blue-300 hover:bg-blue-500/10"
className="h-7 text-xs dg-text-blue hover:text-blue-300 hover:bg-blue-500/10"
onClick={() => navigate(`/monitor-center?host=${encodeURIComponent(h.hostname)}`)}
title="查看监控图表"
>
@@ -431,7 +431,7 @@ export default function HostMonitorDashboard() {
<Button
variant="ghost"
size="sm"
className="h-7 text-xs text-amber-400 hover:text-amber-300 hover:bg-amber-500/10"
className="h-7 text-xs dg-text-yellow hover:text-amber-300 hover:bg-amber-500/10"
onClick={() => navigate(`/alerts?hostname=${encodeURIComponent(h.hostname)}`)}
title="查看报警规则"
>
@@ -58,7 +58,7 @@ const K8sPanel: React.FC<{ d: JsonData}> = ({ d }) => (
]}
/>
<div className="rounded-lg bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] p-3 max-h-60 overflow-y-auto">
<div className="text-xs font-medium mb-2 flex items-center gap-1"><Cpu className="w-3 h-3 text-purple-400" />节点 CPU / 内存利用率</div>
<div className="text-xs font-medium mb-2 flex items-center gap-1"><Cpu className="w-3 h-3 dg-text-purple" />节点 CPU / 内存利用率</div>
{(d.nodeCPU || []).map((n: JsonData, i: number) => {
const mem = (d.nodeMem || []).find((m: JsonData) => m.node === n.node);
return <div key={i} className="flex items-center gap-3 text-[11px] mb-1.5"><span className="w-28 truncate" title={n.node}>{n.node}</span><MiniBar val={Number(n.value||0)} label={`${Number(n.value||0).toFixed(1)}%`} /><MiniBar val={Number(mem?.value||0)} label={`${Number(mem?.value||0).toFixed(1)}%`} color="bg-blue-500" /></div>;
@@ -82,7 +82,7 @@ const NodePanel: React.FC<{ d: JsonData}> = ({ d }) => (
const tx = (d.netTx||[]).find((x: JsonData)=>x.instance===inst);
return <tr key={i} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-3 py-2 font-medium">{n.nodename||inst}</td>
<td className="px-3 py-2"><Badge className={`text-[10px] ${up?.value===1?'bg-emerald-500/20 text-emerald-400':'bg-red-500/20 text-red-400'}`}>{up?.value===1?'UP':'DOWN'}</Badge></td>
<td className="px-3 py-2"><Badge className={`text-[10px] ${up?.value===1?'bg-emerald-500/20 dg-text-green':'bg-red-500/20 dg-text-red'}`}>{up?.value===1?'UP':'DOWN'}</Badge></td>
<td className="px-3 py-2"><MiniBar val={Number(cpu?.value||0)} label={`${Number(cpu?.value||0).toFixed(1)}%`} /></td>
<td className="px-3 py-2"><MiniBar val={Number(mem?.value||0)} label={`${Number(mem?.value||0).toFixed(1)}%`} color="bg-blue-500" /></td>
<td className="px-3 py-2"><MiniBar val={Number(disk?.value||0)} label={`${Number(disk?.value||0).toFixed(1)}%`} color="bg-amber-500" /></td>
@@ -116,8 +116,8 @@ const JvmPanel: React.FC<{ d: JsonData}> = ({ d }) => {
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><div className="text-lg font-bold">{uptime>86400?`${(uptime/86400).toFixed(1)}d`:`${(uptime/3600).toFixed(1)}h`}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Uptime</div></CardContent></Card>
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><MiniBar val={h} label={`${h.toFixed(1)}%`} color={h>80?'bg-orange-500':'bg-purple-500'} /><div className="text-[10px] text-[var(--dg-text-muted)] mt-1">Heap</div></CardContent></Card>
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><MiniBar val={Number(d.nonHeapUsed?.[0]?.value||0)} label={`${Number(d.nonHeapUsed?.[0]?.value||0).toFixed(1)}%`} color="bg-blue-500" /><div className="text-[10px] text-[var(--dg-text-muted)] mt-1">Non-Heap</div></CardContent></Card>
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><div className="text-lg font-bold text-emerald-400">{d.threadCount?.[0]?.value||0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Threads</div></CardContent></Card>
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><div className="text-lg font-bold text-amber-400">{Number(d.gcPause?.[0]?.value||0).toFixed(3)}</div><div className="text-[10px] text-[var(--dg-text-muted)]">GC Pause (s)</div></CardContent></Card>
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><div className="text-lg font-bold dg-text-green">{d.threadCount?.[0]?.value||0}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Threads</div></CardContent></Card>
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><div className="text-lg font-bold dg-text-yellow">{Number(d.gcPause?.[0]?.value||0).toFixed(3)}</div><div className="text-[10px] text-[var(--dg-text-muted)]">GC Pause (s)</div></CardContent></Card>
</div>
);
};
@@ -168,15 +168,15 @@ export const PrometheusDashboard: React.FC<Props> = ({ dsUrl }) => {
<Card className="bg-[var(--dg-bg-secondary)] border-purple-500/20">
<CardHeader className="py-3 px-4 pb-2">
<div className="flex items-center gap-2">
<Activity className="h-4 w-4 text-purple-400" />
<Activity className="h-4 w-4 dg-text-purple" />
<CardTitle className="text-sm font-medium">Prometheus 监控</CardTitle>
{connected && <Badge className="text-[10px] bg-purple-500/10 text-purple-400">{targets} targets</Badge>}
{connected && <Badge className="text-[10px] bg-purple-500/10 dg-text-purple">{targets} targets</Badge>}
{connected && <div className="ml-auto"><Button variant="ghost" size="sm" className="h-6 px-2 text-[10px]" onClick={refresh}><RefreshCw className="w-3 h-3 mr-1" />刷新</Button></div>}
</div>
</CardHeader>
<CardContent className="px-4 pb-3 pt-0">
{loading ? (<div className="text-center py-12"><Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin text-purple-400" /><p className="text-xs text-[var(--dg-text-muted)]">连接中…</p></div>)
: error ? (<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-xs text-red-400"><AlertTriangle className="h-4 w-4 inline mr-1" />{error}</div>)
{loading ? (<div className="text-center py-12"><Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin dg-text-purple" /><p className="text-xs text-[var(--dg-text-muted)]">连接中…</p></div>)
: error ? (<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-xs dg-text-red"><AlertTriangle className="h-4 w-4 inline mr-1" />{error}</div>)
: (
<Tabs value={tab} onValueChange={setTab}>
<TabsList className="bg-[var(--dg-bg-tertiary)] mb-3">
@@ -66,8 +66,8 @@ export const ZabbixDashboard: React.FC<Props> = ({ dsUrl, dsUsername, dsPassword
});
const SI = ({ f }: { f: SortField }) => {
if (sortField !== f) return <ArrowUpDown className="w-3 h-3 text-slate-500" />;
return sortDir === 'asc' ? <ArrowUp className="w-3 h-3 text-blue-400" /> : <ArrowDown className="w-3 h-3 text-blue-400" />;
if (sortField !== f) return <ArrowUpDown className="w-3 h-3 dg-text-muted" />;
return sortDir === 'asc' ? <ArrowUp className="w-3 h-3 dg-text-blue" /> : <ArrowDown className="w-3 h-3 dg-text-blue" />;
};
const bar = (v: number, label: string) => (
@@ -75,7 +75,7 @@ export const ZabbixDashboard: React.FC<Props> = ({ dsUrl, dsUsername, dsPassword
<div className="flex-1 h-2.5 rounded-full bg-[var(--dg-bg-tertiary)] overflow-hidden">
<div className={`h-full rounded-full transition-all duration-500 ${v >= 80 ? 'bg-orange-500' : 'bg-blue-500'}`} style={{ width: `${Math.min(v, 100)}%` }} />
</div>
<span className={`w-14 text-right text-xs font-mono tabular-nums ${v >= 80 ? 'text-orange-400' : ''}`}>{label}</span>
<span className={`w-14 text-right text-xs font-mono tabular-nums ${v >= 80 ? 'dg-text-orange' : ''}`}>{label}</span>
</div>
);
@@ -93,17 +93,17 @@ export const ZabbixDashboard: React.FC<Props> = ({ dsUrl, dsUsername, dsPassword
<Card className="bg-[var(--dg-bg-secondary)] border-blue-500/20">
<CardHeader className="py-3 px-4 pb-2">
<div className="flex items-center gap-2">
<Activity className="h-4 w-4 text-blue-400" />
<Activity className="h-4 w-4 dg-text-blue" />
<CardTitle className="text-sm font-medium">Zabbix 主机监控</CardTitle>
{version && <Badge className="text-[10px] bg-blue-500/10 text-blue-400">v{version}</Badge>}
{version && <Badge className="text-[10px] bg-blue-500/10 dg-text-blue">v{version}</Badge>}
{connected && <span className="text-[10px] text-[var(--dg-text-muted)] ml-2">{hosts.length} 台</span>}
{connected && <div className="ml-auto flex gap-1"><Button variant="ghost" size="sm" className="h-6 px-2 text-[10px]" onClick={refresh}><RefreshCw className="w-3 h-3 mr-1" />刷新</Button></div>}
</div>
</CardHeader>
<CardContent className="px-4 pb-3 pt-0">
{loading ? (<div className="text-center py-12"><Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin text-blue-400" /><p className="text-xs text-[var(--dg-text-muted)]">连接中…</p></div>)
: error ? (<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-xs text-red-400 flex items-start gap-2"><AlertTriangle className="h-4 w-4 mt-0.5" /><span>{error}</span></div>)
: hosts.length === 0 ? (<div className="text-center py-8 text-[var(--dg-text-muted)]"><Server className="h-8 w-8 mx-auto mb-2 text-slate-600" /><p className="text-sm">未找到被监控的 Zabbix 主机</p></div>)
{loading ? (<div className="text-center py-12"><Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin dg-text-blue" /><p className="text-xs text-[var(--dg-text-muted)]">连接中…</p></div>)
: error ? (<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-xs dg-text-red flex items-start gap-2"><AlertTriangle className="h-4 w-4 mt-0.5" /><span>{error}</span></div>)
: hosts.length === 0 ? (<div className="text-center py-8 text-[var(--dg-text-muted)]"><Server className="h-8 w-8 mx-auto mb-2 dg-text-muted" /><p className="text-sm">未找到被监控的 Zabbix 主机</p></div>)
: (
<div className="overflow-x-auto rounded-lg border border-[var(--dg-border)]">
<table className="w-full text-xs">
@@ -119,14 +119,14 @@ export const ZabbixDashboard: React.FC<Props> = ({ dsUrl, dsUsername, dsPassword
<tbody>
{sorted.map((h, i) => (
<tr key={h.hostid || i} className="border-b border-[var(--dg-border)] last:border-0 hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-3 py-2.5 whitespace-nowrap"><Badge className={`text-[10px] ${h.available === '1' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-red-500/20 text-red-400'}`}><Wifi className="w-3 h-3 mr-1" />{h.available === '1' ? 'UP' : 'DOWN'}</Badge> <span className="font-medium text-[var(--dg-text-primary)] ml-1">{h.name || h.hostname}</span></td>
<td className="px-3 py-2.5 whitespace-nowrap"><Badge className={`text-[10px] ${h.available === '1' ? 'bg-emerald-500/20 dg-text-green' : 'bg-red-500/20 dg-text-red'}`}><Wifi className="w-3 h-3 mr-1" />{h.available === '1' ? 'UP' : 'DOWN'}</Badge> <span className="font-medium text-[var(--dg-text-primary)] ml-1">{h.name || h.hostname}</span></td>
<td className="px-3 py-2.5">{bar(Number(h.cpu_usage || 0), `${Number(h.cpu_usage || 0).toFixed(1)}%`)}</td>
<td className="px-3 py-2.5">{bar(Number(h.memory_usage || 0), `${Number(h.memory_usage || 0).toFixed(1)}%`)}</td>
<td className="px-3 py-2.5">{bar(Number(h.disk_usage || 0), `${Number(h.disk_usage || 0).toFixed(1)}%`)}</td>
<td className="px-3 py-2.5 text-[var(--dg-text-secondary)]">{h.load5 > 0 ? h.load5.toFixed(1) : '—'}</td>
<td className="px-3 py-2.5 font-mono text-[var(--dg-text-secondary)]">{fmtBps(h.net_in_bps)}</td>
<td className="px-3 py-2.5 font-mono text-[var(--dg-text-secondary)]">{fmtBps(h.net_out_bps)}</td>
<td className="px-3 py-2.5 text-center"><span className={`font-mono font-bold ${h.problem_count > 0 ? (h.problem_count > 2 ? 'text-red-400' : 'text-amber-400') : 'text-emerald-400'}`}>{h.problem_count > 0 ? <><AlertTriangle className="w-3 h-3 inline mr-1" />{h.problem_count}</> : '0'}</span></td>
<td className="px-3 py-2.5 text-center"><span className={`font-mono font-bold ${h.problem_count > 0 ? (h.problem_count > 2 ? 'dg-text-red' : 'dg-text-yellow') : 'dg-text-green'}`}>{h.problem_count > 0 ? <><AlertTriangle className="w-3 h-3 inline mr-1" />{h.problem_count}</> : '0'}</span></td>
<td className="px-3 py-2.5 text-[var(--dg-text-secondary)]">{h.uptime_days > 0 ? `${h.uptime_days}d` : '—'}</td>
</tr>
))}
@@ -1,47 +1,30 @@
// 数据源管理页面 — Prometheus + Zabbix 双 Tab // Logan.Li
import { useState } from 'react';
import { Database } from 'lucide-react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { PageShell } from '@/components/layout/PageShell';
import PrometheusDatasourceManager from '@/components/monitor/datasource/PrometheusDatasourceManager';
import ZabbixDatasourceManager from '@/components/monitor/datasource/ZabbixDatasourceManager';
export default function DatasourcePage() {
const [activeTab, setActiveTab] = useState('prometheus');
const pageTabs = [
{ id: 'prometheus', label: 'Prometheus' },
{ id: 'zabbix', label: 'Zabbix' },
];
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Database className="h-5 w-5 text-blue-400" />
<h1 className="text-xl font-bold text-[var(--dg-text-primary)]">数据源管理</h1>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<TabsTrigger value="prometheus" className="data-[state=active]:bg-orange-500/10 data-[state=active]:text-orange-400">
<img
src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/prometheus.png"
className="w-4 h-4 rounded mr-1.5"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
Prometheus
</TabsTrigger>
<TabsTrigger value="zabbix" className="data-[state=active]:bg-orange-500/10 data-[state=active]:text-orange-400">
<img
src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/zabbix.png"
className="w-4 h-4 rounded mr-1.5"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
Zabbix
</TabsTrigger>
</TabsList>
<TabsContent value="prometheus" className="mt-4">
<PrometheusDatasourceManager />
</TabsContent>
<TabsContent value="zabbix" className="mt-4">
<ZabbixDatasourceManager />
</TabsContent>
</Tabs>
</div>
<PageShell
variant="list"
title="数据源管理"
subtitle="管理 Prometheus 与 Zabbix 监控数据源"
icon={<Database className="h-5 w-5" />}
tabs={pageTabs}
activeTab={activeTab}
onTabChange={setActiveTab}
>
{activeTab === 'prometheus' && <PrometheusDatasourceManager />}
{activeTab === 'zabbix' && <ZabbixDatasourceManager />}
</PageShell>
);
}
}
@@ -13,7 +13,7 @@ import {
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { usePromDatasources, usePromDatasourceMutations } from '@/hooks/api';
@@ -21,6 +21,9 @@ import { usePromDatasources, usePromDatasourceMutations } from '@/hooks/api';
import type { PromDS } from './prometheus-ds/types';
import { handleTestQuick } from './prometheus-ds/helpers';
import PromDsFormDialog from './prometheus-ds/PromDsFormDialog';
import { PageLoading } from '@/components/layout/PageLoading';
import { PageEmpty } from '@/components/layout/PageEmpty';
import { ContentPanel } from '@/components/layout/ContentPanel';
export default function PrometheusDatasourceManager() {
const _confirm = useConfirm();
@@ -69,145 +72,132 @@ export default function PrometheusDatasourceManager() {
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-96">
<RefreshCw className="animate-spin w-8 h-8 text-[var(--dg-text-secondary)]" />
</div>
);
return <PageLoading />;
}
return (
<div className="p-6 space-y-6 max-w-7xl mx-auto">
{/* 页头 */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">数据源管理</h1>
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">
Prometheus 数据源配置,用于指标监控与监控大屏
</p>
</div>
<Button onClick={openCreate} className="gap-2">
<Plus className="w-4 h-4" />
添加数据源
</Button>
</div>
const toolbar = (
<div className="flex items-center justify-between w-full">
<span className="text-sm dg-text-secondary flex items-center gap-2">
<Database className="w-4 h-4 dg-text-blue" />
Prometheus 数据源
<Badge variant="secondary" className="dg-badge-muted">{sources.length}</Badge>
</span>
<Button onClick={openCreate} size="sm" className="dg-btn-primary">
<Plus className="w-4 h-4 mr-1" />
添加数据源
</Button>
</div>
);
{/* 数据源列表 */}
<Card className="border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]">
<CardHeader className="pb-2">
<CardTitle className="text-base flex items-center gap-2">
<Database className="w-4 h-4 text-blue-400" />
已配置的数据源
<Badge variant="secondary" className="ml-2">{sources.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
{sources.length === 0 ? (
<div className="py-12 text-center text-[var(--dg-text-secondary)]">
<Database className="w-12 h-12 mx-auto mb-3 opacity-30" />
<p className="text-sm">暂无数据源</p>
<p className="text-xs mt-1">点击「添加数据源」开始配置</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border-b border-[var(--dg-border)]">
<tr className="text-xs text-[var(--dg-text-secondary)]">
<th className="text-left py-3 px-4 font-medium">名称</th>
<th className="text-left py-3 px-4 font-medium">URL</th>
<th className="text-left py-3 px-4 font-medium w-20">默认</th>
<th className="text-left py-3 px-4 font-medium w-16">状态</th>
<th className="text-right py-3 px-4 font-medium w-36">操作</th>
return (
<div className="space-y-4">
<ContentPanel toolbar={toolbar} noPadding>
{sources.length === 0 ? (
<PageEmpty
icon={<Database className="h-12 w-12" />}
title="暂无 Prometheus 数据源"
description="添加数据源后可用于指标监控与监控大屏"
action={(
<Button onClick={openCreate} size="sm" className="dg-btn-primary">
<Plus className="w-4 h-4 mr-1" />添加数据源
</Button>
)}
/>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="border-b border-[var(--border-default)]">
<tr className="text-xs dg-text-secondary">
<th className="text-left py-3 px-4 font-medium">名称</th>
<th className="text-left py-3 px-4 font-medium">URL</th>
<th className="text-left py-3 px-4 font-medium w-20">默认</th>
<th className="text-left py-3 px-4 font-medium w-16">状态</th>
<th className="text-right py-3 px-4 font-medium w-36">操作</th>
</tr>
</thead>
<tbody>
{sources.map(ds => (
<tr
key={ds.id}
className="dg-table-row border-b border-[var(--border-subtle)]"
>
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<img
src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/prometheus.png"
alt="Prometheus"
className="w-5 h-5 rounded"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
<div>
<span className="text-sm font-medium text-[var(--text-primary)]">{ds.name}</span>
{ds.basic_auth && <Key className="w-3 h-3 inline ml-1.5 dg-text-yellow" />}
</div>
</div>
</td>
<td className="py-3 px-4">
<code className="text-xs dg-text-blue bg-[var(--surface-inset)] px-2 py-0.5 rounded max-w-[320px] block truncate"
title={ds.url}>
{ds.url}
</code>
</td>
<td className="py-3 px-4">
{ds.is_default ? (
<span className="dg-badge-yellow"><Star className="w-3 h-3 mr-1 inline" />默认</span>
) : (
<span className="text-xs dg-text-muted">--</span>
)}
</td>
<td className="py-3 px-4">
{ds.status === 1 ? (
<span className="dg-status-running">启用</span>
) : (
<span className="dg-badge-muted">禁用</span>
)}
</td>
<td className="py-3 px-4">
<div className="flex items-center justify-end gap-1">
<Button
variant="ghost" size="sm"
className="h-8 px-2"
onClick={() => handleTestQuick(ds)}
title="测试连接"
>
<Plug className="w-3.5 h-3.5" />
</Button>
{!ds.is_default && (
<Button
variant="ghost" size="sm"
className="h-8 px-2 dg-text-yellow"
onClick={() => handleSetDefault(ds)}
title="设为默认"
>
<Star className="w-3.5 h-3.5" />
</Button>
)}
<Button
variant="ghost" size="sm"
className="h-8 px-2"
onClick={() => openEdit(ds)}
>
<Edit className="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost" size="sm"
className="h-8 px-2 dg-text-red hover:dg-text-red"
onClick={() => handleDelete(ds)}
>
<Trash2 className="w-3.5 h-3.5" />
</Button>
</div>
</td>
</tr>
</thead>
<tbody>
{sources.map(ds => (
<tr
key={ds.id}
className="border-b border-[var(--dg-border)]/50 hover:bg-[var(--dg-bg-tertiary)]/50 transition-colors"
>
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<img
src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/prometheus.png"
alt="Prometheus"
className="w-5 h-5 rounded"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
<div>
<span className="text-sm font-medium text-[var(--dg-text-primary)]">{ds.name}</span>
{ds.basic_auth && <Key className="w-3 h-3 inline ml-1.5 text-amber-400" />}
</div>
</div>
</td>
<td className="py-3 px-4">
<code className="text-xs text-blue-300 bg-slate-800/50 px-2 py-0.5 rounded max-w-[320px] block truncate"
title={ds.url}>
{ds.url}
</code>
</td>
<td className="py-3 px-4">
{ds.is_default ? (
<Badge variant="default" className="bg-amber-500/15 text-amber-400 border-amber-500/30 text-xs">
<Star className="w-3 h-3 mr-1" /> 默认
</Badge>
) : (
<span className="text-xs text-[var(--dg-text-secondary)]">--</span>
)}
</td>
<td className="py-3 px-4">
{ds.status === 1 ? (
<Badge variant="default" className="bg-emerald-500/15 text-emerald-400 border-emerald-500/30 text-xs">
启用
</Badge>
) : (
<Badge variant="secondary" className="text-xs">禁用</Badge>
)}
</td>
<td className="py-3 px-4">
<div className="flex items-center justify-end gap-1">
<Button
variant="ghost" size="sm"
className="h-8 px-2"
onClick={() => handleTestQuick(ds)}
title="测试连接"
>
<Plug className="w-3.5 h-3.5" />
</Button>
{!ds.is_default && (
<Button
variant="ghost" size="sm"
className="h-8 px-2 text-amber-400 hover:text-amber-300"
onClick={() => handleSetDefault(ds)}
title="设为默认"
>
<Star className="w-3.5 h-3.5" />
</Button>
)}
<Button
variant="ghost" size="sm"
className="h-8 px-2"
onClick={() => openEdit(ds)}
>
<Edit className="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost" size="sm"
className="h-8 px-2 text-red-400 hover:text-red-300"
onClick={() => handleDelete(ds)}
>
<Trash2 className="w-3.5 h-3.5" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
))}
</tbody>
</table>
</div>
)}
</ContentPanel>
<PromDsFormDialog
open={dialogOpen}
@@ -224,12 +224,12 @@ export default function ZabbixDatasourceManager() {
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<div className={`w-2 h-2 rounded-full ${ds.status === 1 ? 'bg-emerald-400' : 'bg-slate-500'}`} />
<div className={`w-2 h-2 rounded-full ${ds.status === 1 ? 'bg-emerald-400' : 'bg-[var(--dg-bg-active)]'}`} />
<span className="font-medium text-[var(--dg-text-primary)]">{ds.name}</span>
{ds.is_default && <Badge className="bg-amber-500/20 text-amber-400 text-[10px]"><Star className="w-3 h-3 mr-0.5" />默认</Badge>}
{ds.is_default && <Badge className="bg-amber-500/20 dg-text-yellow text-[10px]"><Star className="w-3 h-3 mr-0.5" />默认</Badge>}
{ds.status === 1
? <Badge className="bg-emerald-500/20 text-emerald-400 text-[10px]">启用</Badge>
: <Badge className="bg-slate-500/20 text-slate-400 text-[10px]">禁用</Badge>}
? <Badge className="bg-emerald-500/20 dg-text-green text-[10px]">启用</Badge>
: <Badge className="bg-[var(--dg-bg-active)]/20 dg-text-secondary text-[10px]">禁用</Badge>}
</div>
<div className="text-xs text-[var(--dg-text-muted)] space-x-3">
<code className="text-orange-300">{ds.url}</code>
@@ -239,14 +239,14 @@ export default function ZabbixDatasourceManager() {
</div>
<div className="flex items-center gap-1">
{!ds.is_default && (
<Button variant="ghost" size="sm" className="h-7 text-xs text-amber-400" onClick={() => handleSetDefault(ds)}>
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-yellow" onClick={() => handleSetDefault(ds)}>
<Star className="w-3 h-3 mr-0.5" />默认
</Button>
)}
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => openEdit(ds)}>
<Edit className="w-3 h-3 mr-0.5" />编辑
</Button>
<Button variant="ghost" size="sm" className="h-7 text-xs text-red-400" onClick={() => handleDelete(ds)}>
<Button variant="ghost" size="sm" className="h-7 text-xs dg-text-red" onClick={() => handleDelete(ds)}>
<Trash2 className="w-3 h-3" />
</Button>
</div>
@@ -262,7 +262,7 @@ export default function ZabbixDatasourceManager() {
<DialogContent className="max-w-xl max-h-[85vh] overflow-y-auto bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<Database className="h-5 w-5 text-orange-400" />
<Database className="h-5 w-5 dg-text-orange" />
{editingId ? '编辑 Zabbix 数据源' : '新建 Zabbix 数据源'}
</DialogTitle>
<DialogDescription>
@@ -390,7 +390,7 @@ export default function ZabbixDatasourceManager() {
{/* 测试结果 */}
{testResult && (
<div className={`flex items-center gap-2 p-2.5 rounded text-sm ${
testResult.success ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'
testResult.success ? 'bg-emerald-500/10 dg-text-green' : 'bg-red-500/10 dg-text-red'
}`}>
{testResult.success ? <CheckCircle2 className="h-4 w-4" /> : <XCircle className="h-4 w-4" />}
{testResult.message}
@@ -148,7 +148,7 @@ export default function PromDsFormDialog({
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-lg">
<Database className="w-5 h-5 text-orange-400" />
<Database className="w-5 h-5 dg-text-orange" />
{editingId ? '编辑数据源' : '添加数据源'}
</DialogTitle>
<DialogDescription>
@@ -160,7 +160,7 @@ export default function PromDsFormDialog({
{/* === Name & Default === */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="ds-name" className="text-sm">Name <span className="text-red-400">*</span></Label>
<Label htmlFor="ds-name" className="text-sm">Name <span className="dg-text-red">*</span></Label>
<Input
id="ds-name"
placeholder="Prometheus 数据源名称"
@@ -187,7 +187,7 @@ export default function PromDsFormDialog({
<Globe className="w-4 h-4" /> HTTP
</h4>
<div className="space-y-2">
<Label htmlFor="ds-url" className="text-sm">URL <span className="text-red-400">*</span></Label>
<Label htmlFor="ds-url" className="text-sm">URL <span className="dg-text-red">*</span></Label>
<div className="flex gap-2">
<Select value={form.access} onValueChange={v => updateForm('access', v)}>
<SelectTrigger className="w-32 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm">
@@ -340,7 +340,7 @@ export default function PromDsFormDialog({
className="flex-1 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm h-8"
/>
<Button
variant="ghost" size="sm" className="h-8 w-8 p-0 text-red-400 hover:text-red-300"
variant="ghost" size="sm" className="h-8 w-8 p-0 dg-text-red hover:dg-text-red"
onClick={() => removeHeader(idx)}
>
<X className="w-3.5 h-3.5" />
@@ -482,8 +482,8 @@ export default function PromDsFormDialog({
{testResult && (
<div className={`rounded-lg p-3 flex items-center gap-2 text-sm ${
testResult.success
? 'bg-emerald-500/10 border border-emerald-500/30 text-emerald-400'
: 'bg-red-500/10 border border-red-500/30 text-red-400'
? 'bg-emerald-500/10 border border-emerald-500/30 dg-text-green'
: 'bg-red-500/10 border border-red-500/30 dg-text-red'
}`}>
{testResult.success
? <CheckCircle className="w-4 h-4 shrink-0" />
@@ -33,11 +33,11 @@ type DedupMode = 'none' | 'exact' | 'numbers' | 'signature';
function levelBadge(level?: string): string {
const l = (level || '').toLowerCase();
if (l === 'error' || l === 'fatal' || l === 'err') return 'bg-red-500/15 text-red-400 border-red-500/30';
if (l === 'warn' || l === 'warning') return 'bg-amber-500/15 text-amber-400 border-amber-500/30';
if (l === 'info') return 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30';
if (l === 'debug') return 'bg-slate-500/15 text-slate-400 border-slate-500/30';
return 'bg-slate-500/15 text-slate-400 border-slate-500/30';
if (l === 'error' || l === 'fatal' || l === 'err') return 'bg-red-500/15 dg-text-red border-red-500/30';
if (l === 'warn' || l === 'warning') return 'bg-amber-500/15 dg-text-yellow border-amber-500/30';
if (l === 'info') return 'bg-emerald-500/15 dg-text-green border-emerald-500/30';
if (l === 'debug') return 'bg-[var(--dg-bg-active)]/15 dg-text-secondary border-[var(--dg-border)]';
return 'bg-[var(--dg-bg-active)]/15 dg-text-secondary border-[var(--dg-border)]';
}
function levelColorText(level?: string): string {
@@ -158,7 +158,7 @@ function RawLogView({
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity mt-0.5"
style={{ color: 'var(--dg-text-muted)' }}
>
{copied ? <Check className="h-3 w-3 text-green-400" /> : <Copy className="h-3 w-3" />}
{copied ? <Check className="h-3 w-3 dg-text-green" /> : <Copy className="h-3 w-3" />}
</button>
</div>
@@ -49,9 +49,9 @@ export function LogAlertsPanel() {
};
const severityColor: Record<string, string> = {
critical: 'bg-red-500/15 text-red-400',
warning: 'bg-amber-500/15 text-amber-400',
info: 'bg-blue-500/15 text-blue-400',
critical: 'bg-red-500/15 dg-text-red',
warning: 'bg-amber-500/15 dg-text-yellow',
info: 'bg-blue-500/15 dg-text-blue',
};
return (
@@ -96,7 +96,7 @@ export function LogAlertsPanel() {
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => { setEditing(rule); setDialogOpen(true); }}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-red-400" onClick={() => handleDelete(rule)}>
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-red" onClick={() => handleDelete(rule)}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
@@ -2,6 +2,7 @@
import { useState } from 'react';
import { Navigate, useParams } from 'react-router-dom';
import { ScrollText } from 'lucide-react';
import { PageShell } from '@/components/layout/PageShell';
import { LogQueryProvider } from './LogQueryContext';
import { UnifiedQueryPanel } from './UnifiedQueryPanel';
import { LogDatasourcePage } from './LogDatasourcePage';
@@ -28,23 +29,19 @@ export default function LogCenter() {
return (
<LogQueryProvider>
<div className="space-y-4 pb-20">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-xl bg-[var(--dg-bg-tertiary)] flex items-center justify-center border border-[var(--dg-border)]">
<ScrollText className="h-5 w-5 text-[var(--dg-text-secondary)]" />
</div>
<div>
<h1 className="text-xl font-bold text-[var(--dg-text-primary)]">{meta.title}</h1>
<p className="text-sm text-[var(--dg-text-muted)]">{meta.description}</p>
</div>
</div>
<PageShell
variant="workspace"
title={meta.title}
subtitle={meta.description}
icon={<ScrollText className="h-5 w-5" />}
contentClassName="flex-1 min-h-0 pb-20"
>
{(tab === 'loki' || tab === 'es') && meta.engine && (
<UnifiedQueryPanel engine={meta.engine} onRuleCreated={() => setAlertReloadKey(k => k + 1)} />
)}
{tab === 'datasources' && <LogDatasourcePage />}
{tab === 'alerts' && <LogAlertsPanel key={alertReloadKey} />}
</div>
</PageShell>
</LogQueryProvider>
);
}
}
@@ -4,7 +4,7 @@ import { Info } from 'lucide-react';
export function LogCenterArchBanner() {
return (
<div className="flex gap-3 rounded-lg border border-cyan-500/20 bg-cyan-500/5 px-4 py-3 text-xs text-[var(--dg-text-secondary)] leading-relaxed">
<Info className="h-4 w-4 shrink-0 text-cyan-400 mt-0.5" />
<Info className="h-4 w-4 shrink-0 dg-text-cyan mt-0.5" />
<div>
<p className="text-[var(--dg-text-primary)] font-medium mb-1">无状态查询网关</p>
<p>
@@ -83,7 +83,7 @@ export function LogClusterPanel({ clusters }: Props) {
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
style={{ color: 'var(--dg-text-muted)' }}
>
{copied === i ? <Check className="h-3 w-3 text-green-400" /> : <Copy className="h-3 w-3" />}
{copied === i ? <Check className="h-3 w-3 dg-text-green" /> : <Copy className="h-3 w-3" />}
</button>
</div>
@@ -87,8 +87,8 @@ export function LogDatasourcePage() {
const engineLabel = (t: string) => t === 'loki' ? 'Loki' : 'Elasticsearch';
const engineBadge = (t: string) => t === 'loki'
? 'bg-orange-500/15 text-orange-400 border-orange-500/25'
: 'bg-cyan-500/15 text-cyan-400 border-cyan-500/25';
? 'bg-orange-500/15 dg-text-orange border-orange-500/25'
: 'bg-cyan-500/15 dg-text-cyan border-cyan-500/25';
// DataTable 列定义
const columns: DataTableColumn<LogDataSource>[] = [
@@ -103,7 +103,7 @@ export function LogDatasourcePage() {
},
{
key: 'is_default', header: '默认', width: 60, align: 'center', sortable: true,
render: (ds) => ds.is_default ? <Star className="h-4 w-4 text-amber-400 mx-auto" /> : <span className="text-xs text-[var(--dg-text-muted)]">—</span>,
render: (ds) => ds.is_default ? <Star className="h-4 w-4 dg-text-yellow mx-auto" /> : <span className="text-xs text-[var(--dg-text-muted)]">—</span>,
},
{
key: 'actions', header: '操作', align: 'right', hideable: false,
@@ -114,7 +114,7 @@ export function LogDatasourcePage() {
<Plug className="h-3.5 w-3.5" />
</Button>
{!ds.is_default && (
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-amber-400"
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:dg-text-yellow"
title="设为默认" onClick={() => handleSetDefault(ds)}>
<Star className="h-3.5 w-3.5" />
</Button>
@@ -123,7 +123,7 @@ export function LogDatasourcePage() {
title="编辑" onClick={() => handleEdit(ds)}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-red-400"
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:dg-text-red"
title="删除" onClick={() => handleDelete(ds)}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
@@ -31,12 +31,14 @@ import {
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue
} from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator';
import { ScrollArea } from '@/components/ui/scroll-area';
import { toast } from 'sonner';
import { traceApi } from '@/api';
import { PageShell } from '@/components/layout/PageShell';
import { PageLoading } from '@/components/layout/PageLoading';
import { PageEmpty } from '@/components/layout/PageEmpty';
import type { JsonData } from '@/types/common';
interface Trace {
@@ -162,7 +164,7 @@ const TraceManager: React.FC = () => {
case 'success': return 'bg-green-500';
case 'error': return 'bg-red-500';
case 'pending': return 'bg-yellow-500';
default: return 'bg-gray-500';
default: return 'bg-[var(--dg-bg-active)]';
}
};
@@ -257,28 +259,14 @@ const TraceManager: React.FC = () => {
);
};
return (
<div className="space-y-6">
{/* 统计卡片 */}
<StatStrip
items={[
{ label: '总请求', value: services.reduce((sum, s) => sum + s.requestCount, 0).toLocaleString(), color: 'blue' },
{ label: '错误率', value: `${(services.reduce((sum, s) => sum + s.errorRate, 0) / (services.length || 1)).toFixed(2)}%`, color: 'red' },
{ label: '平均延迟', value: formatDuration(services.reduce((sum, s) => sum + s.avgDuration, 0) / (services.length || 1)), color: 'amber' },
{ label: '服务数', value: services.length, color: 'purple' },
]}
/>
const pageTabs = [
{ id: 'traces', label: '链路追踪' },
{ id: 'services', label: '服务拓扑' },
{ id: 'metrics', label: '性能指标' },
];
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3 lg:w-[400px]">
<TabsTrigger value="traces">链路追踪</TabsTrigger>
<TabsTrigger value="services">服务拓扑</TabsTrigger>
<TabsTrigger value="metrics">性能指标</TabsTrigger>
</TabsList>
<TabsContent value="traces" className="space-y-4">
{/* 查询工具栏 */}
<div className="flex flex-wrap items-center justify-between gap-4">
const tracesToolbar = activeTab === 'traces' ? (
<div className="flex flex-wrap items-center justify-between gap-4 w-full">
<div className="flex items-center gap-2 flex-1 min-w-[400px]">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
@@ -324,16 +312,44 @@ const TraceManager: React.FC = () => {
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={fetchTraces}>
<RefreshCw className="w-4 h-4 mr-2" />
刷新
</Button>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" className="dg-btn-secondary" onClick={fetchTraces}>
<RefreshCw className="w-4 h-4 mr-2" />
刷新
</Button>
</div>
</div>
) : undefined;
{/* Trace列表 */}
<Card>
return (
<PageShell
variant="list"
title="链路追踪"
subtitle="分布式链路查询与服务性能分析"
icon={<GitBranch className="h-5 w-5" />}
tabs={pageTabs}
activeTab={activeTab}
onTabChange={setActiveTab}
toolbar={tracesToolbar}
metrics={(
<StatStrip
items={[
{ label: '总请求', value: services.reduce((sum, s) => sum + s.requestCount, 0).toLocaleString(), color: 'blue' },
{ label: '错误率', value: `${(services.reduce((sum, s) => sum + s.errorRate, 0) / (services.length || 1)).toFixed(2)}%`, color: 'red' },
{ label: '平均延迟', value: formatDuration(services.reduce((sum, s) => sum + s.avgDuration, 0) / (services.length || 1)), color: 'amber' },
{ label: '服务数', value: services.length, color: 'purple' },
]}
/>
)}
actions={activeTab === 'traces' ? undefined : (
<Button variant="outline" className="dg-btn-secondary" onClick={fetchData}>
<RefreshCw className="w-4 h-4 mr-2" />
刷新
</Button>
)}
>
{activeTab === 'traces' && (
<Card>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full">
@@ -351,17 +367,17 @@ const TraceManager: React.FC = () => {
<tbody className="divide-y">
{loading ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-muted-foreground">加载中...</td>
<td colSpan={7} className="px-4 py-2"><PageLoading label="加载链路数据..." /></td>
</tr>
) : filteredTraces.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-muted-foreground">暂无链路数据</td>
<td colSpan={7} className="px-4 py-2"><PageEmpty title="暂无链路数据" className="py-8" /></td>
</tr>
) : (
filteredTraces.map((trace) => (
<tr key={trace.id} className="hover:bg-muted/50">
<td className="px-4 py-3">
<span className="font-mono text-sm text-blue-500">{trace.traceId.slice(0, 16)}...</span>
<span className="font-mono text-sm dg-text-blue">{trace.traceId.slice(0, 16)}...</span>
</td>
<td className="px-4 py-3">
<div className="font-medium">{trace.operationName}</div>
@@ -394,9 +410,9 @@ const TraceManager: React.FC = () => {
</div>
</CardContent>
</Card>
</TabsContent>
)}
<TabsContent value="services" className="space-y-4">
{activeTab === 'services' && (
<Card>
<CardHeader>
<CardTitle>服务性能概览</CardTitle>
@@ -420,7 +436,7 @@ const TraceManager: React.FC = () => {
<td className="px-4 py-3 font-medium">{service.serviceName}</td>
<td className="px-4 py-3">{service.requestCount.toLocaleString()}</td>
<td className="px-4 py-3">
<span className={service.errorRate > 1 ? 'text-red-500' : 'text-green-500'}>
<span className={service.errorRate > 1 ? 'text-red-500' : 'dg-text-green'}>
{service.errorRate.toFixed(2)}%
</span>
</td>
@@ -434,9 +450,9 @@ const TraceManager: React.FC = () => {
</div>
</CardContent>
</Card>
</TabsContent>
)}
<TabsContent value="metrics" className="space-y-4">
{activeTab === 'metrics' && (
<div className="grid grid-cols-2 gap-4">
<Card>
<CardHeader>
@@ -457,8 +473,7 @@ const TraceManager: React.FC = () => {
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
)}
{/* Trace详情对话框 */}
<Dialog open={traceDetailOpen} onOpenChange={setTraceDetailOpen}>
@@ -580,7 +595,7 @@ const TraceManager: React.FC = () => {
)}
</DialogContent>
</Dialog>
</div>
</PageShell>
);
};