feat: 全站 PageShell 设计系统与专业 UI 统一
- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架 - index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩) - 全站路由页接入 PageShell,云账单三 Tab 为标杆 - 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化 - 登录页/侧栏/顶栏去渐变光球,企业克制风 - 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user