mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
feat(frontend): introduce TanStack Query with status polling
Wires @tanstack/react-query into every entry and migrates useStatus to
useStatusQuery as the foundation for the multi-page MPA → SPA migration.
- QueryProvider wraps each entry inside ThemeProvider, with devtools gated
on import.meta.env.DEV
- Shared queryClient: 30s staleTime, refetchOnWindowFocus, 1 retry
- useStatusQuery preserves the { status, fetched, refresh } shape so
IndexPage swaps in without further changes
- refetchIntervalInBackground:false stops the 2s status poll when the
panel tab is hidden, cutting idle traffic against the server
This commit is contained in:
parent
867a145979
commit
538473b2cc
17 changed files with 224 additions and 48 deletions
115
frontend/package-lock.json
generated
115
frontend/package-lock.json
generated
|
|
@ -11,6 +11,8 @@
|
||||||
"@ant-design/icons": "^6.2.3",
|
"@ant-design/icons": "^6.2.3",
|
||||||
"@codemirror/lang-json": "^6.0.2",
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
"@tanstack/react-query": "^5.100.14",
|
||||||
|
"@tanstack/react-query-devtools": "^5.100.14",
|
||||||
"antd": "^6.4.3",
|
"antd": "^6.4.3",
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.16.1",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
|
|
@ -21,7 +23,8 @@
|
||||||
"qs": "^6.15.2",
|
"qs": "^6.15.2",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"react-i18next": "^17.0.8"
|
"react-i18next": "^17.0.8",
|
||||||
|
"react-router-dom": "^7.15.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|
@ -1836,6 +1839,59 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.100.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.14.tgz",
|
||||||
|
"integrity": "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/query-devtools": {
|
||||||
|
"version": "5.100.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.100.14.tgz",
|
||||||
|
"integrity": "sha512-g96SmSSQecYTYcyuAMRXr895GplJv01UGt7qttQWPOUyZ5EGz5tbRc589bMc2m5BsPFD6O0PCEAHdbDYNP6UBw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.100.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.14.tgz",
|
||||||
|
"integrity": "sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.100.14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query-devtools": {
|
||||||
|
"version": "5.100.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.100.14.tgz",
|
||||||
|
"integrity": "sha512-JkP5VDgKOw3t/QSA1OABRHEqx8BuNs5MfvZRooNqdvN57SzTuGq3fKR1a2IH5rqa5HDLUm+FOXUEnB9ueHiLzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-devtools": "5.100.14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tanstack/react-query": "^5.100.14",
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.2",
|
"version": "0.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||||
|
|
@ -2459,6 +2515,19 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/crelt": {
|
"node_modules/crelt": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||||
|
|
@ -3917,6 +3986,44 @@
|
||||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz",
|
||||||
|
"integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "7.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.1.tgz",
|
||||||
|
"integrity": "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-router": "7.15.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
|
||||||
|
|
@ -3976,6 +4083,12 @@
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@
|
||||||
"@ant-design/icons": "^6.2.3",
|
"@ant-design/icons": "^6.2.3",
|
||||||
"@codemirror/lang-json": "^6.0.2",
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
"@tanstack/react-query": "^5.100.14",
|
||||||
|
"@tanstack/react-query-devtools": "^5.100.14",
|
||||||
"antd": "^6.4.3",
|
"antd": "^6.4.3",
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.16.1",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
|
|
@ -29,7 +31,8 @@
|
||||||
"qs": "^6.15.2",
|
"qs": "^6.15.2",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"react-i18next": "^17.0.8"
|
"react-i18next": "^17.0.8",
|
||||||
|
"react-router-dom": "^7.15.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|
|
||||||
16
frontend/src/api/QueryProvider.tsx
Normal file
16
frontend/src/api/QueryProvider.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
|
|
||||||
|
import { queryClient } from '@/queryClient';
|
||||||
|
|
||||||
|
export function QueryProvider({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
{children}
|
||||||
|
{import.meta.env.DEV && (
|
||||||
|
<ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-left" />
|
||||||
|
)}
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
frontend/src/api/queries/useStatusQuery.ts
Normal file
33
frontend/src/api/queries/useStatusQuery.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { HttpUtil } from '@/utils';
|
||||||
|
import { Status } from '@/models/status';
|
||||||
|
import { keys } from '@/api/queryKeys';
|
||||||
|
|
||||||
|
const POLL_INTERVAL_MS = 2000;
|
||||||
|
|
||||||
|
async function fetchStatus(): Promise<Status> {
|
||||||
|
const msg = await HttpUtil.get('/panel/api/server/status', undefined, { silent: true });
|
||||||
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch status');
|
||||||
|
return new Status(msg.obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStatusQuery() {
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: keys.server.status(),
|
||||||
|
queryFn: fetchStatus,
|
||||||
|
refetchInterval: POLL_INTERVAL_MS,
|
||||||
|
refetchIntervalInBackground: false,
|
||||||
|
staleTime: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = useMemo(() => query.data ?? new Status(), [query.data]);
|
||||||
|
const refresh = async () => { await query.refetch(); };
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
fetched: query.data !== undefined,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
5
frontend/src/api/queryKeys.ts
Normal file
5
frontend/src/api/queryKeys.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export const keys = {
|
||||||
|
server: {
|
||||||
|
status: () => ['server', 'status'] as const,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
@ -6,6 +6,7 @@ import { setupAxios } from '@/api/axios-init.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import { readyI18n } from '@/i18n/react';
|
import { readyI18n } from '@/i18n/react';
|
||||||
import { ThemeProvider } from '@/hooks/useTheme';
|
import { ThemeProvider } from '@/hooks/useTheme';
|
||||||
|
import { QueryProvider } from '@/api/QueryProvider';
|
||||||
import ApiDocsPage from '@/pages/api-docs/ApiDocsPage';
|
import ApiDocsPage from '@/pages/api-docs/ApiDocsPage';
|
||||||
|
|
||||||
setupAxios();
|
setupAxios();
|
||||||
|
|
@ -21,7 +22,9 @@ readyI18n().then(() => {
|
||||||
if (root) {
|
if (root) {
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ApiDocsPage />
|
<QueryProvider>
|
||||||
|
<ApiDocsPage />
|
||||||
|
</QueryProvider>
|
||||||
</ThemeProvider>,
|
</ThemeProvider>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { setupAxios } from '@/api/axios-init.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import { readyI18n } from '@/i18n/react';
|
import { readyI18n } from '@/i18n/react';
|
||||||
import { ThemeProvider } from '@/hooks/useTheme';
|
import { ThemeProvider } from '@/hooks/useTheme';
|
||||||
|
import { QueryProvider } from '@/api/QueryProvider';
|
||||||
import ClientsPage from '@/pages/clients/ClientsPage';
|
import ClientsPage from '@/pages/clients/ClientsPage';
|
||||||
|
|
||||||
setupAxios();
|
setupAxios();
|
||||||
|
|
@ -21,7 +22,9 @@ readyI18n().then(() => {
|
||||||
if (root) {
|
if (root) {
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ClientsPage />
|
<QueryProvider>
|
||||||
|
<ClientsPage />
|
||||||
|
</QueryProvider>
|
||||||
</ThemeProvider>,
|
</ThemeProvider>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { setupAxios } from '@/api/axios-init.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import { readyI18n } from '@/i18n/react';
|
import { readyI18n } from '@/i18n/react';
|
||||||
import { ThemeProvider } from '@/hooks/useTheme';
|
import { ThemeProvider } from '@/hooks/useTheme';
|
||||||
|
import { QueryProvider } from '@/api/QueryProvider';
|
||||||
import InboundsPage from '@/pages/inbounds/InboundsPage';
|
import InboundsPage from '@/pages/inbounds/InboundsPage';
|
||||||
|
|
||||||
setupAxios();
|
setupAxios();
|
||||||
|
|
@ -21,7 +22,9 @@ readyI18n().then(() => {
|
||||||
if (root) {
|
if (root) {
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<InboundsPage />
|
<QueryProvider>
|
||||||
|
<InboundsPage />
|
||||||
|
</QueryProvider>
|
||||||
</ThemeProvider>,
|
</ThemeProvider>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { setupAxios } from '@/api/axios-init.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import { readyI18n } from '@/i18n/react';
|
import { readyI18n } from '@/i18n/react';
|
||||||
import { ThemeProvider } from '@/hooks/useTheme';
|
import { ThemeProvider } from '@/hooks/useTheme';
|
||||||
|
import { QueryProvider } from '@/api/QueryProvider';
|
||||||
import IndexPage from '@/pages/index/IndexPage';
|
import IndexPage from '@/pages/index/IndexPage';
|
||||||
|
|
||||||
setupAxios();
|
setupAxios();
|
||||||
|
|
@ -21,7 +22,9 @@ readyI18n().then(() => {
|
||||||
if (root) {
|
if (root) {
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<IndexPage />
|
<QueryProvider>
|
||||||
|
<IndexPage />
|
||||||
|
</QueryProvider>
|
||||||
</ThemeProvider>,
|
</ThemeProvider>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { setupAxios } from '@/api/axios-init.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import { readyI18n } from '@/i18n/react';
|
import { readyI18n } from '@/i18n/react';
|
||||||
import { ThemeProvider } from '@/hooks/useTheme';
|
import { ThemeProvider } from '@/hooks/useTheme';
|
||||||
|
import { QueryProvider } from '@/api/QueryProvider';
|
||||||
import LoginPage from '@/pages/login/LoginPage';
|
import LoginPage from '@/pages/login/LoginPage';
|
||||||
|
|
||||||
setupAxios();
|
setupAxios();
|
||||||
|
|
@ -21,7 +22,9 @@ readyI18n().then(() => {
|
||||||
if (root) {
|
if (root) {
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<LoginPage />
|
<QueryProvider>
|
||||||
|
<LoginPage />
|
||||||
|
</QueryProvider>
|
||||||
</ThemeProvider>,
|
</ThemeProvider>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { setupAxios } from '@/api/axios-init.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import { readyI18n } from '@/i18n/react';
|
import { readyI18n } from '@/i18n/react';
|
||||||
import { ThemeProvider } from '@/hooks/useTheme';
|
import { ThemeProvider } from '@/hooks/useTheme';
|
||||||
|
import { QueryProvider } from '@/api/QueryProvider';
|
||||||
import NodesPage from '@/pages/nodes/NodesPage';
|
import NodesPage from '@/pages/nodes/NodesPage';
|
||||||
|
|
||||||
setupAxios();
|
setupAxios();
|
||||||
|
|
@ -21,7 +22,9 @@ readyI18n().then(() => {
|
||||||
if (root) {
|
if (root) {
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<NodesPage />
|
<QueryProvider>
|
||||||
|
<NodesPage />
|
||||||
|
</QueryProvider>
|
||||||
</ThemeProvider>,
|
</ThemeProvider>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { setupAxios } from '@/api/axios-init.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import { readyI18n } from '@/i18n/react';
|
import { readyI18n } from '@/i18n/react';
|
||||||
import { ThemeProvider } from '@/hooks/useTheme';
|
import { ThemeProvider } from '@/hooks/useTheme';
|
||||||
|
import { QueryProvider } from '@/api/QueryProvider';
|
||||||
import SettingsPage from '@/pages/settings/SettingsPage';
|
import SettingsPage from '@/pages/settings/SettingsPage';
|
||||||
|
|
||||||
setupAxios();
|
setupAxios();
|
||||||
|
|
@ -21,7 +22,9 @@ readyI18n().then(() => {
|
||||||
if (root) {
|
if (root) {
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<SettingsPage />
|
<QueryProvider>
|
||||||
|
<SettingsPage />
|
||||||
|
</QueryProvider>
|
||||||
</ThemeProvider>,
|
</ThemeProvider>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'antd/dist/reset.css';
|
||||||
|
|
||||||
import { readyI18n } from '@/i18n/react';
|
import { readyI18n } from '@/i18n/react';
|
||||||
import { ThemeProvider } from '@/hooks/useTheme';
|
import { ThemeProvider } from '@/hooks/useTheme';
|
||||||
|
import { QueryProvider } from '@/api/QueryProvider';
|
||||||
import SubPage from '@/pages/sub/SubPage';
|
import SubPage from '@/pages/sub/SubPage';
|
||||||
|
|
||||||
const messageContainer = document.getElementById('message');
|
const messageContainer = document.getElementById('message');
|
||||||
|
|
@ -16,7 +17,9 @@ readyI18n().then(() => {
|
||||||
if (root) {
|
if (root) {
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<SubPage />
|
<QueryProvider>
|
||||||
|
<SubPage />
|
||||||
|
</QueryProvider>
|
||||||
</ThemeProvider>,
|
</ThemeProvider>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { setupAxios } from '@/api/axios-init.js';
|
||||||
import { applyDocumentTitle } from '@/utils';
|
import { applyDocumentTitle } from '@/utils';
|
||||||
import { readyI18n } from '@/i18n/react';
|
import { readyI18n } from '@/i18n/react';
|
||||||
import { ThemeProvider } from '@/hooks/useTheme';
|
import { ThemeProvider } from '@/hooks/useTheme';
|
||||||
|
import { QueryProvider } from '@/api/QueryProvider';
|
||||||
import XrayPage from '@/pages/xray/XrayPage';
|
import XrayPage from '@/pages/xray/XrayPage';
|
||||||
|
|
||||||
setupAxios();
|
setupAxios();
|
||||||
|
|
@ -21,7 +22,9 @@ readyI18n().then(() => {
|
||||||
if (root) {
|
if (root) {
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<XrayPage />
|
<QueryProvider>
|
||||||
|
<XrayPage />
|
||||||
|
</QueryProvider>
|
||||||
</ThemeProvider>,
|
</ThemeProvider>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
import { HttpUtil } from '@/utils';
|
|
||||||
import { Status } from '@/models/status';
|
|
||||||
|
|
||||||
const POLL_INTERVAL_MS = 2000;
|
|
||||||
|
|
||||||
export function useStatus() {
|
|
||||||
const [status, setStatus] = useState<Status>(() => new Status());
|
|
||||||
const [fetched, setFetched] = useState(false);
|
|
||||||
const fetchedRef = useRef(false);
|
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const msg = await HttpUtil.get('/panel/api/server/status');
|
|
||||||
if (msg?.success) {
|
|
||||||
setStatus(new Status(msg.obj));
|
|
||||||
if (!fetchedRef.current) {
|
|
||||||
fetchedRef.current = true;
|
|
||||||
setFetched(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to get status:', e);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refresh();
|
|
||||||
const timer = window.setInterval(refresh, POLL_INTERVAL_MS);
|
|
||||||
return () => window.clearInterval(timer);
|
|
||||||
}, [refresh]);
|
|
||||||
|
|
||||||
return { status, fetched, refresh };
|
|
||||||
}
|
|
||||||
|
|
@ -36,7 +36,7 @@ import {
|
||||||
|
|
||||||
import { HttpUtil, SizeFormatter, TimeFormatter, ClipboardManager, FileManager } from '@/utils';
|
import { HttpUtil, SizeFormatter, TimeFormatter, ClipboardManager, FileManager } from '@/utils';
|
||||||
import { useTheme } from '@/hooks/useTheme';
|
import { useTheme } from '@/hooks/useTheme';
|
||||||
import { useStatus } from '@/hooks/useStatus';
|
import { useStatusQuery } from '@/api/queries/useStatusQuery';
|
||||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||||
import AppSidebar from '@/components/AppSidebar';
|
import AppSidebar from '@/components/AppSidebar';
|
||||||
import CustomStatistic from '@/components/CustomStatistic';
|
import CustomStatistic from '@/components/CustomStatistic';
|
||||||
|
|
@ -59,7 +59,7 @@ import './IndexPage.css';
|
||||||
export default function IndexPage() {
|
export default function IndexPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isDark, isUltra, antdThemeConfig } = useTheme();
|
const { isDark, isUltra, antdThemeConfig } = useTheme();
|
||||||
const { status, fetched, refresh } = useStatus();
|
const { status, fetched, refresh } = useStatusQuery();
|
||||||
const { isMobile } = useMediaQuery();
|
const { isMobile } = useMediaQuery();
|
||||||
const [messageApi, messageContextHolder] = message.useMessage();
|
const [messageApi, messageContextHolder] = message.useMessage();
|
||||||
useEffect(() => { setMessageInstance(messageApi); }, [messageApi]);
|
useEffect(() => { setMessageInstance(messageApi); }, [messageApi]);
|
||||||
|
|
|
||||||
14
frontend/src/queryClient.ts
Normal file
14
frontend/src/queryClient.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 30_000,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
retry: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue