mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-28 22:17:13 +00: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:
@@ -38,21 +38,20 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
|
||||
const [outbound, setOutbound] = useState<string | undefined>(undefined);
|
||||
const [rows, setRows] = useState<GeodataAssetRow[]>([]);
|
||||
const [outboundTags, setOutboundTags] = useState<string[]>([]);
|
||||
const templateRef = useRef<Record<string, unknown> | null>(null);
|
||||
const [template, setTemplate] = useState<Record<string, unknown> | null>(null);
|
||||
const outboundTestUrlRef = useRef('');
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post('/panel/api/xray/', undefined, { silent: true });
|
||||
if (!msg?.success || typeof msg.obj !== 'string') return;
|
||||
const payload = JSON.parse(msg.obj) as Record<string, unknown>;
|
||||
const template = (payload.xraySetting || {}) as Record<string, unknown>;
|
||||
templateRef.current = template;
|
||||
const next = (payload.xraySetting || {}) as Record<string, unknown>;
|
||||
setTemplate(next);
|
||||
outboundTestUrlRef.current =
|
||||
typeof payload.outboundTestUrl === 'string' ? payload.outboundTestUrl : '';
|
||||
|
||||
const geodata = (template.geodata || {}) as Record<string, unknown>;
|
||||
const geodata = (next.geodata || {}) as Record<string, unknown>;
|
||||
const assets = Array.isArray(geodata.assets) ? geodata.assets : [];
|
||||
setRows(
|
||||
assets
|
||||
@@ -67,7 +66,7 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
|
||||
// Download outbound candidates: template outbounds + subscription outbounds.
|
||||
// Skip blackhole outbounds — routing a download through one just drops it.
|
||||
const tags = new Set<string>();
|
||||
const outbounds = Array.isArray(template.outbounds) ? template.outbounds : [];
|
||||
const outbounds = Array.isArray(next.outbounds) ? next.outbounds : [];
|
||||
for (const o of outbounds) {
|
||||
if (!o || typeof o !== 'object') continue;
|
||||
const rec = o as Record<string, unknown>;
|
||||
@@ -87,8 +86,14 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [wasActive, setWasActive] = useState(false);
|
||||
if (active !== wasActive) {
|
||||
setWasActive(active);
|
||||
if (active) setLoading(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (active) load();
|
||||
if (active) void load();
|
||||
}, [active, load]);
|
||||
|
||||
function setRow(index: number, patch: Partial<GeodataAssetRow>) {
|
||||
@@ -102,7 +107,6 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
|
||||
}
|
||||
|
||||
function save() {
|
||||
const template = templateRef.current;
|
||||
if (!template) return;
|
||||
const assets = rows
|
||||
.map((r) => ({ url: r.url.trim(), file: r.file.trim() }))
|
||||
@@ -213,7 +217,7 @@ export default function GeodataSection({ active, onBusy, onClose }: GeodataSecti
|
||||
>
|
||||
{t('pages.index.geodataAddFile')}
|
||||
</Button>
|
||||
<Button type="primary" onClick={save} disabled={loading || !templateRef.current}>
|
||||
<Button type="primary" onClick={save} disabled={loading || !template}>
|
||||
{t('pages.index.geodataSaveRestart')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -25,10 +25,8 @@ export default function LogModal({ open, onClose }: LogModalProps) {
|
||||
const [autoUpdate, setAutoUpdate] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const openRef = useRef(open);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const runRefresh = useCallback(async () => {
|
||||
try {
|
||||
const msg = await HttpUtil.post<string[]>(`/panel/api/server/logs/${rows}`, {
|
||||
level,
|
||||
@@ -43,19 +41,28 @@ export default function LogModal({ open, onClose }: LogModalProps) {
|
||||
}
|
||||
}, [rows, level, syslog]);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
setLoading(true);
|
||||
void runRefresh();
|
||||
}, [runRefresh]);
|
||||
|
||||
const refreshRef = useRef(refresh);
|
||||
useEffect(() => {
|
||||
refreshRef.current = refresh;
|
||||
}, [refresh]);
|
||||
});
|
||||
|
||||
// The spinner is raised during render so the fetch effect stays side-effect
|
||||
// free until its response lands.
|
||||
const refreshKey = open ? `${rows}\u0000${level}\u0000${syslog}` : null;
|
||||
const [loadingKey, setLoadingKey] = useState<string | null>(null);
|
||||
if (refreshKey !== loadingKey) {
|
||||
setLoadingKey(refreshKey);
|
||||
if (refreshKey) setLoading(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
openRef.current = open;
|
||||
if (open) refresh();
|
||||
}, [open, refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (openRef.current) refresh();
|
||||
}, [rows, level, syslog, refresh]);
|
||||
if (open) void runRefresh();
|
||||
}, [open, runRefresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !autoUpdate) return;
|
||||
|
||||
@@ -200,16 +200,74 @@ function formatFullTimestamp(unixSec: number): string {
|
||||
return `${MM}-${DD} ${time}`;
|
||||
}
|
||||
|
||||
interface HistoryChart {
|
||||
points: number[];
|
||||
points2: number[];
|
||||
points3: number[];
|
||||
labels: string[];
|
||||
timestamps: number[];
|
||||
}
|
||||
|
||||
const EMPTY_CHART: HistoryChart = {
|
||||
points: [],
|
||||
points2: [],
|
||||
points3: [],
|
||||
labels: [],
|
||||
timestamps: [],
|
||||
};
|
||||
|
||||
async function loadBucket(metric: (typeof METRICS)[number], bucket: number): Promise<HistoryChart> {
|
||||
try {
|
||||
const msg = await HttpUtil.get(`/panel/api/server/history/${metric.key}/${bucket}`);
|
||||
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 MM = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const DD = String(d.getDate()).padStart(2, '0');
|
||||
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 >= 2880
|
||||
? `${MM}-${DD} ${hh}:${mm}`
|
||||
: bucket >= 60
|
||||
? `${hh}:${mm}`
|
||||
: `${hh}:${mm}:${ss}`,
|
||||
);
|
||||
points.push(Number(p.v) || 0);
|
||||
timestamps.push(Number(p.t) || 0);
|
||||
}
|
||||
const fetchAligned = async (key?: string): Promise<number[]> => {
|
||||
if (!key) return [];
|
||||
const m = await HttpUtil.get(`/panel/api/server/history/${key}/${bucket}`);
|
||||
if (!m?.success || !Array.isArray(m.obj)) return [];
|
||||
const byTs = new Map<number, number>();
|
||||
for (const p of m.obj) byTs.set(Number(p.t) || 0, Number(p.v) || 0);
|
||||
return timestamps.map((ts) => byTs.get(ts) ?? 0);
|
||||
};
|
||||
return {
|
||||
labels,
|
||||
points,
|
||||
timestamps,
|
||||
points2: await fetchAligned(metric.key2),
|
||||
points3: await fetchAligned(metric.key3),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch history bucket', e);
|
||||
return EMPTY_CHART;
|
||||
}
|
||||
}
|
||||
|
||||
export default function SystemHistoryModal({ open, status, onClose }: SystemHistoryModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [activeKey, setActiveKey] = useState('cpu');
|
||||
const [bucket, setBucket] = useState(2);
|
||||
const [points, setPoints] = useState<number[]>([]);
|
||||
const [points2, setPoints2] = useState<number[]>([]);
|
||||
const [points3, setPoints3] = useState<number[]>([]);
|
||||
const [labels, setLabels] = useState<string[]>([]);
|
||||
const [timestamps, setTimestamps] = useState<number[]>([]);
|
||||
const [{ points, points2, points3, labels, timestamps }, setChart] =
|
||||
useState<HistoryChart>(EMPTY_CHART);
|
||||
|
||||
const activeMetric = useMemo(() => METRICS.find((m) => m.key === activeKey), [activeKey]);
|
||||
const trName = (n?: string) => (n && n.startsWith('pages.') ? t(n) : n);
|
||||
@@ -237,70 +295,27 @@ export default function SystemHistoryModal({ open, status, onClose }: SystemHist
|
||||
|
||||
const fetchBucket = useCallback(async () => {
|
||||
if (!activeMetric) return;
|
||||
try {
|
||||
const url = `/panel/api/server/history/${activeMetric.key}/${bucket}`;
|
||||
const msg = await HttpUtil.get(url);
|
||||
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 MM = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const DD = String(d.getDate()).padStart(2, '0');
|
||||
const hh = String(d.getHours()).padStart(2, '0');
|
||||
const mm = String(d.getMinutes()).padStart(2, '0');
|
||||
const ss = String(d.getSeconds()).padStart(2, '0');
|
||||
const lab =
|
||||
bucket >= 2880
|
||||
? `${MM}-${DD} ${hh}:${mm}`
|
||||
: bucket >= 60
|
||||
? `${hh}:${mm}`
|
||||
: `${hh}:${mm}:${ss}`;
|
||||
labs.push(lab);
|
||||
vals.push(Number(p.v) || 0);
|
||||
tss.push(Number(p.t) || 0);
|
||||
}
|
||||
setLabels(labs);
|
||||
setPoints(vals);
|
||||
setTimestamps(tss);
|
||||
|
||||
const fetchAligned = async (key?: string): Promise<number[]> => {
|
||||
if (!key) return [];
|
||||
const m = await HttpUtil.get(`/panel/api/server/history/${key}/${bucket}`);
|
||||
if (m?.success && Array.isArray(m.obj)) {
|
||||
const byTs = new Map<number, number>();
|
||||
for (const p of m.obj) byTs.set(Number(p.t) || 0, Number(p.v) || 0);
|
||||
return tss.map((ts) => byTs.get(ts) ?? 0);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
setPoints2(await fetchAligned(activeMetric.key2));
|
||||
setPoints3(await fetchAligned(activeMetric.key3));
|
||||
} else {
|
||||
setLabels([]);
|
||||
setPoints([]);
|
||||
setPoints2([]);
|
||||
setPoints3([]);
|
||||
setTimestamps([]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch history bucket', e);
|
||||
setLabels([]);
|
||||
setPoints([]);
|
||||
setPoints2([]);
|
||||
setPoints3([]);
|
||||
setTimestamps([]);
|
||||
}
|
||||
const next = await loadBucket(activeMetric, bucket);
|
||||
setChart(next);
|
||||
}, [activeMetric, bucket]);
|
||||
|
||||
useEffect(() => {
|
||||
const [wasOpen, setWasOpen] = useState(false);
|
||||
if (open !== wasOpen) {
|
||||
setWasOpen(open);
|
||||
if (open) setActiveKey('cpu');
|
||||
}, [open]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open) fetchBucket();
|
||||
}, [open, activeKey, bucket, fetchBucket]);
|
||||
if (!open || !activeMetric) return;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const next = await loadBucket(activeMetric, bucket);
|
||||
if (!cancelled) setChart(next);
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [open, activeMetric, bucket]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
|
||||
@@ -38,7 +38,6 @@ export default function VersionModal({ open, status, onClose, onBusy }: VersionM
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchVersions = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const msg = await HttpUtil.get<string[]>('/panel/api/server/getXrayVersion');
|
||||
if (msg?.success) setVersions(msg.obj || []);
|
||||
@@ -47,8 +46,14 @@ export default function VersionModal({ open, status, onClose, onBusy }: VersionM
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [wasOpen, setWasOpen] = useState(false);
|
||||
if (open !== wasOpen) {
|
||||
setWasOpen(open);
|
||||
if (open) setLoading(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open) fetchVersions();
|
||||
if (open) void fetchVersions();
|
||||
}, [open, fetchVersions]);
|
||||
|
||||
function switchXrayVersion(version: string) {
|
||||
|
||||
@@ -73,12 +73,10 @@ export default function XrayLogModal({ open, onClose }: XrayLogModalProps) {
|
||||
const [autoUpdate, setAutoUpdate] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [logs, setLogs] = useState<XrayLogEntry[]>([]);
|
||||
const openRef = useRef(open);
|
||||
|
||||
const orderedLogs = useMemo(() => [...logs].reverse(), [logs]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const runRefresh = useCallback(async () => {
|
||||
try {
|
||||
const msg = await HttpUtil.post<XrayLogEntry[]>(`/panel/api/server/xraylogs/${rows}`, {
|
||||
filter,
|
||||
@@ -93,19 +91,30 @@ export default function XrayLogModal({ open, onClose }: XrayLogModalProps) {
|
||||
}
|
||||
}, [rows, filter, showDirect, showBlocked, showProxy]);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
setLoading(true);
|
||||
void runRefresh();
|
||||
}, [runRefresh]);
|
||||
|
||||
const refreshRef = useRef(refresh);
|
||||
useEffect(() => {
|
||||
refreshRef.current = refresh;
|
||||
}, [refresh]);
|
||||
});
|
||||
|
||||
// The spinner is raised during render so the fetch effect stays side-effect
|
||||
// free until its response lands.
|
||||
const refreshKey = open
|
||||
? `${rows}\u0000${showDirect}\u0000${showBlocked}\u0000${showProxy}`
|
||||
: null;
|
||||
const [loadingKey, setLoadingKey] = useState<string | null>(null);
|
||||
if (refreshKey !== loadingKey) {
|
||||
setLoadingKey(refreshKey);
|
||||
if (refreshKey) setLoading(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
openRef.current = open;
|
||||
if (open) refresh();
|
||||
}, [open, refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (openRef.current) refresh();
|
||||
}, [rows, showDirect, showBlocked, showProxy, refresh]);
|
||||
if (open) void runRefresh();
|
||||
}, [open, rows, showDirect, showBlocked, showProxy, runRefresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !autoUpdate) return;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -128,8 +128,11 @@ export function useOverviewHistory(status: Status, hasData: boolean): OverviewHi
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasData) return;
|
||||
// Each polled status is appended during render; an effect would show the
|
||||
// chart one sample behind the numbers beside it.
|
||||
const [sampledStatus, setSampledStatus] = useState<Status | null>(null);
|
||||
if (hasData && status !== sampledStatus) {
|
||||
setSampledStatus(status);
|
||||
setTrend((prev) => {
|
||||
const point = sampleOf(status);
|
||||
const next = emptyWindow();
|
||||
@@ -139,7 +142,7 @@ export function useOverviewHistory(status: Status, hasData: boolean): OverviewHi
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [status, hasData]);
|
||||
}
|
||||
|
||||
const labels = useMemo(() => trend.times.map(TimeFormatter.formatClock), [trend.times]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user