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