feat(admin): Control Center redesign, Explore dual-view и полный RU/EN i18n
CI / test (push) Failing after 3m9s
CI / push-image (push) Has been skipped
CI / ci-done (push) Failing after 0s

Refs EventHub/EventHubFrontAdmin#32
This commit is contained in:
2026-07-14 17:58:33 +03:00
parent de379e28bf
commit fd2c1f10ad
95 changed files with 12699 additions and 4410 deletions
+115 -40
View File
@@ -1,14 +1,68 @@
import React from 'react';
import { Tooltip, Progress } from 'antd';
import { NodeMetric } from '../store/metricsStore';
import { NodeMetric } from '@/store/metricsStore';
import { cn } from '@/lib/utils';
interface Props {
node: string;
stats: NodeMetric;
prevStats?: NodeMetric | null;
/** Outer ring diameter in px (default 48). */
size?: number;
onClick?: () => void;
className?: string;
}
const MetricIndicator: React.FC<Props> = ({ node, stats, prevStats }) => {
const CircleProgress: React.FC<{
percent: number;
size: number;
strokeWidth: number;
color: string;
offset?: number;
}> = ({ percent, size, strokeWidth, color, offset = 0 }) => {
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const clamped = Math.min(100, Math.max(0, percent));
const dashOffset = circumference - (clamped / 100) * circumference;
return (
<svg
width={size}
height={size}
className="absolute -rotate-90"
style={{ top: offset, left: offset }}
aria-hidden
>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="hsl(var(--muted))"
strokeWidth={strokeWidth}
/>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
strokeLinecap="round"
/>
</svg>
);
};
const MetricIndicator: React.FC<Props> = ({
node,
stats,
prevStats,
size = 48,
onClick,
className,
}) => {
const cpu = stats.cpu_utilization ?? 0;
const totalMemory = (stats.memory_total ?? 0) + (stats.memory_available ?? 0);
const usedMemory = stats.memory_total ?? 0;
@@ -19,48 +73,69 @@ const MetricIndicator: React.FC<Props> = ({ node, stats, prevStats }) => {
const prevUsed = prevStats?.memory_total ?? 0;
const prevMemoryPercent = prevTotal > 0 ? (prevUsed / prevTotal) * 100 : null;
const cpuColor = cpu > 80 ? '#ff4d4f' : '#52c41a';
const memColor = memoryPercent > 80 ? '#ff4d4f' : '#1890ff';
const cpuColor = cpu > 80 ? '#ef4444' : '#22c55e';
const memColor = memoryPercent > 80 ? '#ef4444' : '#3b82f6';
return (
<Tooltip
title={
const stroke = Math.max(3, Math.round(size * 0.12));
const innerSize = Math.round(size * (32 / 48));
const innerOffset = Math.round((size - innerSize) / 2);
const fontSize = size <= 36 ? 8 : 10;
const shellClass = cn(
'group relative shrink-0',
onClick && 'cursor-pointer rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring',
className
);
const body = (
<>
<CircleProgress percent={memoryPercent} size={size} strokeWidth={stroke} color={memColor} />
<CircleProgress
percent={cpu}
size={innerSize}
strokeWidth={stroke}
color={cpuColor}
offset={innerOffset}
/>
<div
className="absolute inset-0 flex items-center justify-center font-semibold tabular-nums"
style={{ fontSize }}
>
{cpu.toFixed(0)}%
</div>
<div className="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 hidden w-max max-w-[240px] -translate-x-1/2 rounded-md border bg-popover px-2 py-1.5 text-left text-xs text-popover-foreground shadow-md group-hover:block group-focus-visible:block">
<div className="font-medium">{node}</div>
<div>
<div>{node}</div>
<div>CPU: {cpu.toFixed(1)}% {prevCpu !== undefined ? `(было ${prevCpu.toFixed(1)}%)` : ''}</div>
<div>
Память: {(usedMemory / 1048576).toFixed(0)} / {(totalMemory / 1048576).toFixed(0)} МБ ({memoryPercent.toFixed(1)}%)
{prevMemoryPercent !== null ? ` (было ${prevMemoryPercent.toFixed(1)}%)` : ''}
</div>
CPU: {cpu.toFixed(1)}%{' '}
{prevCpu !== undefined ? `(было ${prevCpu.toFixed(1)}%)` : ''}
</div>
}
>
<div style={{ position: 'relative', width: 48, height: 48, marginRight: 12 }}>
<Progress
type="circle"
percent={memoryPercent}
format={() => ''}
size={48}
strokeColor={memColor}
railColor="#f0f0f0"
strokeWidth={6}
style={{ position: 'absolute', top: 0, left: 0 }}
/>
<Progress
type="circle"
percent={cpu}
format={() => ''}
size={32}
strokeColor={cpuColor}
railColor="#f0f0f0"
strokeWidth={6}
style={{ position: 'absolute', top: 8, left: 8 }}
/>
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', fontSize: 10, fontWeight: 600 }}>
{cpu.toFixed(0)}%
<div>
Память: {(usedMemory / 1048576).toFixed(0)} / {(totalMemory / 1048576).toFixed(0)} МБ (
{memoryPercent.toFixed(1)}%)
{prevMemoryPercent !== null ? ` (было ${prevMemoryPercent.toFixed(1)}%)` : ''}
</div>
</div>
</Tooltip>
</>
);
if (onClick) {
return (
<button
type="button"
className={shellClass}
style={{ width: size, height: size }}
onClick={onClick}
aria-label={`${node}: CPU ${cpu.toFixed(0)}%, память ${memoryPercent.toFixed(0)}%`}
>
{body}
</button>
);
}
return (
<div className={shellClass} style={{ width: size, height: size }} title={node}>
{body}
</div>
);
};