feat(NetworkMapDashboard, SettingsPage): implement ping cache duration settings for network map and enhance ping request handling
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m5s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m5s
This commit is contained in:
@@ -29,6 +29,8 @@ export default function NetworkMapDashboard() {
|
|||||||
const [servers, setServers] = useState([]);
|
const [servers, setServers] = useState([]);
|
||||||
const [connections, setConnections] = useState([]);
|
const [connections, setConnections] = useState([]);
|
||||||
const [pingMap, setPingMap] = useState({});
|
const [pingMap, setPingMap] = useState({});
|
||||||
|
const pingTimestampsRef = useRef({});
|
||||||
|
const [pingCacheSeconds, setPingCacheSeconds] = useState(0);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [pingLoading, setPingLoading] = useState(false);
|
const [pingLoading, setPingLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@@ -39,13 +41,20 @@ export default function NetworkMapDashboard() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const [serversRes, configRes] = await Promise.all([
|
const [serversRes, configRes, uiSettingsRes] = await Promise.all([
|
||||||
api.get('/servers'),
|
api.get('/servers'),
|
||||||
api.get('/network-config').catch(() => ({ data: null })),
|
api.get('/network-config').catch(() => ({ data: null })),
|
||||||
|
api.get('/ui-settings').catch(() => ({ data: {} })),
|
||||||
]);
|
]);
|
||||||
const serversList = Array.isArray(serversRes?.data) ? serversRes.data : [];
|
const serversList = Array.isArray(serversRes?.data) ? serversRes.data : [];
|
||||||
const config = configRes?.data || {};
|
const config = configRes?.data || {};
|
||||||
const tunnelInterfaces = Array.isArray(config.tunnelInterfaces) ? config.tunnelInterfaces : [];
|
const tunnelInterfaces = Array.isArray(config.tunnelInterfaces) ? config.tunnelInterfaces : [];
|
||||||
|
const uiSettings = uiSettingsRes?.data || {};
|
||||||
|
const ttlSeconds = Math.max(
|
||||||
|
0,
|
||||||
|
parseInt(uiSettings.networkMapPingCacheSeconds, 10) || 0
|
||||||
|
);
|
||||||
|
setPingCacheSeconds(ttlSeconds);
|
||||||
|
|
||||||
const getServer = (serverId) =>
|
const getServer = (serverId) =>
|
||||||
serversList.find((s) => s.id === serverId || s.ip === serverId || s.dns === serverId);
|
serversList.find((s) => s.id === serverId || s.ip === serverId || s.dns === serverId);
|
||||||
@@ -88,7 +97,7 @@ export default function NetworkMapDashboard() {
|
|||||||
pingAbortRef.current = false;
|
pingAbortRef.current = false;
|
||||||
setPingLoading(true);
|
setPingLoading(true);
|
||||||
const key = (a, b) => `${a}:${b}`;
|
const key = (a, b) => `${a}:${b}`;
|
||||||
const newMap = {};
|
const ttlMs = Math.max(0, (pingCacheSeconds || 0) * 1000);
|
||||||
|
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
connections.forEach((c) => {
|
connections.forEach((c) => {
|
||||||
@@ -102,24 +111,42 @@ export default function NetworkMapDashboard() {
|
|||||||
const canPingTo =
|
const canPingTo =
|
||||||
srcTo && ['jumphost', 'home'].includes(String(srcTo.type || '').toLowerCase()) && c.internalToFrom;
|
srcTo && ['jumphost', 'home'].includes(String(srcTo.type || '').toLowerCase()) && c.internalToFrom;
|
||||||
|
|
||||||
// Пингуем по ВНУТРЕННИМ адресам туннеля из /network-config:
|
// Проверка кеша: если TTL не истёк, не запускаем новый запрос
|
||||||
// server1 -> remoteIp, server2 -> localIp. serverId — ключ (id/dns/ip) как в servers.json.
|
const keyFromTo = key(from, to);
|
||||||
|
const keyToFrom = key(to, from);
|
||||||
|
const now = Date.now();
|
||||||
|
if (ttlMs > 0) {
|
||||||
|
const tsFrom = pingTimestampsRef.current[keyFromTo];
|
||||||
|
if (tsFrom && now - tsFrom < ttlMs) {
|
||||||
|
// уже есть актуальное значение для from->to
|
||||||
|
} else if (!canPingFrom) {
|
||||||
|
// если пинговать нельзя, но кеша нет — оставляем как есть
|
||||||
|
}
|
||||||
|
const tsTo = pingTimestampsRef.current[keyToFrom];
|
||||||
|
if (tsTo && now - tsTo < ttlMs) {
|
||||||
|
// уже есть актуальное значение для to->from
|
||||||
|
} else if (!canPingTo) {
|
||||||
|
// нельзя пинговать и кеша нет — пропускаем
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пингуем по ВНУТРЕННИМ адресам туннеля из /network-config
|
||||||
if (canPingFrom) {
|
if (canPingFrom) {
|
||||||
tasks.push(async () => {
|
tasks.push(async () => {
|
||||||
if (pingAbortRef.current) return;
|
if (pingAbortRef.current) return;
|
||||||
try {
|
try {
|
||||||
const { data } = await api.post('/mikrotik/ping', {
|
const { data } = await api.post('/mikrotik/ping', {
|
||||||
serverId: c.fromKey,
|
serverId: c.fromKey,
|
||||||
// ВАЖНО: передаём внутренний IP и как target, и как gatewayIp,
|
|
||||||
// чтобы backend мог по gatewayIp найти нужный tunnel interface.
|
|
||||||
target: c.internalFromTo,
|
target: c.internalFromTo,
|
||||||
gatewayIp: c.internalFromTo,
|
gatewayIp: c.internalFromTo,
|
||||||
count: 3,
|
count: 3,
|
||||||
});
|
});
|
||||||
const ms = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null;
|
const ms = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null;
|
||||||
return { key: key(from, to), ms };
|
pingTimestampsRef.current[keyFromTo] = Date.now();
|
||||||
|
setPingMap((prev) => ({ ...prev, [keyFromTo]: ms }));
|
||||||
} catch {
|
} catch {
|
||||||
return { key: key(from, to), ms: null };
|
pingTimestampsRef.current[keyFromTo] = Date.now();
|
||||||
|
setPingMap((prev) => ({ ...prev, [keyFromTo]: null }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -130,29 +157,25 @@ export default function NetworkMapDashboard() {
|
|||||||
try {
|
try {
|
||||||
const { data } = await api.post('/mikrotik/ping', {
|
const { data } = await api.post('/mikrotik/ping', {
|
||||||
serverId: c.toKey,
|
serverId: c.toKey,
|
||||||
// Аналогично для обратного направления
|
|
||||||
target: c.internalToFrom,
|
target: c.internalToFrom,
|
||||||
gatewayIp: c.internalToFrom,
|
gatewayIp: c.internalToFrom,
|
||||||
count: 3,
|
count: 3,
|
||||||
});
|
});
|
||||||
const ms = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null;
|
const ms = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null;
|
||||||
return { key: key(to, from), ms };
|
pingTimestampsRef.current[keyToFrom] = Date.now();
|
||||||
|
setPingMap((prev) => ({ ...prev, [keyToFrom]: ms }));
|
||||||
} catch {
|
} catch {
|
||||||
return { key: key(to, from), ms: null };
|
pingTimestampsRef.current[keyToFrom] = Date.now();
|
||||||
|
setPingMap((prev) => ({ ...prev, [keyToFrom]: null }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const results = await runWithLimit(tasks, 4);
|
await runWithLimit(tasks, 4);
|
||||||
if (pingAbortRef.current) return;
|
if (pingAbortRef.current) return;
|
||||||
results.forEach((r) => {
|
|
||||||
const v = r?.value;
|
|
||||||
if (v && v.key != null) newMap[v.key] = v.ms ?? null;
|
|
||||||
});
|
|
||||||
setPingMap((prev) => ({ ...prev, ...newMap }));
|
|
||||||
setPingLoading(false);
|
setPingLoading(false);
|
||||||
}, [connections]);
|
}, [connections, servers, pingCacheSeconds]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export default function SettingsPage() {
|
|||||||
const [baseAS, setBaseAS] = useState('65001');
|
const [baseAS, setBaseAS] = useState('65001');
|
||||||
const [pingDomain, setPingDomain] = useState('');
|
const [pingDomain, setPingDomain] = useState('');
|
||||||
const [pingCacheMinutes, setPingCacheMinutes] = useState('');
|
const [pingCacheMinutes, setPingCacheMinutes] = useState('');
|
||||||
|
const [networkMapPingCacheSeconds, setNetworkMapPingCacheSeconds] = useState('');
|
||||||
const [ptrZoneReplaceFrom, setPtrZoneReplaceFrom] = useState('');
|
const [ptrZoneReplaceFrom, setPtrZoneReplaceFrom] = useState('');
|
||||||
const [ptrZoneReplaceTo, setPtrZoneReplaceTo] = useState('');
|
const [ptrZoneReplaceTo, setPtrZoneReplaceTo] = useState('');
|
||||||
const [pingServicesSource, setPingServicesSource] = useState('web');
|
const [pingServicesSource, setPingServicesSource] = useState('web');
|
||||||
@@ -206,6 +207,11 @@ export default function SettingsPage() {
|
|||||||
setPingCacheMinutes(
|
setPingCacheMinutes(
|
||||||
data?.pingCacheMinutes != null ? String(data.pingCacheMinutes) : ''
|
data?.pingCacheMinutes != null ? String(data.pingCacheMinutes) : ''
|
||||||
);
|
);
|
||||||
|
setNetworkMapPingCacheSeconds(
|
||||||
|
data?.networkMapPingCacheSeconds != null
|
||||||
|
? String(data.networkMapPingCacheSeconds)
|
||||||
|
: ''
|
||||||
|
);
|
||||||
setPtrZoneReplaceFrom(String(data?.ptrZoneReplaceFrom || ''));
|
setPtrZoneReplaceFrom(String(data?.ptrZoneReplaceFrom || ''));
|
||||||
setPtrZoneReplaceTo(String(data?.ptrZoneReplaceTo || ''));
|
setPtrZoneReplaceTo(String(data?.ptrZoneReplaceTo || ''));
|
||||||
setPingServicesSource(
|
setPingServicesSource(
|
||||||
@@ -300,6 +306,10 @@ export default function SettingsPage() {
|
|||||||
baseAS: String(baseAS || '65001').trim(),
|
baseAS: String(baseAS || '65001').trim(),
|
||||||
pingDomain: String(pingDomain || '').trim(),
|
pingDomain: String(pingDomain || '').trim(),
|
||||||
pingCacheMinutes: Math.max(0, parseInt(pingCacheMinutes, 10) || 0),
|
pingCacheMinutes: Math.max(0, parseInt(pingCacheMinutes, 10) || 0),
|
||||||
|
networkMapPingCacheSeconds: Math.max(
|
||||||
|
0,
|
||||||
|
parseInt(networkMapPingCacheSeconds, 10) || 0
|
||||||
|
),
|
||||||
ptrZoneReplaceFrom: String(ptrZoneReplaceFrom || '').trim(),
|
ptrZoneReplaceFrom: String(ptrZoneReplaceFrom || '').trim(),
|
||||||
ptrZoneReplaceTo: String(ptrZoneReplaceTo || '').trim(),
|
ptrZoneReplaceTo: String(ptrZoneReplaceTo || '').trim(),
|
||||||
pingServicesSource:
|
pingServicesSource:
|
||||||
@@ -528,6 +538,19 @@ export default function SettingsPage() {
|
|||||||
min={0}
|
min={0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<FormField
|
||||||
|
label="Кеш пингов на карте сети (сек)"
|
||||||
|
name="networkMapPingCacheSeconds"
|
||||||
|
type="number"
|
||||||
|
value={networkMapPingCacheSeconds}
|
||||||
|
onChange={setNetworkMapPingCacheSeconds}
|
||||||
|
placeholder="0"
|
||||||
|
helpText="0 — без кеша. Иначе пинги между серверами на карте сети кешируются на указанный срок."
|
||||||
|
disabled={saving}
|
||||||
|
min={0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user