Start on step 17 next
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,13 @@ const { Pagination } = require("../common/Pagination.jsx");
|
||||
*/
|
||||
const ViewLogsScreen = () => {
|
||||
const { navigateBack } = useAppState();
|
||||
const { getLogFiles, readLogFile } = useServices();
|
||||
const {
|
||||
getLogFiles,
|
||||
readLogFile,
|
||||
parseLogContent,
|
||||
getFilteredLogs,
|
||||
filterLogs,
|
||||
} = useServices();
|
||||
|
||||
// State management for log files, selected file, and content
|
||||
const [logFiles, setLogFiles] = React.useState([]);
|
||||
@@ -21,12 +27,28 @@ const ViewLogsScreen = () => {
|
||||
const [selectedFile, setSelectedFile] = React.useState(null);
|
||||
const [logContent, setLogContent] = React.useState("");
|
||||
const [parsedLogs, setParsedLogs] = React.useState([]);
|
||||
const [filteredLogs, setFilteredLogs] = React.useState([]);
|
||||
const [currentPage, setCurrentPage] = React.useState(0);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [loadingContent, setLoadingContent] = React.useState(false);
|
||||
const [contentError, setContentError] = React.useState(null);
|
||||
const [viewMode, setViewMode] = React.useState("parsed"); // "parsed" or "raw"
|
||||
const [scrollOffset, setScrollOffset] = React.useState(0);
|
||||
const [selectedLogIndex, setSelectedLogIndex] = React.useState(0);
|
||||
const [showingContent, setShowingContent] = React.useState(false);
|
||||
|
||||
// Filter and search state
|
||||
const [showFilters, setShowFilters] = React.useState(false);
|
||||
const [filters, setFilters] = React.useState({
|
||||
dateRange: "all",
|
||||
operationType: "all",
|
||||
status: "all",
|
||||
searchTerm: "",
|
||||
});
|
||||
const [filterInputMode, setFilterInputMode] = React.useState(null); // "search", "dateRange", "operationType", "status"
|
||||
const [searchInput, setSearchInput] = React.useState("");
|
||||
const [totalUnfilteredEntries, setTotalUnfilteredEntries] = React.useState(0);
|
||||
|
||||
// Load log files on component mount
|
||||
React.useEffect(() => {
|
||||
@@ -35,10 +57,24 @@ const ViewLogsScreen = () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const files = await getLogFiles();
|
||||
setLogFiles(files);
|
||||
|
||||
// Transform the files to match expected format
|
||||
const transformedFiles = files.map((file) => ({
|
||||
filename: file.name,
|
||||
path: file.path,
|
||||
size: file.size,
|
||||
createdAt: file.created,
|
||||
modifiedAt: file.modified,
|
||||
isMainLog: file.isMain,
|
||||
operationCount: 0, // Will be calculated from content if needed
|
||||
}));
|
||||
|
||||
setLogFiles(transformedFiles);
|
||||
|
||||
// Auto-select the main Progress.md file if it exists
|
||||
const mainLogIndex = files.findIndex((file) => file.isMainLog);
|
||||
const mainLogIndex = transformedFiles.findIndex(
|
||||
(file) => file.isMainLog
|
||||
);
|
||||
if (mainLogIndex !== -1) {
|
||||
setSelectedFileIndex(mainLogIndex);
|
||||
}
|
||||
@@ -52,34 +88,60 @@ const ViewLogsScreen = () => {
|
||||
loadLogFiles();
|
||||
}, [getLogFiles]);
|
||||
|
||||
// Load content for selected file
|
||||
// Load content for selected file with filtering
|
||||
const loadFileContent = React.useCallback(
|
||||
async (file) => {
|
||||
async (file, filterOptions = null) => {
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
setLoadingContent(true);
|
||||
setContentError(null);
|
||||
setCurrentPage(0); // Reset pagination
|
||||
setScrollOffset(0); // Reset scroll
|
||||
setSelectedLogIndex(0); // Reset selection
|
||||
|
||||
const content = await readLogFile(file.filename);
|
||||
setLogContent(content);
|
||||
const currentFilters = filterOptions || filters;
|
||||
|
||||
// Parse the content into structured log entries
|
||||
const { parseLogContent } = useServices();
|
||||
const parsed = parseLogContent(content);
|
||||
setParsedLogs(parsed);
|
||||
// Use getFilteredLogs if we have filtering service available
|
||||
if (getFilteredLogs) {
|
||||
const result = await getFilteredLogs({
|
||||
filePath: file.filename,
|
||||
page: 0,
|
||||
pageSize: 1000, // Load all entries for client-side pagination
|
||||
...currentFilters,
|
||||
});
|
||||
|
||||
setLogContent(result.metadata?.rawContent || "");
|
||||
setParsedLogs(result.entries || []);
|
||||
setFilteredLogs(result.entries || []);
|
||||
setTotalUnfilteredEntries(
|
||||
result.metadata?.totalUnfilteredEntries || 0
|
||||
);
|
||||
} else {
|
||||
// Fallback to original method
|
||||
const content = await readLogFile(file.filename);
|
||||
setLogContent(content);
|
||||
|
||||
// Parse the content into structured log entries
|
||||
const parsed = parseLogContent(content);
|
||||
setParsedLogs(parsed);
|
||||
setFilteredLogs(parsed);
|
||||
setTotalUnfilteredEntries(parsed.length);
|
||||
}
|
||||
|
||||
setSelectedFile(file);
|
||||
setShowingContent(true);
|
||||
} catch (err) {
|
||||
setContentError(`Failed to read log file: ${err.message}`);
|
||||
setLogContent("");
|
||||
setParsedLogs([]);
|
||||
setFilteredLogs([]);
|
||||
setTotalUnfilteredEntries(0);
|
||||
} finally {
|
||||
setLoadingContent(false);
|
||||
}
|
||||
},
|
||||
[readLogFile, useServices]
|
||||
[readLogFile, parseLogContent, getFilteredLogs, filterLogs, filters]
|
||||
);
|
||||
|
||||
// Helper function to format file size
|
||||
@@ -115,31 +177,276 @@ const ViewLogsScreen = () => {
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
// Keyboard navigation for log file selection
|
||||
// Helper function to get color for log level
|
||||
const getLogLevelColor = (level) => {
|
||||
switch (level?.toUpperCase()) {
|
||||
case "ERROR":
|
||||
return "red";
|
||||
case "WARNING":
|
||||
return "yellow";
|
||||
case "SUCCESS":
|
||||
return "green";
|
||||
case "INFO":
|
||||
default:
|
||||
return "white";
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get status icon
|
||||
const getStatusIcon = (level) => {
|
||||
switch (level?.toUpperCase()) {
|
||||
case "ERROR":
|
||||
return "❌";
|
||||
case "WARNING":
|
||||
return "⚠️";
|
||||
case "SUCCESS":
|
||||
return "✅";
|
||||
case "INFO":
|
||||
default:
|
||||
return "ℹ️";
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to format timestamp
|
||||
const formatTimestamp = (timestamp) => {
|
||||
try {
|
||||
return timestamp.toLocaleString();
|
||||
} catch (error) {
|
||||
return "Invalid date";
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to truncate text
|
||||
const truncateText = (text, maxLength = 80) => {
|
||||
if (!text || text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength - 3) + "...";
|
||||
};
|
||||
|
||||
// Apply filters to current log content
|
||||
const applyFilters = React.useCallback(
|
||||
async (newFilters) => {
|
||||
if (!selectedFile) return;
|
||||
|
||||
try {
|
||||
setLoadingContent(true);
|
||||
setContentError(null);
|
||||
setCurrentPage(0); // Reset pagination
|
||||
setSelectedLogIndex(0); // Reset selection
|
||||
|
||||
// Use getFilteredLogs if available
|
||||
if (getFilteredLogs) {
|
||||
const result = await getFilteredLogs({
|
||||
filePath: selectedFile.filename,
|
||||
page: 0,
|
||||
pageSize: 1000, // Load all entries for client-side pagination
|
||||
...newFilters,
|
||||
});
|
||||
|
||||
setFilteredLogs(result.entries || []);
|
||||
setTotalUnfilteredEntries(
|
||||
result.metadata?.totalUnfilteredEntries || 0
|
||||
);
|
||||
} else {
|
||||
// Fallback: filter parsed logs client-side
|
||||
if (filterLogs) {
|
||||
const filtered = filterLogs(parsedLogs, newFilters);
|
||||
setFilteredLogs(filtered);
|
||||
} else {
|
||||
setFilteredLogs(parsedLogs);
|
||||
}
|
||||
}
|
||||
|
||||
setFilters(newFilters);
|
||||
} catch (err) {
|
||||
setContentError(`Failed to apply filters: ${err.message}`);
|
||||
} finally {
|
||||
setLoadingContent(false);
|
||||
}
|
||||
},
|
||||
[selectedFile, getFilteredLogs, parsedLogs, filterLogs]
|
||||
);
|
||||
|
||||
// Clear all filters
|
||||
const clearFilters = React.useCallback(() => {
|
||||
const defaultFilters = {
|
||||
dateRange: "all",
|
||||
operationType: "all",
|
||||
status: "all",
|
||||
searchTerm: "",
|
||||
};
|
||||
setSearchInput("");
|
||||
applyFilters(defaultFilters);
|
||||
}, [applyFilters]);
|
||||
|
||||
// Handle search input
|
||||
const handleSearchInput = React.useCallback(
|
||||
(newSearchTerm) => {
|
||||
setSearchInput(newSearchTerm);
|
||||
const newFilters = { ...filters, searchTerm: newSearchTerm };
|
||||
applyFilters(newFilters);
|
||||
},
|
||||
[filters, applyFilters]
|
||||
);
|
||||
|
||||
// Handle filter changes
|
||||
const handleFilterChange = React.useCallback(
|
||||
(filterType, value) => {
|
||||
const newFilters = { ...filters, [filterType]: value };
|
||||
applyFilters(newFilters);
|
||||
},
|
||||
[filters, applyFilters]
|
||||
);
|
||||
|
||||
// Keyboard navigation for log file selection and content viewing
|
||||
useInput((input, key) => {
|
||||
if (loading) return; // Ignore input while loading
|
||||
|
||||
if (key.escape) {
|
||||
navigateBack();
|
||||
} else if (key.upArrow) {
|
||||
setSelectedFileIndex((prev) => Math.max(0, prev - 1));
|
||||
} else if (key.downArrow) {
|
||||
setSelectedFileIndex((prev) => Math.min(logFiles.length - 1, prev + 1));
|
||||
} else if (key.return) {
|
||||
// Load content for selected file
|
||||
if (logFiles.length > 0 && selectedFileIndex < logFiles.length) {
|
||||
loadFileContent(logFiles[selectedFileIndex]);
|
||||
// Handle filter input mode
|
||||
if (filterInputMode === "search") {
|
||||
if (key.escape) {
|
||||
setFilterInputMode(null);
|
||||
setSearchInput(filters.searchTerm); // Reset to current filter
|
||||
} else if (key.return) {
|
||||
handleSearchInput(searchInput);
|
||||
setFilterInputMode(null);
|
||||
} else if (key.backspace) {
|
||||
setSearchInput((prev) => prev.slice(0, -1));
|
||||
} else if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
||||
setSearchInput((prev) => prev + input);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.escape) {
|
||||
if (showFilters && showingContent) {
|
||||
// Close filters panel
|
||||
setShowFilters(false);
|
||||
} else if (showingContent) {
|
||||
// Go back to file list
|
||||
setShowingContent(false);
|
||||
setSelectedFile(null);
|
||||
setLogContent("");
|
||||
setParsedLogs([]);
|
||||
setFilteredLogs([]);
|
||||
setCurrentPage(0);
|
||||
setScrollOffset(0);
|
||||
setSelectedLogIndex(0);
|
||||
setShowFilters(false);
|
||||
} else {
|
||||
navigateBack();
|
||||
}
|
||||
} else if (showingContent) {
|
||||
// Handle filter panel navigation
|
||||
if (showFilters) {
|
||||
if (input === "s") {
|
||||
// Start search input
|
||||
setFilterInputMode("search");
|
||||
setSearchInput(filters.searchTerm);
|
||||
} else if (input === "d") {
|
||||
// Cycle through date range options
|
||||
const dateOptions = ["all", "today", "yesterday", "week", "month"];
|
||||
const currentIndex = dateOptions.indexOf(filters.dateRange);
|
||||
const nextIndex = (currentIndex + 1) % dateOptions.length;
|
||||
handleFilterChange("dateRange", dateOptions[nextIndex]);
|
||||
} else if (input === "t") {
|
||||
// Cycle through operation type options
|
||||
const typeOptions = ["all", "update", "rollback", "scheduled"];
|
||||
const currentIndex = typeOptions.indexOf(filters.operationType);
|
||||
const nextIndex = (currentIndex + 1) % typeOptions.length;
|
||||
handleFilterChange("operationType", typeOptions[nextIndex]);
|
||||
} else if (input === "l") {
|
||||
// Cycle through status/level options
|
||||
const statusOptions = ["all", "error", "warning", "success", "info"];
|
||||
const currentIndex = statusOptions.indexOf(filters.status);
|
||||
const nextIndex = (currentIndex + 1) % statusOptions.length;
|
||||
handleFilterChange("status", statusOptions[nextIndex]);
|
||||
} else if (input === "c") {
|
||||
// Clear all filters
|
||||
clearFilters();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle content viewing navigation
|
||||
if (viewMode === "parsed") {
|
||||
// Navigation for parsed log entries
|
||||
if (key.upArrow) {
|
||||
setSelectedLogIndex((prev) => Math.max(0, prev - 1));
|
||||
} else if (key.downArrow) {
|
||||
setSelectedLogIndex((prev) =>
|
||||
Math.min(filteredLogs.length - 1, prev + 1)
|
||||
);
|
||||
} else if (key.pageUp) {
|
||||
setCurrentPage((prev) => Math.max(0, prev - 1));
|
||||
} else if (key.pageDown) {
|
||||
const totalPages = Math.ceil(filteredLogs.length / 10);
|
||||
setCurrentPage((prev) => Math.min(totalPages - 1, prev + 1));
|
||||
} else if (input === "v") {
|
||||
// Toggle view mode
|
||||
setViewMode(viewMode === "parsed" ? "raw" : "parsed");
|
||||
} else if (input === "f") {
|
||||
// Toggle filters panel
|
||||
setShowFilters(!showFilters);
|
||||
} else if (input === "r") {
|
||||
// Refresh content
|
||||
if (selectedFile) {
|
||||
loadFileContent(selectedFile);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Navigation for raw content
|
||||
if (key.upArrow) {
|
||||
setScrollOffset((prev) => Math.max(0, prev - 1));
|
||||
} else if (key.downArrow) {
|
||||
const maxLines = logContent.split("\n").length;
|
||||
setScrollOffset((prev) => Math.min(maxLines - 10, prev + 1));
|
||||
} else if (key.pageUp) {
|
||||
setScrollOffset((prev) => Math.max(0, prev - 10));
|
||||
} else if (key.pageDown) {
|
||||
const maxLines = logContent.split("\n").length;
|
||||
setScrollOffset((prev) => Math.min(maxLines - 10, prev + 10));
|
||||
} else if (input === "v") {
|
||||
// Toggle view mode
|
||||
setViewMode(viewMode === "parsed" ? "raw" : "parsed");
|
||||
} else if (input === "r") {
|
||||
// Refresh content
|
||||
if (selectedFile) {
|
||||
loadFileContent(selectedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle file list navigation
|
||||
if (key.upArrow) {
|
||||
setSelectedFileIndex((prev) => Math.max(0, prev - 1));
|
||||
} else if (key.downArrow) {
|
||||
setSelectedFileIndex((prev) => Math.min(logFiles.length - 1, prev + 1));
|
||||
} else if (key.return) {
|
||||
// Load content for selected file
|
||||
if (logFiles.length > 0 && selectedFileIndex < logFiles.length) {
|
||||
loadFileContent(logFiles[selectedFileIndex]);
|
||||
}
|
||||
} else if (input === "r") {
|
||||
// Refresh log files list
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
getLogFiles()
|
||||
.then((files) => {
|
||||
const transformedFiles = files.map((file) => ({
|
||||
filename: file.name,
|
||||
path: file.path,
|
||||
size: file.size,
|
||||
createdAt: file.created,
|
||||
modifiedAt: file.modified,
|
||||
isMainLog: file.isMain,
|
||||
operationCount: 0,
|
||||
}));
|
||||
setLogFiles(transformedFiles);
|
||||
})
|
||||
.catch((err) =>
|
||||
setError(`Failed to discover log files: ${err.message}`)
|
||||
)
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
} else if (input === "r") {
|
||||
// Refresh log files list
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
getLogFiles()
|
||||
.then(setLogFiles)
|
||||
.catch((err) =>
|
||||
setError(`Failed to discover log files: ${err.message}`)
|
||||
)
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -178,87 +485,118 @@ const ViewLogsScreen = () => {
|
||||
}
|
||||
|
||||
// Show file content view if a file is selected
|
||||
if (selectedFile) {
|
||||
if (showingContent && selectedFile) {
|
||||
const pageSize = 10;
|
||||
const totalPages = Math.ceil(filteredLogs.length / pageSize);
|
||||
const startIndex = currentPage * pageSize;
|
||||
const endIndex = Math.min(startIndex + pageSize, filteredLogs.length);
|
||||
const currentPageLogs = filteredLogs.slice(startIndex, endIndex);
|
||||
|
||||
return React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", padding: 2, flexGrow: 1 },
|
||||
// Header
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", marginBottom: 2 },
|
||||
{ flexDirection: "column", marginBottom: 1 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ bold: true, color: "cyan" },
|
||||
"📄 Log File Content"
|
||||
Box,
|
||||
{ flexDirection: "row", justifyContent: "space-between" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ bold: true, color: "cyan" },
|
||||
"📄 Log File Content"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "yellow", bold: true },
|
||||
viewMode === "parsed" ? "PARSED VIEW" : "RAW VIEW"
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
`Viewing: ${selectedFile.filename}`
|
||||
`Viewing: ${selectedFile.filename} (${filteredLogs.length}/${totalUnfilteredEntries} entries)`
|
||||
)
|
||||
),
|
||||
|
||||
// File metadata
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
borderStyle: "single",
|
||||
borderColor: "blue",
|
||||
paddingX: 2,
|
||||
paddingY: 1,
|
||||
marginBottom: 2,
|
||||
},
|
||||
// Filter panel (if enabled)
|
||||
showFilters &&
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
{
|
||||
borderStyle: "single",
|
||||
borderColor: "yellow",
|
||||
paddingX: 1,
|
||||
paddingY: 1,
|
||||
marginBottom: 1,
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", justifyContent: "space-between" },
|
||||
{ flexDirection: "column" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "white", bold: true },
|
||||
selectedFile.filename
|
||||
{ color: "yellow", bold: true },
|
||||
"🔍 Filters & Search"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: selectedFile.isMainLog ? "green" : "gray" },
|
||||
selectedFile.isMainLog ? "MAIN LOG" : "ARCHIVE"
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", marginTop: 1 },
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", width: "50%" },
|
||||
{ flexDirection: "row", marginTop: 1 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "cyan" },
|
||||
`Size: ${formatFileSize(selectedFile.size)}`
|
||||
Box,
|
||||
{ flexDirection: "column", width: "50%" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
`Date Range: ${filters.dateRange}`
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
`Operation: ${filters.operationType}`
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "cyan" },
|
||||
`Operations: ${selectedFile.operationCount}`
|
||||
Box,
|
||||
{ flexDirection: "column", width: "50%" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
`Status: ${filters.status}`
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
`Search: ${filters.searchTerm || "(none)"}`
|
||||
)
|
||||
)
|
||||
),
|
||||
filterInputMode === "search" &&
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
borderStyle: "single",
|
||||
borderColor: "cyan",
|
||||
paddingX: 1,
|
||||
marginTop: 1,
|
||||
},
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "cyan" },
|
||||
`Search: ${searchInput}█`
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", width: "50%" },
|
||||
{ flexDirection: "row", marginTop: 1 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "cyan" },
|
||||
`Created: ${getRelativeTime(selectedFile.createdAt)}`
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "cyan" },
|
||||
`Modified: ${getRelativeTime(selectedFile.modifiedAt)}`
|
||||
{ color: "gray" },
|
||||
"S-Search D-Date T-Type L-Level C-Clear F-Close"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
|
||||
// Content display
|
||||
React.createElement(
|
||||
@@ -269,7 +607,7 @@ const ViewLogsScreen = () => {
|
||||
paddingX: 1,
|
||||
paddingY: 1,
|
||||
flexGrow: 1,
|
||||
marginBottom: 2,
|
||||
marginBottom: 1,
|
||||
},
|
||||
loadingContent
|
||||
? React.createElement(LoadingIndicator, {
|
||||
@@ -280,29 +618,201 @@ const ViewLogsScreen = () => {
|
||||
error: { message: contentError },
|
||||
onRetry: () => loadFileContent(selectedFile),
|
||||
})
|
||||
: logContent
|
||||
: !logContent
|
||||
? React.createElement(
|
||||
Box,
|
||||
{ justifyContent: "center", alignItems: "center", padding: 2 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", bold: true },
|
||||
"📄 Empty Log File"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", marginTop: 1 },
|
||||
"This log file is empty or could not be read."
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", marginTop: 1 },
|
||||
"Log entries are created when operations are performed."
|
||||
)
|
||||
)
|
||||
: viewMode === "parsed"
|
||||
? // Parsed view with syntax highlighting
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
filteredLogs.length === 0
|
||||
? React.createElement(
|
||||
Box,
|
||||
{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 2,
|
||||
},
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", bold: true },
|
||||
totalUnfilteredEntries === 0
|
||||
? "📄 No Parsed Entries"
|
||||
: "🔍 No Matching Entries"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", marginTop: 1 },
|
||||
totalUnfilteredEntries === 0
|
||||
? "The log file contains no parseable entries."
|
||||
: `No entries match the current filters. (${totalUnfilteredEntries} total entries)`
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", marginTop: 1 },
|
||||
totalUnfilteredEntries === 0
|
||||
? "Press 'v' to view raw content."
|
||||
: "Press 'f' to adjust filters or 'c' to clear them."
|
||||
)
|
||||
)
|
||||
: currentPageLogs.map((entry, index) => {
|
||||
const globalIndex = startIndex + index;
|
||||
const isSelected = selectedLogIndex === globalIndex;
|
||||
const levelColor = getLogLevelColor(entry.level);
|
||||
const statusIcon = getStatusIcon(entry.level);
|
||||
|
||||
return React.createElement(
|
||||
Box,
|
||||
{
|
||||
key: entry.id,
|
||||
flexDirection: "column",
|
||||
paddingY: 1,
|
||||
paddingX: 1,
|
||||
backgroundColor: isSelected ? "blue" : undefined,
|
||||
borderStyle: isSelected ? "single" : "none",
|
||||
borderColor: isSelected ? "cyan" : "gray",
|
||||
marginBottom: 1,
|
||||
},
|
||||
// Entry header
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", alignItems: "center" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: levelColor, bold: true },
|
||||
`${statusIcon} ${entry.title}`
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
formatTimestamp(entry.timestamp)
|
||||
)
|
||||
),
|
||||
// Entry message
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: isSelected ? "white" : "gray",
|
||||
marginTop: 1,
|
||||
marginLeft: 2,
|
||||
},
|
||||
truncateText(entry.message, 70)
|
||||
),
|
||||
// Entry details (if available and selected)
|
||||
isSelected &&
|
||||
entry.details &&
|
||||
entry.details.trim() &&
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
borderStyle: "single",
|
||||
borderColor: "gray",
|
||||
paddingX: 1,
|
||||
paddingY: 1,
|
||||
marginTop: 1,
|
||||
marginLeft: 2,
|
||||
},
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "white", wrap: "wrap" },
|
||||
entry.details.trim()
|
||||
)
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
: // Raw view with basic syntax highlighting
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "white", wrap: "wrap" },
|
||||
logContent.substring(0, 2000) // Show first 2000 characters
|
||||
),
|
||||
logContent.length > 2000 &&
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "yellow", marginTop: 1 },
|
||||
`... (${logContent.length - 2000} more characters)`
|
||||
)
|
||||
)
|
||||
: React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
"File is empty or could not be read"
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
logContent
|
||||
.split("\n")
|
||||
.slice(scrollOffset, scrollOffset + 15)
|
||||
.map((line, index) => {
|
||||
const lineNumber = scrollOffset + index + 1;
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// Determine line color based on content
|
||||
let lineColor = "white";
|
||||
if (trimmedLine.startsWith("##")) {
|
||||
lineColor = "cyan"; // Operation headers
|
||||
} else if (
|
||||
trimmedLine.startsWith("**") &&
|
||||
trimmedLine.endsWith("**")
|
||||
) {
|
||||
lineColor = "yellow"; // Section headers
|
||||
} else if (trimmedLine.includes("❌")) {
|
||||
lineColor = "red"; // Errors
|
||||
} else if (trimmedLine.includes("⚠️")) {
|
||||
lineColor = "yellow"; // Warnings
|
||||
} else if (trimmedLine.includes("✅")) {
|
||||
lineColor = "green"; // Success
|
||||
} else if (trimmedLine.startsWith("-")) {
|
||||
lineColor = "gray"; // List items
|
||||
}
|
||||
|
||||
return React.createElement(
|
||||
Box,
|
||||
{
|
||||
key: lineNumber,
|
||||
flexDirection: "row",
|
||||
},
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", width: 4 },
|
||||
String(lineNumber).padStart(3, " ") + " "
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: lineColor, wrap: "wrap" },
|
||||
line || " "
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Pagination for parsed view
|
||||
viewMode === "parsed" &&
|
||||
filteredLogs.length > pageSize &&
|
||||
React.createElement(Pagination, {
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalItems: filteredLogs.length,
|
||||
itemsPerPage: pageSize,
|
||||
onPageChange: setCurrentPage,
|
||||
compact: true,
|
||||
}),
|
||||
|
||||
// Instructions
|
||||
React.createElement(
|
||||
Box,
|
||||
@@ -311,13 +821,61 @@ const ViewLogsScreen = () => {
|
||||
borderTopStyle: "single",
|
||||
borderColor: "gray",
|
||||
paddingTop: 1,
|
||||
marginTop: 1,
|
||||
},
|
||||
React.createElement(Text, { color: "gray", bold: true }, "Navigation:"),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", marginTop: 1 },
|
||||
" Esc - Back to file list R - Refresh content"
|
||||
)
|
||||
Box,
|
||||
{ flexDirection: "row", marginTop: 1 },
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", width: "33%" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
" ↑/↓ - Navigate entries"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
" PgUp/PgDn - Page navigation"
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", width: "33%" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
" V - Toggle view mode"
|
||||
),
|
||||
React.createElement(Text, { color: "gray" }, " F - Toggle filters")
|
||||
),
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", width: "34%" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
" R - Refresh content"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
" Esc - Back to file list"
|
||||
)
|
||||
)
|
||||
),
|
||||
showFilters &&
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", marginTop: 1 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "yellow" },
|
||||
"Filter Controls: S-Search D-Date T-Type L-Level C-Clear"
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
62
src/tui/components/screens/withErrorBoundary.jsx
Normal file
62
src/tui/components/screens/withErrorBoundary.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
const React = require("react");
|
||||
const { ScreenErrorBoundary } = require("../common");
|
||||
const { enhanceError, logError } = require("../../utils/errorHandler");
|
||||
|
||||
/**
|
||||
* Higher-order component that wraps screens with error boundaries
|
||||
* Requirements: 4.5, 16.1
|
||||
*/
|
||||
function withErrorBoundary(
|
||||
WrappedComponent,
|
||||
screenName,
|
||||
screenSpecificTips = []
|
||||
) {
|
||||
const WrappedScreenComponent = (props) => {
|
||||
const handleError = (error, errorInfo) => {
|
||||
// Log the error with screen context
|
||||
logError(error, {
|
||||
screen: screenName,
|
||||
errorInfo,
|
||||
props: Object.keys(props),
|
||||
});
|
||||
};
|
||||
|
||||
const handleRetry = (retryCount) => {
|
||||
console.info(`Retrying ${screenName}, attempt ${retryCount}`);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
console.info(`Resetting ${screenName} after error`);
|
||||
};
|
||||
|
||||
const handleExit = () => {
|
||||
// Navigate back to main menu
|
||||
if (props.navigateBack) {
|
||||
props.navigateBack();
|
||||
} else if (props.onExit) {
|
||||
props.onExit();
|
||||
}
|
||||
};
|
||||
|
||||
return React.createElement(
|
||||
ScreenErrorBoundary,
|
||||
{
|
||||
screenName,
|
||||
screenSpecificTips,
|
||||
onError: handleError,
|
||||
onRetry: handleRetry,
|
||||
onReset: handleReset,
|
||||
onExit: handleExit,
|
||||
maxRetries: 3,
|
||||
},
|
||||
React.createElement(WrappedComponent, props)
|
||||
);
|
||||
};
|
||||
|
||||
// Set display name for debugging
|
||||
WrappedScreenComponent.displayName = `withErrorBoundary(${screenName})`;
|
||||
|
||||
return WrappedScreenComponent;
|
||||
}
|
||||
|
||||
module.exports = withErrorBoundary;
|
||||
Reference in New Issue
Block a user