feat: Enhance GraphView component with custom control panel for zooming and fullscreen functionality, and add country flag display for improved user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 4m58s

This commit is contained in:
2025-08-11 08:16:43 +07:00
parent 21f6f46bd3
commit eafd0c0957
+72 -15
View File
@@ -2,7 +2,6 @@ import React, { useMemo, useCallback, useEffect, useRef } from 'react';
import {
ReactFlow,
Background,
Controls,
MiniMap,
useEdgesState,
useNodesState,
@@ -10,10 +9,12 @@ import {
MarkerType,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { IconZoomIn, IconZoomOut, IconMaximize, IconMinimize } from '@tabler/icons-react';
function GraphView({ servers, connections }) {
const flowRef = useRef(null);
const instanceRef = useRef(null);
const containerRef = useRef(null);
const getTunnelStyle = useCallback((tunnelType) => {
const map = {
@@ -89,6 +90,15 @@ function GraphView({ servers, connections }) {
server: ({ data }) => {
const s = data.server || {};
const countryColor = getCountryColor(s.country);
const getFlagEmoji = (code) => {
if (!code) return '';
const map = { SWE: 'SE', UK: 'GB' };
const ccRaw = String(code).trim().toUpperCase();
const cc = (map[ccRaw] || ccRaw).slice(0, 2);
if (cc.length !== 2) return ccRaw;
return cc.replace(/./g, (ch) => String.fromCodePoint(127397 + ch.charCodeAt()));
};
const flag = getFlagEmoji(s.country);
return (
<div
className="card shadow-sm"
@@ -101,18 +111,21 @@ function GraphView({ servers, connections }) {
}}
>
<div className="px-2 py-1 d-flex align-items-center" style={{ background: '#f8fafc', borderBottom: '1px solid #eef2f7' }}>
<span
className="badge"
style={{
background: '#fff',
color: '#475569',
border: `1px solid ${countryColor}`,
borderRadius: 8,
fontSize: 11,
}}
>
{String(s.country || '').toUpperCase().slice(0, 3)}
</span>
<div className="d-flex align-items-center" style={{ gap: 6 }}>
<span style={{ fontSize: 16, lineHeight: '1' }}>{flag}</span>
<span
className="badge"
style={{
background: '#fff',
color: '#475569',
border: `1px solid ${countryColor}`,
borderRadius: 8,
fontSize: 11,
}}
>
{String(s.country || '').toUpperCase().slice(0, 3)}
</span>
</div>
<div className="ms-2 text-truncate" style={{ fontWeight: 700, color: '#1f2937', fontSize: 13 }}>{s.dns}</div>
</div>
<div className="px-2 py-2" style={{ lineHeight: 1.2 }}>
@@ -160,7 +173,7 @@ function GraphView({ servers, connections }) {
}
return (
<div style={{ width: '100%', height: 500, border: '1px solid #e5e7eb', borderRadius: 8, overflow: 'hidden' }}>
<div ref={containerRef} style={{ width: '100%', height: 500, border: '1px solid #e5e7eb', borderRadius: 8, overflow: 'hidden', position: 'relative' }}>
<ReactFlow
ref={flowRef}
nodes={nodes}
@@ -182,8 +195,52 @@ function GraphView({ servers, connections }) {
nodeStrokeWidth={2}
maskColor="rgba(0,0,0,0.05)"
/>
<Controls position="top-right" showInteractive={false} />
</ReactFlow>
{/* Своя панель управления: -, +, Fit, Fullscreen */}
<div className="position-absolute" style={{ right: 10, top: 10, zIndex: 10, pointerEvents: 'auto' }}>
<div className="btn-group btn-group-sm">
<button
type="button"
className="btn btn-outline-secondary"
onClick={() => instanceRef.current?.zoomOut?.()}
title="Уменьшить"
>
<IconZoomOut size={16} />
</button>
<button
type="button"
className="btn btn-outline-secondary"
onClick={() => instanceRef.current?.zoomIn?.()}
title="Увеличить"
>
<IconZoomIn size={16} />
</button>
<button
type="button"
className="btn btn-outline-secondary"
onClick={() => instanceRef.current?.fitView?.({ padding: 0.2 })}
title="Подогнать к окну"
>
Fit
</button>
<button
type="button"
className="btn btn-outline-secondary"
onClick={() => {
const el = containerRef.current;
if (!el) return;
if (!document.fullscreenElement) {
el.requestFullscreen?.();
} else {
document.exitFullscreen?.();
}
}}
title="Во весь экран"
>
{document.fullscreenElement ? <IconMinimize size={16} /> : <IconMaximize size={16} />}
</button>
</div>
</div>
</div>
);
}