diff --git a/frontend/src/app/providers/layout/navigation/navigation.tsx b/frontend/src/app/providers/layout/navigation/navigation.tsx index cdfd348..9442195 100644 --- a/frontend/src/app/providers/layout/navigation/navigation.tsx +++ b/frontend/src/app/providers/layout/navigation/navigation.tsx @@ -8,6 +8,7 @@ import { FaUsers, FaRocket, FaKey, + FaFileAlt, } from "react-icons/fa"; import { useAuthStore } from "@/modules/auth/store/useAuthStore"; @@ -22,6 +23,7 @@ export const Navigation = () => { { path: "/templates", label: "Шаблоны", icon: FaCode }, { path: "/add-agents", label: "Деплой", icon: FaRocket }, { path: "/registration", label: "Регистрация", icon: FaKey }, + { path: "/logs", label: "Логи", icon: FaFileAlt }, { path: "/admin", label: "Админка", icon: FaUsers, adminOnly: true }, { path: "/themes", label: "Темы", icon: FaPalette }, ]; diff --git a/frontend/src/app/providers/routing/routing.tsx b/frontend/src/app/providers/routing/routing.tsx index b454166..21d9cea 100644 --- a/frontend/src/app/providers/routing/routing.tsx +++ b/frontend/src/app/providers/routing/routing.tsx @@ -12,6 +12,7 @@ import { IDEPage } from "@/pages/ide.page"; import { TemplatesPage } from "@/pages/templates.page"; import { AdminPage } from "@/pages/admin.page"; import { RegistrationTokenPage } from "@/pages/registration.page"; +import { LogsPage } from "@/pages/logs.page"; export const mockGraphData: GraphData = { nodes: [ @@ -123,6 +124,7 @@ export const Routing = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/modules/agent/store/logFilter.store.ts b/frontend/src/modules/agent/store/logFilter.store.ts new file mode 100644 index 0000000..c736e60 --- /dev/null +++ b/frontend/src/modules/agent/store/logFilter.store.ts @@ -0,0 +1,86 @@ +import { create } from "zustand"; + +export type LogLevel = "INFO" | "WARNING" | "ERROR" | "FATAL"; + +interface LogFilterState { + searchQuery: string; + startDate: Date | null; + endDate: Date | null; + selectedLogLevels: LogLevel[]; + selectedService: string; + selectedAgent: string; + limit: number; + offset: number; + + setSearchQuery: (query: string) => void; + setStartDate: (date: Date | null) => void; + setEndDate: (date: Date | null) => void; + toggleLogLevel: (level: LogLevel) => void; + setSelectedService: (service: string) => void; + setSelectedAgent: (agent: string) => void; + setLimit: (limit: number) => void; + setOffset: (offset: number) => void; + resetFilters: () => void; + getFilters: () => { + level?: string; + service?: string; + agent?: string; + date_from?: string; + date_to?: string; + limit: number; + offset: number; + }; +} + +export const useLogFilterStore = create((set, get) => ({ + searchQuery: "", + startDate: null, + endDate: null, + selectedLogLevels: ["INFO", "WARNING", "ERROR", "FATAL"], + selectedService: "", + selectedAgent: "", + limit: 100, + offset: 0, + + setSearchQuery: (query) => set({ searchQuery: query }), + setStartDate: (date) => set({ startDate: date }), + setEndDate: (date) => set({ endDate: date }), + toggleLogLevel: (level) => { + const { selectedLogLevels } = get(); + if (selectedLogLevels.includes(level)) { + set({ selectedLogLevels: selectedLogLevels.filter((l) => l !== level) }); + } else { + set({ selectedLogLevels: [...selectedLogLevels, level] }); + } + }, + setSelectedService: (service) => set({ selectedService: service }), + setSelectedAgent: (agent) => set({ selectedAgent: agent }), + setLimit: (limit) => set({ limit }), + setOffset: (offset) => set({ offset }), + + resetFilters: () => { + set({ + searchQuery: "", + startDate: null, + endDate: null, + selectedLogLevels: ["INFO", "WARNING", "ERROR", "FATAL"], + selectedService: "", + selectedAgent: "", + limit: 100, + offset: 0, + }); + }, + + getFilters: () => { + const { selectedLogLevels, selectedService, selectedAgent, startDate, endDate, limit, offset } = get(); + return { + level: selectedLogLevels.length > 0 ? selectedLogLevels.join(",") : undefined, + service: selectedService || undefined, + agent: selectedAgent || undefined, + date_from: startDate ? startDate.toISOString() : undefined, + date_to: endDate ? endDate.toISOString() : undefined, + limit, + offset, + }; + }, +})); diff --git a/frontend/src/modules/agent/ui/LogFilters.tsx b/frontend/src/modules/agent/ui/LogFilters.tsx new file mode 100644 index 0000000..4457628 --- /dev/null +++ b/frontend/src/modules/agent/ui/LogFilters.tsx @@ -0,0 +1,399 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { + FiSearch, + FiX, + FiFilter, + FiCalendar, + FiTag, + FiCheck, +} from "react-icons/fi"; +import { useLogFilterStore, type LogLevel } from "../store/logFilter.store"; + +const logLevelColors: Record = { + INFO: { bg: "var(--info-bg)", text: "var(--info-text)", border: "var(--info-border)" }, + WARNING: { bg: "var(--warning-bg)", text: "var(--warning-text)", border: "var(--warning-border)" }, + ERROR: { bg: "var(--error-bg)", text: "var(--error-text)", border: "var(--error-border)" }, + FATAL: { bg: "var(--fatal-bg)", text: "var(--fatal-text)", border: "var(--fatal-border)" }, +}; + +interface LogFiltersProps { + onApply: () => void; + availableServices: string[]; + availableAgents: string[]; +} + +export const LogFilters: React.FC = ({ onApply, availableServices, availableAgents }) => { + const { + searchQuery, + startDate, + endDate, + selectedLogLevels, + selectedService, + selectedAgent, + setSearchQuery, + setStartDate, + setEndDate, + toggleLogLevel, + setSelectedService, + setSelectedAgent, + resetFilters, + } = useLogFilterStore(); + + const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery); + const [localStartDate, setLocalStartDate] = useState(startDate); + const [localEndDate, setLocalEndDate] = useState(endDate); + const [localService, setLocalService] = useState(selectedService); + const [localAgent, setLocalAgent] = useState(selectedAgent); + + useEffect(() => { + setLocalSearchQuery(searchQuery); + }, [searchQuery]); + + useEffect(() => { + setLocalStartDate(startDate); + }, [startDate]); + + useEffect(() => { + setLocalEndDate(endDate); + }, [endDate]); + + useEffect(() => { + setLocalService(selectedService); + }, [selectedService]); + + useEffect(() => { + setLocalAgent(selectedAgent); + }, [selectedAgent]); + + const handleApply = useCallback(() => { + setSearchQuery(localSearchQuery); + setStartDate(localStartDate); + setEndDate(localEndDate); + setSelectedService(localService); + setSelectedAgent(localAgent); + onApply(); + }, [localSearchQuery, localStartDate, localEndDate, localService, localAgent, onApply]); + + const handleReset = useCallback(() => { + setLocalSearchQuery(""); + setLocalStartDate(null); + setLocalEndDate(null); + setLocalService(""); + setLocalAgent(""); + resetFilters(); + onApply(); + }, [resetFilters, onApply]); + + const getActiveFiltersCount = () => { + let count = 0; + if (searchQuery) count++; + if (startDate) count++; + if (endDate) count++; + if (selectedService) count++; + if (selectedAgent) count++; + if (selectedLogLevels.length < 4) count++; + return count; + }; + + const formatDate = (date: Date | null) => { + if (!date) return null; + return date.toLocaleDateString("ru-RU"); + }; + + const activeFiltersCount = getActiveFiltersCount(); + + const inputStyle: React.CSSProperties = { + width: "100%", + padding: "8px 12px", + border: "1px solid var(--border)", + borderRadius: "6px", + backgroundColor: "var(--input-bg)", + color: "var(--text-primary)", + fontSize: "13px", + }; + + const selectStyle: React.CSSProperties = { + ...inputStyle, + cursor: "pointer", + }; + + return ( +
+
+ {/* Header */} +
+
+ +

+ Фильтры логов +

+
+ + Активно: {activeFiltersCount} + +
+ + {/* Filters Grid */} +
+ {/* Search */} +
+ + setLocalSearchQuery(e.target.value)} + placeholder="Поиск по сообщению..." + style={{ ...inputStyle, paddingLeft: "32px" }} + onKeyDown={(e) => e.key === "Enter" && handleApply()} + /> +
+ + {/* Service Select */} + + + {/* Agent Select */} + + + {/* Date Range */} +
+ setLocalStartDate(e.target.value ? new Date(e.target.value) : null)} + style={inputStyle} + placeholder="Дата от" + /> + setLocalEndDate(e.target.value ? new Date(e.target.value) : null)} + style={inputStyle} + placeholder="Дата до" + /> +
+
+ + {/* Log Levels */} +
+
+ + + Уровни логов + + + ({selectedLogLevels.length}/4) + +
+
+ {(["INFO", "WARNING", "ERROR", "FATAL"] as LogLevel[]).map((level) => { + const isSelected = selectedLogLevels.includes(level); + const colors = logLevelColors[level]; + return ( + + ); + })} +
+
+ + {/* Action Buttons */} +
+ + +
+ + {/* Active Filters Display */} + {activeFiltersCount > 0 && ( +
+
+ + + Активные фильтры: + +
+
+ {searchQuery && ( +
+ + Поиск: {searchQuery} + +
+ )} + {selectedService && ( +
+ + Сервис: {selectedService} + +
+ )} + {selectedAgent && ( +
+ + Агент: {selectedAgent} + +
+ )} + {startDate && ( +
+ + С: {formatDate(startDate)} + +
+ )} + {endDate && ( +
+ + По: {formatDate(endDate)} + +
+ )} +
+
+ )} +
+
+ ); +}; diff --git a/frontend/src/pages/logs.page.tsx b/frontend/src/pages/logs.page.tsx new file mode 100644 index 0000000..bedc742 --- /dev/null +++ b/frontend/src/pages/logs.page.tsx @@ -0,0 +1,295 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { agentApiService } from "@/modules/agent"; +import type { LogEntry } from "@/modules/agent"; +import { LogFilters } from "@/modules/agent/ui/LogFilters"; +import { useLogFilterStore } from "@/modules/agent/store/logFilter.store"; +import { + FiFileText, + FiRefreshCw, + FiChevronLeft, + FiChevronRight, + FiInfo, + FiAlertTriangle, + FiAlertCircle, + FiXOctagon, +} from "react-icons/fi"; + +const logLevelIcons: Record = { + INFO: , + WARNING: , + ERROR: , + FATAL: , +}; + +const logLevelColors: Record = { + INFO: { bg: "var(--info-bg)", text: "var(--info-text)", border: "var(--info-border)" }, + WARNING: { bg: "var(--warning-bg)", text: "var(--warning-text)", border: "var(--warning-border)" }, + ERROR: { bg: "var(--error-bg)", text: "var(--error-text)", border: "var(--error-border)" }, + FATAL: { bg: "var(--fatal-bg)", text: "var(--fatal-text)", border: "var(--fatal-border)" }, +}; + +export const LogsPage: React.FC = () => { + const [logs, setLogs] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [availableServices, setAvailableServices] = useState([]); + const [availableAgents, setAvailableAgents] = useState([]); + const [totalLogs, setTotalLogs] = useState(0); + + const { getFilters, limit, offset, setOffset } = useLogFilterStore(); + + const fetchLogs = useCallback(async () => { + setIsLoading(true); + setError(null); + try { + const filters = getFilters(); + const data = await agentApiService.searchLogs(filters); + setLogs(data); + setTotalLogs(data.length); + } catch (err) { + setError(err instanceof Error ? err.message : "Ошибка при загрузке логов"); + } finally { + setIsLoading(false); + } + }, [getFilters]); + + const fetchDistinctData = useCallback(async () => { + try { + const [services, agents] = await Promise.all([ + agentApiService.getDistinctServices(), + agentApiService.getDistinctAgents(), + ]); + setAvailableServices(services); + setAvailableAgents(agents); + } catch (err) { + console.error("Failed to fetch distinct data:", err); + } + }, []); + + useEffect(() => { + fetchDistinctData(); + }, [fetchDistinctData]); + + useEffect(() => { + fetchLogs(); + }, [fetchLogs, offset, limit]); + + const handleFilterApply = () => { + setOffset(0); + fetchLogs(); + }; + + const handleNextPage = () => { + setOffset(offset + limit); + }; + + const handlePrevPage = () => { + setOffset(Math.max(0, offset - limit)); + }; + + const formatTimestamp = (timestamp: string) => { + const date = new Date(timestamp); + return date.toLocaleString("ru-RU", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + }; + + return ( +
+
+ {/* Header */} +
+
+
+ +
+
+

+ Поиск логов +

+

+ Фильтрация и анализ логов системы +

+
+
+
+ + {/* Filters */} +
+ +
+ + {/* Error Message */} + {error && ( +
+ {error} +
+ )} + + {/* Logs Table */} +
+ {/* Table Header */} +
+ + Найдено: {totalLogs} записей + + +
+ + {isLoading ? ( +
+ + Загрузка логов... +
+ ) : logs.length === 0 ? ( +
+ Логи не найдены +
+ ) : ( + <> +
+ + + + + + + + + + + + {logs.map((log, index) => { + const colors = logLevelColors[log.level] || logLevelColors.INFO; + return ( + + + + + + + + ); + })} + +
+ Время + + Уровень + + Сервис + + Агент + + Сообщение +
+ {formatTimestamp(log.timestamp)} + + + {logLevelIcons[log.level]} + {log.level} + + + {log.service} + + {log.agent} + + {log.message} +
+
+ + {/* Pagination */} +
+ + + Показано {logs.length} записей (смещение: {offset}) + + +
+ + )} +
+
+
+ ); +};