mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-07 13:45:01 +03:00
chore(frontend): update dependencies and adapt to oxlint 1.79
npm install was failing with ERESOLVE: the lockfile pinned storybook 10.5.7 and vitest 4.1.10 as peers while package.json asked for ^10.5.9 and ^4.1.11, and npm would not move either. Neither npm update, a targeted install, nor --package-lock-only broke the cycle, so node_modules and package-lock.json were regenerated from scratch (601 packages, 0 vulnerabilities). oxlint 1.79.0 then promoted five React Compiler rules into the correctness category, flagging 101 pre-existing sites. 1.78.0 exits 0 on the same tree, so nothing in our code changed - the rule set grew. They are fixed rather than suppressed: - refs (31): latest-value ref writes moved out of render into an effect. onlineClientsRef turned out to be write-only and is gone; expireDiffRef and trafficDiffRef were replaced by reading the values directly. - set-state-in-effect (55): reset-on-open modals now adjust state during render; where an effect mixed a synchronous reset with an async fetch, the reset moved to render and the effect kept only the request. useMediaQuery became useSyncExternalStore. - preserve-manual-memoization (11): optional-chained deps the compiler cannot match, hoisted to locals or dropped where the memo wrapped a string concat. - purity (3): Date.now() in render replaced by a state-backed clock, which also refreshes the expiry tag every 60s instead of freezing it until the next unrelated re-render. - immutability (1): applyClientStatsEvent merged websocket traffic into DBInbound rows in place; it now rebuilds only the rows it touches. Two things fell out of that. clientCount is derived with useMemo instead of an imperative rebuildClientCount() called from five sites, which also fixes a staleness bug where changing the expiry or traffic threshold left the counts alone until some later rebuild. statsVersion existed only to force a re-render after an in-place mutation, is meaningless now that rows are replaced, and nothing read it, so it is removed. Also adds a lint:fix script - oxlint --fix was previously only reachable through the lint-staged hook.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Modal, Select, Tabs, Tag } from 'antd';
|
||||
@@ -147,19 +147,71 @@ function formatFullTimestamp(unixSec: number): string {
|
||||
return `${MM}-${DD} ${time}`;
|
||||
}
|
||||
|
||||
interface MetricsChart {
|
||||
points: number[];
|
||||
labels: string[];
|
||||
timestamps: number[];
|
||||
}
|
||||
|
||||
const EMPTY_CHART: MetricsChart = { points: [], labels: [], timestamps: [] };
|
||||
|
||||
function toChart(msg: Msg<{ t: number; v: number }[]> | null | undefined, bucket: number) {
|
||||
if (!msg?.success || !Array.isArray(msg.obj)) return EMPTY_CHART;
|
||||
const points: number[] = [];
|
||||
const labels: string[] = [];
|
||||
const timestamps: number[] = [];
|
||||
for (const p of msg.obj) {
|
||||
const d = new Date(p.t * 1000);
|
||||
const hh = String(d.getHours()).padStart(2, '0');
|
||||
const mm = String(d.getMinutes()).padStart(2, '0');
|
||||
const ss = String(d.getSeconds()).padStart(2, '0');
|
||||
labels.push(bucket >= 60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`);
|
||||
points.push(Number(p.v) || 0);
|
||||
timestamps.push(Number(p.t) || 0);
|
||||
}
|
||||
return { points, labels, timestamps };
|
||||
}
|
||||
|
||||
async function loadHistory(url: string | null, bucket: number): Promise<MetricsChart> {
|
||||
if (!url) return EMPTY_CHART;
|
||||
try {
|
||||
return toChart(await HttpUtil.get<{ t: number; v: number }[]>(url), bucket);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch xray metrics bucket', e);
|
||||
return EMPTY_CHART;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadState(): Promise<XrayState | null> {
|
||||
try {
|
||||
const msg = await HttpUtil.get<XrayState>('/panel/api/server/xrayMetricsState');
|
||||
return msg?.success && msg.obj ? msg.obj : null;
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch xray metrics state', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadObservatory(): Promise<ObservatoryTag[]> {
|
||||
try {
|
||||
const msg = await HttpUtil.get<ObservatoryTag[]>('/panel/api/server/xrayObservatory');
|
||||
return msg?.success && Array.isArray(msg.obj) ? msg.obj : [];
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch observatory snapshot', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [activeKey, setActiveKey] = useState('xrAlloc');
|
||||
const [bucket, setBucket] = useState(2);
|
||||
const [points, setPoints] = useState<number[]>([]);
|
||||
const [labels, setLabels] = useState<string[]>([]);
|
||||
const [timestamps, setTimestamps] = useState<number[]>([]);
|
||||
const [{ points, labels, timestamps }, setChart] = useState<MetricsChart>(EMPTY_CHART);
|
||||
const [state, setState] = useState<XrayState>({ enabled: false, listen: '', reason: '' });
|
||||
const [obsTags, setObsTags] = useState<ObservatoryTag[]>([]);
|
||||
const [obsActiveTag, setObsActiveTag] = useState('');
|
||||
const obsTimerRef = useRef<number | null>(null);
|
||||
const openRef = useRef(open);
|
||||
const [obsTick, setObsTick] = useState(0);
|
||||
|
||||
const activeMetric = useMemo(() => METRICS.find((m) => m.key === activeKey), [activeKey]);
|
||||
const isObservatory = activeKey === OBS_KEY;
|
||||
@@ -184,151 +236,63 @@ export default function XrayMetricsModal({ open, onClose }: XrayMetricsModalProp
|
||||
[tsLookup],
|
||||
);
|
||||
|
||||
const applyHistory = useCallback(
|
||||
(msg: Msg<{ t: number; v: number }[]> | null | undefined, currentBucket: number) => {
|
||||
if (msg?.success && Array.isArray(msg.obj)) {
|
||||
const vals: number[] = [];
|
||||
const labs: string[] = [];
|
||||
const tss: number[] = [];
|
||||
for (const p of msg.obj) {
|
||||
const d = new Date(p.t * 1000);
|
||||
const hh = String(d.getHours()).padStart(2, '0');
|
||||
const mm = String(d.getMinutes()).padStart(2, '0');
|
||||
const ss = String(d.getSeconds()).padStart(2, '0');
|
||||
labs.push(currentBucket >= 60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`);
|
||||
vals.push(Number(p.v) || 0);
|
||||
tss.push(Number(p.t) || 0);
|
||||
}
|
||||
setLabels(labs);
|
||||
setPoints(vals);
|
||||
setTimestamps(tss);
|
||||
} else {
|
||||
setLabels([]);
|
||||
setPoints([]);
|
||||
setTimestamps([]);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchState = useCallback(async () => {
|
||||
try {
|
||||
const msg = await HttpUtil.get<XrayState>('/panel/api/server/xrayMetricsState');
|
||||
if (msg?.success && msg.obj) setState(msg.obj);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch xray metrics state', e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchObservatory = useCallback(async () => {
|
||||
try {
|
||||
const msg = await HttpUtil.get<ObservatoryTag[]>('/panel/api/server/xrayObservatory');
|
||||
if (msg?.success && Array.isArray(msg.obj)) {
|
||||
const tags = msg.obj;
|
||||
setObsTags(tags);
|
||||
setObsActiveTag((prev) => {
|
||||
if (tags.find((tg) => tg.tag === prev)) return prev;
|
||||
return tags[0]?.tag || '';
|
||||
});
|
||||
} else {
|
||||
setObsTags([]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch observatory snapshot', e);
|
||||
setObsTags([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchMetricBucket = useCallback(async () => {
|
||||
if (!activeMetric) return;
|
||||
try {
|
||||
const url = `/panel/api/server/xrayMetricsHistory/${activeMetric.key}/${bucket}`;
|
||||
const msg = await HttpUtil.get<{ t: number; v: number }[]>(url);
|
||||
applyHistory(msg, bucket);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch xray metrics bucket', e);
|
||||
setLabels([]);
|
||||
setPoints([]);
|
||||
setTimestamps([]);
|
||||
}
|
||||
}, [activeMetric, bucket, applyHistory]);
|
||||
|
||||
const fetchObsBucket = useCallback(async () => {
|
||||
if (!obsActiveTag) {
|
||||
setLabels([]);
|
||||
setPoints([]);
|
||||
setTimestamps([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const url = `/panel/api/server/xrayObservatoryHistory/${encodeURIComponent(obsActiveTag)}/${bucket}`;
|
||||
const msg = await HttpUtil.get<{ t: number; v: number }[]>(url);
|
||||
applyHistory(msg, bucket);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch observatory bucket', e);
|
||||
setLabels([]);
|
||||
setPoints([]);
|
||||
setTimestamps([]);
|
||||
}
|
||||
}, [obsActiveTag, bucket, applyHistory]);
|
||||
|
||||
const stopObsPolling = useCallback(() => {
|
||||
if (obsTimerRef.current != null) {
|
||||
window.clearInterval(obsTimerRef.current);
|
||||
obsTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
openRef.current = open;
|
||||
if (open) {
|
||||
setActiveKey('xrAlloc');
|
||||
fetchState();
|
||||
} else {
|
||||
stopObsPolling();
|
||||
}
|
||||
}, [open, fetchState, stopObsPolling]);
|
||||
const [wasOpen, setWasOpen] = useState(false);
|
||||
if (open !== wasOpen) {
|
||||
setWasOpen(open);
|
||||
if (open) setActiveKey('xrAlloc');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (isObservatory) {
|
||||
fetchObservatory();
|
||||
fetchObsBucket();
|
||||
stopObsPolling();
|
||||
obsTimerRef.current = window.setInterval(async () => {
|
||||
if (!openRef.current || !isObservatory) return;
|
||||
await fetchObservatory();
|
||||
fetchObsBucket();
|
||||
}, 2000);
|
||||
} else {
|
||||
stopObsPolling();
|
||||
fetchMetricBucket();
|
||||
}
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const next = await loadState();
|
||||
if (!cancelled && next) setState(next);
|
||||
})();
|
||||
return () => {
|
||||
stopObsPolling();
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
open,
|
||||
activeKey,
|
||||
isObservatory,
|
||||
fetchObservatory,
|
||||
fetchObsBucket,
|
||||
fetchMetricBucket,
|
||||
stopObsPolling,
|
||||
]);
|
||||
}, [open]);
|
||||
|
||||
// The observatory snapshot is a live view, so it re-polls; obsTick then pulls
|
||||
// the chart along with it.
|
||||
useEffect(() => {
|
||||
if (!open || !isObservatory) return;
|
||||
let cancelled = false;
|
||||
const tick = async () => {
|
||||
const tags = await loadObservatory();
|
||||
if (cancelled) return;
|
||||
setObsTags(tags);
|
||||
setObsActiveTag((prev) => (tags.find((tg) => tg.tag === prev) ? prev : tags[0]?.tag || ''));
|
||||
setObsTick((n) => n + 1);
|
||||
};
|
||||
void tick();
|
||||
const id = window.setInterval(() => void tick(), 2000);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearInterval(id);
|
||||
};
|
||||
}, [open, isObservatory]);
|
||||
|
||||
const historyUrl = isObservatory
|
||||
? obsActiveTag
|
||||
? `/panel/api/server/xrayObservatoryHistory/${encodeURIComponent(obsActiveTag)}/${bucket}`
|
||||
: null
|
||||
: activeMetric
|
||||
? `/panel/api/server/xrayMetricsHistory/${activeMetric.key}/${bucket}`
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (isObservatory) {
|
||||
fetchObsBucket();
|
||||
} else {
|
||||
fetchMetricBucket();
|
||||
}
|
||||
}, [open, bucket, isObservatory, fetchObsBucket, fetchMetricBucket]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && isObservatory) fetchObsBucket();
|
||||
}, [open, obsActiveTag, isObservatory, fetchObsBucket]);
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const next = await loadHistory(historyUrl, bucket);
|
||||
if (!cancelled) setChart(next);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [open, historyUrl, bucket, obsTick]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
Reference in New Issue
Block a user