import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Rocket, AlertTriangle, ArrowRight, Cloud, Shield, Clock3, Globe, Database, Server, Monitor, Activity, } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { AlertEventRecord, CertExpiryRecord } from '@/types'; /** 仪表盘卡片数据类型 — 与 Dashboard API 响应对齐 */ export interface DashboardRelease { id: number | string; status: string; app_name: string; version: string; environment?: string; commit_id?: string; [key: string]: unknown; } export interface DashboardCluster { id: number | string; code?: string; name: string; status: string; cpu_usage?: number; memory_usage?: number; node_count?: number; pod_count?: number; ingress_count?: number; deployment_count?: number; service_count?: number; [key: string]: unknown; } export interface DashboardAutomationStats { cron_job_count?: number; script_count?: number; today_executions?: number; success_rate?: number; } export interface DashboardOpsStats { domain_count?: number; site_task_count?: number; middleware_count?: number; snmp_device_count?: number; monitor_target_count?: number; } export interface DashboardPipelineStats { today_count?: number; success?: number; failed?: number; running?: number; } /** * DashboardRecent - 优化后的仪表盘底部卡片组件 * * 优化点: * 1. 使用 dg-card 工具类替代硬编码的 slate 颜色 * 2. 统一的状态指示器样式 * 3. 更好的 hover 效果和过渡动画 * 4. ScrollArea 使用 scrollbar-thin 类 * 5. 空状态显示更友好 */ // Status dot component function StatusDot({ severity }: { severity: string }) { return (
); } // Severity badge function SeverityBadge({ severity }: { severity: string }) { const { t } = useTranslation(); return ( {severity === 'critical' ? t('common.error') : severity === 'warning' ? t('common.warning') : t('common.info')} ); } export function AlertMessagesCard({ alerts }: { alerts: AlertEventRecord[] }) { const { t } = useTranslation(); const navigate = useNavigate(); const recentAlerts = alerts.filter((a) => a.status === 'firing').slice(0, 8); return (
{t('dashboard.alerts.title')} {t('dashboard.alerts.description')}
{recentAlerts.length === 0 ? (

{t('dashboard.alerts.empty')}

{t('dashboard.alerts.emptyHint')}

) : (
{recentAlerts.map((alert) => (
navigate('/alert-center')} >
{alert.rule_name}

{alert.target}

{alert.message}

{alert.created_at ? new Date(alert.created_at).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) : ''}
))}
)}
); } export function CertificateExpiryCard({ expCerts }: { expCerts: CertExpiryRecord[] }) { const { t } = useTranslation(); const expiringDomains = (expCerts ?? []) .map((d) => ({ ...d, daysUntilExpiry: d.days_until_expiry ?? Math.ceil((new Date(d.expire_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24)), })) .sort((a, b) => a.daysUntilExpiry - b.daysUntilExpiry); const criticalCount = expiringDomains.filter((d) => d.daysUntilExpiry < 30).length; const warningCount = expiringDomains.filter((d) => d.daysUntilExpiry >= 30 && d.daysUntilExpiry < 60).length; return (
{t('dashboard.certificates.title')} {t('dashboard.certificates.description')}
{criticalCount > 0 && ( {criticalCount} {t('dashboard.certificates.expiringSoon')} )} {warningCount > 0 && ( {warningCount} {t('dashboard.certificates.expiringIn30Days')} )}
{expiringDomains.length === 0 ? (

{t('dashboard.certificates.empty')}

) : (
{expiringDomains.map((domain) => (

{domain.name}

{t('dashboard.certificates.expireDate')}: {domain.expire_date?.substring(0, 10)}

{domain.daysUntilExpiry < 0 ? t('dashboard.certificates.expired') : `${domain.daysUntilExpiry} ${t('dashboard.certificates.days')}`}
))}
)}
); } export function ReleaseHistoryCard({ releases }: { releases: DashboardRelease[] }) { const { t } = useTranslation(); const navigate = useNavigate(); const releaseList = releases?.slice(0, 5) ?? []; return (
{t('dashboard.releases.title')} {t('dashboard.releases.description')}
{releaseList.length === 0 ? (

{t('dashboard.releases.empty')}

) : (
{releaseList.map((release) => (
{release.app_name} {release.version}
{release.environment} · {release.commit_id}
{release.status === 'success' ? t('dashboard.releases.success') : release.status === 'failed' ? t('dashboard.releases.failed') : release.status === 'deploying' ? t('dashboard.releases.deploying') : release.status === 'rolled_back' ? t('dashboard.releases.rolledBack') : t('dashboard.releases.building')}
))}
)}
); } /** 运维概览 — 资产 / 监控 / 任务快捷入口 // Logan.Li */ export function OpsOverviewCard({ automation, ops, serverCount, hostMonitorCount, firingAlerts, }: { automation?: DashboardAutomationStats; ops?: DashboardOpsStats; serverCount?: number; hostMonitorCount?: number; firingAlerts?: number; }) { const { t } = useTranslation(); const navigate = useNavigate(); const tiles = [ { key: 'servers', label: t('dashboard.ops.servers'), value: serverCount ?? 0, icon: Server, path: '/servers', color: 'text-[var(--dg-blue)]' }, { key: 'domains', label: t('dashboard.ops.domains'), value: ops?.domain_count ?? 0, icon: Globe, path: '/domains', color: 'text-[var(--dg-cyan)]' }, { key: 'host', label: t('dashboard.ops.hostMonitor'), value: hostMonitorCount ?? serverCount ?? 0, icon: Monitor, path: '/host-monitor', color: 'text-[var(--dg-green)]' }, { key: 'mw', label: t('dashboard.ops.middleware'), value: ops?.middleware_count ?? 0, icon: Database, path: '/monitor-center?tab=middleware', color: 'text-[var(--dg-red)]' }, { key: 'site', label: t('dashboard.ops.siteProbes'), value: ops?.site_task_count ?? 0, icon: Activity, path: '/monitor-center?tab=site', color: 'text-[var(--dg-yellow)]' }, { key: 'cron', label: t('dashboard.ops.cronJobs'), value: automation?.cron_job_count ?? 0, icon: Clock3, path: '/cronjobs', color: 'text-[var(--dg-purple)]' }, ]; return (
{t('dashboard.ops.title')} {t('dashboard.ops.description')}
{tiles.map((tile) => ( ))}
{t('dashboard.ops.todayExecutions')}: {automation?.today_executions ?? 0}
); } export function KubernetesClusterCard({ clusters }: { clusters: DashboardCluster[] }) { const { t } = useTranslation(); const navigate = useNavigate(); return (
{t('dashboard.clusters.title')} {t('dashboard.clusters.description')}
{clusters.length === 0 ? (

{t('dashboard.clusters.empty')}

) : (
{clusters.map((cluster) => { const cpuUsage = Number(cluster.cpu_usage ?? 0); const memoryUsage = Number(cluster.memory_usage ?? 0); const hasUsage = cpuUsage > 0 || memoryUsage > 0; const metrics = hasUsage ? [ { label: t('dashboard.clusters.nodes'), value: cluster.node_count ?? 0 }, { label: t('dashboard.clusters.pods'), value: cluster.pod_count ?? 0 }, { label: t('dashboard.clusters.cpu'), value: `${cpuUsage.toFixed(1)}%` }, { label: t('dashboard.clusters.memory'), value: `${memoryUsage.toFixed(1)}%` }, ] : [ { label: t('dashboard.clusters.nodes'), value: cluster.node_count ?? 0 }, { label: t('dashboard.clusters.pods'), value: cluster.pod_count ?? 0 }, { label: t('dashboard.clusters.ingress'), value: cluster.ingress_count ?? 0 }, { label: t('dashboard.clusters.deployments'), value: cluster.deployment_count ?? 0 }, ]; return (
navigate(`/kubernetes/${cluster.code || cluster.id}`)} >
{cluster.name}
{cluster.status === 'healthy' ? t('dashboard.clusters.healthy') : cluster.status === 'warning' ? t('dashboard.clusters.warning') : t('dashboard.clusters.error')}
{metrics.map((item) => (
{item.value}
{item.label}
))}
); })}
)}
); }