From 71db3cc305fcf3c9c21c401a3d947fade9ac9e79 Mon Sep 17 00:00:00 2001 From: domverse Date: Fri, 27 Feb 2026 15:04:29 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20implement=20Phase=207=20=E2=80=94=20Log?= =?UTF-8?q?s=20page=20redesign?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auto-refresh toggle (RefreshCw icon, animates when active, polls every 5s) - Horizontal filter bar: level select (140px) + search input + Clear button - Clear button only rendered when level or search is active - Level badges: INFO=blue, WARNING=amber, ERROR=red, CRITICAL=dark red, DEBUG=grey - Row background tints: ERROR=#FFF5F5, WARNING=#FFFBF0, CRITICAL=#FFF0F0 - Message text in font-mono, metadata line with · separators - Right-aligned timestamp: time on first line, date below - Skeleton loading (8× SkeletonTableRow) while fetching Co-Authored-By: Claude Sonnet 4.6 --- flight-comparator/frontend/src/pages/Logs.tsx | 357 ++++++++++-------- 1 file changed, 209 insertions(+), 148 deletions(-) diff --git a/flight-comparator/frontend/src/pages/Logs.tsx b/flight-comparator/frontend/src/pages/Logs.tsx index 7148721..9ad818e 100644 --- a/flight-comparator/frontend/src/pages/Logs.tsx +++ b/flight-comparator/frontend/src/pages/Logs.tsx @@ -1,194 +1,255 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { Search, RefreshCw, X } from 'lucide-react'; import { logApi } from '../api'; import type { LogEntry } from '../api'; +import { SkeletonTableRow } from '../components/SkeletonCard'; +import { cn } from '../lib/utils'; + +// ── Level config ─────────────────────────────────────────────────────────── + +const LEVEL_CONFIG: Record = { + DEBUG: { badge: 'bg-[#F1F3F4] text-[#5F6368]', row: '' }, + INFO: { badge: 'bg-[#E8F0FE] text-[#1557B0]', row: '' }, + WARNING: { badge: 'bg-[#FEF7E0] text-[#7A5200]', row: 'bg-[#FFFBF0]' }, + ERROR: { badge: 'bg-[#FDECEA] text-[#A50E0E]', row: 'bg-[#FFF5F5]' }, + CRITICAL: { badge: 'bg-[#FCE0DE] text-[#7D1A0E]', row: 'bg-[#FFF0F0]' }, +}; + +const levelConfig = (level: string) => + LEVEL_CONFIG[level] ?? LEVEL_CONFIG.DEBUG; + +const formatTime = (ts: string) => { + const d = new Date(ts); + return d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); +}; + +const formatDate = (ts: string) => + new Date(ts).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }); + +// ────────────────────────────────────────────────────────────────────────── export default function Logs() { - const [logs, setLogs] = useState([]); - const [loading, setLoading] = useState(true); - const [page, setPage] = useState(1); + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); - const [level, setLevel] = useState(''); - const [search, setSearch] = useState(''); + const [level, setLevel] = useState(''); + const [search, setSearch] = useState(''); const [searchQuery, setSearchQuery] = useState(''); + const [autoRefresh, setAutoRefresh] = useState(false); + const debounceTimer = useRef | undefined>(undefined); + const intervalRef = useRef | undefined>(undefined); useEffect(() => { loadLogs(); }, [page, level, searchQuery]); + // Auto-refresh + useEffect(() => { + if (autoRefresh) { + intervalRef.current = setInterval(() => loadLogs(), 5000); + } else { + if (intervalRef.current) clearInterval(intervalRef.current); + } + return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; + }, [autoRefresh, page, level, searchQuery]); + const loadLogs = async () => { try { setLoading(true); const response = await logApi.list(page, 50, level || undefined, searchQuery || undefined); setLogs(response.data.data); setTotalPages(response.data.pagination.pages); - } catch (error) { - console.error('Failed to load logs:', error); + } catch (err) { + console.error('Failed to load logs:', err); } finally { setLoading(false); } }; - const handleSearch = (e: React.FormEvent) => { - e.preventDefault(); - setSearchQuery(search); + const handleSearchChange = (e: React.ChangeEvent) => { + const val = e.target.value; + setSearch(val); + if (debounceTimer.current) clearTimeout(debounceTimer.current); + debounceTimer.current = setTimeout(() => { + setSearchQuery(val); + setPage(1); + }, 400); + }; + + const handleLevelChange = (e: React.ChangeEvent) => { + setLevel(e.target.value); setPage(1); }; - const getLevelColor = (logLevel: string) => { - switch (logLevel) { - case 'DEBUG': return 'bg-gray-100 text-gray-700'; - case 'INFO': return 'bg-blue-100 text-blue-700'; - case 'WARNING': return 'bg-yellow-100 text-yellow-700'; - case 'ERROR': return 'bg-red-100 text-red-700'; - case 'CRITICAL': return 'bg-red-200 text-red-900'; - default: return 'bg-gray-100 text-gray-700'; - } + const clearFilters = () => { + setLevel(''); + setSearch(''); + setSearchQuery(''); + setPage(1); }; - const formatTimestamp = (timestamp: string) => { - return new Date(timestamp).toLocaleString(); - }; + const hasFilters = !!(level || searchQuery); return ( -
-

Logs

+
- {/* Filters */} -
-
- {/* Level Filter */} -
- - -
+ {/* ── Auto-refresh toggle (top-right of page) ──────────────── */} +
+ +
- {/* Search */} -
- -
- setSearch(e.target.value)} - placeholder="Search log messages..." - className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - /> - -
-
+ {/* ── Filter bar ───────────────────────────────────────────── */} +
+ {/* Level select */} + + + {/* Search input */} +
+
- {/* Clear Filters */} - {(level || searchQuery) && ( -
- -
+ {/* Clear filters — only when active */} + {hasFilters && ( + )}
- {/* Logs List */} - {loading ? ( -
-
Loading logs...
+ {/* ── Log list ─────────────────────────────────────────────── */} +
+
+

Log Entries

- ) : ( -
-
-

Log Entries

-
- {logs.length === 0 ? ( -
- No logs found -
- ) : ( - <> -
- {logs.map((log, index) => ( -
-
- - {log.level} - -
-

- {log.message} -

-
- {formatTimestamp(log.timestamp)} - {log.module && Module: {log.module}} - {log.function && Function: {log.function}} - {log.line && Line: {log.line}} -
+ {loading ? ( + + + {Array.from({ length: 8 }).map((_, i) => ( + + ))} + +
+ ) : logs.length === 0 ? ( +

+ {hasFilters ? 'No logs match the current filters.' : 'No log entries yet.'} +

+ ) : ( + <> +
+ {logs.map((log, i) => { + const cfg = levelConfig(log.level); + return ( +
+ {/* Level badge */} + + {log.level} + + + {/* Message + metadata */} +
+

+ {log.message} +

+
+ {log.module && {log.module}} + {log.function && <>·{log.function}} + {log.line && <>·line {log.line}}
-
- ))} -
- {/* Pagination */} - {totalPages > 1 && ( -
-
- Page {page} of {totalPages} -
-
- - + {/* Timestamp */} +
+

{formatTime(log.timestamp)}

+

{formatDate(log.timestamp)}

+
+ ); + })} +
+ + {/* Pagination */} + {totalPages > 1 && ( +
+ + Page {page} of {totalPages} + +
+ +
- )} - - )} -
- )} +
+ )} + + )} +
+
); }