mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
feat(frontend): migrate useNodes to TanStack Query
Splits the hand-rolled useNodes hook into useNodesQuery (server data +
NodeRecord type + derived totals) and useNodeMutations (add/update/del/
setEnable/probe/test). Mutations invalidate ['nodes'] on success, so
the list refreshes without each call awaiting a manual refresh().
NodesPage drops useWebSocket({ nodes: applyNodesEvent }) — the
WebSocket → query bridge now forwards the 'nodes' push to
setQueryData(['nodes', 'list']) once at the SPA root.
InboundsPage and the inbound form/list components import NodeRecord
from its new home next to the query hook.
This commit is contained in:
parent
d9def73ee5
commit
dff509b394
11 changed files with 195 additions and 202 deletions
63
frontend/src/api/queries/useNodeMutations.ts
Normal file
63
frontend/src/api/queries/useNodeMutations.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { HttpUtil } from '@/utils';
|
||||||
|
import { keys } from '@/api/queryKeys';
|
||||||
|
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||||
|
|
||||||
|
interface ApiMsg<T = unknown> {
|
||||||
|
success?: boolean;
|
||||||
|
msg?: string;
|
||||||
|
obj?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProbeResult {
|
||||||
|
status: string;
|
||||||
|
latencyMs?: number;
|
||||||
|
xrayVersion?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNodeMutations() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const invalidate = () => queryClient.invalidateQueries({ queryKey: keys.nodes.root() });
|
||||||
|
|
||||||
|
const createMut = useMutation({
|
||||||
|
mutationFn: (payload: Partial<NodeRecord>) =>
|
||||||
|
HttpUtil.post('/panel/api/nodes/add', payload) as Promise<ApiMsg>,
|
||||||
|
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMut = useMutation({
|
||||||
|
mutationFn: ({ id, payload }: { id: number; payload: Partial<NodeRecord> }) =>
|
||||||
|
HttpUtil.post(`/panel/api/nodes/update/${id}`, payload) as Promise<ApiMsg>,
|
||||||
|
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeMut = useMutation({
|
||||||
|
mutationFn: (id: number) =>
|
||||||
|
HttpUtil.post(`/panel/api/nodes/del/${id}`) as Promise<ApiMsg>,
|
||||||
|
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||||
|
});
|
||||||
|
|
||||||
|
const setEnableMut = useMutation({
|
||||||
|
mutationFn: ({ id, enable }: { id: number; enable: boolean }) =>
|
||||||
|
HttpUtil.post(`/panel/api/nodes/setEnable/${id}`, { enable }) as Promise<ApiMsg>,
|
||||||
|
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||||
|
});
|
||||||
|
|
||||||
|
const probeMut = useMutation({
|
||||||
|
mutationFn: (id: number) =>
|
||||||
|
HttpUtil.post(`/panel/api/nodes/probe/${id}`) as Promise<ApiMsg<ProbeResult>>,
|
||||||
|
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
create: (payload: Partial<NodeRecord>) => createMut.mutateAsync(payload),
|
||||||
|
update: (id: number, payload: Partial<NodeRecord>) => updateMut.mutateAsync({ id, payload }),
|
||||||
|
remove: (id: number) => removeMut.mutateAsync(id),
|
||||||
|
setEnable: (id: number, enable: boolean) => setEnableMut.mutateAsync({ id, enable }),
|
||||||
|
probe: (id: number) => probeMut.mutateAsync(id),
|
||||||
|
testConnection: (payload: Partial<NodeRecord>) =>
|
||||||
|
HttpUtil.post('/panel/api/nodes/test', payload) as Promise<ApiMsg<ProbeResult>>,
|
||||||
|
};
|
||||||
|
}
|
||||||
108
frontend/src/api/queries/useNodesQuery.ts
Normal file
108
frontend/src/api/queries/useNodesQuery.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { HttpUtil } from '@/utils';
|
||||||
|
import { keys } from '@/api/queryKeys';
|
||||||
|
|
||||||
|
export interface NodeRecord {
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
remark?: string;
|
||||||
|
scheme?: string;
|
||||||
|
address?: string;
|
||||||
|
port?: number;
|
||||||
|
basePath?: string;
|
||||||
|
apiToken?: string;
|
||||||
|
enable?: boolean;
|
||||||
|
status?: 'online' | 'offline' | string;
|
||||||
|
latencyMs?: number;
|
||||||
|
cpuPct?: number;
|
||||||
|
memPct?: number;
|
||||||
|
xrayVersion?: string;
|
||||||
|
panelVersion?: string;
|
||||||
|
uptimeSecs?: number;
|
||||||
|
inboundCount?: number;
|
||||||
|
clientCount?: number;
|
||||||
|
onlineCount?: number;
|
||||||
|
depletedCount?: number;
|
||||||
|
lastHeartbeat?: number;
|
||||||
|
lastError?: string;
|
||||||
|
allowPrivateAddress?: boolean;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeTotals {
|
||||||
|
total: number;
|
||||||
|
online: number;
|
||||||
|
offline: number;
|
||||||
|
avgLatency: number;
|
||||||
|
inbounds: number;
|
||||||
|
clients: number;
|
||||||
|
onlineClients: number;
|
||||||
|
depleted: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiMsg<T = unknown> {
|
||||||
|
success?: boolean;
|
||||||
|
msg?: string;
|
||||||
|
obj?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchNodes(): Promise<NodeRecord[]> {
|
||||||
|
const msg = await HttpUtil.get('/panel/api/nodes/list', undefined, { silent: true }) as ApiMsg<NodeRecord[]>;
|
||||||
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch nodes');
|
||||||
|
return Array.isArray(msg.obj) ? msg.obj : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNodesQuery() {
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: keys.nodes.list(),
|
||||||
|
queryFn: fetchNodes,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodes = useMemo(() => query.data ?? [], [query.data]);
|
||||||
|
|
||||||
|
const totals = useMemo<NodeTotals>(() => {
|
||||||
|
let online = 0;
|
||||||
|
let offline = 0;
|
||||||
|
let latencySum = 0;
|
||||||
|
let latencyCount = 0;
|
||||||
|
let inbounds = 0;
|
||||||
|
let clients = 0;
|
||||||
|
let onlineClients = 0;
|
||||||
|
let depleted = 0;
|
||||||
|
for (const n of nodes) {
|
||||||
|
inbounds += n.inboundCount || 0;
|
||||||
|
clients += n.clientCount || 0;
|
||||||
|
onlineClients += n.onlineCount || 0;
|
||||||
|
depleted += n.depletedCount || 0;
|
||||||
|
if (!n.enable) continue;
|
||||||
|
if (n.status === 'online') {
|
||||||
|
online += 1;
|
||||||
|
if (n.latencyMs && n.latencyMs > 0) {
|
||||||
|
latencySum += n.latencyMs;
|
||||||
|
latencyCount += 1;
|
||||||
|
}
|
||||||
|
} else if (n.status === 'offline') {
|
||||||
|
offline += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
total: nodes.length,
|
||||||
|
online,
|
||||||
|
offline,
|
||||||
|
avgLatency: latencyCount > 0 ? Math.round(latencySum / latencyCount) : 0,
|
||||||
|
inbounds,
|
||||||
|
clients,
|
||||||
|
onlineClients,
|
||||||
|
depleted,
|
||||||
|
};
|
||||||
|
}, [nodes]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes,
|
||||||
|
totals,
|
||||||
|
loading: query.isFetching,
|
||||||
|
fetched: query.data !== undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -2,4 +2,8 @@ export const keys = {
|
||||||
server: {
|
server: {
|
||||||
status: () => ['server', 'status'] as const,
|
status: () => ['server', 'status'] as const,
|
||||||
},
|
},
|
||||||
|
nodes: {
|
||||||
|
root: () => ['nodes'] as const,
|
||||||
|
list: () => ['nodes', 'list'] as const,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useEffect } from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { WebSocketClient } from '@/api/websocket.js';
|
import { WebSocketClient } from '@/api/websocket.js';
|
||||||
|
import { keys } from '@/api/queryKeys';
|
||||||
|
|
||||||
type Handler = (payload: unknown) => void;
|
type Handler = (payload: unknown) => void;
|
||||||
|
|
||||||
|
|
@ -46,13 +47,20 @@ export function useWebSocketBridge() {
|
||||||
queryClient.setQueryData(['xray', 'outboundsTraffic'], payload);
|
queryClient.setQueryData(['xray', 'outboundsTraffic'], payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onNodes: Handler = (payload) => {
|
||||||
|
if (!Array.isArray(payload)) return;
|
||||||
|
queryClient.setQueryData(keys.nodes.list(), payload);
|
||||||
|
};
|
||||||
|
|
||||||
client.on('invalidate', onInvalidate);
|
client.on('invalidate', onInvalidate);
|
||||||
client.on('outbounds', onOutbounds);
|
client.on('outbounds', onOutbounds);
|
||||||
|
client.on('nodes', onNodes);
|
||||||
client.connect();
|
client.connect();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
client.off('invalidate', onInvalidate);
|
client.off('invalidate', onInvalidate);
|
||||||
client.off('outbounds', onOutbounds);
|
client.off('outbounds', onOutbounds);
|
||||||
|
client.off('nodes', onNodes);
|
||||||
if (invalidateTimer != null) {
|
if (invalidateTimer != null) {
|
||||||
clearTimeout(invalidateTimer);
|
clearTimeout(invalidateTimer);
|
||||||
invalidateTimer = null;
|
invalidateTimer = null;
|
||||||
|
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { HttpUtil } from '@/utils';
|
|
||||||
|
|
||||||
export interface NodeRecord {
|
|
||||||
id: number;
|
|
||||||
name?: string;
|
|
||||||
remark?: string;
|
|
||||||
scheme?: string;
|
|
||||||
address?: string;
|
|
||||||
port?: number;
|
|
||||||
basePath?: string;
|
|
||||||
apiToken?: string;
|
|
||||||
enable?: boolean;
|
|
||||||
status?: 'online' | 'offline' | string;
|
|
||||||
latencyMs?: number;
|
|
||||||
cpuPct?: number;
|
|
||||||
memPct?: number;
|
|
||||||
xrayVersion?: string;
|
|
||||||
panelVersion?: string;
|
|
||||||
uptimeSecs?: number;
|
|
||||||
inboundCount?: number;
|
|
||||||
clientCount?: number;
|
|
||||||
onlineCount?: number;
|
|
||||||
depletedCount?: number;
|
|
||||||
lastHeartbeat?: number;
|
|
||||||
lastError?: string;
|
|
||||||
allowPrivateAddress?: boolean;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApiMsg<T = unknown> {
|
|
||||||
success?: boolean;
|
|
||||||
msg?: string;
|
|
||||||
obj?: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NodeTotals {
|
|
||||||
total: number;
|
|
||||||
online: number;
|
|
||||||
offline: number;
|
|
||||||
avgLatency: number;
|
|
||||||
inbounds: number;
|
|
||||||
clients: number;
|
|
||||||
onlineClients: number;
|
|
||||||
depleted: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useNodes() {
|
|
||||||
const [nodes, setNodes] = useState<NodeRecord[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [fetched, setFetched] = useState(false);
|
|
||||||
const fetchedRef = useRef(false);
|
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const msg = await HttpUtil.get('/panel/api/nodes/list') as ApiMsg<NodeRecord[]>;
|
|
||||||
if (msg?.success) {
|
|
||||||
setNodes(Array.isArray(msg.obj) ? msg.obj : []);
|
|
||||||
}
|
|
||||||
fetchedRef.current = true;
|
|
||||||
setFetched(true);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const applyNodesEvent = useCallback((payload: unknown) => {
|
|
||||||
if (Array.isArray(payload)) {
|
|
||||||
setNodes(payload as NodeRecord[]);
|
|
||||||
if (!fetchedRef.current) {
|
|
||||||
fetchedRef.current = true;
|
|
||||||
setFetched(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const create = useCallback(async (payload: Partial<NodeRecord>) => {
|
|
||||||
const msg = await HttpUtil.post('/panel/api/nodes/add', payload) as ApiMsg;
|
|
||||||
if (msg?.success) await refresh();
|
|
||||||
return msg;
|
|
||||||
}, [refresh]);
|
|
||||||
|
|
||||||
const update = useCallback(async (id: number, payload: Partial<NodeRecord>) => {
|
|
||||||
const msg = await HttpUtil.post(`/panel/api/nodes/update/${id}`, payload) as ApiMsg;
|
|
||||||
if (msg?.success) await refresh();
|
|
||||||
return msg;
|
|
||||||
}, [refresh]);
|
|
||||||
|
|
||||||
const remove = useCallback(async (id: number) => {
|
|
||||||
const msg = await HttpUtil.post(`/panel/api/nodes/del/${id}`) as ApiMsg;
|
|
||||||
if (msg?.success) await refresh();
|
|
||||||
return msg;
|
|
||||||
}, [refresh]);
|
|
||||||
|
|
||||||
const setEnable = useCallback(async (id: number, enable: boolean) => {
|
|
||||||
const msg = await HttpUtil.post(`/panel/api/nodes/setEnable/${id}`, { enable }) as ApiMsg;
|
|
||||||
if (msg?.success) await refresh();
|
|
||||||
return msg;
|
|
||||||
}, [refresh]);
|
|
||||||
|
|
||||||
const testConnection = useCallback(async (payload: Partial<NodeRecord>) => {
|
|
||||||
return await HttpUtil.post('/panel/api/nodes/test', payload) as ApiMsg<{
|
|
||||||
status: string;
|
|
||||||
latencyMs?: number;
|
|
||||||
xrayVersion?: string;
|
|
||||||
error?: string;
|
|
||||||
}>;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const probe = useCallback(async (id: number) => {
|
|
||||||
const msg = await HttpUtil.post(`/panel/api/nodes/probe/${id}`) as ApiMsg<{
|
|
||||||
status: string;
|
|
||||||
latencyMs?: number;
|
|
||||||
error?: string;
|
|
||||||
}>;
|
|
||||||
if (msg?.success) await refresh();
|
|
||||||
return msg;
|
|
||||||
}, [refresh]);
|
|
||||||
|
|
||||||
const totals = useMemo<NodeTotals>(() => {
|
|
||||||
let online = 0;
|
|
||||||
let offline = 0;
|
|
||||||
let latencySum = 0;
|
|
||||||
let latencyCount = 0;
|
|
||||||
let inbounds = 0;
|
|
||||||
let clients = 0;
|
|
||||||
let onlineClients = 0;
|
|
||||||
let depleted = 0;
|
|
||||||
for (const n of nodes) {
|
|
||||||
inbounds += n.inboundCount || 0;
|
|
||||||
clients += n.clientCount || 0;
|
|
||||||
onlineClients += n.onlineCount || 0;
|
|
||||||
depleted += n.depletedCount || 0;
|
|
||||||
if (!n.enable) continue;
|
|
||||||
if (n.status === 'online') {
|
|
||||||
online += 1;
|
|
||||||
if (n.latencyMs && n.latencyMs > 0) {
|
|
||||||
latencySum += n.latencyMs;
|
|
||||||
latencyCount += 1;
|
|
||||||
}
|
|
||||||
} else if (n.status === 'offline') {
|
|
||||||
offline += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
total: nodes.length,
|
|
||||||
online,
|
|
||||||
offline,
|
|
||||||
avgLatency: latencyCount > 0 ? Math.round(latencySum / latencyCount) : 0,
|
|
||||||
inbounds,
|
|
||||||
clients,
|
|
||||||
onlineClients,
|
|
||||||
depleted,
|
|
||||||
};
|
|
||||||
}, [nodes]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}, [refresh]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
nodes,
|
|
||||||
loading,
|
|
||||||
fetched,
|
|
||||||
totals,
|
|
||||||
refresh,
|
|
||||||
applyNodesEvent,
|
|
||||||
create,
|
|
||||||
update,
|
|
||||||
remove,
|
|
||||||
setEnable,
|
|
||||||
testConnection,
|
|
||||||
probe,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -60,7 +60,7 @@ import { DBInbound } from '@/models/dbinbound.js';
|
||||||
import FinalMaskForm from '@/components/FinalMaskForm';
|
import FinalMaskForm from '@/components/FinalMaskForm';
|
||||||
import DateTimePicker from '@/components/DateTimePicker';
|
import DateTimePicker from '@/components/DateTimePicker';
|
||||||
import JsonEditor from '@/components/JsonEditor';
|
import JsonEditor from '@/components/JsonEditor';
|
||||||
import type { NodeRecord } from '@/hooks/useNodes';
|
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||||
import './InboundFormModal.css';
|
import './InboundFormModal.css';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import {
|
||||||
import { HttpUtil, SizeFormatter, IntlUtil, ColorUtils } from '@/utils';
|
import { HttpUtil, SizeFormatter, IntlUtil, ColorUtils } from '@/utils';
|
||||||
import InfinityIcon from '@/components/InfinityIcon';
|
import InfinityIcon from '@/components/InfinityIcon';
|
||||||
import { useDatepicker } from '@/hooks/useDatepicker';
|
import { useDatepicker } from '@/hooks/useDatepicker';
|
||||||
import type { NodeRecord } from '@/hooks/useNodes';
|
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||||
import './InboundList.css';
|
import './InboundList.css';
|
||||||
|
|
||||||
type ProtocolFlags = {
|
type ProtocolFlags = {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import { coerceInboundJsonField } from '@/models/dbinbound.js';
|
||||||
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 { useWebSocket } from '@/hooks/useWebSocket';
|
||||||
import { useNodes } from '@/hooks/useNodes';
|
import { useNodesQuery } from '@/api/queries/useNodesQuery';
|
||||||
import AppSidebar from '@/components/AppSidebar';
|
import AppSidebar from '@/components/AppSidebar';
|
||||||
import CustomStatistic from '@/components/CustomStatistic';
|
import CustomStatistic from '@/components/CustomStatistic';
|
||||||
const TextModal = lazy(() => import('@/components/TextModal'));
|
const TextModal = lazy(() => import('@/components/TextModal'));
|
||||||
|
|
@ -85,9 +85,9 @@ export default function InboundsPage() {
|
||||||
const [messageApi, messageContextHolder] = message.useMessage();
|
const [messageApi, messageContextHolder] = message.useMessage();
|
||||||
useEffect(() => { setMessageInstance(messageApi); }, [messageApi]);
|
useEffect(() => { setMessageInstance(messageApi); }, [messageApi]);
|
||||||
|
|
||||||
const { nodes: nodesList } = useNodes();
|
const { nodes: nodesList } = useNodesQuery();
|
||||||
const nodesById = useMemo(() => {
|
const nodesById = useMemo(() => {
|
||||||
const map = new Map<number, ReturnType<typeof useNodes>['nodes'][number]>();
|
const map = new Map<number, ReturnType<typeof useNodesQuery>['nodes'][number]>();
|
||||||
for (const n of nodesList || []) map.set(n.id, n);
|
for (const n of nodesList || []) map.set(n.id, n);
|
||||||
return map;
|
return map;
|
||||||
}, [nodesList]);
|
}, [nodesList]);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
Switch,
|
Switch,
|
||||||
message,
|
message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { NodeRecord } from '@/hooks/useNodes';
|
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||||
import './NodeFormModal.css';
|
import './NodeFormModal.css';
|
||||||
|
|
||||||
type Mode = 'add' | 'edit';
|
type Mode = 'add' | 'edit';
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import {
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
import NodeHistoryPanel from './NodeHistoryPanel';
|
import NodeHistoryPanel from './NodeHistoryPanel';
|
||||||
import type { NodeRecord } from '@/hooks/useNodes';
|
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||||
import './NodeList.css';
|
import './NodeList.css';
|
||||||
|
|
||||||
interface NodeListProps {
|
interface NodeListProps {
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ import {
|
||||||
|
|
||||||
import { useTheme } from '@/hooks/useTheme';
|
import { useTheme } from '@/hooks/useTheme';
|
||||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||||
import { useNodes } from '@/hooks/useNodes';
|
import { useNodesQuery } from '@/api/queries/useNodesQuery';
|
||||||
import type { NodeRecord } from '@/hooks/useNodes';
|
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||||
import { useWebSocket } from '@/hooks/useWebSocket';
|
import { useNodeMutations } from '@/api/queries/useNodeMutations';
|
||||||
import AppSidebar from '@/components/AppSidebar';
|
import AppSidebar from '@/components/AppSidebar';
|
||||||
import CustomStatistic from '@/components/CustomStatistic';
|
import CustomStatistic from '@/components/CustomStatistic';
|
||||||
import NodeList from './NodeList';
|
import NodeList from './NodeList';
|
||||||
|
|
@ -29,21 +29,8 @@ export default function NodesPage() {
|
||||||
const [messageApi, messageContextHolder] = message.useMessage();
|
const [messageApi, messageContextHolder] = message.useMessage();
|
||||||
useEffect(() => { setMessageInstance(messageApi); }, [messageApi]);
|
useEffect(() => { setMessageInstance(messageApi); }, [messageApi]);
|
||||||
|
|
||||||
const {
|
const { nodes, loading, fetched, totals } = useNodesQuery();
|
||||||
nodes,
|
const { create, update, remove, setEnable, testConnection, probe } = useNodeMutations();
|
||||||
loading,
|
|
||||||
fetched,
|
|
||||||
totals,
|
|
||||||
applyNodesEvent,
|
|
||||||
create,
|
|
||||||
update,
|
|
||||||
remove,
|
|
||||||
setEnable,
|
|
||||||
testConnection,
|
|
||||||
probe,
|
|
||||||
} = useNodes();
|
|
||||||
|
|
||||||
useWebSocket({ nodes: applyNodesEvent });
|
|
||||||
|
|
||||||
const [formOpen, setFormOpen] = useState(false);
|
const [formOpen, setFormOpen] = useState(false);
|
||||||
const [formMode, setFormMode] = useState<'add' | 'edit'>('add');
|
const [formMode, setFormMode] = useState<'add' | 'edit'>('add');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue