feat: Enhance GraphView component with connection creation delegation and fullscreen mode handling, and update ServerManager to support new connection logic
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m25s

This commit is contained in:
2025-08-11 09:00:04 +07:00
parent eafd0c0957
commit 7419762681
2 changed files with 58 additions and 7 deletions
+50 -5
View File
@@ -1,4 +1,4 @@
import React, { useMemo, useCallback, useEffect, useRef } from 'react';
import React, { useMemo, useCallback, useEffect, useRef, useState } from 'react';
import {
ReactFlow,
Background,
@@ -7,14 +7,17 @@ import {
useNodesState,
addEdge,
MarkerType,
Handle,
Position,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { IconZoomIn, IconZoomOut, IconMaximize, IconMinimize } from '@tabler/icons-react';
function GraphView({ servers, connections }) {
function GraphView({ servers, connections, onCreateConnection }) {
const flowRef = useRef(null);
const instanceRef = useRef(null);
const containerRef = useRef(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const getTunnelStyle = useCallback((tunnelType) => {
const map = {
@@ -83,7 +86,16 @@ function GraphView({ servers, connections }) {
setEdges(initialEdgesData);
}, [initialEdgesData, setEdges]);
const onConnect = useCallback((params) => setEdges((eds) => addEdge({ ...params, type: 'default' }, eds)), [setEdges]);
const onConnect = useCallback(
(params) => {
// Делегируем создание связи в родителя, чтобы сохранить единую логику хранения
if (!params?.source || !params?.target || params.source === params.target) return;
if (onCreateConnection) {
onCreateConnection({ from: params.source, to: params.target });
}
},
[onCreateConnection]
);
const nodeTypes = useMemo(
() => ({
@@ -110,6 +122,8 @@ function GraphView({ servers, connections }) {
background: '#fff',
}}
>
{/* Точка входа соединений */}
<Handle type="target" position={Position.Left} style={{ background: countryColor }} />
<div className="px-2 py-1 d-flex align-items-center" style={{ background: '#f8fafc', borderBottom: '1px solid #eef2f7' }}>
<div className="d-flex align-items-center" style={{ gap: 6 }}>
<span style={{ fontSize: 16, lineHeight: '1' }}>{flag}</span>
@@ -132,6 +146,8 @@ function GraphView({ servers, connections }) {
<div style={{ fontSize: 12, color: '#334155' }}>{s.ip}</div>
<div style={{ fontSize: 10, color: '#6b7280' }}>{s.provider}</div>
</div>
{/* Точка исхода соединений */}
<Handle type="source" position={Position.Right} style={{ background: countryColor }} />
</div>
);
},
@@ -161,6 +177,23 @@ function GraphView({ servers, connections }) {
return () => clearTimeout(t);
}, [servers, connections]);
// Трек полноэкранного режима и авто-fit после входа/выхода
useEffect(() => {
const handler = () => {
const fs = Boolean(document.fullscreenElement);
setIsFullscreen(fs);
if (instanceRef.current) {
setTimeout(() => {
try {
instanceRef.current.fitView({ padding: 0.2 });
} catch {}
}, 60);
}
};
document.addEventListener('fullscreenchange', handler);
return () => document.removeEventListener('fullscreenchange', handler);
}, []);
if (!servers || servers.length === 0) {
return (
<div className="text-center py-5">
@@ -173,7 +206,18 @@ function GraphView({ servers, connections }) {
}
return (
<div ref={containerRef} style={{ width: '100%', height: 500, border: '1px solid #e5e7eb', borderRadius: 8, overflow: 'hidden', position: 'relative' }}>
<div
ref={containerRef}
style={{
width: '100%',
height: isFullscreen ? '100vh' : 500,
border: '1px solid #e5e7eb',
borderRadius: 8,
overflow: 'hidden',
position: 'relative',
background: '#f8fafc',
}}
>
<ReactFlow
ref={flowRef}
nodes={nodes}
@@ -188,6 +232,7 @@ function GraphView({ servers, connections }) {
markerEnd: { type: MarkerType.ArrowClosed },
}}
onInit={onInit}
style={{ width: '100%', height: '100%', background: '#f8fafc' }}
>
<Background variant="dots" gap={20} size={1} color="#e5e7eb" />
<MiniMap
@@ -237,7 +282,7 @@ function GraphView({ servers, connections }) {
}}
title="Во весь экран"
>
{document.fullscreenElement ? <IconMinimize size={16} /> : <IconMaximize size={16} />}
{isFullscreen ? <IconMinimize size={16} /> : <IconMaximize size={16} />}
</button>
</div>
</div>
+8 -2
View File
@@ -765,7 +765,13 @@ function ServerManager() {
</div>
<GraphView
servers={servers.filter(s => (!graphCountryFilter || s.country === graphCountryFilter) && (!graphProviderFilter || s.provider === graphProviderFilter))}
connections={connections.filter(c => graphTunnelFilters[c.tunnelType])}
connections={connections.filter(c => graphTunnelFilters[c.tunnelType])}
onCreateConnection={({ from, to }) => {
// Открываем модал добавления связи и проставляем выбранные узлы
setNewConnection(prev => ({ ...prev, from, to }));
const el = document.querySelector('#add-connection-anchor');
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}}
/>
<div className="mt-3">
<div className="d-flex align-items-center gap-3 flex-wrap">
@@ -777,7 +783,7 @@ function ServerManager() {
</div>
</div>
</div>
<div className="card w-100 mb-4">
<div id="add-connection-anchor" className="card w-100 mb-4">
<div className="card-header">
<h3 className="card-title mb-0">Добавить связь между серверами</h3>
</div>