mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
feat(admin-remnawave): provider favicon, mobile node card, meaningful icons
- Render the provider favicon in the node badge (new provider_favicon / provider_name fields), with graceful fallback. - Mobile-friendly node card: stop the versions block from shifting right on wrapped rows, truncate long addresses, cap the provider badge. - Give node-summary cards (total/online/offline/disabled) and traffic-period cards (2d/7d/30d/month/year) distinct, meaningful icons instead of all-the-same globe / bar-chart. - Show traffic as 'used / limit' (e.g. 2.35 TB / ∞). - Route every icon through the central barrel (drop the direct react-icons/pi import).
This commit is contained in:
@@ -134,6 +134,8 @@ export interface NodeInfo {
|
|||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
provider_uuid?: string;
|
provider_uuid?: string;
|
||||||
|
provider_name?: string | null;
|
||||||
|
provider_favicon?: string | null;
|
||||||
versions?: { xray: string; node: string } | null;
|
versions?: { xray: string; node: string } | null;
|
||||||
system?: {
|
system?: {
|
||||||
info: {
|
info: {
|
||||||
|
|||||||
@@ -23,6 +23,19 @@ import {
|
|||||||
ServerIcon,
|
ServerIcon,
|
||||||
ChartIcon,
|
ChartIcon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
|
HeartbeatIcon,
|
||||||
|
PowerIcon,
|
||||||
|
WarningCircleIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
CalendarBlankIcon,
|
||||||
|
CalendarStarIcon,
|
||||||
|
ChartPieIcon,
|
||||||
|
ChartDonutIcon,
|
||||||
|
CpuIcon,
|
||||||
|
MemoryIcon,
|
||||||
|
PulseIcon,
|
||||||
|
DevicesIcon,
|
||||||
|
StatUptimeIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
SyncIcon,
|
SyncIcon,
|
||||||
RefreshIcon,
|
RefreshIcon,
|
||||||
@@ -36,7 +49,6 @@ import {
|
|||||||
BackIcon,
|
BackIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
} from '../components/icons';
|
} from '../components/icons';
|
||||||
import { PiCpu, PiMemory, PiTimer, PiDevices, PiPulse } from 'react-icons/pi';
|
|
||||||
|
|
||||||
const formatBytes = (bytes: number): string => {
|
const formatBytes = (bytes: number): string => {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
@@ -128,6 +140,9 @@ function NodeCard({ node, providerName, onAction, isLoading }: NodeCardProps) {
|
|||||||
const limit = node.traffic_limit_bytes ?? 0;
|
const limit = node.traffic_limit_bytes ?? 0;
|
||||||
const trafficPct = limit > 0 ? Math.min(100, (used / limit) * 100) : null;
|
const trafficPct = limit > 0 ? Math.min(100, (used / limit) * 100) : null;
|
||||||
|
|
||||||
|
// Provider name: realtime metrics first, fall back to the node's own provider.
|
||||||
|
const providerLabel = providerName || node.provider_name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-dark-700 bg-dark-800/50 p-3.5 transition-colors hover:border-dark-600">
|
<div className="rounded-xl border border-dark-700 bg-dark-800/50 p-3.5 transition-colors hover:border-dark-600">
|
||||||
{/* Identity + actions */}
|
{/* Identity + actions */}
|
||||||
@@ -145,9 +160,19 @@ function NodeCard({ node, providerName, onAction, isLoading }: NodeCardProps) {
|
|||||||
{getCountryFlag(node.country_code)}
|
{getCountryFlag(node.country_code)}
|
||||||
</span>
|
</span>
|
||||||
<h3 className="truncate font-semibold text-dark-100">{node.name}</h3>
|
<h3 className="truncate font-semibold text-dark-100">{node.name}</h3>
|
||||||
{providerName && (
|
{(providerLabel || node.provider_favicon) && (
|
||||||
<span className="shrink-0 truncate rounded-md bg-accent-500/15 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-accent-300">
|
<span className="flex min-w-0 max-w-[7rem] shrink items-center gap-1 rounded-md bg-accent-500/15 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-accent-300">
|
||||||
{providerName}
|
{node.provider_favicon && (
|
||||||
|
<img
|
||||||
|
src={node.provider_favicon}
|
||||||
|
alt=""
|
||||||
|
className="h-3 w-3 shrink-0 rounded-[2px]"
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{providerLabel && <span className="truncate">{providerLabel}</span>}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -186,9 +211,9 @@ function NodeCard({ node, providerName, onAction, isLoading }: NodeCardProps) {
|
|||||||
|
|
||||||
{/* Address + traffic + uptime */}
|
{/* Address + traffic + uptime */}
|
||||||
<div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-dark-400">
|
<div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-dark-400">
|
||||||
<span className="flex items-center gap-1 font-mono text-dark-500">
|
<span className="flex min-w-0 max-w-full items-center gap-1 font-mono text-dark-500">
|
||||||
<GlobeIcon className="h-3 w-3" />
|
<GlobeIcon className="h-3 w-3 shrink-0" />
|
||||||
{node.address}
|
<span className="truncate">{node.address}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
<span className="text-dark-300">{formatBytes(used)}</span>
|
<span className="text-dark-300">{formatBytes(used)}</span>
|
||||||
@@ -200,11 +225,11 @@ function NodeCard({ node, providerName, onAction, isLoading }: NodeCardProps) {
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="text-dark-500">{limit > 0 ? formatBytes(limit) : '∞'}</span>
|
<span className="text-dark-500">/ {limit > 0 ? formatBytes(limit) : '∞'}</span>
|
||||||
</span>
|
</span>
|
||||||
{node.xray_uptime > 0 && (
|
{node.xray_uptime > 0 && (
|
||||||
<span className="flex items-center gap-1 text-dark-500">
|
<span className="flex items-center gap-1 text-dark-500">
|
||||||
<PiTimer className="h-3 w-3" />
|
<StatUptimeIcon className="h-3 w-3" />
|
||||||
{formatUptime(node.xray_uptime)}
|
{formatUptime(node.xray_uptime)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -246,7 +271,7 @@ function NodeCard({ node, providerName, onAction, isLoading }: NodeCardProps) {
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
{(node.versions?.node || node.versions?.xray) && (
|
{(node.versions?.node || node.versions?.xray) && (
|
||||||
<span className="ml-auto flex items-center gap-2 text-dark-600">
|
<span className="flex w-full items-center gap-2 text-dark-600 sm:ml-auto sm:w-auto">
|
||||||
{node.versions?.node && <span>{node.versions.node}</span>}
|
{node.versions?.node && <span>{node.versions.node}</span>}
|
||||||
{node.versions?.xray && <span>xray {node.versions.xray}</span>}
|
{node.versions?.xray && <span>xray {node.versions.xray}</span>}
|
||||||
</span>
|
</span>
|
||||||
@@ -527,20 +552,20 @@ function OverviewTab({
|
|||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.overview.cpu', 'CPU Cores')}
|
label={t('admin.remnawave.overview.cpu', 'CPU Cores')}
|
||||||
value={stats.server_info.cpu_cores}
|
value={stats.server_info.cpu_cores}
|
||||||
icon={<PiCpu className="h-5 w-5" />}
|
icon={<CpuIcon className="h-5 w-5" />}
|
||||||
color="accent"
|
color="accent"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.overview.memory', 'Memory')}
|
label={t('admin.remnawave.overview.memory', 'Memory')}
|
||||||
value={`${memoryUsedPercent}%`}
|
value={`${memoryUsedPercent}%`}
|
||||||
subValue={`${formatBytes(stats.server_info.memory_used)} / ${formatBytes(stats.server_info.memory_total)}`}
|
subValue={`${formatBytes(stats.server_info.memory_used)} / ${formatBytes(stats.server_info.memory_total)}`}
|
||||||
icon={<PiMemory className="h-5 w-5" />}
|
icon={<MemoryIcon className="h-5 w-5" />}
|
||||||
color={memoryUsedPercent > 80 ? 'red' : memoryUsedPercent > 60 ? 'orange' : 'green'}
|
color={memoryUsedPercent > 80 ? 'red' : memoryUsedPercent > 60 ? 'orange' : 'green'}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.overview.uptime', 'Uptime')}
|
label={t('admin.remnawave.overview.uptime', 'Uptime')}
|
||||||
value={formatUptime(stats.server_info.uptime_seconds)}
|
value={formatUptime(stats.server_info.uptime_seconds)}
|
||||||
icon={<PiTimer className="h-5 w-5" />}
|
icon={<StatUptimeIcon className="h-5 w-5" />}
|
||||||
color="blue"
|
color="blue"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -556,31 +581,31 @@ function OverviewTab({
|
|||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.overview.traffic2days', '2 days')}
|
label={t('admin.remnawave.overview.traffic2days', '2 days')}
|
||||||
value={formatBytes(stats.traffic_periods.last_2_days.current)}
|
value={formatBytes(stats.traffic_periods.last_2_days.current)}
|
||||||
icon={<ChartIcon className="h-5 w-5" />}
|
icon={<CalendarIcon className="h-5 w-5" />}
|
||||||
color="accent"
|
color="accent"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.overview.traffic7days', '7 days')}
|
label={t('admin.remnawave.overview.traffic7days', '7 days')}
|
||||||
value={formatBytes(stats.traffic_periods.last_7_days.current)}
|
value={formatBytes(stats.traffic_periods.last_7_days.current)}
|
||||||
icon={<ChartIcon className="h-5 w-5" />}
|
icon={<ChartDonutIcon className="h-5 w-5" />}
|
||||||
color="blue"
|
color="blue"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.overview.traffic30days', '30 days')}
|
label={t('admin.remnawave.overview.traffic30days', '30 days')}
|
||||||
value={formatBytes(stats.traffic_periods.last_30_days.current)}
|
value={formatBytes(stats.traffic_periods.last_30_days.current)}
|
||||||
icon={<ChartIcon className="h-5 w-5" />}
|
icon={<ChartPieIcon className="h-5 w-5" />}
|
||||||
color="green"
|
color="green"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.overview.trafficMonth', 'Month')}
|
label={t('admin.remnawave.overview.trafficMonth', 'Month')}
|
||||||
value={formatBytes(stats.traffic_periods.current_month.current)}
|
value={formatBytes(stats.traffic_periods.current_month.current)}
|
||||||
icon={<ChartIcon className="h-5 w-5" />}
|
icon={<CalendarBlankIcon className="h-5 w-5" />}
|
||||||
color="purple"
|
color="purple"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.overview.trafficYear', 'Year')}
|
label={t('admin.remnawave.overview.trafficYear', 'Year')}
|
||||||
value={formatBytes(stats.traffic_periods.current_year.current)}
|
value={formatBytes(stats.traffic_periods.current_year.current)}
|
||||||
icon={<ChartIcon className="h-5 w-5" />}
|
icon={<CalendarStarIcon className="h-5 w-5" />}
|
||||||
color="orange"
|
color="orange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -650,7 +675,7 @@ function OverviewTab({
|
|||||||
{devicesStats && (devicesStats.by_platform.length > 0 || devicesStats.by_app.length > 0) && (
|
{devicesStats && (devicesStats.by_platform.length > 0 || devicesStats.by_app.length > 0) && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-3 flex items-center gap-2 text-sm font-medium text-dark-300">
|
<h3 className="mb-3 flex items-center gap-2 text-sm font-medium text-dark-300">
|
||||||
<PiDevices className="h-4 w-4" />
|
<DevicesIcon className="h-4 w-4" />
|
||||||
{t('admin.remnawave.overview.devices', 'Устройства')} ·{' '}
|
{t('admin.remnawave.overview.devices', 'Устройства')} ·{' '}
|
||||||
{devicesStats.total_hwid_devices} ({devicesStats.average_devices_per_user.toFixed(1)}/
|
{devicesStats.total_hwid_devices} ({devicesStats.average_devices_per_user.toFixed(1)}/
|
||||||
{t('admin.remnawave.overview.perUser', 'юзер')})
|
{t('admin.remnawave.overview.perUser', 'юзер')})
|
||||||
@@ -717,7 +742,7 @@ function OverviewTab({
|
|||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.overview.panelRam', 'RAM процесса')}
|
label={t('admin.remnawave.overview.panelRam', 'RAM процесса')}
|
||||||
value={formatBytes(health.rss_bytes)}
|
value={formatBytes(health.rss_bytes)}
|
||||||
icon={<PiMemory className="h-5 w-5" />}
|
icon={<MemoryIcon className="h-5 w-5" />}
|
||||||
color="blue"
|
color="blue"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
@@ -730,13 +755,13 @@ function OverviewTab({
|
|||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.overview.eventLoopP99', 'Event-loop p99')}
|
label={t('admin.remnawave.overview.eventLoopP99', 'Event-loop p99')}
|
||||||
value={`${health.event_loop_p99_ms.toFixed(1)} ms`}
|
value={`${health.event_loop_p99_ms.toFixed(1)} ms`}
|
||||||
icon={<PiPulse className="h-5 w-5" />}
|
icon={<PulseIcon className="h-5 w-5" />}
|
||||||
color={health.event_loop_p99_ms > 50 ? 'red' : 'green'}
|
color={health.event_loop_p99_ms > 50 ? 'red' : 'green'}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.overview.panelUptime', 'Аптайм панели')}
|
label={t('admin.remnawave.overview.panelUptime', 'Аптайм панели')}
|
||||||
value={formatUptime(health.uptime_seconds)}
|
value={formatUptime(health.uptime_seconds)}
|
||||||
icon={<PiTimer className="h-5 w-5" />}
|
icon={<StatUptimeIcon className="h-5 w-5" />}
|
||||||
color="accent"
|
color="accent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -809,25 +834,25 @@ function NodesTab({
|
|||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.nodes.stats.total', 'Total')}
|
label={t('admin.remnawave.nodes.stats.total', 'Total')}
|
||||||
value={stats.total}
|
value={stats.total}
|
||||||
icon={<GlobeIcon />}
|
icon={<ServerIcon />}
|
||||||
color="accent"
|
color="accent"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.nodes.stats.online', 'Online')}
|
label={t('admin.remnawave.nodes.stats.online', 'Online')}
|
||||||
value={stats.online}
|
value={stats.online}
|
||||||
icon={<GlobeIcon />}
|
icon={<HeartbeatIcon />}
|
||||||
color="green"
|
color="green"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.nodes.stats.offline', 'Offline')}
|
label={t('admin.remnawave.nodes.stats.offline', 'Offline')}
|
||||||
value={stats.offline}
|
value={stats.offline}
|
||||||
icon={<GlobeIcon />}
|
icon={<WarningCircleIcon />}
|
||||||
color="red"
|
color="red"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label={t('admin.remnawave.nodes.stats.disabled', 'Disabled')}
|
label={t('admin.remnawave.nodes.stats.disabled', 'Disabled')}
|
||||||
value={stats.disabled}
|
value={stats.disabled}
|
||||||
icon={<GlobeIcon />}
|
icon={<PowerIcon />}
|
||||||
color="accent"
|
color="accent"
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
|
|||||||
Reference in New Issue
Block a user