feat(frontend): 控制台 next-intl 中英双语

接入无 URL 前缀的 zh-CN/en,顶栏与外观设置可切换语言;选择写入 cookie 后刷新生效。
This commit is contained in:
ryan
2026-08-16 16:49:16 +08:00
parent 64fbaa7ef1
commit 5a8722ff07
219 changed files with 21437 additions and 5212 deletions
@@ -3,6 +3,7 @@
import { useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Loader2, Trash2 } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
import {
@@ -120,18 +121,19 @@ function SystemProfileCard({
profile: NodeSystemProfile;
nodeName: string;
}) {
const t = useTranslations('nodes');
return (
<div className='grid gap-4 md:grid-cols-2'>
<div className='space-y-4 rounded-lg border px-4 py-4'>
<div>
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
主机名
{t('obs.hostname')}
</p>
<p className='mt-2 text-sm'>{profile.hostname || nodeName}</p>
</div>
<div>
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
操作系统
{t('obs.os')}
</p>
<p className='mt-2 text-sm'>
{profile.os_name || 'unknown'}
@@ -140,7 +142,7 @@ function SystemProfileCard({
</div>
<div>
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
内核 / 架构
{t('obs.kernelArch')}
</p>
<p className='mt-2 text-sm'>
{profile.kernel_version || 'unknown'} ·{' '}
@@ -149,25 +151,27 @@ function SystemProfileCard({
</div>
<div>
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
在线时长
{t('obs.uptime')}
</p>
<p className='mt-2 text-sm'>
{formatUptime(profile.uptime_seconds, t)}
</p>
<p className='mt-2 text-sm'>{formatUptime(profile.uptime_seconds)}</p>
</div>
</div>
<div className='space-y-4 rounded-lg border px-4 py-4'>
<div>
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
CPU
{t('obs.cpu')}
</p>
<p className='mt-2 text-sm'>{profile.cpu_model || 'unknown'}</p>
<p className='mt-1 text-xs text-muted-foreground'>
{profile.cpu_cores || 0} 核
{t('obs.cpuCores', { count: profile.cpu_cores || 0 })}
</p>
</div>
<div>
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
总内存
{t('obs.totalMemory')}
</p>
<p className='mt-2 text-sm'>
{formatBytes(profile.total_memory_bytes)}
@@ -175,7 +179,7 @@ function SystemProfileCard({
</div>
<div>
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
总存储
{t('obs.totalStorage')}
</p>
<p className='mt-2 text-sm'>
{formatBytes(profile.total_disk_bytes)}
@@ -183,7 +187,7 @@ function SystemProfileCard({
</div>
<div>
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
上报时间
{t('obs.reportedAt')}
</p>
<p className='mt-2 text-sm'>
{isMeaningfulTime(profile.reported_at)
@@ -211,16 +215,15 @@ function HealthEventTimeline({
onCleanup: () => void;
cleanupPending: boolean;
}) {
const t = useTranslations('nodes');
return (
<Card className='border-dashed shadow-none'>
<CardHeader className='flex-row items-center justify-between space-y-0'>
<div>
<CardTitle className='text-base font-semibold'>
健康事件时间线
{t('obs.eventsTitle')}
</CardTitle>
<CardDescription>
保留活动与已恢复事件,帮助判断运行状态。
</CardDescription>
<CardDescription>{t('obs.eventsDesc')}</CardDescription>
</div>
<Button
variant='outline'
@@ -230,7 +233,7 @@ function HealthEventTimeline({
onClick={onCleanup}
>
<Trash2 className='size-3.5 mr-1' />
清理日志
{t('obs.cleanupLogs')}
</Button>
</CardHeader>
<CardContent className='space-y-4'>
@@ -239,9 +242,9 @@ function HealthEventTimeline({
<div className='flex flex-wrap gap-2'>
{(
[
['all', '全部事件'],
['active', '活动中'],
['resolved', '已恢复'],
['all', t('obs.filterAll')],
['active', t('obs.filterActive')],
['resolved', t('obs.filterResolved')],
] as const
).map(([filter, label]) => (
<Button
@@ -261,7 +264,7 @@ function HealthEventTimeline({
{events.length === 0 ? (
<p className='text-sm text-muted-foreground'>
当前筛选下没有健康事件。
{t('obs.filterEmpty')}
</p>
) : null}
@@ -276,28 +279,32 @@ function HealthEventTimeline({
tone={getHealthEventTone(event)}
/>
<NodeStatusBadge
label={event.status === 'active' ? '活动中' : '已恢复'}
label={
event.status === 'active'
? t('obs.filterActive')
: t('obs.filterResolved')
}
tone={event.status === 'active' ? 'warning' : 'success'}
/>
</div>
<p className='mt-2 text-sm text-muted-foreground'>
{event.message || '暂无详细消息'}
{event.message || t('obs.noMessage')}
</p>
<div className='mt-2 grid gap-1 text-xs text-muted-foreground md:grid-cols-3'>
<p>
首次触发:
{t('obs.firstTriggered')}
{isMeaningfulTime(event.first_triggered_at)
? ` ${formatDateTime(event.first_triggered_at)}`
: ' —'}
</p>
<p>
最近触发:
{t('obs.lastTriggered')}
{isMeaningfulTime(event.last_triggered_at)
? ` ${formatDateTime(event.last_triggered_at)}`
: ' —'}
</p>
<p>
恢复时间:
{t('obs.resolvedAt')}
{isMeaningfulTime(event.resolved_at) && event.resolved_at
? ` ${formatDateTime(event.resolved_at)}`
: ' —'}
@@ -308,7 +315,7 @@ function HealthEventTimeline({
</>
) : (
<p className='text-sm text-muted-foreground'>
节点当前还没有上报健康事件记录。
{t('obs.noEvents')}
</p>
)}
</CardContent>
@@ -320,13 +327,16 @@ export function NodeObservability({
nodeId,
node,
variant = 'edge',
connectionHint = 'OpenResty 当前连接',
connectionHint,
}: {
nodeId: number;
node?: NodeItem;
variant?: NodeObservabilityVariant;
connectionHint?: string;
}) {
const t = useTranslations('nodes');
const tc = useTranslations('common');
const resolvedConnectionHint = connectionHint ?? t('edge.connectionHint');
const queryClient = useQueryClient();
const [healthEventFilter, setHealthEventFilter] =
useState<HealthEventFilter>('all');
@@ -344,8 +354,8 @@ export function NodeObservability({
onSuccess: async (result) => {
toast.success(
result.deleted_count > 0
? `已清理 ${result.deleted_count} 条健康事件日志`
: '当前没有可清理的健康事件日志',
? t('obs.cleaned', { count: result.deleted_count })
: t('obs.nothingToClean'),
);
setCleanupOpen(false);
await Promise.all([
@@ -357,7 +367,7 @@ export function NodeObservability({
}),
]);
},
onError: (error) => toast.error(getErrorMessage(error)),
onError: (error) => toast.error(getErrorMessage(error, t('requestFailed'))),
});
const observability = observabilityQuery.data ?? null;
@@ -432,7 +442,7 @@ export function NodeObservability({
<Card className='border-dashed shadow-none'>
<CardContent className='flex items-center justify-center py-10 text-sm text-muted-foreground'>
<Loader2 className='size-4 mr-2 animate-spin' />
加载运行观测数据中...
{t('obs.loading')}
</CardContent>
</Card>
);
@@ -443,7 +453,7 @@ export function NodeObservability({
<Card className='border-dashed shadow-none'>
<CardContent className='py-6'>
<p className='text-sm text-destructive'>
{getErrorMessage(observabilityQuery.error)}
{getErrorMessage(observabilityQuery.error, t('requestFailed'))}
</p>
</CardContent>
</Card>
@@ -455,20 +465,20 @@ export function NodeObservability({
<div className='space-y-6'>
<div className='grid gap-4 md:grid-cols-3'>
<SummaryStat
label='运行诊断'
label={t('obs.diag')}
value={
activeHealthEvents.length
? `${activeHealthEvents.length} 个活动异常`
: '运行稳定'
? t('obs.activeIssues', { count: activeHealthEvents.length })
: t('obs.stable')
}
hint={
latestHealthEvent
? getHealthEventLabel(latestHealthEvent)
: '当前没有活动中的健康事件'
: t('obs.noActiveEvents')
}
/>
<SummaryStat
label='系统核心'
label={t('obs.systemCore')}
value={profile?.hostname || '—'}
hint={
profile
@@ -477,9 +487,9 @@ export function NodeObservability({
}
/>
<SummaryStat
label='在线时长'
value={formatUptime(profile?.uptime_seconds)}
hint='来自节点系统画像上报'
label={t('obs.uptime')}
value={formatUptime(profile?.uptime_seconds, t)}
hint={t('obs.uptimeFromProfile')}
/>
</div>
@@ -487,16 +497,16 @@ export function NodeObservability({
<Card className='border-dashed shadow-none'>
<CardHeader>
<CardTitle className='text-base font-semibold'>
系统画像
{t('obs.profileTitle')}
</CardTitle>
<CardDescription>节点上报的主机与硬件信息</CardDescription>
<CardDescription>{t('obs.profileDesc')}</CardDescription>
</CardHeader>
<CardContent>
{profile ? (
<SystemProfileCard profile={profile} nodeName={nodeName} />
) : (
<p className='text-sm text-muted-foreground'>
节点已接入,但尚未上报完整系统画像。
{t('obs.noProfile')}
</p>
)}
</CardContent>
@@ -505,45 +515,50 @@ export function NodeObservability({
<Card className='border-dashed shadow-none'>
<CardHeader>
<CardTitle className='text-base font-semibold'>
实时资源快照
{t('obs.snapshotTitle')}
</CardTitle>
<CardDescription>最近一次 metrics 上报</CardDescription>
<CardDescription>{t('obs.snapshotDesc')}</CardDescription>
</CardHeader>
<CardContent>
{latestMetric ? (
<div className='grid gap-3 sm:grid-cols-2'>
<MetricBar
label='CPU'
label={t('obs.cpu')}
value={formatPercent(latestMetric.cpu_usage_percent)}
progress={latestMetric.cpu_usage_percent}
hint={
isMeaningfulTime(latestMetric.captured_at)
? `快照 ${formatRelativeTime(latestMetric.captured_at)}`
? t('obs.snapshotAgo', {
time: formatRelativeTime(
latestMetric.captured_at,
t,
),
})
: undefined
}
/>
<MetricBar
label='内存'
label={t('obs.memory')}
value={`${formatBytes(latestMetric.memory_used_bytes)} / ${formatBytes(latestMetric.memory_total_bytes)}`}
progress={memoryUsageRatio}
/>
<MetricBar
label='存储'
label={t('obs.storage')}
value={`${formatBytes(latestMetric.storage_used_bytes)} / ${formatBytes(latestMetric.storage_total_bytes)}`}
progress={storageUsageRatio}
/>
<MetricBar
label='连接数'
label={t('obs.connections')}
value={formatMetricCount(
latestMetric.openresty_connections,
)}
progress={null}
hint={connectionHint}
hint={resolvedConnectionHint}
/>
</div>
) : (
<p className='text-sm text-muted-foreground'>
节点已接入,但尚未上报资源快照。
{t('obs.noSnapshot')}
</p>
)}
</CardContent>
@@ -562,21 +577,23 @@ export function NodeObservability({
<AlertDialog open={cleanupOpen} onOpenChange={setCleanupOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>清理健康事件日志</AlertDialogTitle>
<AlertDialogTitle>{t('obs.cleanupTitle')}</AlertDialogTitle>
<AlertDialogDescription>
该操作会删除此节点在控制端记录的所有健康诊断事件历史,不会影响后续新事件上报。
{t('obs.cleanupDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={cleanupMutation.isPending}>
取消
{tc('cancel')}
</AlertDialogCancel>
<AlertDialogAction
className='bg-destructive text-white hover:bg-destructive/90'
disabled={cleanupMutation.isPending}
onClick={() => cleanupMutation.mutate()}
>
{cleanupMutation.isPending ? '清理中...' : '确认清理'}
{cleanupMutation.isPending
? t('obs.cleaning')
: t('obs.confirmCleanup')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -589,43 +606,57 @@ export function NodeObservability({
<div className='space-y-6'>
<div className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
<SummaryStat
label='运行诊断'
label={t('obs.diag')}
value={
activeHealthEvents.length
? `${activeHealthEvents.length} 个活动异常`
: '运行稳定'
? t('obs.activeIssues', { count: activeHealthEvents.length })
: t('obs.stable')
}
hint={
latestHealthEvent
? getHealthEventLabel(latestHealthEvent)
: '当前没有活动中的健康事件'
: t('obs.noActiveEvents')
}
/>
<SummaryStat
label='查询窗口请求'
label={t('obs.windowRequests')}
value={formatMetricCount(trafficSummary?.request_count)}
hint={
trafficSummary
? `查询窗口独立访客 ${formatMetricCount(trafficSummary.unique_visitor_count)} · 错误率 ${trafficSummary.error_rate_percent.toFixed(1)}%`
: '当前没有可展示的请求窗口摘要'
? t('obs.windowVisitorsHint', {
visitors: formatMetricCount(
trafficSummary.unique_visitor_count,
),
rate: trafficSummary.error_rate_percent.toFixed(1),
})
: t('obs.noWindowSummary')
}
/>
<SummaryStat
label='容量压力'
value={healthSummary?.has_capacity_risk ? '需要关注' : '正常范围'}
label={t('obs.capacityPressure')}
value={
healthSummary?.has_capacity_risk
? t('obs.needsAttention')
: t('obs.normalRange')
}
hint={
latestMetric
? `CPU ${formatPercent(latestMetric.cpu_usage_percent)} · 存储 ${formatPercent(storageUsageRatio)}`
: '当前没有资源快照'
? t('obs.cpuStorage', {
cpu: formatPercent(latestMetric.cpu_usage_percent),
storage: formatPercent(storageUsageRatio),
})
: t('obs.noResourceSnapshot')
}
/>
<SummaryStat
label='来源信号'
label={t('obs.sourceSignal')}
value={topSourceCountry?.key ?? '—'}
hint={
topSourceCountry
? `${topSourceCountry.value.toLocaleString('zh-CN')} 次请求`
: '当前没有来源分布数据'
? t('obs.requestTimes', {
count: topSourceCountry.value.toLocaleString('zh-CN'),
})
: t('obs.noSourceDist')
}
/>
</div>
@@ -633,14 +664,16 @@ export function NodeObservability({
<div className='grid gap-6 xl:grid-cols-3'>
<Card className='border-dashed shadow-none'>
<CardHeader>
<CardTitle className='text-base font-semibold'>系统信息</CardTitle>
<CardTitle className='text-base font-semibold'>
{t('obs.systemInfo')}
</CardTitle>
</CardHeader>
<CardContent>
{profile ? (
<SystemProfileCard profile={profile} nodeName={nodeName} />
) : (
<p className='text-sm text-muted-foreground'>
节点已经接入,但还没有上报完整系统画像。
{t('obs.noProfileLong')}
</p>
)}
</CardContent>
@@ -648,41 +681,45 @@ export function NodeObservability({
<Card className='border-dashed shadow-none'>
<CardHeader>
<CardTitle className='text-base font-semibold'>实时资源</CardTitle>
<CardTitle className='text-base font-semibold'>
{t('obs.realtimeResources')}
</CardTitle>
</CardHeader>
<CardContent>
{latestMetric ? (
<div className='grid gap-3 sm:grid-cols-2'>
<MetricBar
label='CPU'
label={t('obs.cpu')}
value={formatPercent(latestMetric.cpu_usage_percent)}
progress={latestMetric.cpu_usage_percent}
hint={
isMeaningfulTime(latestMetric.captured_at)
? `快照 ${formatRelativeTime(latestMetric.captured_at)}`
? t('obs.snapshotAgo', {
time: formatRelativeTime(latestMetric.captured_at, t),
})
: undefined
}
/>
<MetricBar
label='内存'
label={t('obs.memory')}
value={`${formatBytes(latestMetric.memory_used_bytes)} / ${formatBytes(latestMetric.memory_total_bytes)}`}
progress={memoryUsageRatio}
/>
<MetricBar
label='存储'
label={t('obs.storage')}
value={`${formatBytes(latestMetric.storage_used_bytes)} / ${formatBytes(latestMetric.storage_total_bytes)}`}
progress={storageUsageRatio}
/>
<MetricBar
label='连接数'
label={t('obs.connections')}
value={formatMetricCount(latestMetric.openresty_connections)}
progress={null}
hint={connectionHint}
hint={resolvedConnectionHint}
/>
</div>
) : (
<p className='text-sm text-muted-foreground'>
节点已经接入,但还没有上报资源快照。
{t('obs.noSnapshotLong')}
</p>
)}
</CardContent>
@@ -690,7 +727,9 @@ export function NodeObservability({
<Card className='border-dashed shadow-none'>
<CardHeader>
<CardTitle className='text-base font-semibold'>网络流量</CardTitle>
<CardTitle className='text-base font-semibold'>
{t('obs.networkTraffic')}
</CardTitle>
</CardHeader>
<CardContent className='space-y-4'>
{latestMetric ? (
@@ -698,18 +737,20 @@ export function NodeObservability({
{node ? (
<div className='flex flex-wrap gap-2'>
<NodeStatusBadge
label={getNodeStatusLabel(node.status)}
label={getNodeStatusLabel(node.status, t)}
tone={getNodeStatusTone(node.status)}
/>
<NodeStatusBadge
label={getOpenrestyStatusLabel(node.openresty_status)}
label={getOpenrestyStatusLabel(node.openresty_status, t)}
tone={getOpenrestyStatusTone(node.openresty_status)}
/>
<NodeStatusBadge
label={
activeHealthEvents.length
? `${activeHealthEvents.length} 个活动异常`
: '无活动异常'
? t('obs.activeIssues', {
count: activeHealthEvents.length,
})
: t('obs.noActiveIssue')
}
tone={activeHealthEvents.length ? 'warning' : 'success'}
/>
@@ -719,16 +760,14 @@ export function NodeObservability({
<div className='grid gap-3 sm:grid-cols-2'>
<div className='rounded-lg border px-3 py-3 sm:col-span-2'>
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
OpenResty 连接
{t('obs.openrestyConn')}
</p>
<div className='mt-3 space-y-2 text-sm text-muted-foreground'>
<p>
当前连接:
{t('obs.currentConn')}
{latestMetric.openresty_connections ?? 0}
</p>
<p className='text-xs'>
业务吞吐请看访问日志趋势(已提供/接收数据)
</p>
<p className='text-xs'>{t('obs.throughputHint')}</p>
</div>
</div>
</div>
@@ -736,35 +775,42 @@ export function NodeObservability({
<div className='grid gap-3 sm:grid-cols-2'>
<div className='rounded-lg border px-3 py-3'>
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
查询窗口独立访客
{t('obs.windowVisitors')}
</p>
<p className='mt-3 text-2xl font-semibold'>
{formatMetricCount(trafficSummary?.unique_visitor_count)}
</p>
<p className='mt-2 text-sm text-muted-foreground'>
{trafficSummary
? `请求 ${formatMetricCount(trafficSummary.request_count)} · 估算 QPS ${trafficSummary.estimated_qps.toFixed(2)}`
: '暂无窗口流量摘要'}
? t('obs.reqQps', {
requests: formatMetricCount(
trafficSummary.request_count,
),
qps: trafficSummary.estimated_qps.toFixed(2),
})
: t('obs.noTrafficSummary')}
</p>
</div>
<div className='rounded-lg border px-3 py-3'>
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
查询窗口错误
{t('obs.windowErrors')}
</p>
<p className='mt-3 text-2xl font-semibold'>
{formatMetricCount(trafficSummary?.error_count)}
</p>
<p className='mt-2 text-sm text-muted-foreground'>
{trafficSummary
? `错误率 ${trafficSummary.error_rate_percent.toFixed(1)}%`
: '暂无错误率摘要'}
? t('obs.errorRate', {
rate: trafficSummary.error_rate_percent.toFixed(1),
})
: t('obs.noErrorSummary')}
</p>
</div>
</div>
</>
) : (
<p className='text-sm text-muted-foreground'>
节点已经接入,但还没有上报网络流量相关快照。
{t('obs.noNetworkSnapshot')}
</p>
)}
</CardContent>
@@ -774,11 +820,11 @@ export function NodeObservability({
<div className='grid gap-6 xl:grid-cols-2'>
<TrafficTrendChart
points={trends?.traffic_24h ?? []}
description='按小时聚合该节点的请求量和错误量。'
description={t('obs.trafficTrendDesc')}
/>
<CapacityTrendChart
points={trends?.capacity_24h ?? []}
description='观察该节点 CPU 与内存使用率在 24 小时内的变化。'
description={t('obs.capacityTrendDesc')}
/>
</div>
@@ -791,50 +837,47 @@ export function NodeObservability({
<Card className='border-dashed shadow-none'>
<CardHeader>
<CardTitle className='text-base font-semibold'>
请求结构分布
{t('obs.structureTitle')}
</CardTitle>
<CardDescription>
聚合最近 24
小时窗口上报,帮助判断错误集中在哪些状态码、流量集中在哪些域名。
</CardDescription>
<CardDescription>{t('obs.structureDesc')}</CardDescription>
</CardHeader>
<CardContent>
<div className='mb-6 grid gap-4 md:grid-cols-3'>
<div className='rounded-lg border px-4 py-4'>
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
主状态码
{t('obs.mainStatus')}
</p>
<p className='mt-3 text-2xl font-semibold'>
{dominantStatusCode?.label ?? '—'}
</p>
<p className='mt-2 text-sm text-muted-foreground'>
{dominantStatusCode
? `${dominantStatusCode.value} 次`
: '暂无状态码分布'}
? t('obs.times', { count: dominantStatusCode.value })
: t('obs.noStatusDist')}
</p>
</div>
<div className='rounded-lg border px-4 py-4'>
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
Top Domain
{t('obs.topDomain')}
</p>
<p className='mt-3 truncate text-2xl font-semibold'>
{dominantDomain?.label ?? '—'}
</p>
<p className='mt-2 text-sm text-muted-foreground'>
{dominantDomain
? `${dominantDomain.value} 次`
: '暂无域名分布'}
? t('obs.times', { count: dominantDomain.value })
: t('obs.noDomainDist')}
</p>
</div>
<div className='rounded-lg border px-4 py-4'>
<p className='text-xs text-muted-foreground uppercase tracking-wide'>
已恢复事件
{t('obs.resolvedEvents')}
</p>
<p className='mt-3 text-2xl font-semibold'>
{resolvedHealthEvents.length}
</p>
<p className='mt-2 text-sm text-muted-foreground'>
最近 24 小时已恢复健康事件
{t('obs.resolvedHint')}
</p>
</div>
</div>
@@ -842,20 +885,20 @@ export function NodeObservability({
<div className='grid gap-6 xl:grid-cols-2'>
<div>
<p className='mb-4 text-xs text-muted-foreground uppercase tracking-wide'>
状态码分布
{t('obs.statusDist')}
</p>
<DistributionList
items={statusCodeDistribution}
emptyMessage='暂无状态码分布'
emptyMessage={t('obs.noStatusDist')}
/>
</div>
<div>
<p className='mb-4 text-xs text-muted-foreground uppercase tracking-wide'>
Top Domain
{t('obs.topDomain')}
</p>
<DistributionList
items={topDomains}
emptyMessage='暂无域名分布'
emptyMessage={t('obs.noDomainDist')}
/>
</div>
</div>
@@ -875,21 +918,23 @@ export function NodeObservability({
<AlertDialog open={cleanupOpen} onOpenChange={setCleanupOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>清理健康事件日志</AlertDialogTitle>
<AlertDialogTitle>{t('obs.cleanupTitle')}</AlertDialogTitle>
<AlertDialogDescription>
该操作会删除此节点在控制端记录的所有健康诊断事件历史,不会影响后续新事件上报。
{t('obs.cleanupDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={cleanupMutation.isPending}>
取消
{tc('cancel')}
</AlertDialogCancel>
<AlertDialogAction
className='bg-destructive text-white hover:bg-destructive/90'
disabled={cleanupMutation.isPending}
onClick={() => cleanupMutation.mutate()}
>
{cleanupMutation.isPending ? '清理中...' : '确认清理'}
{cleanupMutation.isPending
? t('obs.cleaning')
: t('obs.confirmCleanup')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>