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 {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
Background,
|
Background,
|
||||||
Controls,
|
|
||||||
MiniMap,
|
MiniMap,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
useNodesState,
|
useNodesState,
|
||||||
@@ -10,10 +9,12 @@ import {
|
|||||||
MarkerType,
|
MarkerType,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
|
import { IconZoomIn, IconZoomOut, IconMaximize, IconMinimize } from '@tabler/icons-react';
|
||||||
|
|
||||||
function GraphView({ servers, connections }) {
|
function GraphView({ servers, connections }) {
|
||||||
const flowRef = useRef(null);
|
const flowRef = useRef(null);
|
||||||
const instanceRef = useRef(null);
|
const instanceRef = useRef(null);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
const getTunnelStyle = useCallback((tunnelType) => {
|
const getTunnelStyle = useCallback((tunnelType) => {
|
||||||
const map = {
|
const map = {
|
||||||
@@ -89,6 +90,15 @@ function GraphView({ servers, connections }) {
|
|||||||
server: ({ data }) => {
|
server: ({ data }) => {
|
||||||
const s = data.server || {};
|
const s = data.server || {};
|
||||||
const countryColor = getCountryColor(s.country);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="card shadow-sm"
|
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' }}>
|
<div className="px-2 py-1 d-flex align-items-center" style={{ background: '#f8fafc', borderBottom: '1px solid #eef2f7' }}>
|
||||||
<span
|
<div className="d-flex align-items-center" style={{ gap: 6 }}>
|
||||||
className="badge"
|
<span style={{ fontSize: 16, lineHeight: '1' }}>{flag}</span>
|
||||||
style={{
|
<span
|
||||||
background: '#fff',
|
className="badge"
|
||||||
color: '#475569',
|
style={{
|
||||||
border: `1px solid ${countryColor}`,
|
background: '#fff',
|
||||||
borderRadius: 8,
|
color: '#475569',
|
||||||
fontSize: 11,
|
border: `1px solid ${countryColor}`,
|
||||||
}}
|
borderRadius: 8,
|
||||||
>
|
fontSize: 11,
|
||||||
{String(s.country || '').toUpperCase().slice(0, 3)}
|
}}
|
||||||
</span>
|
>
|
||||||
|
{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 className="ms-2 text-truncate" style={{ fontWeight: 700, color: '#1f2937', fontSize: 13 }}>{s.dns}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-2" style={{ lineHeight: 1.2 }}>
|
<div className="px-2 py-2" style={{ lineHeight: 1.2 }}>
|
||||||
@@ -160,7 +173,7 @@ function GraphView({ servers, connections }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<ReactFlow
|
||||||
ref={flowRef}
|
ref={flowRef}
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -182,8 +195,52 @@ function GraphView({ servers, connections }) {
|
|||||||
nodeStrokeWidth={2}
|
nodeStrokeWidth={2}
|
||||||
maskColor="rgba(0,0,0,0.05)"
|
maskColor="rgba(0,0,0,0.05)"
|
||||||
/>
|
/>
|
||||||
<Controls position="top-right" showInteractive={false} />
|
|
||||||
</ReactFlow>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user