372 lines
12 KiB
TypeScript
372 lines
12 KiB
TypeScript
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>
|
||
);
|
||
};
|