feat: add shared
ci-front / build (push) Successful in 1m58s

This commit is contained in:
2026-04-03 20:00:51 +03:00
parent add00401de
commit f073756e92
11 changed files with 1461 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
import { apiClient } from "./axios.instance";
import type { AxiosResponse } from "axios";
export interface ApiResponse<T = any> {
data: T;
message: string;
success: boolean;
}
class ApiService {
async get<T>(url: string, config?: any): Promise<T> {
const response: AxiosResponse<T> = await apiClient.get(url, config);
return response.data;
}
async post<T, D = any>(url: string, data?: D, config?: any): Promise<T> {
const response: AxiosResponse<T> = await apiClient.post(url, data, config);
return response.data;
}
async put<T, D = any>(url: string, data?: D, config?: any): Promise<T> {
const response: AxiosResponse<T> = await apiClient.put(url, data, config);
return response.data;
}
async patch<T, D = any>(url: string, data?: D, config?: any): Promise<T> {
const response: AxiosResponse<T> = await apiClient.patch(url, data, config);
return response.data;
}
async delete<T>(url: string, config?: any): Promise<T> {
const response: AxiosResponse<T> = await apiClient.delete(url, config);
return response.data;
}
}
export const apiService = new ApiService();
+64
View File
@@ -0,0 +1,64 @@
import axios, {
type AxiosInstance,
type AxiosResponse,
type AxiosError,
type InternalAxiosRequestConfig,
} from "axios";
export interface ApiResponse<T = any> {
data: T;
message?: string;
status: number;
}
class ApiClient {
private axiosInstance: AxiosInstance;
constructor() {
this.axiosInstance = axios.create({
baseURL: "http://194.113.106.59:8080/api/v1",
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
validateStatus: (status) => {
return status >= 200 && status < 500;
},
});
this.setupInterceptors();
}
private setupInterceptors(): void {
this.axiosInstance.interceptors.request.use(
(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
return config;
},
(error: AxiosError): Promise<AxiosError> => {
console.error("[Request Error]", error);
return Promise.reject(error);
},
);
this.axiosInstance.interceptors.response.use(
(response: AxiosResponse): AxiosResponse => {
console.log(`[Response] ${response.status} ${response.config.url}`);
return response;
},
async (error: AxiosError): Promise<any> => {
if (error.response?.status === 401) {
window.location.href = "/auth";
return Promise.reject(error);
}
return Promise.reject(error);
},
);
}
public getInstance(): AxiosInstance {
return this.axiosInstance;
}
}
export const apiClient = new ApiClient().getInstance();
+32
View File
@@ -0,0 +1,32 @@
import { useState, useCallback } from "react";
export function useApi() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const request = useCallback(
async <T>(apiCall: () => Promise<T>): Promise<T | undefined> => {
setIsLoading(true);
setError(null);
try {
const result = await apiCall();
return result;
} catch (err: any) {
const errorMessage =
err.response?.data?.message || err.message || "Произошла ошибка";
setError(errorMessage);
return undefined;
} finally {
setIsLoading(false);
}
},
[],
);
return {
isLoading,
error,
request,
};
}
+2
View File
@@ -0,0 +1,2 @@
export { apiClient } from "./axios.instance";
export { useApi } from "./hooks/use.api";
@@ -0,0 +1,136 @@
// shared/api/websocket.service.ts
import { useAgentStore } from "@/components/layout/sidebar/store/agent.store";
import { useWebSocket, type LogMessage } from "@/shared/hooks/useWebSocket";
import { useEffect, useRef, useCallback, useMemo } from "react";
interface WebSocketServiceProps {
onLogMessage?: (message: LogMessage) => void;
}
export const useWebSocketService = ({
onLogMessage,
}: WebSocketServiceProps = {}) => {
const { agents } = useAgentStore();
const lastFilterRef = useRef<{ hosts: string[]; services: string[] }>({
hosts: [],
services: [],
});
// Токен для аутентификации
const TOKEN =
"H0AB91gb7427xswom0xalJHq7Ked0tLt6F0gOyqw5yMWPDrroOcX8CjPXeD8uzsU";
// Получаем выбранные агенты и сервисы синхронно
const getSelectedServices = useCallback(() => {
const selectedServices: string[] = [];
const selectedHosts: string[] = [];
agents.forEach((agent) => {
agent.services.forEach((service) => {
if (service.isSelected) {
selectedServices.push(service.name);
selectedHosts.push(agent.token);
}
});
});
return { hosts: selectedHosts, services: selectedServices };
}, [agents]);
// Формируем URL синхронно
const wsUrl = useMemo(() => {
const { hosts, services } = getSelectedServices();
const params = new URLSearchParams();
if (hosts.length === 0 && services.length === 0) {
params.append("all", "true");
} else {
hosts.forEach((host) => {
params.append("hosts", host);
});
services.forEach((service) => {
params.append("services", service);
});
}
const queryString = params.toString();
const url = `${import.meta.env.VITE_WS_URL}/ws?${queryString}`;
console.log("Generated WebSocket URL:", url);
return url;
}, [getSelectedServices]);
const {
isConnected,
isAuthenticated,
lastMessage,
error,
reconnect,
connect: wsConnect,
disconnect: wsDisconnect,
updateFilter,
} = useWebSocket({
url: wsUrl,
token: TOKEN,
autoConnect: false, // Отключаем авто-подключение, будем управлять вручную
reconnectInterval: 3000,
maxReconnectAttempts: 10,
});
// Функция для подключения
const connect = useCallback(() => {
if (!isManualPausedRef.current) {
wsConnect();
}
}, [wsConnect]);
// Функция для отключения
const disconnect = useCallback(() => {
wsDisconnect();
}, [wsDisconnect]);
// Реф для отслеживания ручной паузы
const isManualPausedRef = useRef(false);
// Принудительно переподключаемся при изменении URL, если не на паузе
useEffect(() => {
if (wsUrl && !isManualPausedRef.current) {
console.log("URL changed, reconnecting...");
setTimeout(() => {
reconnect();
}, 100);
}
}, [wsUrl, reconnect]);
// Обновляем фильтр при изменении выбранных сервисов
useEffect(() => {
const { hosts, services } = getSelectedServices();
const currentFilter = { hosts, services };
const hasChanged =
JSON.stringify(currentFilter) !== JSON.stringify(lastFilterRef.current);
if (hasChanged && isConnected && !isManualPausedRef.current) {
console.log("Updating filter:", currentFilter);
updateFilter(hosts, services);
lastFilterRef.current = currentFilter;
}
}, [agents, isConnected, updateFilter, getSelectedServices]);
// Передаем сообщения в callback
useEffect(() => {
if (lastMessage && onLogMessage) {
onLogMessage(lastMessage);
}
}, [lastMessage, onLogMessage]);
return {
isConnected: isConnected && isAuthenticated,
isAuthenticated,
error,
selectedCount: getSelectedServices().services.length,
connect,
disconnect,
};
};