gitscope
← marzneshin / v0.2.0 / app/dashboard/src/stores/nodes.store.ts
https://dev.kronoz.win/r/marzneshin.git
app/dashboard/src/stores/nodes.store.ts · TypeScript
Blame Raw
  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 async 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  nodesFilters: NodesFilterType;
 83  refetchNodes: () => void;
 84  refetchCertificate: () => void;
 85  fetchNodesUsage: (query: FilterUsageType) => Promise<void>;
 86  addNode: (node: NodeCreate) => Promise<unknown>;
 87  deleteNode: (node: NodeCreate) => Promise<unknown>;
 88  updateNode: (node: NodeCreate) => Promise<unknown>;
 89  reconnectNode: (node: NodeType) => Promise<unknown>;
 90  onDeletingNode: (node: NodeCreate | undefined) => void;
 91  onEditingNode: (node: NodeType | undefined) => void;
 92  onAddingNode: (isAddingNode: boolean) => void;
 93  onFilterChange: (filters: Partial<NodesFilterType>) => void;
 94  onShowingNodesUsage: (isShowingNodesUsage: boolean) => void;
 95};
 96
 97// TODO: Refactor all useQuery for inbounds, hosts, 
 98// services and uses to be a custom hook like this.
 99export const useNodesQuery = () => {
100  return useQuery({
101    queryKey: [FetchNodesQueryKey],
102    queryFn: fetchNodes,
103    refetchInterval: useNodes.getState().editingNode ? 3000 : undefined,
104    refetchOnWindowFocus: false,
105  });
106};
107
108export const useNodes = create<NodeStore>((set, get) => ({
109  certificate: '',
110  editingNode: null,
111  deletingNode: null,
112  isAddingNode: false,
113  isEditingNode: false,
114  isShowingNodesUsage: false,
115  isEditingCore: false,
116  nodesFilters: {
117    name: '',
118    limit: pageSizeManagers.nodes.getPageSize(),
119    sort: '-created_at',
120  },
121  onFilterChange: (filters) => {
122    set({
123      nodesFilters: {
124        ...get().nodesFilters,
125        ...filters,
126      },
127    });
128    get().refetchNodes();
129  },
130  addNode: async (body) => {
131    return fetch('/nodes', { method: 'POST', body }).then(() => { get().refetchCertificate() });
132  },
133  refetchNodes: () => {
134    queryClient.invalidateQueries({ queryKey: [queryIds.nodes] });
135  },
136  refetchCertificate: () => {
137    fetch('/nodes/settings').then((res) => {
138      set({ certificate: res.certificate });
139    })
140  },
141  fetchNodesUsage: (query: FilterUsageType) => {
142    return fetch('/nodes/usage', { query });
143  },
144  updateNode: (body) => {
145    console.log('update: ' + body.id)
146    return fetch(`/nodes/${body.id}`, {
147      method: 'PUT',
148      body,
149    });
150  },
151  onDeletingNode: (node: NodeCreate | undefined) => {
152    if (node !== undefined) {
153      set({ deletingNode: node });
154    }
155    else {
156      set({ deletingNode: undefined });
157    }
158  },
159  reconnectNode: (body) => {
160    return fetch(`/nodes/${body.id}/reconnect`, {
161      method: 'POST',
162    });
163  },
164  deleteNode: () => {
165    return fetch(`/nodes/${get().deletingNode?.id}`, {
166      method: 'DELETE',
167    });
168  },
169  onEditingNode: (editingNode: NodeType | undefined) => {
170    if (editingNode !== undefined) {
171      const { status, ...rest } = editingNode;
172      set({ editingNode: { ...rest, status: (status === 'healthy' || status === 'unhealthy') ? 'none' : 'disabled' }, isEditingNode: true });
173    }
174    else {
175      set({ editingNode: undefined, isEditingNode: false });
176
177    }
178  },
179  onAddingNode: (isAddingNode: boolean) => {
180    set({ isAddingNode });
181  },
182  onShowingNodesUsage: (isShowingNodesUsage: boolean) => {
183    set({ isShowingNodesUsage });
184  },
185}));