1import { useQuery } from '@tanstack/react-query';
2import { fetch } from 'service/http';
3import { create } from 'zustand';
4import { z } from 'zod';
5import { FilterUsageType, NodesFilterType } from 'types/filter';
6import { pageSizeManagers } from 'utils/userPreferenceStorage';
7import { useDashboard } from './dashboard.store';
8import { queryClient } from 'service/react-query';
9import { queryIds } from 'constants/query-ids';
10
11export const NodeSchema = z.object({
12 name: z.string().min(1),
13 address: z.string().min(1),
14 port: z
15 .number()
16 .min(1)
17 .or(z.string().transform((v) => parseFloat(v))),
18 xray_version: z.string().nullable().optional(),
19 id: z.number().nullable().optional(),
20 status: z
21 .enum(['healthy', 'unhealthy', 'disabled']),
22 message: z.string().nullable().optional(),
23 usage_coefficient: z.number().default(1),
24 add_as_new_host: z.boolean(),
25});
26
27export const NodeCreateSchema = z.object({
28 name: z.string().min(1),
29 address: z.string().min(1),
30 port: z
31 .number()
32 .min(1)
33 .or(z.string().transform((v) => parseFloat(v))),
34 id: z.number().nullable().optional(),
35 status: z
36 .enum(['none', 'disabled']),
37 usage_coefficient: z.number().default(1),
38 add_as_new_host: z.boolean().optional(),
39});
40
41export type NodeType = z.infer<typeof NodeSchema>;
42export type NodeCreate = z.infer<typeof NodeCreateSchema>;
43
44export const getNodeDefaultValues = (): NodeType => ({
45 name: '',
46 address: '',
47 status: 'unhealthy',
48 port: 62050,
49 xray_version: '',
50 add_as_new_host: false,
51 usage_coefficient: 1,
52});
53
54export const getNodeCreateDefaultValues = (): NodeCreate => ({
55 name: '',
56 address: '',
57 status: 'none',
58 port: 62050,
59 add_as_new_host: false,
60 usage_coefficient: 1,
61 id: 0,
62});
63
64export const FetchNodesQueryKey = 'fetch-nodes-query-key';
65
66export function fetchNodes(): Promise<NodeType[]> {
67 useDashboard.setState({ loading: true })
68 return fetch('/nodes').then((nodes) => {
69 return nodes;
70 }).finally(() => {
71 useDashboard.setState({ loading: false })
72 });
73}
74
75export type NodeStore = {
76 certificate: string,
77 deletingNode?: NodeCreate | null;
78 editingNode: NodeCreate | null;
79 isAddingNode: boolean;
80 isShowingNodesUsage: boolean;
81 isEditingNode: boolean;
82 isEditingCore: boolean;
83 nodesFilters: NodesFilterType;
84 refetchNodes: () => void;
85 refetchCertificate: () => void;
86 fetchNodesUsage: (query: FilterUsageType) => Promise<void>;
87 addNode: (node: NodeCreate) => Promise<unknown>;
88 deleteNode: (node: NodeCreate) => Promise<unknown>;
89 updateNode: (node: NodeCreate) => Promise<unknown>;
90 reconnectNode: (node: NodeType) => Promise<unknown>;
91 onDeletingNode: (node: NodeCreate | undefined) => void;
92 onEditingNode: (node: NodeType | undefined) => void;
93 onAddingNode: (isAddingNode: boolean) => void;
94 onFilterChange: (filters: Partial<NodesFilterType>) => void;
95 onShowingNodesUsage: (isShowingNodesUsage: boolean) => void;
96};
97
98// TODO: Refactor all useQuery for inbounds, hosts,
99// services and uses to be a custom hook like this.
100export const useNodesQuery = () => {
101 return useQuery({
102 queryKey: [FetchNodesQueryKey],
103 queryFn: fetchNodes,
104 refetchInterval: useNodes.getState().editingNode ? 3000 : undefined,
105 refetchOnWindowFocus: false,
106 });
107};
108
109export const useNodes = create<NodeStore>((set, get) => ({
110 certificate: '',
111 editingNode: null,
112 deletingNode: null,
113 isAddingNode: false,
114 isEditingNode: false,
115 isShowingNodesUsage: false,
116 isEditingCore: false,
117 nodesFilters: {
118 name: '',
119 limit: pageSizeManagers.nodes.getPageSize(),
120 sort: '-created_at',
121 },
122 onFilterChange: (filters) => {
123 set({
124 nodesFilters: {
125 ...get().nodesFilters,
126 ...filters,
127 },
128 });
129 get().refetchNodes();
130 },
131 addNode: async (body) => {
132 return fetch('/nodes', { method: 'POST', body }).then(() => { get().refetchCertificate() });
133 },
134 refetchNodes: () => {
135 queryClient.invalidateQueries({ queryKey: [queryIds.nodes] });
136 },
137 refetchCertificate: () => {
138 fetch('/nodes/settings').then((res) => {
139 set({ certificate: res.certificate });
140 })
141 },
142 fetchNodesUsage: (query: FilterUsageType) => {
143 return fetch('/nodes/usage', { query });
144 },
145 updateNode: (body) => {
146 console.log('update: ' + body.id)
147 return fetch(`/nodes/${body.id}`, {
148 method: 'PUT',
149 body,
150 });
151 },
152 onDeletingNode: (node: NodeCreate | undefined) => {
153 if (node !== undefined) {
154 set({ deletingNode: node });
155 }
156 else {
157 set({ deletingNode: undefined });
158 }
159 },
160 reconnectNode: (body) => {
161 return fetch(`/nodes/${body.id}/reconnect`, {
162 method: 'POST',
163 });
164 },
165 deleteNode: () => {
166 return fetch(`/nodes/${get().deletingNode?.id}`, {
167 method: 'DELETE',
168 });
169 },
170 onEditingNode: (editingNode: NodeType | undefined) => {
171 if (editingNode !== undefined) {
172 const { status, ...rest } = editingNode;
173 set({ editingNode: { ...rest, status: (status === 'healthy' || status === 'unhealthy') ? 'none' : 'disabled' }, isEditingNode: true });
174 }
175 else {
176 set({ editingNode: undefined, isEditingNode: false });
177
178 }
179 },
180 onAddingNode: (isAddingNode: boolean) => {
181 set({ isAddingNode });
182 },
183 onShowingNodesUsage: (isShowingNodesUsage: boolean) => {
184 set({ isShowingNodesUsage });
185 },
186}));