mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
feat(frontend): route useXraySetting fetches through TanStack Query
Keeps the bidirectional xraySetting ↔ templateSettings editor sync and
the 1s dirty-tracking interval intact (those are local editor state,
not server data). All seven server calls move:
- config + traffic → useQuery on ['xray', 'config'] and
['xray', 'outboundsTraffic']
- saveAll → useMutation that invalidates the config query
- resetOutboundsTraffic → useMutation that invalidates the traffic
query
- restartXray → useMutation (fires the restart, then reads the
result string)
- resetToDefault → useMutation (fetch default config, push it into
the editor via setTemplateSettings)
The WebSocket 'outbounds' event already lands in
keys.xray.outboundsTraffic() via the bridge, so XrayPage drops its
useWebSocket({ outbounds: applyOutboundsEvent }) wiring entirely and
the hook no longer exposes applyOutboundsEvent.
A useEffect seeds xraySetting / templateSettings / tags / test URL
from query data on first fetch and on every refetch, mirroring what
the original fetchAll() did.
This commit is contained in:
parent
967b9aba4b
commit
6a6f44c884
4 changed files with 133 additions and 100 deletions
|
|
@ -22,4 +22,9 @@ export const keys = {
|
||||||
onlines: () => ['clients', 'onlines'] as const,
|
onlines: () => ['clients', 'onlines'] as const,
|
||||||
lastOnline: () => ['clients', 'lastOnline'] as const,
|
lastOnline: () => ['clients', 'lastOnline'] as const,
|
||||||
},
|
},
|
||||||
|
xray: {
|
||||||
|
root: () => ['xray'] as const,
|
||||||
|
config: () => ['xray', 'config'] as const,
|
||||||
|
outboundsTraffic: () => ['xray', 'outboundsTraffic'] as const,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export function useWebSocketBridge() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onOutbounds: Handler = (payload) => {
|
const onOutbounds: Handler = (payload) => {
|
||||||
queryClient.setQueryData(['xray', 'outboundsTraffic'], payload);
|
queryClient.setQueryData(keys.xray.outboundsTraffic(), payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onNodes: Handler = (payload) => {
|
const onNodes: Handler = (payload) => {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { HttpUtil, PromiseUtil } from '@/utils';
|
import { HttpUtil, PromiseUtil } from '@/utils';
|
||||||
|
import { keys } from '@/api/queryKeys';
|
||||||
|
|
||||||
const DIRTY_POLL_MS = 1000;
|
const DIRTY_POLL_MS = 1000;
|
||||||
|
const DEFAULT_TEST_URL = 'https://www.google.com/generate_204';
|
||||||
|
|
||||||
export interface OutboundTrafficRow {
|
export interface OutboundTrafficRow {
|
||||||
tag: string;
|
tag: string;
|
||||||
|
|
@ -70,7 +73,6 @@ export interface UseXraySettingResult {
|
||||||
fetchAll: () => Promise<void>;
|
fetchAll: () => Promise<void>;
|
||||||
fetchOutboundsTraffic: () => Promise<void>;
|
fetchOutboundsTraffic: () => Promise<void>;
|
||||||
resetOutboundsTraffic: (tag: string) => Promise<void>;
|
resetOutboundsTraffic: (tag: string) => Promise<void>;
|
||||||
applyOutboundsEvent: (payload: unknown) => void;
|
|
||||||
testOutbound: (
|
testOutbound: (
|
||||||
index: number,
|
index: number,
|
||||||
outbound: unknown,
|
outbound: unknown,
|
||||||
|
|
@ -82,18 +84,59 @@ export interface UseXraySettingResult {
|
||||||
restartXray: () => Promise<void>;
|
restartXray: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApiMsg<T = unknown> {
|
||||||
|
success?: boolean;
|
||||||
|
obj?: T;
|
||||||
|
msg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface XrayConfigPayload {
|
||||||
|
xraySetting: XraySettingsValue;
|
||||||
|
inboundTags?: string[];
|
||||||
|
clientReverseTags?: string[];
|
||||||
|
outboundTestUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchXrayConfig(): Promise<XrayConfigPayload> {
|
||||||
|
const msg = await HttpUtil.post('/panel/xray/', undefined, { silent: true }) as ApiMsg<string>;
|
||||||
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to load xray config');
|
||||||
|
if (typeof msg.obj !== 'string') throw new Error('Malformed xray config response: expected string');
|
||||||
|
try {
|
||||||
|
return JSON.parse(msg.obj) as XrayConfigPayload;
|
||||||
|
} catch (e) {
|
||||||
|
const err = e as Error;
|
||||||
|
throw new Error(`Malformed xray config response: ${err.message}`, { cause: e });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchOutboundsTraffic(): Promise<OutboundTrafficRow[]> {
|
||||||
|
const msg = await HttpUtil.get('/panel/xray/getOutboundsTraffic', undefined, { silent: true }) as ApiMsg<OutboundTrafficRow[]>;
|
||||||
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch outbounds traffic');
|
||||||
|
return Array.isArray(msg.obj) ? msg.obj : [];
|
||||||
|
}
|
||||||
|
|
||||||
export function useXraySetting(): UseXraySettingResult {
|
export function useXraySetting(): UseXraySettingResult {
|
||||||
const [fetched, setFetched] = useState(false);
|
const queryClient = useQueryClient();
|
||||||
const [spinning, setSpinning] = useState(false);
|
|
||||||
|
const configQuery = useQuery({
|
||||||
|
queryKey: keys.xray.config(),
|
||||||
|
queryFn: fetchXrayConfig,
|
||||||
|
staleTime: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
|
const trafficQuery = useQuery({
|
||||||
|
queryKey: keys.xray.outboundsTraffic(),
|
||||||
|
queryFn: fetchOutboundsTraffic,
|
||||||
|
staleTime: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
const [saveDisabled, setSaveDisabled] = useState(true);
|
const [saveDisabled, setSaveDisabled] = useState(true);
|
||||||
const [fetchError, setFetchError] = useState('');
|
|
||||||
const [xraySetting, setXraySettingState] = useState('');
|
const [xraySetting, setXraySettingState] = useState('');
|
||||||
const [templateSettings, setTemplateSettingsState] = useState<XraySettingsValue | null>(null);
|
const [templateSettings, setTemplateSettingsState] = useState<XraySettingsValue | null>(null);
|
||||||
const [outboundTestUrl, setOutboundTestUrlState] = useState('https://www.google.com/generate_204');
|
const [outboundTestUrl, setOutboundTestUrlState] = useState(DEFAULT_TEST_URL);
|
||||||
const [inboundTags, setInboundTags] = useState<string[]>([]);
|
const [inboundTags, setInboundTags] = useState<string[]>([]);
|
||||||
const [clientReverseTags, setClientReverseTags] = useState<string[]>([]);
|
const [clientReverseTags, setClientReverseTags] = useState<string[]>([]);
|
||||||
const [restartResult, setRestartResult] = useState('');
|
const [restartResult, setRestartResult] = useState('');
|
||||||
const [outboundsTraffic, setOutboundsTraffic] = useState<OutboundTrafficRow[]>([]);
|
|
||||||
const [outboundTestStates, setOutboundTestStates] = useState<Record<number, OutboundTestState>>({});
|
const [outboundTestStates, setOutboundTestStates] = useState<Record<number, OutboundTestState>>({});
|
||||||
const [testingAll, setTestingAll] = useState(false);
|
const [testingAll, setTestingAll] = useState(false);
|
||||||
|
|
||||||
|
|
@ -108,6 +151,28 @@ export function useXraySetting(): UseXraySettingResult {
|
||||||
outboundTestUrlRef.current = outboundTestUrl;
|
outboundTestUrlRef.current = outboundTestUrl;
|
||||||
templateSettingsRef.current = templateSettings;
|
templateSettingsRef.current = templateSettings;
|
||||||
|
|
||||||
|
// Seed local editor state from the config query. Runs on first fetch and
|
||||||
|
// every time the query refetches (e.g. after a successful save).
|
||||||
|
useEffect(() => {
|
||||||
|
if (!configQuery.data) return;
|
||||||
|
const obj = configQuery.data;
|
||||||
|
const pretty = JSON.stringify(obj.xraySetting, null, 2);
|
||||||
|
syncingRef.current = true;
|
||||||
|
setXraySettingState(pretty);
|
||||||
|
setTemplateSettingsState(obj.xraySetting);
|
||||||
|
oldXraySettingRef.current = pretty;
|
||||||
|
syncingRef.current = false;
|
||||||
|
setInboundTags(obj.inboundTags || []);
|
||||||
|
setClientReverseTags(obj.clientReverseTags || []);
|
||||||
|
const nextUrl = obj.outboundTestUrl || DEFAULT_TEST_URL;
|
||||||
|
setOutboundTestUrlState(nextUrl);
|
||||||
|
oldOutboundTestUrlRef.current = nextUrl;
|
||||||
|
setSaveDisabled(true);
|
||||||
|
}, [configQuery.data]);
|
||||||
|
|
||||||
|
const fetched = configQuery.data !== undefined || configQuery.isError;
|
||||||
|
const fetchError = configQuery.error ? (configQuery.error as Error).message : '';
|
||||||
|
|
||||||
const setXraySetting = useCallback((next: string) => {
|
const setXraySetting = useCallback((next: string) => {
|
||||||
setXraySettingState(next);
|
setXraySettingState(next);
|
||||||
if (syncingRef.current) return;
|
if (syncingRef.current) return;
|
||||||
|
|
@ -142,63 +207,59 @@ export function useXraySetting(): UseXraySettingResult {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchAll = useCallback(async () => {
|
const fetchAll = useCallback(async () => {
|
||||||
setFetchError('');
|
await queryClient.invalidateQueries({ queryKey: keys.xray.config() });
|
||||||
const msg = await HttpUtil.post('/panel/xray/');
|
}, [queryClient]);
|
||||||
if (!msg?.success) {
|
|
||||||
setFetchError(msg?.msg || 'Failed to load xray config');
|
|
||||||
setFetched(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let obj;
|
|
||||||
try {
|
|
||||||
obj = JSON.parse(msg.obj);
|
|
||||||
} catch (e) {
|
|
||||||
const err = e as Error;
|
|
||||||
setFetchError(`Malformed xray config response: ${err?.message || String(err)}`);
|
|
||||||
setFetched(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pretty = JSON.stringify(obj.xraySetting, null, 2);
|
|
||||||
syncingRef.current = true;
|
|
||||||
setXraySettingState(pretty);
|
|
||||||
setTemplateSettingsState(obj.xraySetting);
|
|
||||||
oldXraySettingRef.current = pretty;
|
|
||||||
syncingRef.current = false;
|
|
||||||
setInboundTags(obj.inboundTags || []);
|
|
||||||
setClientReverseTags(obj.clientReverseTags || []);
|
|
||||||
const nextUrl = obj.outboundTestUrl || 'https://www.google.com/generate_204';
|
|
||||||
setOutboundTestUrlState(nextUrl);
|
|
||||||
oldOutboundTestUrlRef.current = nextUrl;
|
|
||||||
setFetched(true);
|
|
||||||
setSaveDisabled(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const saveAll = useCallback(async () => {
|
const fetchOutboundsTrafficCb = useCallback(async () => {
|
||||||
setSpinning(true);
|
await queryClient.invalidateQueries({ queryKey: keys.xray.outboundsTraffic() });
|
||||||
try {
|
}, [queryClient]);
|
||||||
const msg = await HttpUtil.post('/panel/xray/update', {
|
|
||||||
|
const saveMut = useMutation({
|
||||||
|
mutationFn: async () =>
|
||||||
|
HttpUtil.post('/panel/xray/update', {
|
||||||
xraySetting: xraySettingRef.current,
|
xraySetting: xraySettingRef.current,
|
||||||
outboundTestUrl: outboundTestUrlRef.current || 'https://www.google.com/generate_204',
|
outboundTestUrl: outboundTestUrlRef.current || DEFAULT_TEST_URL,
|
||||||
});
|
}) as Promise<ApiMsg>,
|
||||||
if (msg?.success) await fetchAll();
|
onSuccess: (msg) => {
|
||||||
} finally {
|
if (msg?.success) queryClient.invalidateQueries({ queryKey: keys.xray.config() });
|
||||||
setSpinning(false);
|
},
|
||||||
}
|
});
|
||||||
}, [fetchAll]);
|
|
||||||
|
|
||||||
const fetchOutboundsTraffic = useCallback(async () => {
|
const resetTrafficMut = useMutation({
|
||||||
const msg = await HttpUtil.get('/panel/xray/getOutboundsTraffic');
|
mutationFn: (tag: string) =>
|
||||||
if (msg?.success) setOutboundsTraffic(msg.obj || []);
|
HttpUtil.post('/panel/xray/resetOutboundsTraffic', { tag }) as Promise<ApiMsg>,
|
||||||
}, []);
|
onSuccess: (msg) => {
|
||||||
|
if (msg?.success) queryClient.invalidateQueries({ queryKey: keys.xray.outboundsTraffic() });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const resetOutboundsTraffic = useCallback(async (tag: string) => {
|
const restartMut = useMutation({
|
||||||
const msg = await HttpUtil.post('/panel/xray/resetOutboundsTraffic', { tag });
|
mutationFn: async () => {
|
||||||
if (msg?.success) await fetchOutboundsTraffic();
|
const msg = await HttpUtil.post('/panel/api/server/restartXrayService') as ApiMsg;
|
||||||
}, [fetchOutboundsTraffic]);
|
if (!msg?.success) return msg;
|
||||||
|
await PromiseUtil.sleep(500);
|
||||||
|
const r = await HttpUtil.get('/panel/xray/getXrayResult') as ApiMsg<string>;
|
||||||
|
if (r?.success) setRestartResult(r.obj || '');
|
||||||
|
return msg;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const applyOutboundsEvent = useCallback((payload: unknown) => {
|
const resetDefaultMut = useMutation({
|
||||||
if (Array.isArray(payload)) setOutboundsTraffic(payload as OutboundTrafficRow[]);
|
mutationFn: async () => HttpUtil.get('/panel/setting/getDefaultJsonConfig') as Promise<ApiMsg<XraySettingsValue>>,
|
||||||
}, []);
|
onSuccess: (msg) => {
|
||||||
|
if (msg?.success && msg.obj) {
|
||||||
|
const cloned = JSON.parse(JSON.stringify(msg.obj));
|
||||||
|
setTemplateSettings(cloned);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveAll = useCallback(async () => { await saveMut.mutateAsync(); }, [saveMut]);
|
||||||
|
const resetOutboundsTraffic = useCallback(async (tag: string) => { await resetTrafficMut.mutateAsync(tag); }, [resetTrafficMut]);
|
||||||
|
const restartXray = useCallback(async () => { await restartMut.mutateAsync(); }, [restartMut]);
|
||||||
|
const resetToDefault = useCallback(async () => { await resetDefaultMut.mutateAsync(); }, [resetDefaultMut]);
|
||||||
|
|
||||||
|
const spinning = saveMut.isPending || restartMut.isPending || resetDefaultMut.isPending;
|
||||||
|
|
||||||
const testOutbound = useCallback(
|
const testOutbound = useCallback(
|
||||||
async (index: number, outbound: unknown, mode = 'tcp'): Promise<OutboundTestResult | null> => {
|
async (index: number, outbound: unknown, mode = 'tcp'): Promise<OutboundTestResult | null> => {
|
||||||
|
|
@ -212,11 +273,11 @@ export function useXraySetting(): UseXraySettingResult {
|
||||||
outbound: JSON.stringify(outbound),
|
outbound: JSON.stringify(outbound),
|
||||||
allOutbounds: JSON.stringify(templateSettingsRef.current?.outbounds || []),
|
allOutbounds: JSON.stringify(templateSettingsRef.current?.outbounds || []),
|
||||||
mode,
|
mode,
|
||||||
});
|
}) as ApiMsg<OutboundTestResult>;
|
||||||
if (msg?.success) {
|
if (msg?.success && msg.obj) {
|
||||||
setOutboundTestStates((prev) => ({
|
setOutboundTestStates((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[index]: { testing: false, result: msg.obj },
|
[index]: { testing: false, result: msg.obj as OutboundTestResult },
|
||||||
}));
|
}));
|
||||||
return msg.obj;
|
return msg.obj;
|
||||||
}
|
}
|
||||||
|
|
@ -273,43 +334,16 @@ export function useXraySetting(): UseXraySettingResult {
|
||||||
}
|
}
|
||||||
}, [testingAll, testOutbound]);
|
}, [testingAll, testOutbound]);
|
||||||
|
|
||||||
const resetToDefault = useCallback(async () => {
|
|
||||||
setSpinning(true);
|
|
||||||
try {
|
|
||||||
const msg = await HttpUtil.get('/panel/setting/getDefaultJsonConfig');
|
|
||||||
if (msg?.success) {
|
|
||||||
const cloned = JSON.parse(JSON.stringify(msg.obj));
|
|
||||||
setTemplateSettings(cloned);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setSpinning(false);
|
|
||||||
}
|
|
||||||
}, [setTemplateSettings]);
|
|
||||||
|
|
||||||
const restartXray = useCallback(async () => {
|
|
||||||
setSpinning(true);
|
|
||||||
try {
|
|
||||||
const msg = await HttpUtil.post('/panel/api/server/restartXrayService');
|
|
||||||
if (msg?.success) {
|
|
||||||
await PromiseUtil.sleep(500);
|
|
||||||
const r = await HttpUtil.get('/panel/xray/getXrayResult');
|
|
||||||
if (r?.success) setRestartResult(r.obj || '');
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setSpinning(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAll();
|
|
||||||
fetchOutboundsTraffic();
|
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
const dirtyXray = oldXraySettingRef.current !== xraySettingRef.current;
|
const dirtyXray = oldXraySettingRef.current !== xraySettingRef.current;
|
||||||
const dirtyUrl = oldOutboundTestUrlRef.current !== outboundTestUrlRef.current;
|
const dirtyUrl = oldOutboundTestUrlRef.current !== outboundTestUrlRef.current;
|
||||||
setSaveDisabled(!(dirtyXray || dirtyUrl));
|
setSaveDisabled(!(dirtyXray || dirtyUrl));
|
||||||
}, DIRTY_POLL_MS);
|
}, DIRTY_POLL_MS);
|
||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
}, [fetchAll, fetchOutboundsTraffic]);
|
}, []);
|
||||||
|
|
||||||
|
const outboundsTraffic = useMemo(() => trafficQuery.data ?? [], [trafficQuery.data]);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
@ -330,9 +364,8 @@ export function useXraySetting(): UseXraySettingResult {
|
||||||
outboundTestStates,
|
outboundTestStates,
|
||||||
testingAll,
|
testingAll,
|
||||||
fetchAll,
|
fetchAll,
|
||||||
fetchOutboundsTraffic,
|
fetchOutboundsTraffic: fetchOutboundsTrafficCb,
|
||||||
resetOutboundsTraffic,
|
resetOutboundsTraffic,
|
||||||
applyOutboundsEvent,
|
|
||||||
testOutbound,
|
testOutbound,
|
||||||
testAllOutbounds,
|
testAllOutbounds,
|
||||||
saveAll,
|
saveAll,
|
||||||
|
|
@ -357,9 +390,8 @@ export function useXraySetting(): UseXraySettingResult {
|
||||||
outboundTestStates,
|
outboundTestStates,
|
||||||
testingAll,
|
testingAll,
|
||||||
fetchAll,
|
fetchAll,
|
||||||
fetchOutboundsTraffic,
|
fetchOutboundsTrafficCb,
|
||||||
resetOutboundsTraffic,
|
resetOutboundsTraffic,
|
||||||
applyOutboundsEvent,
|
|
||||||
testOutbound,
|
testOutbound,
|
||||||
testAllOutbounds,
|
testAllOutbounds,
|
||||||
saveAll,
|
saveAll,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ import {
|
||||||
|
|
||||||
import { useTheme } from '@/hooks/useTheme';
|
import { useTheme } from '@/hooks/useTheme';
|
||||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||||
import { useWebSocket } from '@/hooks/useWebSocket';
|
|
||||||
import { useXraySetting } from '@/hooks/useXraySetting';
|
import { useXraySetting } from '@/hooks/useXraySetting';
|
||||||
import type { XraySettingsValue } from '@/hooks/useXraySetting';
|
import type { XraySettingsValue } from '@/hooks/useXraySetting';
|
||||||
import AppSidebar from '@/components/AppSidebar';
|
import AppSidebar from '@/components/AppSidebar';
|
||||||
|
|
@ -89,7 +88,6 @@ export default function XrayPage() {
|
||||||
testingAll,
|
testingAll,
|
||||||
fetchAll,
|
fetchAll,
|
||||||
resetOutboundsTraffic,
|
resetOutboundsTraffic,
|
||||||
applyOutboundsEvent,
|
|
||||||
testOutbound,
|
testOutbound,
|
||||||
testAllOutbounds,
|
testAllOutbounds,
|
||||||
saveAll,
|
saveAll,
|
||||||
|
|
@ -97,8 +95,6 @@ export default function XrayPage() {
|
||||||
restartXray,
|
restartXray,
|
||||||
} = xs;
|
} = xs;
|
||||||
|
|
||||||
useWebSocket({ outbounds: applyOutboundsEvent as never });
|
|
||||||
|
|
||||||
const [modal, modalContextHolder] = Modal.useModal();
|
const [modal, modalContextHolder] = Modal.useModal();
|
||||||
const [warpOpen, setWarpOpen] = useState(false);
|
const [warpOpen, setWarpOpen] = useState(false);
|
||||||
const [nordOpen, setNordOpen] = useState(false);
|
const [nordOpen, setNordOpen] = useState(false);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue