Just a whole lot of crap
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,101 +1,47 @@
|
||||
const React = require("react");
|
||||
const { Box, Text, useInput, useApp } = require("ink");
|
||||
const { useAppState } = require("../../providers/AppProvider.jsx");
|
||||
const SelectInput = require("ink-select-input").default;
|
||||
const LogReaderService = require("../../../services/logReader");
|
||||
|
||||
/**
|
||||
* Log Viewer Screen Component
|
||||
* Displays application logs with filtering and navigation capabilities
|
||||
* Requirements: 7.5, 7.6
|
||||
* Displays application logs with pagination, filtering and navigation capabilities
|
||||
* Requirements: 6.1, 6.4, 10.3
|
||||
*/
|
||||
const LogViewerScreen = () => {
|
||||
const { appState, navigateBack } = useAppState();
|
||||
const { exit } = useApp();
|
||||
|
||||
// Mock log data (in a real implementation, this would read from log files)
|
||||
const mockLogs = [
|
||||
{
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 5),
|
||||
level: "INFO",
|
||||
message: "Application started",
|
||||
details: "Shopify Price Updater v1.0.0",
|
||||
},
|
||||
{
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 4),
|
||||
level: "INFO",
|
||||
message: "Configuration loaded",
|
||||
details: "Target tag: 'sale', Adjustment: 10%",
|
||||
},
|
||||
{
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 3),
|
||||
level: "INFO",
|
||||
message: "Testing Shopify connection",
|
||||
details: "Connecting to store...",
|
||||
},
|
||||
{
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 2),
|
||||
level: "SUCCESS",
|
||||
message: "Connection successful",
|
||||
details: "API version: 2023-01",
|
||||
},
|
||||
{
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 1),
|
||||
level: "INFO",
|
||||
message: "Fetching products",
|
||||
details: "Query: tag:sale",
|
||||
},
|
||||
{
|
||||
timestamp: new Date(Date.now() - 1000 * 60 * 1),
|
||||
level: "INFO",
|
||||
message: "Products found",
|
||||
details: "Found 15 products with tag 'sale'",
|
||||
},
|
||||
{
|
||||
timestamp: new Date(Date.now() - 1000 * 30),
|
||||
level: "INFO",
|
||||
message: "Starting price updates",
|
||||
details: "Processing 15 products, 42 variants total",
|
||||
},
|
||||
{
|
||||
timestamp: new Date(Date.now() - 1000 * 25),
|
||||
level: "INFO",
|
||||
message: "Updating product",
|
||||
details: "Product: 'Summer T-Shirt' - New price: $22.00",
|
||||
},
|
||||
{
|
||||
timestamp: new Date(Date.now() - 1000 * 20),
|
||||
level: "INFO",
|
||||
message: "Updating product",
|
||||
details: "Product: 'Winter Jacket' - New price: $110.00",
|
||||
},
|
||||
{
|
||||
timestamp: new Date(Date.now() - 1000 * 15),
|
||||
level: "WARNING",
|
||||
message: "Price update failed",
|
||||
details: "Product: 'Limited Edition Sneaker' - Insufficient permissions",
|
||||
},
|
||||
{
|
||||
timestamp: new Date(Date.now() - 1000 * 10),
|
||||
level: "INFO",
|
||||
message: "Updating product",
|
||||
details: "Product: 'Casual Jeans' - New price: $45.00",
|
||||
},
|
||||
{
|
||||
timestamp: new Date(Date.now() - 1000 * 5),
|
||||
level: "SUCCESS",
|
||||
message: "Operation completed",
|
||||
details: "Updated 40 of 42 variants (95.2% success rate)",
|
||||
},
|
||||
];
|
||||
// Initialize log reader service
|
||||
const [logReader] = React.useState(() => new LogReaderService());
|
||||
|
||||
// State for log viewing
|
||||
const [logs, setLogs] = React.useState(mockLogs);
|
||||
const [filteredLogs, setFilteredLogs] = React.useState(mockLogs);
|
||||
const [filterLevel, setFilterLevel] = React.useState("ALL");
|
||||
const [selectedLog, setSelectedLog] = React.useState(null);
|
||||
// State for log viewing with pagination
|
||||
const [logData, setLogData] = React.useState({
|
||||
entries: [],
|
||||
pagination: {
|
||||
currentPage: 0,
|
||||
pageSize: 10,
|
||||
totalEntries: 0,
|
||||
totalPages: 0,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startIndex: 1,
|
||||
endIndex: 0,
|
||||
},
|
||||
filters: {
|
||||
levelFilter: "ALL",
|
||||
searchTerm: "",
|
||||
},
|
||||
});
|
||||
|
||||
const [selectedLog, setSelectedLog] = React.useState(0);
|
||||
const [showDetails, setShowDetails] = React.useState(false);
|
||||
const [scrollPosition, setScrollPosition] = React.useState(0);
|
||||
const [maxScroll, setMaxScroll] = React.useState(0);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [stats, setStats] = React.useState(null);
|
||||
const [autoRefresh, setAutoRefresh] = React.useState(true);
|
||||
const [lastRefresh, setLastRefresh] = React.useState(new Date());
|
||||
const [refreshing, setRefreshing] = React.useState(false);
|
||||
|
||||
// Filter options
|
||||
const filterOptions = [
|
||||
@@ -106,17 +52,99 @@ const LogViewerScreen = () => {
|
||||
{ value: "SUCCESS", label: "Success" },
|
||||
];
|
||||
|
||||
// Filter logs based on selected level
|
||||
const filterLogs = () => {
|
||||
if (filterLevel === "ALL") {
|
||||
setFilteredLogs(logs);
|
||||
} else {
|
||||
setFilteredLogs(logs.filter((log) => log.level === filterLevel));
|
||||
// Load log data with current filters and pagination
|
||||
const loadLogData = React.useCallback(
|
||||
async (options = {}, isAutoRefresh = false) => {
|
||||
try {
|
||||
if (isAutoRefresh) {
|
||||
setRefreshing(true);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
const loadOptions = {
|
||||
page: logData.pagination.currentPage,
|
||||
pageSize: logData.pagination.pageSize,
|
||||
levelFilter: logData.filters.levelFilter,
|
||||
searchTerm: logData.filters.searchTerm,
|
||||
...options,
|
||||
};
|
||||
|
||||
const result = await logReader.getPaginatedEntries(loadOptions);
|
||||
setLogData(result);
|
||||
|
||||
// Reset selection if current selection is out of bounds
|
||||
if (selectedLog >= result.entries.length) {
|
||||
setSelectedLog(Math.max(0, result.entries.length - 1));
|
||||
}
|
||||
|
||||
setShowDetails(false);
|
||||
setLastRefresh(new Date());
|
||||
} catch (err) {
|
||||
setError(`Failed to load logs: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
logReader,
|
||||
logData.pagination.currentPage,
|
||||
logData.pagination.pageSize,
|
||||
logData.filters.levelFilter,
|
||||
logData.filters.searchTerm,
|
||||
selectedLog,
|
||||
]
|
||||
);
|
||||
|
||||
// Load statistics
|
||||
const loadStats = React.useCallback(async () => {
|
||||
try {
|
||||
const statistics = await logReader.getLogStatistics();
|
||||
setStats(statistics);
|
||||
} catch (err) {
|
||||
console.warn("Failed to load log statistics:", err.message);
|
||||
}
|
||||
};
|
||||
}, [logReader]);
|
||||
|
||||
// Initial load
|
||||
React.useEffect(() => {
|
||||
loadLogData();
|
||||
loadStats();
|
||||
}, []);
|
||||
|
||||
// Auto-refresh functionality with file watching
|
||||
React.useEffect(() => {
|
||||
if (!autoRefresh) return;
|
||||
|
||||
const cleanup = logReader.watchFile(() => {
|
||||
loadLogData({}, true); // Mark as auto-refresh
|
||||
loadStats();
|
||||
});
|
||||
return cleanup;
|
||||
}, [logReader, loadLogData, loadStats, autoRefresh]);
|
||||
|
||||
// Periodic refresh as backup (every 30 seconds)
|
||||
React.useEffect(() => {
|
||||
if (!autoRefresh) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
// Only refresh if not currently loading
|
||||
if (!loading && !refreshing) {
|
||||
logReader.clearCache();
|
||||
loadLogData({}, true);
|
||||
loadStats();
|
||||
}
|
||||
}, 30000); // 30 seconds
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [logReader, loadLogData, loadStats, autoRefresh, loading, refreshing]);
|
||||
|
||||
// Handle keyboard input
|
||||
useInput((input, key) => {
|
||||
if (loading) return; // Ignore input while loading
|
||||
|
||||
if (key.escape) {
|
||||
// Go back to main menu
|
||||
navigateBack();
|
||||
@@ -128,29 +156,80 @@ const LogViewerScreen = () => {
|
||||
}
|
||||
} else if (key.downArrow) {
|
||||
// Navigate down in log list
|
||||
if (selectedLog < filteredLogs.length - 1) {
|
||||
if (selectedLog < logData.entries.length - 1) {
|
||||
setSelectedLog(selectedLog + 1);
|
||||
setShowDetails(false);
|
||||
}
|
||||
} else if (key.leftArrow) {
|
||||
// Previous page
|
||||
if (logData.pagination.hasPreviousPage) {
|
||||
loadLogData({ page: logData.pagination.currentPage - 1 });
|
||||
}
|
||||
} else if (key.rightArrow) {
|
||||
// Next page
|
||||
if (logData.pagination.hasNextPage) {
|
||||
loadLogData({ page: logData.pagination.currentPage + 1 });
|
||||
}
|
||||
} else if (key.return || key.enter) {
|
||||
// Toggle log details
|
||||
if (selectedLog !== null) {
|
||||
if (selectedLog < logData.entries.length) {
|
||||
setShowDetails(!showDetails);
|
||||
}
|
||||
} else if (key.r) {
|
||||
} else if (key.r || input === "r") {
|
||||
// Refresh logs
|
||||
setLogs(mockLogs);
|
||||
setFilteredLogs(mockLogs);
|
||||
setSelectedLog(null);
|
||||
setShowDetails(false);
|
||||
logReader.clearCache();
|
||||
loadLogData();
|
||||
loadStats();
|
||||
} else if (input >= "1" && input <= "5") {
|
||||
// Quick filter by number
|
||||
const filterMap = {
|
||||
1: "ALL",
|
||||
2: "ERROR",
|
||||
3: "WARNING",
|
||||
4: "INFO",
|
||||
5: "SUCCESS",
|
||||
};
|
||||
const newFilter = filterMap[input];
|
||||
if (newFilter !== logData.filters.levelFilter) {
|
||||
loadLogData({
|
||||
levelFilter: newFilter,
|
||||
page: 0, // Reset to first page when filtering
|
||||
});
|
||||
}
|
||||
} else if (input === "s") {
|
||||
// Toggle search mode (simplified - cycle through common search terms)
|
||||
const searchTerms = ["", "error", "update", "rollback", "product"];
|
||||
const currentIndex = searchTerms.indexOf(logData.filters.searchTerm);
|
||||
const nextIndex = (currentIndex + 1) % searchTerms.length;
|
||||
const newSearchTerm = searchTerms[nextIndex];
|
||||
|
||||
loadLogData({
|
||||
searchTerm: newSearchTerm,
|
||||
page: 0, // Reset to first page when searching
|
||||
});
|
||||
} else if (input === "c") {
|
||||
// Clear all filters
|
||||
loadLogData({
|
||||
levelFilter: "ALL",
|
||||
searchTerm: "",
|
||||
page: 0,
|
||||
});
|
||||
} else if (input === "a") {
|
||||
// Toggle auto-refresh
|
||||
setAutoRefresh(!autoRefresh);
|
||||
} else if (key.pageUp) {
|
||||
// Jump to first page
|
||||
if (logData.pagination.currentPage > 0) {
|
||||
loadLogData({ page: 0 });
|
||||
}
|
||||
} else if (key.pageDown) {
|
||||
// Jump to last page
|
||||
if (logData.pagination.currentPage < logData.pagination.totalPages - 1) {
|
||||
loadLogData({ page: logData.pagination.totalPages - 1 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle filter change
|
||||
const handleFilterChange = (option) => {
|
||||
setFilterLevel(option.value);
|
||||
};
|
||||
|
||||
// Get log level color
|
||||
const getLogLevelColor = (level) => {
|
||||
switch (level) {
|
||||
@@ -176,22 +255,91 @@ const LogViewerScreen = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// Format date for display
|
||||
const formatDate = (date) => {
|
||||
return date.toLocaleDateString([], {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
// Truncate text for display
|
||||
const truncateText = (text, maxLength = 60) => {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength - 3) + "...";
|
||||
};
|
||||
|
||||
// Show loading state
|
||||
if (loading && logData.entries.length === 0) {
|
||||
return React.createElement(
|
||||
Box,
|
||||
{
|
||||
flexDirection: "column",
|
||||
padding: 2,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
React.createElement(Text, { color: "blue" }, "Loading logs..."),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
"Please wait while we read the log files"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Show error state
|
||||
if (error) {
|
||||
return React.createElement(
|
||||
Box,
|
||||
{
|
||||
flexDirection: "column",
|
||||
padding: 2,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "red", bold: true },
|
||||
"Error Loading Logs"
|
||||
),
|
||||
React.createElement(Text, { color: "gray" }, error),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", marginTop: 1 },
|
||||
"Press 'r' to retry or Esc to go back"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", padding: 2, flexGrow: 1 },
|
||||
// Header
|
||||
// Header with statistics
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", marginBottom: 2 },
|
||||
React.createElement(Text, { bold: true, color: "cyan" }, "📋 Log Viewer"),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
"View application logs and operation history"
|
||||
Box,
|
||||
{ flexDirection: "row", justifyContent: "space-between" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
"View application logs and operation history"
|
||||
),
|
||||
stats &&
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
`${stats.totalEntries} entries | ${stats.operations.total} operations`
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Filter controls
|
||||
// Filter and pagination controls
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
@@ -204,36 +352,54 @@ const LogViewerScreen = () => {
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ bold: true, color: "blue" },
|
||||
"Log Filters:"
|
||||
),
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", alignItems: "center" },
|
||||
React.createElement(Text, { color: "white" }, "Level: "),
|
||||
React.createElement(SelectInput, {
|
||||
items: filterOptions,
|
||||
selectedIndex: filterOptions.findIndex(
|
||||
(opt) => opt.value === filterLevel
|
||||
{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", alignItems: "center" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "white", bold: true },
|
||||
"Filter: "
|
||||
),
|
||||
onSelect: handleFilterChange,
|
||||
itemComponent: ({ label, isSelected }) =>
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: isSelected ? "blue" : "white",
|
||||
bold: isSelected,
|
||||
},
|
||||
label
|
||||
),
|
||||
})
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "blue" },
|
||||
logData.filters.levelFilter
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", marginLeft: 2 },
|
||||
"(1-5 to change)"
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", alignItems: "center" },
|
||||
React.createElement(Text, { color: "white", bold: true }, "Page: "),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "blue" },
|
||||
`${logData.pagination.currentPage + 1}/${
|
||||
logData.pagination.totalPages
|
||||
}`
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", marginLeft: 2 },
|
||||
"(←/→ to navigate)"
|
||||
)
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", fontSize: "small" },
|
||||
`Showing ${filteredLogs.length} of ${logs.length} log entries`
|
||||
{ color: "gray" },
|
||||
`Showing ${logData.pagination.startIndex}-${logData.pagination.endIndex} of ${logData.pagination.totalEntries} entries`
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -246,58 +412,74 @@ const LogViewerScreen = () => {
|
||||
borderStyle: "single",
|
||||
borderColor: "gray",
|
||||
flexDirection: "column",
|
||||
minHeight: 10,
|
||||
},
|
||||
filteredLogs.map((log, index) => {
|
||||
const isSelected = selectedLog === index;
|
||||
const isHighlighted = isSelected && !showDetails;
|
||||
|
||||
return React.createElement(
|
||||
Box,
|
||||
{
|
||||
key: index,
|
||||
borderStyle: "single",
|
||||
borderColor: isSelected ? "blue" : "transparent",
|
||||
paddingX: 1,
|
||||
paddingY: 0.5,
|
||||
backgroundColor: isHighlighted ? "blue" : undefined,
|
||||
},
|
||||
React.createElement(
|
||||
logData.entries.length === 0
|
||||
? React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", alignItems: "center" },
|
||||
{ justifyContent: "center", alignItems: "center", padding: 2 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: getLogLevelColor(log.level),
|
||||
bold: true,
|
||||
width: 8,
|
||||
},
|
||||
log.level
|
||||
{ color: "gray" },
|
||||
"No log entries found"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: isHighlighted ? "white" : "gray",
|
||||
width: 8,
|
||||
},
|
||||
formatTimestamp(log.timestamp)
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: isHighlighted ? "white" : "white",
|
||||
flexGrow: 1,
|
||||
},
|
||||
log.message
|
||||
{ color: "gray" },
|
||||
"Try changing the filter or refresh with 'r'"
|
||||
)
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
: logData.entries.map((log, index) => {
|
||||
const isSelected = selectedLog === index;
|
||||
const isHighlighted = isSelected && !showDetails;
|
||||
|
||||
return React.createElement(
|
||||
Box,
|
||||
{
|
||||
key: log.id || index,
|
||||
borderStyle: isSelected ? "single" : "none",
|
||||
borderColor: isSelected ? "blue" : "transparent",
|
||||
paddingX: 1,
|
||||
paddingY: 0,
|
||||
backgroundColor: isHighlighted ? "blue" : undefined,
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", alignItems: "center" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: getLogLevelColor(log.level),
|
||||
bold: true,
|
||||
width: 8,
|
||||
},
|
||||
log.level
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: isHighlighted ? "white" : "gray",
|
||||
width: 12,
|
||||
},
|
||||
formatDate(log.timestamp)
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: isHighlighted ? "white" : "white",
|
||||
flexGrow: 1,
|
||||
},
|
||||
truncateText(log.message, 50)
|
||||
)
|
||||
)
|
||||
);
|
||||
})
|
||||
),
|
||||
|
||||
// Log details (when selected)
|
||||
showDetails &&
|
||||
selectedLog !== null &&
|
||||
filteredLogs[selectedLog] &&
|
||||
selectedLog < logData.entries.length &&
|
||||
logData.entries[selectedLog] &&
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
@@ -306,6 +488,7 @@ const LogViewerScreen = () => {
|
||||
paddingX: 1,
|
||||
paddingY: 1,
|
||||
marginTop: 2,
|
||||
maxHeight: 8,
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
@@ -325,8 +508,19 @@ const LogViewerScreen = () => {
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: getLogLevelColor(filteredLogs[selectedLog].level) },
|
||||
filteredLogs[selectedLog].level
|
||||
{ color: getLogLevelColor(logData.entries[selectedLog].level) },
|
||||
logData.entries[selectedLog].level
|
||||
),
|
||||
React.createElement(Text, { color: "gray", marginLeft: 2 }, "|"),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "white", bold: true, marginLeft: 1 },
|
||||
"Type: "
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "cyan" },
|
||||
logData.entries[selectedLog].type || "unknown"
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
@@ -336,7 +530,7 @@ const LogViewerScreen = () => {
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
filteredLogs[selectedLog].timestamp.toLocaleString()
|
||||
logData.entries[selectedLog].timestamp.toLocaleString()
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
@@ -350,23 +544,24 @@ const LogViewerScreen = () => {
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "white" },
|
||||
filteredLogs[selectedLog].message
|
||||
logData.entries[selectedLog].message
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", marginTop: 1 },
|
||||
logData.entries[selectedLog].details &&
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "white", bold: true },
|
||||
"Details:"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", italic: true },
|
||||
filteredLogs[selectedLog].details
|
||||
Box,
|
||||
{ flexDirection: "column", marginTop: 1 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "white", bold: true },
|
||||
"Details:"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", italic: true },
|
||||
truncateText(logData.entries[selectedLog].details, 200)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
@@ -378,13 +573,40 @@ const LogViewerScreen = () => {
|
||||
marginTop: 2,
|
||||
borderTopStyle: "single",
|
||||
borderColor: "gray",
|
||||
paddingTop: 2,
|
||||
paddingTop: 1,
|
||||
},
|
||||
React.createElement(Text, { color: "gray" }, "Controls:"),
|
||||
React.createElement(Text, { color: "gray" }, " ↑/↓ - Navigate logs"),
|
||||
React.createElement(Text, { color: "gray" }, " Enter - View details"),
|
||||
React.createElement(Text, { color: "gray" }, " R - Refresh logs"),
|
||||
React.createElement(Text, { color: "gray" }, " Esc - Back to menu")
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", justifyContent: "space-between" },
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
"Navigation: ↑/↓ entries | ←/→ pages | Enter details"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
"Filters: 1=All 2=Error 3=Warning 4=Info 5=Success"
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
"Search: S cycle terms | C clear | A auto-refresh"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
"Actions: R refresh | PgUp/PgDn jump | Esc back"
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Status bar
|
||||
@@ -394,15 +616,34 @@ const LogViewerScreen = () => {
|
||||
borderStyle: "single",
|
||||
borderColor: "gray",
|
||||
paddingX: 1,
|
||||
paddingY: 0.5,
|
||||
paddingY: 0,
|
||||
marginTop: 1,
|
||||
},
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", fontSize: "small" },
|
||||
`Selected: ${
|
||||
selectedLog !== null ? `Log #${selectedLog + 1}` : "None"
|
||||
} | Details: ${showDetails ? "Visible" : "Hidden"}`
|
||||
Box,
|
||||
{ flexDirection: "row", justifyContent: "space-between" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
`Entry ${selectedLog + 1}/${logData.entries.length} | Details: ${
|
||||
showDetails ? "ON" : "OFF"
|
||||
}`
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: loading ? "yellow" : refreshing ? "cyan" : "gray" },
|
||||
loading
|
||||
? "Loading..."
|
||||
: refreshing
|
||||
? "Refreshing..."
|
||||
: `Filter: ${logData.filters.levelFilter}${
|
||||
logData.filters.searchTerm
|
||||
? ` | Search: "${logData.filters.searchTerm}"`
|
||||
: ""
|
||||
} | Auto: ${
|
||||
autoRefresh ? "ON" : "OFF"
|
||||
} | ${lastRefresh.toLocaleTimeString()}`
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
const React = require("react");
|
||||
const { Box, Text, useInput } = require("ink");
|
||||
const { useAppState } = require("../../providers/AppProvider.jsx");
|
||||
const {
|
||||
createKeyboardHandler,
|
||||
navigationKeys,
|
||||
} = require("../../utils/keyboardHandlers.js");
|
||||
const ResponsiveContainer = require("../common/ResponsiveContainer.jsx");
|
||||
const ResponsiveText = require("../common/ResponsiveText.jsx");
|
||||
const ScrollableContainer = require("../common/ScrollableContainer.jsx");
|
||||
|
||||
/**
|
||||
* Main Menu Screen Component
|
||||
@@ -8,7 +15,15 @@ const { useAppState } = require("../../providers/AppProvider.jsx");
|
||||
* Requirements: 5.1, 5.3, 7.1
|
||||
*/
|
||||
const MainMenuScreen = () => {
|
||||
const { appState, navigateTo, updateUIState } = useAppState();
|
||||
const {
|
||||
appState,
|
||||
navigateTo,
|
||||
navigateBack,
|
||||
updateUIState,
|
||||
toggleHelp,
|
||||
showHelp,
|
||||
hideHelp,
|
||||
} = useAppState();
|
||||
|
||||
// Menu items configuration
|
||||
const menuItems = [
|
||||
@@ -40,161 +55,206 @@ const MainMenuScreen = () => {
|
||||
{ id: "exit", label: "Exit", description: "Quit the application" },
|
||||
];
|
||||
|
||||
// Handle keyboard input
|
||||
useInput((input, key) => {
|
||||
if (key.upArrow) {
|
||||
// Navigate up in menu
|
||||
const newIndex = Math.max(0, appState.uiState.selectedMenuIndex - 1);
|
||||
updateUIState({ selectedMenuIndex: newIndex });
|
||||
} else if (key.downArrow) {
|
||||
// Navigate down in menu
|
||||
const newIndex = Math.min(
|
||||
menuItems.length - 1,
|
||||
appState.uiState.selectedMenuIndex + 1
|
||||
);
|
||||
updateUIState({ selectedMenuIndex: newIndex });
|
||||
} else if (key.return || key.enter || input === " ") {
|
||||
// Select menu item
|
||||
// Create screen-specific keyboard handler
|
||||
const screenKeyboardHandler = (input, key) => {
|
||||
// Handle menu navigation
|
||||
const wasNavigationHandled = navigationKeys.handleMenuNavigation(
|
||||
key,
|
||||
appState.uiState.selectedMenuIndex,
|
||||
menuItems.length - 1,
|
||||
(newIndex) => updateUIState({ selectedMenuIndex: newIndex })
|
||||
);
|
||||
|
||||
if (wasNavigationHandled) return;
|
||||
|
||||
// Handle menu selection
|
||||
if (key.return || key.enter || input === " ") {
|
||||
const selectedItem = menuItems[appState.uiState.selectedMenuIndex];
|
||||
if (selectedItem.id === "exit") {
|
||||
// Exit the application
|
||||
process.exit(0);
|
||||
} else {
|
||||
// Navigate to selected screen
|
||||
navigateTo(selectedItem.id);
|
||||
}
|
||||
} else if (input === "q" || input === "Q") {
|
||||
// Quick exit with 'q'
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Use global keyboard handler with screen-specific handler
|
||||
const context = {
|
||||
appState,
|
||||
navigateTo,
|
||||
navigateBack,
|
||||
updateUIState,
|
||||
toggleHelp,
|
||||
showHelp,
|
||||
hideHelp,
|
||||
};
|
||||
useInput(createKeyboardHandler(screenKeyboardHandler, context));
|
||||
|
||||
return React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", padding: 1 },
|
||||
// Header
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", marginBottom: 1 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ bold: true, color: "cyan" },
|
||||
"🛍️ Shopify Price Updater"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", fontSize: "small" },
|
||||
"Terminal User Interface"
|
||||
)
|
||||
),
|
||||
|
||||
// Welcome message
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", marginBottom: 1 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "green", fontSize: "small" },
|
||||
"Shopify Price Updater TUI"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", fontSize: "small" },
|
||||
"Arrow keys: Navigate | Enter: Select"
|
||||
)
|
||||
),
|
||||
|
||||
// Menu items
|
||||
ResponsiveContainer,
|
||||
{ componentType: "menu" },
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
menuItems.map((item, index) => {
|
||||
const isSelected = index === appState.uiState.selectedMenuIndex;
|
||||
const isConfigured = appState.configuration.isValid;
|
||||
// Header
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", marginBottom: 1 },
|
||||
React.createElement(
|
||||
ResponsiveText,
|
||||
{ styleType: "title" },
|
||||
"🛍️ Shopify Price Updater"
|
||||
),
|
||||
React.createElement(
|
||||
ResponsiveText,
|
||||
{ styleType: "subtitle" },
|
||||
"Terminal User Interface"
|
||||
)
|
||||
),
|
||||
|
||||
return React.createElement(
|
||||
// Welcome message
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", marginBottom: 1 },
|
||||
React.createElement(
|
||||
ResponsiveText,
|
||||
{ styleType: "emphasis" },
|
||||
"Shopify Price Updater TUI"
|
||||
),
|
||||
React.createElement(
|
||||
ResponsiveText,
|
||||
{ styleType: "normal" },
|
||||
"Arrow keys: Navigate | Enter: Select"
|
||||
)
|
||||
),
|
||||
|
||||
// Menu items with scrollable container
|
||||
React.createElement(ScrollableContainer, {
|
||||
items: menuItems,
|
||||
itemHeight: 3,
|
||||
renderItem: (item, index) => {
|
||||
const isSelected = index === appState.uiState.selectedMenuIndex;
|
||||
const isConfigured = appState.configuration.isValid;
|
||||
|
||||
return React.createElement(
|
||||
Box,
|
||||
{
|
||||
borderStyle: "single",
|
||||
borderColor: isSelected ? "blue" : "gray",
|
||||
paddingX: 1,
|
||||
paddingY: 1,
|
||||
flexDirection: "column",
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", alignItems: "center" },
|
||||
React.createElement(
|
||||
ResponsiveText,
|
||||
{
|
||||
styleType: isSelected ? "emphasis" : "normal",
|
||||
bold: isSelected,
|
||||
},
|
||||
`${isSelected ? "▶" : " "} ${item.label}`
|
||||
),
|
||||
// Configuration status indicator
|
||||
item.id === "operation" &&
|
||||
!isConfigured &&
|
||||
React.createElement(
|
||||
Box,
|
||||
{ marginLeft: 2 },
|
||||
React.createElement(
|
||||
ResponsiveText,
|
||||
{ styleType: "error" },
|
||||
"⚠️ Not Configured"
|
||||
)
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
ResponsiveText,
|
||||
{
|
||||
styleType: "subtitle",
|
||||
truncate: true,
|
||||
},
|
||||
` ${item.description}`
|
||||
)
|
||||
);
|
||||
},
|
||||
}),
|
||||
|
||||
// Footer with instructions (hide on small screens)
|
||||
React.createElement(
|
||||
ResponsiveContainer,
|
||||
{
|
||||
componentType: "secondary-info",
|
||||
hideOnSmall: true,
|
||||
padding: false,
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
key: item.id,
|
||||
borderStyle: "single",
|
||||
borderColor: isSelected ? "blue" : "gray",
|
||||
paddingX: 1,
|
||||
paddingY: 1,
|
||||
marginBottom: 1,
|
||||
flexDirection: "column",
|
||||
marginTop: 2,
|
||||
borderTopStyle: "single",
|
||||
borderColor: "gray",
|
||||
paddingTop: 1,
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", alignItems: "center" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
bold: isSelected,
|
||||
color: isSelected ? "blue" : "white",
|
||||
},
|
||||
`${isSelected ? "▶" : " "} ${item.label}`
|
||||
),
|
||||
// Configuration status indicator
|
||||
item.id === "operation" &&
|
||||
!isConfigured &&
|
||||
React.createElement(
|
||||
Box,
|
||||
{ marginLeft: 2 },
|
||||
React.createElement(Text, { color: "red" }, "⚠️ Not Configured")
|
||||
)
|
||||
ResponsiveText,
|
||||
{ styleType: "subtitle" },
|
||||
"Navigation:"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: isSelected ? "cyan" : "gray",
|
||||
italic: true,
|
||||
},
|
||||
` ${item.description}`
|
||||
ResponsiveText,
|
||||
{ styleType: "normal" },
|
||||
" ↑/↓ - Navigate menu"
|
||||
),
|
||||
React.createElement(
|
||||
ResponsiveText,
|
||||
{ styleType: "normal" },
|
||||
" Enter/Space - Select item"
|
||||
),
|
||||
React.createElement(
|
||||
ResponsiveText,
|
||||
{ styleType: "normal" },
|
||||
" h - Show help"
|
||||
),
|
||||
React.createElement(
|
||||
ResponsiveText,
|
||||
{ styleType: "normal" },
|
||||
" q - Quick exit"
|
||||
),
|
||||
React.createElement(
|
||||
ResponsiveText,
|
||||
{ styleType: "normal" },
|
||||
" Esc - Back (when available)"
|
||||
)
|
||||
);
|
||||
})
|
||||
),
|
||||
|
||||
// Footer with instructions
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
flexDirection: "column",
|
||||
marginTop: 3,
|
||||
borderTopStyle: "single",
|
||||
borderColor: "gray",
|
||||
paddingTop: 2,
|
||||
},
|
||||
React.createElement(Text, { color: "gray" }, "Navigation:"),
|
||||
React.createElement(Text, { color: "gray" }, " ↑/↓ - Navigate menu"),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
" Enter/Space - Select item"
|
||||
)
|
||||
),
|
||||
React.createElement(Text, { color: "gray" }, " q - Quick exit"),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
" Esc - Back (when available)"
|
||||
)
|
||||
),
|
||||
|
||||
// Configuration status
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", justifyContent: "space-between", marginTop: 2 },
|
||||
// Configuration status
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: appState.configuration.isValid ? "green" : "red" },
|
||||
`Configuration: ${
|
||||
appState.configuration.isValid ? "✓ Complete" : "⚠ Incomplete"
|
||||
}`
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
`Mode: ${appState.configuration.operationMode.toUpperCase()}`
|
||||
Box,
|
||||
{ flexDirection: "row", justifyContent: "space-between", marginTop: 1 },
|
||||
React.createElement(
|
||||
ResponsiveText,
|
||||
{
|
||||
styleType: appState.configuration.isValid ? "success" : "error",
|
||||
truncate: true,
|
||||
maxWidth: 30,
|
||||
},
|
||||
`Configuration: ${
|
||||
appState.configuration.isValid ? "✓ Complete" : "⚠ Incomplete"
|
||||
}`
|
||||
),
|
||||
React.createElement(
|
||||
ResponsiveText,
|
||||
{
|
||||
styleType: "normal",
|
||||
truncate: true,
|
||||
maxWidth: 20,
|
||||
},
|
||||
`Mode: ${appState.configuration.operationMode.toUpperCase()}`
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
582
src/tui/components/screens/OptimizedLogViewerScreen.jsx
Normal file
582
src/tui/components/screens/OptimizedLogViewerScreen.jsx
Normal file
@@ -0,0 +1,582 @@
|
||||
const React = require("react");
|
||||
const { Box, Text, useInput, useApp } = require("ink");
|
||||
const { useAppState } = require("../../providers/AppProvider.jsx");
|
||||
const LogReaderService = require("../../../services/logReader");
|
||||
const VirtualScrollableContainer = require("../common/VirtualScrollableContainer.jsx");
|
||||
|
||||
/**
|
||||
* Optimized Log Viewer Screen Component with virtual scrolling and performance enhancements
|
||||
* Requirements: 4.1, 4.3, 4.4, 6.1, 6.4, 10.3
|
||||
*/
|
||||
|
||||
// Memoized log entry component to prevent unnecessary re-renders
|
||||
const LogEntry = React.memo(
|
||||
({
|
||||
log,
|
||||
index,
|
||||
isSelected,
|
||||
isHighlighted,
|
||||
getLogLevelColor,
|
||||
formatDate,
|
||||
truncateText,
|
||||
}) => (
|
||||
<Box
|
||||
borderStyle={isSelected ? "single" : "none"}
|
||||
borderColor={isSelected ? "blue" : "transparent"}
|
||||
paddingX={1}
|
||||
paddingY={0}
|
||||
backgroundColor={isHighlighted ? "blue" : undefined}
|
||||
>
|
||||
<Box flexDirection="row" alignItems="center">
|
||||
<Text color={getLogLevelColor(log.level)} bold={true} width={8}>
|
||||
{log.level}
|
||||
</Text>
|
||||
<Text color={isHighlighted ? "white" : "gray"} width={12}>
|
||||
{formatDate(log.timestamp)}
|
||||
</Text>
|
||||
<Text color={isHighlighted ? "white" : "white"} flexGrow={1}>
|
||||
{truncateText(log.message, 50)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
|
||||
// Memoized log details component
|
||||
const LogDetails = React.memo(({ log, getLogLevelColor }) => (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor="green"
|
||||
paddingX={1}
|
||||
paddingY={1}
|
||||
marginTop={2}
|
||||
maxHeight={8}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
<Text bold={true} color="green">
|
||||
Log Details:
|
||||
</Text>
|
||||
<Box flexDirection="row" alignItems="center">
|
||||
<Text color="white" bold={true}>
|
||||
Level:
|
||||
</Text>
|
||||
<Text color={getLogLevelColor(log.level)}>{log.level}</Text>
|
||||
<Text color="gray" marginLeft={2}>
|
||||
|
|
||||
</Text>
|
||||
<Text color="white" bold={true} marginLeft={1}>
|
||||
Type:
|
||||
</Text>
|
||||
<Text color="cyan">{log.type || "unknown"}</Text>
|
||||
</Box>
|
||||
<Box flexDirection="row" alignItems="center">
|
||||
<Text color="white" bold={true}>
|
||||
Time:{" "}
|
||||
</Text>
|
||||
<Text color="gray">{log.timestamp.toLocaleString()}</Text>
|
||||
</Box>
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color="white" bold={true}>
|
||||
Message:
|
||||
</Text>
|
||||
<Text color="white">{log.message}</Text>
|
||||
</Box>
|
||||
{log.details && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color="white" bold={true}>
|
||||
Details:
|
||||
</Text>
|
||||
<Text color="gray" italic={true}>
|
||||
{log.details.length > 200
|
||||
? log.details.substring(0, 200) + "..."
|
||||
: log.details}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
));
|
||||
|
||||
// Debounced state update hook
|
||||
const useDebouncedState = (initialValue, delay = 100) => {
|
||||
const [value, setValue] = React.useState(initialValue);
|
||||
const [debouncedValue, setDebouncedValue] = React.useState(initialValue);
|
||||
const timeoutRef = React.useRef(null);
|
||||
|
||||
const updateValue = React.useCallback(
|
||||
(newValue) => {
|
||||
setValue(newValue);
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setDebouncedValue(newValue);
|
||||
}, delay);
|
||||
},
|
||||
[delay]
|
||||
);
|
||||
|
||||
return [value, debouncedValue, updateValue];
|
||||
};
|
||||
|
||||
const OptimizedLogViewerScreen = React.memo(() => {
|
||||
const { appState, navigateBack } = useAppState();
|
||||
const { exit } = useApp();
|
||||
|
||||
// Initialize log reader service with memoization
|
||||
const [logReader] = React.useState(() => new LogReaderService());
|
||||
|
||||
// Optimized state management with debouncing
|
||||
const [logData, setLogData] = React.useState({
|
||||
entries: [],
|
||||
pagination: {
|
||||
currentPage: 0,
|
||||
pageSize: 50, // Increased for better virtual scrolling performance
|
||||
totalEntries: 0,
|
||||
totalPages: 0,
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startIndex: 1,
|
||||
endIndex: 0,
|
||||
},
|
||||
filters: {
|
||||
levelFilter: "ALL",
|
||||
searchTerm: "",
|
||||
},
|
||||
});
|
||||
|
||||
const [selectedLog, setSelectedLog] = useDebouncedState(0, 50);
|
||||
const [showDetails, setShowDetails] = React.useState(false);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [stats, setStats] = React.useState(null);
|
||||
const [autoRefresh, setAutoRefresh] = React.useState(true);
|
||||
const [lastRefresh, setLastRefresh] = React.useState(new Date());
|
||||
const [refreshing, setRefreshing] = React.useState(false);
|
||||
|
||||
// Memoized filter options
|
||||
const filterOptions = React.useMemo(
|
||||
() => [
|
||||
{ value: "ALL", label: "All Levels" },
|
||||
{ value: "ERROR", label: "Errors" },
|
||||
{ value: "WARNING", label: "Warnings" },
|
||||
{ value: "INFO", label: "Info" },
|
||||
{ value: "SUCCESS", label: "Success" },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// Memoized utility functions
|
||||
const getLogLevelColor = React.useCallback((level) => {
|
||||
switch (level) {
|
||||
case "ERROR":
|
||||
return "red";
|
||||
case "WARNING":
|
||||
return "yellow";
|
||||
case "INFO":
|
||||
return "blue";
|
||||
case "SUCCESS":
|
||||
return "green";
|
||||
default:
|
||||
return "white";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const formatTimestamp = React.useCallback((date) => {
|
||||
return date.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
}, []);
|
||||
|
||||
const formatDate = React.useCallback((date) => {
|
||||
return date.toLocaleDateString([], {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}, []);
|
||||
|
||||
const truncateText = React.useCallback((text, maxLength = 60) => {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength - 3) + "...";
|
||||
}, []);
|
||||
|
||||
// Optimized load log data function with memoization
|
||||
const loadLogData = React.useCallback(
|
||||
async (options = {}, isAutoRefresh = false) => {
|
||||
try {
|
||||
if (isAutoRefresh) {
|
||||
setRefreshing(true);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
const loadOptions = {
|
||||
page: logData.pagination.currentPage,
|
||||
pageSize: logData.pagination.pageSize,
|
||||
levelFilter: logData.filters.levelFilter,
|
||||
searchTerm: logData.filters.searchTerm,
|
||||
...options,
|
||||
};
|
||||
|
||||
const result = await logReader.getPaginatedEntries(loadOptions);
|
||||
setLogData(result);
|
||||
|
||||
// Reset selection if current selection is out of bounds
|
||||
if (selectedLog >= result.entries.length) {
|
||||
setSelectedLog(Math.max(0, result.entries.length - 1));
|
||||
}
|
||||
|
||||
setShowDetails(false);
|
||||
setLastRefresh(new Date());
|
||||
} catch (err) {
|
||||
setError(`Failed to load logs: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
logReader,
|
||||
logData.pagination.currentPage,
|
||||
logData.pagination.pageSize,
|
||||
logData.filters.levelFilter,
|
||||
logData.filters.searchTerm,
|
||||
selectedLog,
|
||||
]
|
||||
);
|
||||
|
||||
// Optimized load statistics function
|
||||
const loadStats = React.useCallback(async () => {
|
||||
try {
|
||||
const statistics = await logReader.getLogStatistics();
|
||||
setStats(statistics);
|
||||
} catch (err) {
|
||||
console.warn("Failed to load log statistics:", err.message);
|
||||
}
|
||||
}, [logReader]);
|
||||
|
||||
// Initial load with optimization
|
||||
React.useEffect(() => {
|
||||
const loadInitialData = async () => {
|
||||
await Promise.all([loadLogData(), loadStats()]);
|
||||
};
|
||||
loadInitialData();
|
||||
}, []);
|
||||
|
||||
// Optimized auto-refresh with file watching
|
||||
React.useEffect(() => {
|
||||
if (!autoRefresh) return;
|
||||
|
||||
const cleanup = logReader.watchFile(() => {
|
||||
loadLogData({}, true);
|
||||
loadStats();
|
||||
});
|
||||
return cleanup;
|
||||
}, [logReader, loadLogData, loadStats, autoRefresh]);
|
||||
|
||||
// Optimized periodic refresh
|
||||
React.useEffect(() => {
|
||||
if (!autoRefresh) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (!loading && !refreshing) {
|
||||
logReader.clearCache();
|
||||
loadLogData({}, true);
|
||||
loadStats();
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [logReader, loadLogData, loadStats, autoRefresh, loading, refreshing]);
|
||||
|
||||
// Optimized keyboard input handler with debouncing
|
||||
const handleKeyboardInput = React.useCallback(
|
||||
(input, key) => {
|
||||
if (loading) return;
|
||||
|
||||
if (key.escape) {
|
||||
navigateBack();
|
||||
} else if (key.upArrow) {
|
||||
if (selectedLog > 0) {
|
||||
setSelectedLog(selectedLog - 1);
|
||||
setShowDetails(false);
|
||||
}
|
||||
} else if (key.downArrow) {
|
||||
if (selectedLog < logData.entries.length - 1) {
|
||||
setSelectedLog(selectedLog + 1);
|
||||
setShowDetails(false);
|
||||
}
|
||||
} else if (key.leftArrow) {
|
||||
if (logData.pagination.hasPreviousPage) {
|
||||
loadLogData({ page: logData.pagination.currentPage - 1 });
|
||||
}
|
||||
} else if (key.rightArrow) {
|
||||
if (logData.pagination.hasNextPage) {
|
||||
loadLogData({ page: logData.pagination.currentPage + 1 });
|
||||
}
|
||||
} else if (key.return || key.enter) {
|
||||
if (selectedLog < logData.entries.length) {
|
||||
setShowDetails(!showDetails);
|
||||
}
|
||||
} else if (key.r || input === "r") {
|
||||
logReader.clearCache();
|
||||
loadLogData();
|
||||
loadStats();
|
||||
} else if (input >= "1" && input <= "5") {
|
||||
const filterMap = {
|
||||
1: "ALL",
|
||||
2: "ERROR",
|
||||
3: "WARNING",
|
||||
4: "INFO",
|
||||
5: "SUCCESS",
|
||||
};
|
||||
const newFilter = filterMap[input];
|
||||
if (newFilter !== logData.filters.levelFilter) {
|
||||
loadLogData({ levelFilter: newFilter, page: 0 });
|
||||
}
|
||||
} else if (input === "s") {
|
||||
const searchTerms = ["", "error", "update", "rollback", "product"];
|
||||
const currentIndex = searchTerms.indexOf(logData.filters.searchTerm);
|
||||
const nextIndex = (currentIndex + 1) % searchTerms.length;
|
||||
const newSearchTerm = searchTerms[nextIndex];
|
||||
loadLogData({ searchTerm: newSearchTerm, page: 0 });
|
||||
} else if (input === "c") {
|
||||
loadLogData({ levelFilter: "ALL", searchTerm: "", page: 0 });
|
||||
} else if (input === "a") {
|
||||
setAutoRefresh(!autoRefresh);
|
||||
} else if (key.pageUp) {
|
||||
if (logData.pagination.currentPage > 0) {
|
||||
loadLogData({ page: 0 });
|
||||
}
|
||||
} else if (key.pageDown) {
|
||||
if (
|
||||
logData.pagination.currentPage <
|
||||
logData.pagination.totalPages - 1
|
||||
) {
|
||||
loadLogData({ page: logData.pagination.totalPages - 1 });
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
loading,
|
||||
navigateBack,
|
||||
selectedLog,
|
||||
logData,
|
||||
showDetails,
|
||||
loadLogData,
|
||||
loadStats,
|
||||
logReader,
|
||||
autoRefresh,
|
||||
]
|
||||
);
|
||||
|
||||
useInput(handleKeyboardInput);
|
||||
|
||||
// Memoized render function for log entries
|
||||
const renderLogEntry = React.useCallback(
|
||||
(log, index) => {
|
||||
const isSelected = selectedLog === index;
|
||||
const isHighlighted = isSelected && !showDetails;
|
||||
|
||||
return (
|
||||
<LogEntry
|
||||
log={log}
|
||||
index={index}
|
||||
isSelected={isSelected}
|
||||
isHighlighted={isHighlighted}
|
||||
getLogLevelColor={getLogLevelColor}
|
||||
formatDate={formatDate}
|
||||
truncateText={truncateText}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[selectedLog, showDetails, getLogLevelColor, formatDate, truncateText]
|
||||
);
|
||||
|
||||
// Show loading state
|
||||
if (loading && logData.entries.length === 0) {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
padding={2}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text color="blue">Loading logs...</Text>
|
||||
<Text color="gray">Please wait while we read the log files</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Show error state
|
||||
if (error) {
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
padding={2}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text color="red" bold={true}>
|
||||
Error Loading Logs
|
||||
</Text>
|
||||
<Text color="gray">{error}</Text>
|
||||
<Text color="gray" marginTop={1}>
|
||||
Press 'r' to retry or Esc to go back
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" padding={2} flexGrow={1}>
|
||||
{/* Header with statistics */}
|
||||
<Box flexDirection="column" marginBottom={2}>
|
||||
<Text bold={true} color="cyan">
|
||||
📋 Log Viewer
|
||||
</Text>
|
||||
<Box flexDirection="row" justifyContent="space-between">
|
||||
<Text color="gray">View application logs and operation history</Text>
|
||||
{stats && (
|
||||
<Text color="gray">
|
||||
{stats.totalEntries} entries | {stats.operations.total} operations
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Filter and pagination controls */}
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor="blue"
|
||||
paddingX={1}
|
||||
paddingY={1}
|
||||
marginBottom={2}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
<Box
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box flexDirection="row" alignItems="center">
|
||||
<Text color="white" bold={true}>
|
||||
Filter:{" "}
|
||||
</Text>
|
||||
<Text color="blue">{logData.filters.levelFilter}</Text>
|
||||
<Text color="gray" marginLeft={2}>
|
||||
(1-5 to change)
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexDirection="row" alignItems="center">
|
||||
<Text color="white" bold={true}>
|
||||
Page:{" "}
|
||||
</Text>
|
||||
<Text color="blue">
|
||||
{logData.pagination.currentPage + 1}/
|
||||
{logData.pagination.totalPages}
|
||||
</Text>
|
||||
<Text color="gray" marginLeft={2}>
|
||||
(←/→ to navigate)
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Text color="gray">
|
||||
Showing {logData.pagination.startIndex}-
|
||||
{logData.pagination.endIndex} of {logData.pagination.totalEntries}{" "}
|
||||
entries
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Virtual scrolled log list */}
|
||||
<VirtualScrollableContainer
|
||||
items={logData.entries}
|
||||
renderItem={renderLogEntry}
|
||||
itemHeight={1}
|
||||
showScrollIndicators={true}
|
||||
overscan={10}
|
||||
borderStyle="single"
|
||||
borderColor="gray"
|
||||
flexGrow={1}
|
||||
minHeight={10}
|
||||
/>
|
||||
|
||||
{/* Log details */}
|
||||
{showDetails &&
|
||||
selectedLog < logData.entries.length &&
|
||||
logData.entries[selectedLog] && (
|
||||
<LogDetails
|
||||
log={logData.entries[selectedLog]}
|
||||
getLogLevelColor={getLogLevelColor}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Instructions */}
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={2}
|
||||
borderTopStyle="single"
|
||||
borderColor="gray"
|
||||
paddingTop={1}
|
||||
>
|
||||
<Box flexDirection="row" justifyContent="space-between">
|
||||
<Box flexDirection="column">
|
||||
<Text color="gray">
|
||||
Navigation: ↑/↓ entries | ←/→ pages | Enter details
|
||||
</Text>
|
||||
<Text color="gray">
|
||||
Filters: 1=All 2=Error 3=Warning 4=Info 5=Success
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
<Text color="gray">
|
||||
Search: S cycle terms | C clear | A auto-refresh
|
||||
</Text>
|
||||
<Text color="gray">
|
||||
Actions: R refresh | PgUp/PgDn jump | Esc back
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Status bar */}
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor="gray"
|
||||
paddingX={1}
|
||||
paddingY={0}
|
||||
marginTop={1}
|
||||
>
|
||||
<Box flexDirection="row" justifyContent="space-between">
|
||||
<Text color="gray">
|
||||
Entry {selectedLog + 1}/{logData.entries.length} | Details:{" "}
|
||||
{showDetails ? "ON" : "OFF"}
|
||||
</Text>
|
||||
<Text color={loading ? "yellow" : refreshing ? "cyan" : "gray"}>
|
||||
{loading
|
||||
? "Loading..."
|
||||
: refreshing
|
||||
? "Refreshing..."
|
||||
: `Filter: ${logData.filters.levelFilter}${
|
||||
logData.filters.searchTerm
|
||||
? ` | Search: "${logData.filters.searchTerm}"`
|
||||
: ""
|
||||
} | Auto: ${
|
||||
autoRefresh ? "ON" : "OFF"
|
||||
} | ${lastRefresh.toLocaleTimeString()}`}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
module.exports = OptimizedLogViewerScreen;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,69 +2,29 @@ const React = require("react");
|
||||
const { Box, Text, useInput, useApp } = require("ink");
|
||||
const { useAppState } = require("../../providers/AppProvider.jsx");
|
||||
const SelectInput = require("ink-select-input").default;
|
||||
const TagAnalysisService = require("../../../services/tagAnalysis");
|
||||
|
||||
/**
|
||||
* Tag Analysis Screen Component
|
||||
* Analyzes product tags and provides insights for price update operations
|
||||
* Requirements: 7.7, 7.8
|
||||
* Requirements: 7.1, 7.2, 7.3
|
||||
*/
|
||||
const TagAnalysisScreen = () => {
|
||||
const { appState, navigateBack } = useAppState();
|
||||
const { exit } = useApp();
|
||||
|
||||
// Mock tag analysis data
|
||||
const mockTagAnalysis = {
|
||||
totalProducts: 150,
|
||||
tagCounts: [
|
||||
{ tag: "sale", count: 45, percentage: 30.0 },
|
||||
{ tag: "new", count: 32, percentage: 21.3 },
|
||||
{ tag: "featured", count: 28, percentage: 18.7 },
|
||||
{ tag: "clearance", count: 22, percentage: 14.7 },
|
||||
{ tag: "limited", count: 15, percentage: 10.0 },
|
||||
{ tag: "seasonal", count: 8, percentage: 5.3 },
|
||||
],
|
||||
priceRanges: {
|
||||
sale: { min: 9.99, max: 199.99, average: 59.5 },
|
||||
new: { min: 19.99, max: 299.99, average: 89.75 },
|
||||
featured: { min: 29.99, max: 399.99, average: 129.5 },
|
||||
clearance: { min: 4.99, max: 149.99, average: 39.25 },
|
||||
limited: { min: 49.99, max: 499.99, average: 199.5 },
|
||||
seasonal: { min: 14.99, max: 249.99, average: 74.25 },
|
||||
},
|
||||
recommendations: [
|
||||
{
|
||||
type: "high_impact",
|
||||
title: "High-Impact Tags",
|
||||
description:
|
||||
"Tags with many products that would benefit most from price updates",
|
||||
tags: ["sale", "clearance"],
|
||||
reason:
|
||||
"These tags have the highest product counts and are most likely to need price adjustments",
|
||||
},
|
||||
{
|
||||
type: "high_value",
|
||||
title: "High-Value Tags",
|
||||
description: "Tags with products having higher average prices",
|
||||
tags: ["limited", "featured"],
|
||||
reason:
|
||||
"These tags contain premium products where price adjustments have the most financial impact",
|
||||
},
|
||||
{
|
||||
type: "caution",
|
||||
title: "Use Caution",
|
||||
description: "Tags that may require special handling",
|
||||
tags: ["new", "seasonal"],
|
||||
reason:
|
||||
"These tags may have products with special pricing strategies that shouldn't be automatically adjusted",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// State for tag analysis
|
||||
const [analysisData, setAnalysisData] = React.useState(mockTagAnalysis);
|
||||
const [analysisData, setAnalysisData] = React.useState(null);
|
||||
const [selectedTag, setSelectedTag] = React.useState(null);
|
||||
const [showDetails, setShowDetails] = React.useState(false);
|
||||
const [analysisType, setAnalysisType] = React.useState("overview");
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [sampleProducts, setSampleProducts] = React.useState([]);
|
||||
const [loadingSamples, setLoadingSamples] = React.useState(false);
|
||||
|
||||
// Initialize tag analysis service
|
||||
const tagAnalysisService = React.useMemo(() => new TagAnalysisService(), []);
|
||||
|
||||
// Analysis type options
|
||||
const analysisOptions = [
|
||||
@@ -74,33 +34,84 @@ const TagAnalysisScreen = () => {
|
||||
{ value: "recommendations", label: "Recommendations" },
|
||||
];
|
||||
|
||||
// Load tag analysis data on component mount
|
||||
React.useEffect(() => {
|
||||
loadTagAnalysis();
|
||||
}, []);
|
||||
|
||||
// Load tag analysis data
|
||||
const loadTagAnalysis = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const analysis = await tagAnalysisService.getTagAnalysis();
|
||||
setAnalysisData(analysis);
|
||||
setSelectedTag(null);
|
||||
setShowDetails(false);
|
||||
setSampleProducts([]);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load sample products for selected tag
|
||||
const loadSampleProducts = async (tag) => {
|
||||
if (!tag) return;
|
||||
|
||||
setLoadingSamples(true);
|
||||
try {
|
||||
const samples = await tagAnalysisService.getSampleProductsForTag(tag, 3);
|
||||
setSampleProducts(samples);
|
||||
} catch (err) {
|
||||
setSampleProducts([]);
|
||||
} finally {
|
||||
setLoadingSamples(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle keyboard input
|
||||
useInput((input, key) => {
|
||||
if (loading) return; // Ignore input while loading
|
||||
|
||||
if (key.escape) {
|
||||
// Go back to main menu
|
||||
navigateBack();
|
||||
} else if (key.upArrow) {
|
||||
} else if (key.upArrow && analysisData) {
|
||||
// Navigate up in list
|
||||
if (selectedTag > 0) {
|
||||
if (selectedTag === null) {
|
||||
setSelectedTag(analysisData.tagCounts.length - 1);
|
||||
} else if (selectedTag > 0) {
|
||||
setSelectedTag(selectedTag - 1);
|
||||
setShowDetails(false);
|
||||
}
|
||||
} else if (key.downArrow) {
|
||||
// Navigate down in list
|
||||
if (selectedTag < analysisData.tagCounts.length - 1) {
|
||||
setSelectedTag(selectedTag + 1);
|
||||
setShowDetails(false);
|
||||
}
|
||||
} else if (key.return || key.enter) {
|
||||
// Toggle tag details
|
||||
if (selectedTag !== null) {
|
||||
setShowDetails(!showDetails);
|
||||
}
|
||||
} else if (key.r) {
|
||||
// Refresh analysis
|
||||
setAnalysisData(mockTagAnalysis);
|
||||
setSelectedTag(null);
|
||||
setShowDetails(false);
|
||||
setSampleProducts([]);
|
||||
} else if (key.downArrow && analysisData) {
|
||||
// Navigate down in list
|
||||
if (selectedTag === null) {
|
||||
setSelectedTag(0);
|
||||
} else if (selectedTag < analysisData.tagCounts.length - 1) {
|
||||
setSelectedTag(selectedTag + 1);
|
||||
}
|
||||
setShowDetails(false);
|
||||
setSampleProducts([]);
|
||||
} else if ((key.return || key.enter) && analysisData) {
|
||||
// Toggle tag details and load samples
|
||||
if (selectedTag !== null) {
|
||||
const newShowDetails = !showDetails;
|
||||
setShowDetails(newShowDetails);
|
||||
|
||||
if (newShowDetails) {
|
||||
const tagName = analysisData.tagCounts[selectedTag].tag;
|
||||
loadSampleProducts(tagName);
|
||||
} else {
|
||||
setSampleProducts([]);
|
||||
}
|
||||
}
|
||||
} else if (input === "r" || input === "R") {
|
||||
// Refresh analysis
|
||||
loadTagAnalysis();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -117,9 +128,30 @@ const TagAnalysisScreen = () => {
|
||||
return "green";
|
||||
};
|
||||
|
||||
// Render overview section
|
||||
const renderOverview = () =>
|
||||
// Render loading state
|
||||
const renderLoading = () =>
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", alignItems: "center", justifyContent: "center", height: 10 },
|
||||
React.createElement(Text, { color: "blue" }, "🔄 Loading tag analysis..."),
|
||||
React.createElement(Text, { color: "gray" }, "This may take a moment...")
|
||||
);
|
||||
|
||||
// Render error state
|
||||
const renderError = () =>
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", alignItems: "center", justifyContent: "center", height: 10 },
|
||||
React.createElement(Text, { color: "red", bold: true }, "❌ Error loading tag analysis"),
|
||||
React.createElement(Text, { color: "white" }, error),
|
||||
React.createElement(Text, { color: "gray", marginTop: 1 }, "Press 'R' to retry")
|
||||
);
|
||||
|
||||
// Render overview section
|
||||
const renderOverview = () => {
|
||||
if (!analysisData) return null;
|
||||
|
||||
return React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
React.createElement(
|
||||
@@ -155,6 +187,11 @@ const TagAnalysisScreen = () => {
|
||||
`Most Common Tag: ${analysisData.tagCounts[0]?.tag || "N/A"} (${
|
||||
analysisData.tagCounts[0]?.count || 0
|
||||
} products)`
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", marginTop: 1 },
|
||||
`Last Updated: ${new Date(analysisData.analyzedAt).toLocaleString()}`
|
||||
)
|
||||
)
|
||||
),
|
||||
@@ -226,8 +263,10 @@ const TagAnalysisScreen = () => {
|
||||
);
|
||||
|
||||
// Render pricing analysis
|
||||
const renderPricingAnalysis = () =>
|
||||
React.createElement(
|
||||
const renderPricingAnalysis = () => {
|
||||
if (!analysisData) return null;
|
||||
|
||||
return React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
React.createElement(
|
||||
@@ -269,7 +308,7 @@ const TagAnalysisScreen = () => {
|
||||
color: isSelected ? "white" : "gray",
|
||||
fontSize: "small",
|
||||
},
|
||||
`${tagInfo.count} products`
|
||||
`${tagInfo.count} products (${analysisData.priceRanges[tagInfo.tag]?.count || 0} variants)`
|
||||
),
|
||||
React.createElement(
|
||||
Box,
|
||||
@@ -280,14 +319,14 @@ const TagAnalysisScreen = () => {
|
||||
color: isSelected ? "white" : "cyan",
|
||||
bold: true,
|
||||
},
|
||||
"Range: "
|
||||
"Range: $"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: isSelected ? "white" : "white",
|
||||
},
|
||||
`$${priceRange.min} - $${priceRange.max}`
|
||||
`${priceRange.min.toFixed(2)} - $${priceRange.max.toFixed(2)}`
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
@@ -299,7 +338,7 @@ const TagAnalysisScreen = () => {
|
||||
color: isSelected ? "white" : "cyan",
|
||||
bold: true,
|
||||
},
|
||||
"Avg: "
|
||||
"Avg: $"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
@@ -313,10 +352,13 @@ const TagAnalysisScreen = () => {
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Render recommendations
|
||||
const renderRecommendations = () =>
|
||||
React.createElement(
|
||||
const renderRecommendations = () => {
|
||||
if (!analysisData) return null;
|
||||
|
||||
return React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
React.createElement(
|
||||
@@ -331,8 +373,14 @@ const TagAnalysisScreen = () => {
|
||||
return "green";
|
||||
case "high_value":
|
||||
return "blue";
|
||||
case "optimal":
|
||||
return "magenta";
|
||||
case "consistency":
|
||||
return "red";
|
||||
case "caution":
|
||||
return "yellow";
|
||||
case "low_count":
|
||||
return "gray";
|
||||
default:
|
||||
return "white";
|
||||
}
|
||||
@@ -344,13 +392,33 @@ const TagAnalysisScreen = () => {
|
||||
return "⭐";
|
||||
case "high_value":
|
||||
return "💎";
|
||||
case "optimal":
|
||||
return "🎯";
|
||||
case "consistency":
|
||||
return "⚖️";
|
||||
case "caution":
|
||||
return "⚠️";
|
||||
case "low_count":
|
||||
return "🔍";
|
||||
default:
|
||||
return "ℹ️";
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityBadge = (priority) => {
|
||||
const colors = {
|
||||
high: "red",
|
||||
medium: "yellow",
|
||||
low: "blue",
|
||||
info: "gray"
|
||||
};
|
||||
return React.createElement(
|
||||
Text,
|
||||
{ color: colors[priority] || "white", bold: true },
|
||||
`[${priority.toUpperCase()}]`
|
||||
);
|
||||
};
|
||||
|
||||
return React.createElement(
|
||||
Box,
|
||||
{
|
||||
@@ -364,6 +432,7 @@ const TagAnalysisScreen = () => {
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
// Header with icon, title, and priority
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", alignItems: "center", marginBottom: 1 },
|
||||
@@ -373,31 +442,96 @@ const TagAnalysisScreen = () => {
|
||||
color: getTypeColor(rec.type),
|
||||
bold: true,
|
||||
},
|
||||
`${getTypeIcon(rec.type)} ${rec.title}`
|
||||
`${getTypeIcon(rec.type)} ${rec.title} `
|
||||
),
|
||||
getPriorityBadge(rec.priority),
|
||||
rec.actionable && React.createElement(
|
||||
Text,
|
||||
{ color: "green", marginLeft: 1 },
|
||||
"[ACTIONABLE]"
|
||||
)
|
||||
),
|
||||
// Description
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "white", marginBottom: 1 },
|
||||
rec.description
|
||||
),
|
||||
// Tags
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", italic: true, marginBottom: 1 },
|
||||
rec.reason
|
||||
Box,
|
||||
{ flexDirection: "row", marginBottom: 1 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "cyan", bold: true },
|
||||
"Tags: "
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "white" },
|
||||
rec.tags.join(", ")
|
||||
)
|
||||
),
|
||||
// Estimated impact
|
||||
rec.estimatedImpact && React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", marginBottom: 1 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "green", bold: true },
|
||||
"Impact: "
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "white" },
|
||||
rec.estimatedImpact
|
||||
)
|
||||
),
|
||||
// Detailed information for some recommendation types
|
||||
rec.details && rec.details.length > 0 && React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", marginTop: 1, marginLeft: 2 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", bold: true },
|
||||
"Details:"
|
||||
),
|
||||
rec.details.slice(0, 3).map((detail, detailIdx) =>
|
||||
React.createElement(
|
||||
Text,
|
||||
{ key: detailIdx, color: "gray" },
|
||||
rec.type === 'high_impact' ?
|
||||
`• ${detail.tag}: ${detail.count} products (${detail.percentage.toFixed(1)}%)` :
|
||||
rec.type === 'high_value' ?
|
||||
`• ${detail.tag}: $${detail.averagePrice.toFixed(2)} avg, ${detail.count} products` :
|
||||
rec.type === 'optimal' ?
|
||||
`• ${detail.tag}: Score ${detail.score.toFixed(1)}, ${detail.count} products` :
|
||||
rec.type === 'consistency' ?
|
||||
`• ${detail.tag}: ${detail.issue} (${detail.variationRatio}x variation)` :
|
||||
rec.type === 'caution' ?
|
||||
`• ${detail.tag}: ${detail.count} products (${detail.riskLevel} risk)` :
|
||||
`• ${detail.tag}: ${detail.count} products`
|
||||
)
|
||||
)
|
||||
),
|
||||
// Reason
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "cyan", bold: true },
|
||||
"Tags: " + rec.tags.join(", ")
|
||||
{ color: "gray", italic: true, marginTop: 1 },
|
||||
rec.reason
|
||||
)
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Render current analysis view
|
||||
const renderCurrentView = () => {
|
||||
if (loading) return renderLoading();
|
||||
if (error) return renderError();
|
||||
if (!analysisData) return renderError();
|
||||
|
||||
switch (analysisType) {
|
||||
case "overview":
|
||||
return renderOverview();
|
||||
@@ -599,7 +733,39 @@ const TagAnalysisScreen = () => {
|
||||
].average.toFixed(2)}`
|
||||
)
|
||||
)
|
||||
),
|
||||
// Sample products section
|
||||
sampleProducts.length > 0 && React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", marginTop: 1 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "white", bold: true },
|
||||
"Sample Products:"
|
||||
),
|
||||
sampleProducts.map((product, idx) =>
|
||||
React.createElement(
|
||||
Box,
|
||||
{ key: idx, flexDirection: "column", marginLeft: 2, marginTop: 0.5 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "cyan" },
|
||||
`• ${product.title}`
|
||||
),
|
||||
product.variants.length > 0 && React.createElement(
|
||||
Text,
|
||||
{ color: "gray", marginLeft: 2 },
|
||||
`Price: $${product.variants[0].price}${product.variants[0].compareAtPrice ? ` (was $${product.variants[0].compareAtPrice})` : ''}`
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
// Loading indicator for samples
|
||||
loadingSamples && React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", alignItems: "center", marginTop: 1 },
|
||||
React.createElement(Text, { color: "blue" }, "🔄 Loading sample products...")
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
@@ -615,7 +781,7 @@ const TagAnalysisScreen = () => {
|
||||
},
|
||||
React.createElement(Text, { color: "gray" }, "Controls:"),
|
||||
React.createElement(Text, { color: "gray" }, " ↑/↓ - Navigate items"),
|
||||
React.createElement(Text, { color: "gray" }, " Enter - View details"),
|
||||
React.createElement(Text, { color: "gray" }, " Enter - View details & sample products"),
|
||||
React.createElement(Text, { color: "gray" }, " R - Refresh analysis"),
|
||||
React.createElement(Text, { color: "gray" }, " Esc - Back to menu")
|
||||
),
|
||||
|
||||
525
src/tui/components/screens/ViewLogsScreen.jsx
Normal file
525
src/tui/components/screens/ViewLogsScreen.jsx
Normal file
@@ -0,0 +1,525 @@
|
||||
const React = require("react");
|
||||
const { Box, Text, useInput } = require("ink");
|
||||
const { useAppState } = require("../../providers/AppProvider.jsx");
|
||||
const { useServices } = require("../../hooks/useServices.js");
|
||||
const { LoadingIndicator } = require("../common/LoadingIndicator.jsx");
|
||||
const ErrorDisplay = require("../common/ErrorDisplay.jsx");
|
||||
const { Pagination } = require("../common/Pagination.jsx");
|
||||
|
||||
/**
|
||||
* View Logs Screen Component
|
||||
* Log file list view with keyboard navigation and metadata display
|
||||
* Requirements: 2.1, 2.8, 4.1, 4.2
|
||||
*/
|
||||
const ViewLogsScreen = () => {
|
||||
const { navigateBack } = useAppState();
|
||||
const { getLogFiles, readLogFile } = useServices();
|
||||
|
||||
// State management for log files, selected file, and content
|
||||
const [logFiles, setLogFiles] = React.useState([]);
|
||||
const [selectedFileIndex, setSelectedFileIndex] = React.useState(0);
|
||||
const [selectedFile, setSelectedFile] = React.useState(null);
|
||||
const [logContent, setLogContent] = React.useState("");
|
||||
const [parsedLogs, setParsedLogs] = 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"
|
||||
|
||||
// Load log files on component mount
|
||||
React.useEffect(() => {
|
||||
const loadLogFiles = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const files = await getLogFiles();
|
||||
setLogFiles(files);
|
||||
|
||||
// Auto-select the main Progress.md file if it exists
|
||||
const mainLogIndex = files.findIndex((file) => file.isMainLog);
|
||||
if (mainLogIndex !== -1) {
|
||||
setSelectedFileIndex(mainLogIndex);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`Failed to discover log files: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadLogFiles();
|
||||
}, [getLogFiles]);
|
||||
|
||||
// Load content for selected file
|
||||
const loadFileContent = React.useCallback(
|
||||
async (file) => {
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
setLoadingContent(true);
|
||||
setContentError(null);
|
||||
setCurrentPage(0); // Reset pagination
|
||||
|
||||
const content = await readLogFile(file.filename);
|
||||
setLogContent(content);
|
||||
|
||||
// Parse the content into structured log entries
|
||||
const { parseLogContent } = useServices();
|
||||
const parsed = parseLogContent(content);
|
||||
setParsedLogs(parsed);
|
||||
|
||||
setSelectedFile(file);
|
||||
} catch (err) {
|
||||
setContentError(`Failed to read log file: ${err.message}`);
|
||||
setLogContent("");
|
||||
setParsedLogs([]);
|
||||
} finally {
|
||||
setLoadingContent(false);
|
||||
}
|
||||
},
|
||||
[readLogFile, useServices]
|
||||
);
|
||||
|
||||
// Helper function to format file size
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
// Helper function to format date
|
||||
const formatDate = (date) => {
|
||||
try {
|
||||
return date.toLocaleString();
|
||||
} catch (error) {
|
||||
return "Invalid date";
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get relative time
|
||||
const getRelativeTime = (date) => {
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return "Just now";
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
// Keyboard navigation for log file selection
|
||||
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]);
|
||||
}
|
||||
} 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));
|
||||
}
|
||||
});
|
||||
|
||||
// Show loading state
|
||||
if (loading) {
|
||||
return React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", padding: 2, flexGrow: 1 },
|
||||
React.createElement(Text, { bold: true, color: "cyan" }, "📋 View Logs"),
|
||||
React.createElement(LoadingIndicator, {
|
||||
message: "Discovering log files...",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Show error state
|
||||
if (error) {
|
||||
return React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", padding: 2, flexGrow: 1 },
|
||||
React.createElement(Text, { bold: true, color: "cyan" }, "📋 View Logs"),
|
||||
React.createElement(ErrorDisplay, {
|
||||
error: { message: error },
|
||||
onRetry: () => {
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
getLogFiles()
|
||||
.then(setLogFiles)
|
||||
.catch((err) =>
|
||||
setError(`Failed to discover log files: ${err.message}`)
|
||||
)
|
||||
.finally(() => setLoading(false));
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Show file content view if a file is selected
|
||||
if (selectedFile) {
|
||||
return React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", padding: 2, flexGrow: 1 },
|
||||
// Header
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", marginBottom: 2 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ bold: true, color: "cyan" },
|
||||
"📄 Log File Content"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
`Viewing: ${selectedFile.filename}`
|
||||
)
|
||||
),
|
||||
|
||||
// File metadata
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
borderStyle: "single",
|
||||
borderColor: "blue",
|
||||
paddingX: 2,
|
||||
paddingY: 1,
|
||||
marginBottom: 2,
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", justifyContent: "space-between" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "white", bold: true },
|
||||
selectedFile.filename
|
||||
),
|
||||
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%" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "cyan" },
|
||||
`Size: ${formatFileSize(selectedFile.size)}`
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "cyan" },
|
||||
`Operations: ${selectedFile.operationCount}`
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", width: "50%" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "cyan" },
|
||||
`Created: ${getRelativeTime(selectedFile.createdAt)}`
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "cyan" },
|
||||
`Modified: ${getRelativeTime(selectedFile.modifiedAt)}`
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Content display
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
borderStyle: "single",
|
||||
borderColor: "gray",
|
||||
paddingX: 1,
|
||||
paddingY: 1,
|
||||
flexGrow: 1,
|
||||
marginBottom: 2,
|
||||
},
|
||||
loadingContent
|
||||
? React.createElement(LoadingIndicator, {
|
||||
message: "Loading file content...",
|
||||
})
|
||||
: contentError
|
||||
? React.createElement(ErrorDisplay, {
|
||||
error: { message: contentError },
|
||||
onRetry: () => loadFileContent(selectedFile),
|
||||
})
|
||||
: logContent
|
||||
? 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"
|
||||
)
|
||||
),
|
||||
|
||||
// Instructions
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
flexDirection: "column",
|
||||
borderTopStyle: "single",
|
||||
borderColor: "gray",
|
||||
paddingTop: 1,
|
||||
},
|
||||
React.createElement(Text, { color: "gray", bold: true }, "Navigation:"),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", marginTop: 1 },
|
||||
" Esc - Back to file list R - Refresh content"
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", padding: 2, flexGrow: 1 },
|
||||
// Header
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", marginBottom: 2 },
|
||||
React.createElement(Text, { bold: true, color: "cyan" }, "📋 View Logs"),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
"Select a log file to view its contents and operation history"
|
||||
)
|
||||
),
|
||||
|
||||
// Log file list view
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
borderStyle: "single",
|
||||
borderColor: "blue",
|
||||
paddingX: 1,
|
||||
paddingY: 1,
|
||||
marginBottom: 2,
|
||||
flexGrow: 1,
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ bold: true, color: "blue", marginBottom: 1 },
|
||||
`📁 Available Log Files (${logFiles.length})`
|
||||
),
|
||||
logFiles.length === 0
|
||||
? React.createElement(
|
||||
Box,
|
||||
{ justifyContent: "center", alignItems: "center", padding: 2 },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
"No log files found"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", marginTop: 1 },
|
||||
"Log files are created when operations are performed"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray", marginTop: 1 },
|
||||
"Run some price update operations to generate logs"
|
||||
)
|
||||
)
|
||||
: React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column" },
|
||||
logFiles.map((file, index) => {
|
||||
const isSelected = selectedFileIndex === index;
|
||||
|
||||
return React.createElement(
|
||||
Box,
|
||||
{
|
||||
key: file.filename,
|
||||
flexDirection: "column",
|
||||
paddingY: 1,
|
||||
paddingX: 1,
|
||||
backgroundColor: isSelected ? "blue" : undefined,
|
||||
borderStyle: isSelected ? "single" : "none",
|
||||
borderColor: isSelected ? "cyan" : "gray",
|
||||
marginBottom: 1,
|
||||
},
|
||||
// File name and status
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", justifyContent: "space-between" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: isSelected ? "white" : "white",
|
||||
bold: isSelected,
|
||||
},
|
||||
`${isSelected ? "► " : " "}${file.filename}`
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: file.isMainLog ? "green" : "gray", bold: true },
|
||||
file.isMainLog ? "MAIN" : "ARCHIVE"
|
||||
)
|
||||
),
|
||||
// File metadata
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 1,
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: isSelected ? "gray" : "gray",
|
||||
marginLeft: isSelected ? 2 : 2,
|
||||
},
|
||||
`${formatFileSize(file.size)}`
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: isSelected ? "gray" : "gray",
|
||||
marginLeft: 2,
|
||||
},
|
||||
`• ${file.operationCount} ops`
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: isSelected ? "gray" : "gray",
|
||||
},
|
||||
getRelativeTime(file.modifiedAt)
|
||||
)
|
||||
),
|
||||
// Creation date
|
||||
React.createElement(
|
||||
Text,
|
||||
{
|
||||
color: isSelected ? "gray" : "gray",
|
||||
marginLeft: isSelected ? 2 : 2,
|
||||
marginTop: 1,
|
||||
},
|
||||
`Created: ${formatDate(file.createdAt)}`
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Instructions
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
flexDirection: "column",
|
||||
borderTopStyle: "single",
|
||||
borderColor: "gray",
|
||||
paddingTop: 1,
|
||||
},
|
||||
React.createElement(Text, { color: "gray", bold: true }, "Navigation:"),
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", marginTop: 1 },
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", width: "50%" },
|
||||
React.createElement(Text, { color: "gray" }, " ↑/↓ - Select file"),
|
||||
React.createElement(Text, { color: "gray" }, " Enter - View content")
|
||||
),
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "column", width: "50%" },
|
||||
React.createElement(Text, { color: "gray" }, " R - Refresh list"),
|
||||
React.createElement(Text, { color: "gray" }, " Esc - Back to menu")
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// Status bar
|
||||
React.createElement(
|
||||
Box,
|
||||
{
|
||||
borderStyle: "single",
|
||||
borderColor: "gray",
|
||||
paddingX: 1,
|
||||
paddingY: 0,
|
||||
marginTop: 1,
|
||||
},
|
||||
React.createElement(
|
||||
Box,
|
||||
{ flexDirection: "row", justifyContent: "space-between" },
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
logFiles.length > 0
|
||||
? `File ${selectedFileIndex + 1}/${logFiles.length}`
|
||||
: "No files available"
|
||||
),
|
||||
React.createElement(
|
||||
Text,
|
||||
{ color: "gray" },
|
||||
"Select a file to view detailed log content"
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = ViewLogsScreen;
|
||||
Reference in New Issue
Block a user