mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-16 10:55:00 +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:
@@ -749,8 +749,12 @@ const withDatabases = withFiles([GEOSITE_FILE, GEOIP_FILE]);
|
||||
function BrowserDemo(props: GeoBrowserModalProps) {
|
||||
const [open, setOpen] = useState(props.open);
|
||||
const [value, setValue] = useState(props.value);
|
||||
useEffect(() => setOpen(props.open), [props.open]);
|
||||
useEffect(() => setValue(props.value), [props.value]);
|
||||
const [synced, setSynced] = useState({ open: props.open, value: props.value });
|
||||
if (synced.open !== props.open || synced.value !== props.value) {
|
||||
setSynced({ open: props.open, value: props.value });
|
||||
setOpen(props.open);
|
||||
setValue(props.value);
|
||||
}
|
||||
return (
|
||||
<Space orientation="vertical" size={12}>
|
||||
<Space size={8}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Alert,
|
||||
@@ -69,13 +69,16 @@ export default function GeoBrowserModal({
|
||||
const [entryPage, setEntryPage] = useState(1);
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
|
||||
const knownRef = useRef<Set<string>>(new Set());
|
||||
const seededFilesRef = useRef<Set<string>>(new Set());
|
||||
const [known, setKnown] = useState<Set<string>>(() => new Set());
|
||||
const [seededFiles, setSeededFiles] = useState<Set<string>>(() => new Set());
|
||||
|
||||
const filesQuery = useGeodataFiles(open);
|
||||
const files = useMemo(() => databasesFor(filesQuery.data ?? [], kind), [filesQuery.data, kind]);
|
||||
const activeFile = files.find((candidate) => candidate.name === file);
|
||||
const fileKind: GeoKind = activeFile?.kind ?? kind;
|
||||
const activeFile = useMemo(
|
||||
() => files.find((candidate) => candidate.name === file),
|
||||
[files, file],
|
||||
);
|
||||
const fileKind: GeoKind = useMemo(() => activeFile?.kind ?? kind, [activeFile, kind]);
|
||||
|
||||
const categoriesQuery = useGeodataCategories(file, '', open && !!file);
|
||||
// While a newly picked database loads, the query still serves the previous
|
||||
@@ -117,28 +120,27 @@ export default function GeoBrowserModal({
|
||||
return () => window.clearTimeout(handle);
|
||||
}, [entryQuery, entryFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
knownRef.current = new Set();
|
||||
seededFilesRef.current = new Set();
|
||||
setCategoryQuery('');
|
||||
setEntryQuery('');
|
||||
setEntryFilter('');
|
||||
setActiveCode(undefined);
|
||||
setEntryPage(1);
|
||||
setSelected([]);
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || file || files.length === 0) return;
|
||||
// Opening, picking the default database and seeding the selection are all
|
||||
// render-time adjustments — an effect would paint the previous state first.
|
||||
const [wasOpen, setWasOpen] = useState(false);
|
||||
if (open !== wasOpen) {
|
||||
setWasOpen(open);
|
||||
if (open) {
|
||||
setKnown(new Set());
|
||||
setSeededFiles(new Set());
|
||||
setCategoryQuery('');
|
||||
setEntryQuery('');
|
||||
setEntryFilter('');
|
||||
setActiveCode(undefined);
|
||||
setEntryPage(1);
|
||||
setSelected([]);
|
||||
}
|
||||
} else if (open && !file && files.length > 0) {
|
||||
setFile(preferredFile(files, kind));
|
||||
}, [open, file, files, kind]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !file || categories.length === 0 || seededFilesRef.current.has(file)) return;
|
||||
} else if (open && file && categories.length > 0 && !seededFiles.has(file)) {
|
||||
const tokens = categories.map((category) => tokenFor(file, category.code, fileKind));
|
||||
for (const token of tokens) knownRef.current.add(token);
|
||||
seededFilesRef.current.add(file);
|
||||
setKnown(new Set([...known, ...tokens]));
|
||||
setSeededFiles(new Set(seededFiles).add(file));
|
||||
const fromValue = selectionFromValue(value, new Set(tokens));
|
||||
if (fromValue.length > 0) {
|
||||
setSelected((previous) => [
|
||||
@@ -146,7 +148,7 @@ export default function GeoBrowserModal({
|
||||
...fromValue.filter((token) => !previous.includes(token)),
|
||||
]);
|
||||
}
|
||||
}, [open, file, categories, fileKind, value]);
|
||||
}
|
||||
|
||||
const visibleCategories = useMemo(() => {
|
||||
const query = categoryQuery.trim().toLowerCase();
|
||||
@@ -276,7 +278,7 @@ export default function GeoBrowserModal({
|
||||
title={t('pages.xray.geoBrowser.title')}
|
||||
width={880}
|
||||
onCancel={onClose}
|
||||
onOk={() => onApply(mergeSelection(value, selected, knownRef.current))}
|
||||
onOk={() => onApply(mergeSelection(value, selected, known))}
|
||||
okText={t('pages.xray.geoBrowser.apply')}
|
||||
cancelText={t('close')}
|
||||
className="geo-browser-modal"
|
||||
|
||||
@@ -216,7 +216,11 @@ const withGeodata: Decorator = function GeodataBackend(Story) {
|
||||
|
||||
function ControlledTokenInput({ value = '', id = 'geo-rule', ...rest }: GeoTokenInputProps) {
|
||||
const [current, setCurrent] = useState(value);
|
||||
useEffect(() => setCurrent(value), [value]);
|
||||
const [synced, setSynced] = useState(value);
|
||||
if (synced !== value) {
|
||||
setSynced(value);
|
||||
setCurrent(value);
|
||||
}
|
||||
return (
|
||||
<Space orientation="vertical" size={4} style={{ width: 460 }}>
|
||||
<label htmlFor={id}>{rest.kind === 'ip' ? 'Target IP' : 'Target domain'}</label>
|
||||
|
||||
@@ -49,13 +49,21 @@ export default function GeoTokenInput({
|
||||
const validate = useValidateGeoTokens();
|
||||
const { mutateAsync } = validate;
|
||||
|
||||
useEffect(() => {
|
||||
const tokens = parseTokens(value);
|
||||
if (tokens.length === 0) {
|
||||
// An empty field has nothing to validate, so it clears during render rather
|
||||
// than waiting a commit for the effect to catch up.
|
||||
const isEmpty = parseTokens(value).length === 0;
|
||||
const [wasEmpty, setWasEmpty] = useState(isEmpty);
|
||||
if (isEmpty !== wasEmpty) {
|
||||
setWasEmpty(isEmpty);
|
||||
if (isEmpty) {
|
||||
setIssues([]);
|
||||
setCheckFailed(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const tokens = parseTokens(value);
|
||||
if (tokens.length === 0) return;
|
||||
let cancelled = false;
|
||||
const timer = setTimeout(() => {
|
||||
mutateAsync({ tokens, kind })
|
||||
|
||||
Reference in New Issue
Block a user