mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-22 10:57:14 +00:00
fix(frontend): preserve edited server drafts (#6156)
* fix(frontend): preserve edited server drafts * fix(frontend): retain Xray server projections * fix(frontend): keep draft controls internal * fix(frontend): rehydrate saved redacted settings * fix(frontend): order saved draft hydration * fix(frontend): preserve draft baselines on security saves --------- Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
export function useServerDraft<T>(server: T | undefined, clone: (value: T) => T, equals: (left: T, right: T) => boolean) {
|
||||
const cloneRef = useRef(clone);
|
||||
const equalsRef = useRef(equals);
|
||||
cloneRef.current = clone;
|
||||
equalsRef.current = equals;
|
||||
|
||||
const [draft, setDraft] = useState<T | undefined>();
|
||||
const [baseline, setBaseline] = useState<T | undefined>();
|
||||
const draftRef = useRef(draft);
|
||||
const baselineRef = useRef(baseline);
|
||||
draftRef.current = draft;
|
||||
baselineRef.current = baseline;
|
||||
|
||||
useEffect(() => {
|
||||
if (server === undefined) return;
|
||||
const currentDraft = draftRef.current;
|
||||
const currentBaseline = baselineRef.current;
|
||||
const isDirty = currentDraft !== undefined
|
||||
&& (currentBaseline === undefined || !equalsRef.current(currentDraft, currentBaseline));
|
||||
setBaseline(server);
|
||||
if (isDirty && !equalsRef.current(currentDraft, server)) return;
|
||||
setDraft(cloneRef.current(server));
|
||||
}, [server]);
|
||||
|
||||
const markSaved = useCallback((value: T) => {
|
||||
setBaseline(cloneRef.current(value));
|
||||
}, []);
|
||||
|
||||
const isDirty = useMemo(
|
||||
() => draft !== undefined && (baseline === undefined || !equalsRef.current(draft, baseline)),
|
||||
[baseline, draft],
|
||||
);
|
||||
|
||||
return { draft, setDraft, isDirty, markSaved };
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
type OutboundTrafficRow,
|
||||
} from '@/schemas/xray';
|
||||
|
||||
const DIRTY_POLL_MS = 1000;
|
||||
const DEFAULT_TEST_URL = 'https://www.google.com/generate_204';
|
||||
// One HTTP-mode batch request tests this many outbounds through a single
|
||||
// shared temp xray instance; chunking keeps responses bounded (~30s worst
|
||||
@@ -22,6 +21,10 @@ const DEFAULT_TEST_URL = 'https://www.google.com/generate_204';
|
||||
// results progressively.
|
||||
const HTTP_BATCH_CHUNK = 16;
|
||||
|
||||
function normalizeOutboundTestUrl(url: string) {
|
||||
return url || DEFAULT_TEST_URL;
|
||||
}
|
||||
|
||||
export function isUdpOutbound(outbound: unknown): boolean {
|
||||
const o = outbound as { protocol?: string; streamSettings?: { network?: string } } | null | undefined;
|
||||
const p = o?.protocol;
|
||||
@@ -125,10 +128,11 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
const [saveDisabled, setSaveDisabled] = useState(true);
|
||||
const [xraySetting, setXraySettingState] = useState('');
|
||||
const [templateSettings, setTemplateSettingsState] = useState<XraySettingsValue | null>(null);
|
||||
const [outboundTestUrl, setOutboundTestUrlState] = useState(DEFAULT_TEST_URL);
|
||||
const [savedXraySetting, setSavedXraySetting] = useState('');
|
||||
const [savedOutboundTestUrl, setSavedOutboundTestUrl] = useState(DEFAULT_TEST_URL);
|
||||
const [inboundTags, setInboundTags] = useState<string[]>([]);
|
||||
const [clientReverseTags, setClientReverseTags] = useState<string[]>([]);
|
||||
const [subscriptionOutbounds, setSubscriptionOutbounds] = useState<unknown[]>([]);
|
||||
@@ -139,38 +143,40 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
const [subscriptionTestStates, setSubscriptionTestStates] = useState<Record<string, OutboundTestState>>({});
|
||||
const [testingAll, setTestingAll] = useState(false);
|
||||
|
||||
const oldXraySettingRef = useRef('');
|
||||
const oldOutboundTestUrlRef = useRef('');
|
||||
const syncingRef = useRef(false);
|
||||
const xraySettingRef = useRef('');
|
||||
const outboundTestUrlRef = useRef(outboundTestUrl);
|
||||
const savedXraySettingRef = useRef(savedXraySetting);
|
||||
const savedOutboundTestUrlRef = useRef(savedOutboundTestUrl);
|
||||
const templateSettingsRef = useRef<XraySettingsValue | null>(null);
|
||||
const subscriptionOutboundsRef = useRef<unknown[]>([]);
|
||||
|
||||
xraySettingRef.current = xraySetting;
|
||||
outboundTestUrlRef.current = outboundTestUrl;
|
||||
savedXraySettingRef.current = savedXraySetting;
|
||||
savedOutboundTestUrlRef.current = savedOutboundTestUrl;
|
||||
templateSettingsRef.current = templateSettings;
|
||||
subscriptionOutboundsRef.current = subscriptionOutbounds;
|
||||
|
||||
// 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;
|
||||
const nextUrl = normalizeOutboundTestUrl(obj.outboundTestUrl || '');
|
||||
setInboundTags(obj.inboundTags || []);
|
||||
setClientReverseTags(obj.clientReverseTags || []);
|
||||
setSubscriptionOutbounds(obj.subscriptionOutbounds || []);
|
||||
setSubscriptionOutboundTags(obj.subscriptionOutboundTags || []);
|
||||
const nextUrl = obj.outboundTestUrl || DEFAULT_TEST_URL;
|
||||
const isDirty = savedXraySettingRef.current !== xraySettingRef.current
|
||||
|| savedOutboundTestUrlRef.current !== normalizeOutboundTestUrl(outboundTestUrlRef.current);
|
||||
if (isDirty) return;
|
||||
syncingRef.current = true;
|
||||
setXraySettingState(pretty);
|
||||
setTemplateSettingsState(obj.xraySetting);
|
||||
setSavedXraySetting(pretty);
|
||||
syncingRef.current = false;
|
||||
setOutboundTestUrlState(nextUrl);
|
||||
oldOutboundTestUrlRef.current = nextUrl;
|
||||
setSaveDisabled(true);
|
||||
setSavedOutboundTestUrl(nextUrl);
|
||||
}, [configQuery.data]);
|
||||
|
||||
const fetched = configQuery.data !== undefined || configQuery.isError;
|
||||
@@ -220,7 +226,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
const saveMut = useMutation({
|
||||
mutationFn: async () => {
|
||||
const sentXraySetting = xraySettingRef.current;
|
||||
const sentTestUrl = outboundTestUrlRef.current || DEFAULT_TEST_URL;
|
||||
const sentTestUrl = normalizeOutboundTestUrl(outboundTestUrlRef.current);
|
||||
const msg = await HttpUtil.post('/panel/api/xray/update', {
|
||||
xraySetting: sentXraySetting,
|
||||
outboundTestUrl: sentTestUrl,
|
||||
@@ -229,9 +235,8 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
},
|
||||
onSuccess: ({ msg, sentXraySetting, sentTestUrl }) => {
|
||||
if (!msg?.success) return;
|
||||
oldXraySettingRef.current = sentXraySetting;
|
||||
oldOutboundTestUrlRef.current = sentTestUrl;
|
||||
setSaveDisabled(true);
|
||||
setSavedXraySetting(sentXraySetting);
|
||||
setSavedOutboundTestUrl(sentTestUrl);
|
||||
queryClient.invalidateQueries({ queryKey: keys.xray.config() });
|
||||
},
|
||||
});
|
||||
@@ -425,14 +430,8 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
}
|
||||
}, [testingAll, testOutbound, testSubscriptionOutbound, postOutboundTestBatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
const dirtyXray = oldXraySettingRef.current !== xraySettingRef.current;
|
||||
const dirtyUrl = oldOutboundTestUrlRef.current !== outboundTestUrlRef.current;
|
||||
setSaveDisabled(!(dirtyXray || dirtyUrl));
|
||||
}, DIRTY_POLL_MS);
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
const saveDisabled = savedXraySetting === xraySetting
|
||||
&& savedOutboundTestUrl === normalizeOutboundTestUrl(outboundTestUrl);
|
||||
|
||||
const outboundsTraffic = useMemo(() => trafficQuery.data ?? [], [trafficQuery.data]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user