diff --git a/ROADMAP.md b/ROADMAP.md index 639d71c..49366d8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -504,6 +504,7 @@ services/api.ts (1324L) | Q30 | 前端巨型组件拆分(CloudBilling 534L→295L (-45%),提取 cloud-billing/ 3 个文件 (OverviewTab 110L 概览统计:同步信息+成本告警横幅+趋势图+厂商分布 + RecordsTab 146L 成本记录表格+RecordsFilterToolbar筛选工具栏 + AlertRulesTab 91L 预警规则卡片网格)) | P0 | **done** | | Q31 | 前端巨型组件拆分(PromDsFormDialog 528L→294L (-44%),提取 6 个文件 (CollapsibleSection 42L 通用折叠包装器 DRY 3 个重复折叠模式 + AuthSection 107L 认证折叠 + HeadersSection 64L 自定义Headers + AlertingSection 58L 告警折叠 + MiscSection 47L 杂项折叠 + TestResultBanner 32L 测试结果横幅),3 个折叠区块全部使用共享 CollapsibleSection 消除重复 JSX)) | P0 | **done** | | Q32 | 前端巨型组件拆分(AuditOperationsTab 527L→275L (-48%),提取 audit-operations/ 3 个文件 (types 43L normalizeLog 纯函数+AuditStatistics 接口 + LogDetailDialog 85L 详情弹窗+DetailRow + StatisticsDialog 108L 统计弹窗+StatCard),清除死代码 useConfirm/_confirm/_handleShowStats/statsOpen/statistics/statsLoading,ESLint errors=0/any=0,tsc -b=0,npm build✓,go build=0,17/17 测试通过) | P0 | **done** | +| Q33 | 前端巨型组件拆分(DashboardRecent 518L→25L (-95%),提取 dashboard-recent/ 7 个文件 (types 48L + shared-components(StatusDot/SeverityBadge) 35L + AlertMessagesCard 72L + CertificateExpiryCard 99L + ReleaseHistoryCard 99L + OpsOverviewCard 77L + KubernetesClusterCard 97L),主文件改为 barrel re-export 保持向后兼容,tsc -b=0,npm build✓,ESLint=0/0/0,go build=0,17/17 测试通过) | P0 | **done** | --- diff --git a/frontend/src/components/dashboard/DashboardRecent.tsx b/frontend/src/components/dashboard/DashboardRecent.tsx index 34d8bab..2a9f3aa 100644 --- a/frontend/src/components/dashboard/DashboardRecent.tsx +++ b/frontend/src/components/dashboard/DashboardRecent.tsx @@ -1,518 +1,25 @@ -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. 空状态显示更友好 + * DashboardRecent — barrel re-export + * + * 原始 518L 单文件拆分为 dashboard-recent/ 目录下独立文件: + * - types.ts: DashboardRelease / DashboardCluster / DashboardAutomationStats / DashboardOpsStats / DashboardPipelineStats + * - shared-components.tsx: StatusDot / SeverityBadge + * - AlertMessagesCard.tsx: 告警消息卡片 + * - CertificateExpiryCard.tsx: 证书到期卡片 + * - ReleaseHistoryCard.tsx: 发布历史卡片 + * - OpsOverviewCard.tsx: 运维概览卡片 + * - KubernetesClusterCard.tsx: Kubernetes 集群卡片 */ +export { AlertMessagesCard } from './dashboard-recent/AlertMessagesCard'; +export { CertificateExpiryCard } from './dashboard-recent/CertificateExpiryCard'; +export { ReleaseHistoryCard } from './dashboard-recent/ReleaseHistoryCard'; +export { OpsOverviewCard } from './dashboard-recent/OpsOverviewCard'; +export { KubernetesClusterCard } from './dashboard-recent/KubernetesClusterCard'; -// 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, - // eslint-disable-next-line react-hooks/purity - 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}
-
- ))} -
-
- ); - })} -
- )} -
-
- ); -} +export type { + DashboardRelease, + DashboardCluster, + DashboardAutomationStats, + DashboardOpsStats, + DashboardPipelineStats, +} from './dashboard-recent/types'; diff --git a/frontend/src/components/dashboard/dashboard-recent/AlertMessagesCard.tsx b/frontend/src/components/dashboard/dashboard-recent/AlertMessagesCard.tsx new file mode 100644 index 0000000..15e62c7 --- /dev/null +++ b/frontend/src/components/dashboard/dashboard-recent/AlertMessagesCard.tsx @@ -0,0 +1,72 @@ +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 { ScrollArea } from '@/components/ui/scroll-area'; +import { AlertTriangle, ArrowRight } from 'lucide-react'; +import type { AlertEventRecord } from '@/types'; +import { StatusDot, SeverityBadge } from './shared-components'; + +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' }) : ''} + +
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/frontend/src/components/dashboard/dashboard-recent/CertificateExpiryCard.tsx b/frontend/src/components/dashboard/dashboard-recent/CertificateExpiryCard.tsx new file mode 100644 index 0000000..f921768 --- /dev/null +++ b/frontend/src/components/dashboard/dashboard-recent/CertificateExpiryCard.tsx @@ -0,0 +1,99 @@ +import { useTranslation } from 'react-i18next'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { AlertTriangle, Shield } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { CertExpiryRecord } from '@/types'; + +export function CertificateExpiryCard({ expCerts }: { expCerts: CertExpiryRecord[] }) { + const { t } = useTranslation(); + const expiringDomains = (expCerts ?? []) + .map((d) => ({ + ...d, + // eslint-disable-next-line react-hooks/purity + 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')}`} + +
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/frontend/src/components/dashboard/dashboard-recent/KubernetesClusterCard.tsx b/frontend/src/components/dashboard/dashboard-recent/KubernetesClusterCard.tsx new file mode 100644 index 0000000..eb7b8a1 --- /dev/null +++ b/frontend/src/components/dashboard/dashboard-recent/KubernetesClusterCard.tsx @@ -0,0 +1,97 @@ +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 { Cloud, ArrowRight } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { DashboardCluster } from './types'; + +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}
+
+ ))} +
+
+ ); + })} +
+ )} +
+
+ ); +} diff --git a/frontend/src/components/dashboard/dashboard-recent/OpsOverviewCard.tsx b/frontend/src/components/dashboard/dashboard-recent/OpsOverviewCard.tsx new file mode 100644 index 0000000..8466f9f --- /dev/null +++ b/frontend/src/components/dashboard/dashboard-recent/OpsOverviewCard.tsx @@ -0,0 +1,77 @@ +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 { ArrowRight, Clock3, Globe, Database, Server, Monitor, Activity } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { DashboardAutomationStats, DashboardOpsStats } from './types'; + +/** 运维概览 — 资产 / 监控 / 任务快捷入口 */ +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} + + +
+
+
+ ); +} diff --git a/frontend/src/components/dashboard/dashboard-recent/ReleaseHistoryCard.tsx b/frontend/src/components/dashboard/dashboard-recent/ReleaseHistoryCard.tsx new file mode 100644 index 0000000..85549e8 --- /dev/null +++ b/frontend/src/components/dashboard/dashboard-recent/ReleaseHistoryCard.tsx @@ -0,0 +1,99 @@ +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 { Rocket, ArrowRight } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import type { DashboardRelease } from './types'; + +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')} + +
+ ))} +
+ )} +
+
+ ); +} diff --git a/frontend/src/components/dashboard/dashboard-recent/shared-components.tsx b/frontend/src/components/dashboard/dashboard-recent/shared-components.tsx new file mode 100644 index 0000000..e1e48ec --- /dev/null +++ b/frontend/src/components/dashboard/dashboard-recent/shared-components.tsx @@ -0,0 +1,35 @@ +import { useTranslation } from 'react-i18next'; +import { Badge } from '@/components/ui/badge'; +import { cn } from '@/lib/utils'; + +/** Status dot component */ +export function StatusDot({ severity }: { severity: string }) { + return ( +
+ ); +} + +/** Severity badge */ +export function SeverityBadge({ severity }: { severity: string }) { + const { t } = useTranslation(); + return ( + + {severity === 'critical' ? t('common.error') : severity === 'warning' ? t('common.warning') : t('common.info')} + + ); +} diff --git a/frontend/src/components/dashboard/dashboard-recent/types.ts b/frontend/src/components/dashboard/dashboard-recent/types.ts new file mode 100644 index 0000000..1c40bbc --- /dev/null +++ b/frontend/src/components/dashboard/dashboard-recent/types.ts @@ -0,0 +1,48 @@ +/** Dashboard 卡片数据类型 — 与 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; +}