Merge branch 'frontend' of gitea.d3m0k1d.ru:d3m0k1d/HellreigN into HEAD
ci-front / build (push) Successful in 2m27s
ci-front / build (push) Successful in 2m27s
This commit is contained in:
@@ -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