feat: create logs
ci-front / build (push) Successful in 2m24s

This commit is contained in:
2026-04-04 06:13:12 +03:00
parent 96f82b4162
commit 95a6902dae
5 changed files with 784 additions and 0 deletions
+295
View File
@@ -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>
);
};