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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 4m58s
This commit is contained in:
+72
-15
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user