Files
EventHubFrontAdmin/src/components/MetricIndicator.tsx
T
aleksey fd2c1f10ad
CI / test (push) Failing after 3m9s
CI / push-image (push) Has been skipped
CI / ci-done (push) Failing after 0s
feat(admin): Control Center redesign, Explore dual-view и полный RU/EN i18n
Refs EventHub/EventHubFrontAdmin#32
2026-07-14 17:58:33 +03:00

143 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
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 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;
const memoryPercent = totalMemory > 0 ? (usedMemory / totalMemory) * 100 : 0;
const prevCpu = prevStats?.cpu_utilization;
const prevTotal = (prevStats?.memory_total ?? 0) + (prevStats?.memory_available ?? 0);
const prevUsed = prevStats?.memory_total ?? 0;
const prevMemoryPercent = prevTotal > 0 ? (prevUsed / prevTotal) * 100 : null;
const cpuColor = cpu > 80 ? '#ef4444' : '#22c55e';
const memColor = memoryPercent > 80 ? '#ef4444' : '#3b82f6';
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>
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>
</div>
</>
);
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>
);
};
export default MetricIndicator;