@@ -7,6 +7,7 @@ import {
|
|||||||
FaUsers,
|
FaUsers,
|
||||||
FaRocket,
|
FaRocket,
|
||||||
FaKey,
|
FaKey,
|
||||||
|
FaFileAlt,
|
||||||
} from "react-icons/fa";
|
} from "react-icons/fa";
|
||||||
import { useAuthStore } from "@/modules/auth/store/useAuthStore";
|
import { useAuthStore } from "@/modules/auth/store/useAuthStore";
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ export const Navigation = () => {
|
|||||||
{ path: "/", label: "Главная", icon: FaHome },
|
{ path: "/", label: "Главная", icon: FaHome },
|
||||||
{ path: "/add-agents", label: "Деплой", icon: FaRocket },
|
{ path: "/add-agents", label: "Деплой", icon: FaRocket },
|
||||||
{ path: "/registration", label: "Регистрация", icon: FaKey },
|
{ path: "/registration", label: "Регистрация", icon: FaKey },
|
||||||
|
{ path: "/logs", label: "Логи", icon: FaFileAlt },
|
||||||
{ path: "/admin", label: "Админка", icon: FaUsers, adminOnly: true },
|
{ path: "/admin", label: "Админка", icon: FaUsers, adminOnly: true },
|
||||||
{ path: "/themes", label: "Темы", icon: FaPalette },
|
{ path: "/themes", label: "Темы", icon: FaPalette },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { AddAgentsPage } from "@/pages/add-agents.page";
|
|||||||
import { IDEPage } from "@/pages/ide.page";
|
import { IDEPage } from "@/pages/ide.page";
|
||||||
import { AdminPage } from "@/pages/admin.page";
|
import { AdminPage } from "@/pages/admin.page";
|
||||||
import { RegistrationTokenPage } from "@/pages/registration.page";
|
import { RegistrationTokenPage } from "@/pages/registration.page";
|
||||||
|
import { LogsPage } from "@/pages/logs.page";
|
||||||
|
|
||||||
export const mockGraphData: GraphData = {
|
export const mockGraphData: GraphData = {
|
||||||
nodes: [
|
nodes: [
|
||||||
@@ -122,6 +123,7 @@ export const Routing = () => {
|
|||||||
<Route path="/themes" element={<ThemesPage />} />
|
<Route path="/themes" element={<ThemesPage />} />
|
||||||
<Route path="/add-agents" element={<AddAgentsPage />} />
|
<Route path="/add-agents" element={<AddAgentsPage />} />
|
||||||
<Route path="/registration" element={<RegistrationTokenPage />} />
|
<Route path="/registration" element={<RegistrationTokenPage />} />
|
||||||
|
<Route path="/logs" element={<LogsPage />} />
|
||||||
<Route path="/admin" element={<AdminPage />} />
|
<Route path="/admin" element={<AdminPage />} />
|
||||||
<Route path="/IDE" element={<IDEPage />} />
|
<Route path="/IDE" element={<IDEPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -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<LogFilterState>((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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -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<LogLevel, { bg: string; text: string; border: string }> = {
|
||||||
|
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<LogFiltersProps> = ({ 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<Date | null>(startDate);
|
||||||
|
const [localEndDate, setLocalEndDate] = useState<Date | null>(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 (
|
||||||
|
<div
|
||||||
|
className="rounded-xl border"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--card-bg)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="p-4">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FiFilter size={14} style={{ color: "var(--accent)" }} />
|
||||||
|
<h3 className="text-sm font-semibold" style={{ color: "var(--text-primary)" }}>
|
||||||
|
Фильтры логов
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
Активно: {activeFiltersCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<FiSearch
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: "10px",
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={localSearchQuery}
|
||||||
|
onChange={(e) => setLocalSearchQuery(e.target.value)}
|
||||||
|
placeholder="Поиск по сообщению..."
|
||||||
|
style={{ ...inputStyle, paddingLeft: "32px" }}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && handleApply()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Service Select */}
|
||||||
|
<select
|
||||||
|
value={localService}
|
||||||
|
onChange={(e) => setLocalService(e.target.value)}
|
||||||
|
style={selectStyle}
|
||||||
|
>
|
||||||
|
<option value="">Все сервисы</option>
|
||||||
|
{availableServices.map((service) => (
|
||||||
|
<option key={service} value={service}>
|
||||||
|
{service}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Agent Select */}
|
||||||
|
<select
|
||||||
|
value={localAgent}
|
||||||
|
onChange={(e) => setLocalAgent(e.target.value)}
|
||||||
|
style={selectStyle}
|
||||||
|
>
|
||||||
|
<option value="">Все агенты</option>
|
||||||
|
{availableAgents.map((agent) => (
|
||||||
|
<option key={agent} value={agent}>
|
||||||
|
{agent}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Date Range */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={localStartDate ? localStartDate.toISOString().split("T")[0] : ""}
|
||||||
|
onChange={(e) => setLocalStartDate(e.target.value ? new Date(e.target.value) : null)}
|
||||||
|
style={inputStyle}
|
||||||
|
placeholder="Дата от"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={localEndDate ? localEndDate.toISOString().split("T")[0] : ""}
|
||||||
|
onChange={(e) => setLocalEndDate(e.target.value ? new Date(e.target.value) : null)}
|
||||||
|
style={inputStyle}
|
||||||
|
placeholder="Дата до"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Log Levels */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<FiTag size={12} style={{ color: "var(--text-secondary)" }} />
|
||||||
|
<span className="text-xs font-medium" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
Уровни логов
|
||||||
|
</span>
|
||||||
|
<span className="text-xs" style={{ color: "var(--text-muted)" }}>
|
||||||
|
({selectedLogLevels.length}/4)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{(["INFO", "WARNING", "ERROR", "FATAL"] as LogLevel[]).map((level) => {
|
||||||
|
const isSelected = selectedLogLevels.includes(level);
|
||||||
|
const colors = logLevelColors[level];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={level}
|
||||||
|
onClick={() => toggleLogLevel(level)}
|
||||||
|
className="px-3 py-1.5 rounded-lg text-xs font-medium transition-all border"
|
||||||
|
style={{
|
||||||
|
backgroundColor: isSelected ? colors.bg : "transparent",
|
||||||
|
color: isSelected ? colors.text : "var(--text-secondary)",
|
||||||
|
borderColor: isSelected ? colors.border : "var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSelected && <FiCheck size={10} className="inline mr-1" />}
|
||||||
|
{level}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleApply}
|
||||||
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg transition-all text-sm font-medium"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--button-primary)",
|
||||||
|
color: "var(--button-primary-text)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FiCheck size={14} />
|
||||||
|
Применить
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleReset}
|
||||||
|
className="px-4 py-2 rounded-lg transition-all text-sm font-medium border"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
color: "var(--text-secondary)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FiX size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active Filters Display */}
|
||||||
|
{activeFiltersCount > 0 && (
|
||||||
|
<div className="mt-4 pt-4 border-t" style={{ borderColor: "var(--border)" }}>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<FiFilter size={10} style={{ color: "var(--accent)" }} />
|
||||||
|
<span className="text-xs" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
Активные фильтры:
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{searchQuery && (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1 px-2 py-1 rounded-lg border text-xs"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--bg-secondary)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FiSearch size={10} />
|
||||||
|
<span style={{ color: "var(--text-primary)" }}>Поиск: {searchQuery}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setLocalSearchQuery("");
|
||||||
|
setSearchQuery("");
|
||||||
|
onApply();
|
||||||
|
}}
|
||||||
|
style={{ background: "none", border: "none", cursor: "pointer", color: "var(--text-muted)" }}
|
||||||
|
>
|
||||||
|
<FiX size={10} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedService && (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1 px-2 py-1 rounded-lg border text-xs"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--bg-secondary)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FiTag size={10} />
|
||||||
|
<span style={{ color: "var(--text-primary)" }}>Сервис: {selectedService}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setLocalService("");
|
||||||
|
setSelectedService("");
|
||||||
|
onApply();
|
||||||
|
}}
|
||||||
|
style={{ background: "none", border: "none", cursor: "pointer", color: "var(--text-muted)" }}
|
||||||
|
>
|
||||||
|
<FiX size={10} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedAgent && (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1 px-2 py-1 rounded-lg border text-xs"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--bg-secondary)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FiTag size={10} />
|
||||||
|
<span style={{ color: "var(--text-primary)" }}>Агент: {selectedAgent}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setLocalAgent("");
|
||||||
|
setSelectedAgent("");
|
||||||
|
onApply();
|
||||||
|
}}
|
||||||
|
style={{ background: "none", border: "none", cursor: "pointer", color: "var(--text-muted)" }}
|
||||||
|
>
|
||||||
|
<FiX size={10} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{startDate && (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1 px-2 py-1 rounded-lg border text-xs"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--bg-secondary)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FiCalendar size={10} />
|
||||||
|
<span style={{ color: "var(--text-primary)" }}>С: {formatDate(startDate)}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setLocalStartDate(null);
|
||||||
|
setStartDate(null);
|
||||||
|
onApply();
|
||||||
|
}}
|
||||||
|
style={{ background: "none", border: "none", cursor: "pointer", color: "var(--text-muted)" }}
|
||||||
|
>
|
||||||
|
<FiX size={10} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{endDate && (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1 px-2 py-1 rounded-lg border text-xs"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--bg-secondary)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FiCalendar size={10} />
|
||||||
|
<span style={{ color: "var(--text-primary)" }}>По: {formatDate(endDate)}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setLocalEndDate(null);
|
||||||
|
setEndDate(null);
|
||||||
|
onApply();
|
||||||
|
}}
|
||||||
|
style={{ background: "none", border: "none", cursor: "pointer", color: "var(--text-muted)" }}
|
||||||
|
>
|
||||||
|
<FiX size={10} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<string, React.ReactNode> = {
|
||||||
|
INFO: <FiInfo size={14} />,
|
||||||
|
WARNING: <FiAlertTriangle size={14} />,
|
||||||
|
ERROR: <FiAlertCircle size={14} />,
|
||||||
|
FATAL: <FiXOctagon size={14} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const logLevelColors: Record<string, { bg: string; text: string; border: string }> = {
|
||||||
|
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<LogEntry[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [availableServices, setAvailableServices] = useState<string[]>([]);
|
||||||
|
const [availableAgents, setAvailableAgents] = useState<string[]>([]);
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className="min-h-screen py-8 px-4"
|
||||||
|
style={{ backgroundColor: "var(--bg-primary)" }}
|
||||||
|
>
|
||||||
|
<div style={{ maxWidth: "1400px", margin: "0 auto" }}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<div
|
||||||
|
className="w-14 h-14 rounded-xl flex items-center justify-center"
|
||||||
|
style={{ backgroundColor: "var(--bg-secondary)" }}
|
||||||
|
>
|
||||||
|
<FiFileText className="w-7 h-7" style={{ color: "var(--accent)" }} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1
|
||||||
|
className="text-3xl font-bold mb-1"
|
||||||
|
style={{ color: "var(--text-primary)" }}
|
||||||
|
>
|
||||||
|
Поиск логов
|
||||||
|
</h1>
|
||||||
|
<p style={{ color: "var(--text-secondary)", fontSize: "16px" }}>
|
||||||
|
Фильтрация и анализ логов системы
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<LogFilters
|
||||||
|
onApply={handleFilterApply}
|
||||||
|
availableServices={availableServices}
|
||||||
|
availableAgents={availableAgents}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
{error && (
|
||||||
|
<div
|
||||||
|
className="mb-6 p-4 rounded-lg border text-sm"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--error-bg)",
|
||||||
|
borderColor: "var(--error-border)",
|
||||||
|
color: "var(--error-text)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Logs Table */}
|
||||||
|
<div
|
||||||
|
className="rounded-xl border overflow-hidden"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--card-bg)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Table Header */}
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-b" style={{ borderColor: "var(--border)" }}>
|
||||||
|
<span className="text-sm font-medium" style={{ color: "var(--text-primary)" }}>
|
||||||
|
Найдено: {totalLogs} записей
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={fetchLogs}
|
||||||
|
className="flex items-center gap-2 px-3 py-1.5 rounded-lg transition-all text-xs font-medium border"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
color: "var(--accent)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FiRefreshCw size={12} className={isLoading ? "animate-spin" : ""} />
|
||||||
|
Обновить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-12" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
<FiRefreshCw size={24} className="animate-spin mr-3" />
|
||||||
|
Загрузка логов...
|
||||||
|
</div>
|
||||||
|
) : logs.length === 0 ? (
|
||||||
|
<div className="text-center py-12" style={{ color: "var(--text-muted)" }}>
|
||||||
|
Логи не найдены
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr style={{ backgroundColor: "var(--bg-secondary)" }}>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
Время
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
Уровень
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
Сервис
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
Агент
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
Сообщение
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{logs.map((log, index) => {
|
||||||
|
const colors = logLevelColors[log.level] || logLevelColors.INFO;
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={index}
|
||||||
|
className="border-t"
|
||||||
|
style={{
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
backgroundColor: index % 2 === 0 ? "var(--card-bg)" : "var(--bg-secondary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td className="px-4 py-3 text-sm font-mono whitespace-nowrap" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
{formatTimestamp(log.timestamp)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium border"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.bg,
|
||||||
|
color: colors.text,
|
||||||
|
borderColor: colors.border,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{logLevelIcons[log.level]}
|
||||||
|
{log.level}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm" style={{ color: "var(--text-primary)" }}>
|
||||||
|
{log.service}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm font-mono" style={{ color: "var(--text-primary)" }}>
|
||||||
|
{log.agent}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm" style={{ color: "var(--text-primary)" }}>
|
||||||
|
{log.message}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-t" style={{ borderColor: "var(--border)" }}>
|
||||||
|
<button
|
||||||
|
onClick={handlePrevPage}
|
||||||
|
disabled={offset === 0}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-all text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed border"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--input-bg)",
|
||||||
|
color: "var(--text-primary)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FiChevronLeft size={16} />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
<span className="text-sm" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
Показано {logs.length} записей (смещение: {offset})
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={handleNextPage}
|
||||||
|
disabled={logs.length < limit}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-all text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed border"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--input-bg)",
|
||||||
|
color: "var(--text-primary)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Далее
|
||||||
|
<FiChevronRight size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user