fix time filter and loader

This commit is contained in:
2026-04-29 10:30:49 +07:00
parent c5e8dca8ac
commit d09abd2500

View File

@@ -676,6 +676,57 @@
.fade-in { .fade-in {
animation: fadeIn .2s ease forwards animation: fadeIn .2s ease forwards
} }
.log-container {
position: relative;
/* Crucial: pins the loader to this div */
width: 100%;
height: 400px;
border: 1px solid #333;
}
/* The Overlay */
.loading-overlay {
position: absolute;
inset: 0;
/* Modern shorthand for top, left, bottom, right: 0 */
background: rgba(0, 0, 0, 0.7);
/* Dim the background */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
/* Ensures it stays on top of the chart */
color: white;
transition: opacity 0.3s ease;
visibility: hidden;
/* Hidden by default */
opacity: 0;
}
/* Show state */
.loading-overlay.is-active {
visibility: visible;
opacity: 1;
}
/* Simple Spinner Animation */
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top-color: #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style> </style>
</head> </head>
@@ -694,7 +745,7 @@
<!-- Header --> <!-- Header -->
<div id="header"> <div id="header">
<div id="logo">log<span>scope</span></div> <div id="logo">log<span>viewer</span></div>
<div class="hdr-sep"></div> <div class="hdr-sep"></div>
<div id="file-info">No file loaded — drag & drop or select a file</div> <div id="file-info">No file loaded — drag & drop or select a file</div>
<button id="file-btn" onclick="document.getElementById('file-input').click()"> <button id="file-btn" onclick="document.getElementById('file-input').click()">
@@ -707,88 +758,95 @@
<input type="file" id="file-input" accept=".log,.txt,.json,text/plain"> <input type="file" id="file-input" accept=".log,.txt,.json,text/plain">
</div> </div>
<!-- Filter bar --> <div id="log-container">
<div id="filter-bar"> <!-- Filter bar -->
<div id="search-wrap"> <div id="filter-bar">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none"> <div id="search-wrap">
<circle cx="6.5" cy="6.5" r="4.5" stroke="currentColor" stroke-width="1.4" /> <svg width="13" height="13" viewBox="0 0 16 16" fill="none">
<path d="M10.5 10.5L14 14" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" /> <circle cx="6.5" cy="6.5" r="4.5" stroke="currentColor" stroke-width="1.4" />
</svg> <path d="M10.5 10.5L14 14" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
<input type="text" id="search" placeholder="Filter messages… (regex supported)" autocomplete="off">
<span id="regex-badge">.*</span>
</div>
<div class="hdr-sep"></div>
<div class="level-pills" id="level-pills">
<button class="level-pill" data-level="TRACE"><span class="pill-dot"></span>Trace<span class="pill-count"
id="cnt-TRACE">0</span></button>
<button class="level-pill" data-level="DEBUG"><span class="pill-dot"></span>Debug<span class="pill-count"
id="cnt-DEBUG">0</span></button>
<button class="level-pill" data-level="INFO"><span class="pill-dot"></span>Info<span class="pill-count"
id="cnt-INFO">0</span></button>
<button class="level-pill" data-level="WARN"><span class="pill-dot"></span>Warn<span class="pill-count"
id="cnt-WARN">0</span></button>
<button class="level-pill" data-level="ERROR"><span class="pill-dot"></span>Error<span class="pill-count"
id="cnt-ERROR">0</span></button>
<button class="level-pill" data-level="FATAL"><span class="pill-dot"></span>Fatal<span class="pill-count"
id="cnt-FATAL">0</span></button>
</div>
<div class="filter-actions">
<div id="time-badge">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.4" />
<path d="M8 5v3l2 2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
</svg> </svg>
<span id="time-badge-label">time range</span> <input type="text" id="search" placeholder="Filter messages… (regex supported)" autocomplete="off">
<span class="tx-close"></span> <span id="regex-badge">.*</span>
</div> </div>
<button id="clear-filters">Clear filters</button> <div class="hdr-sep"></div>
</div> <div class="level-pills" id="level-pills">
</div> <button class="level-pill" data-level="TRACE"><span class="pill-dot"></span>Trace<span class="pill-count"
id="cnt-TRACE">0</span></button>
<!-- Main --> <button class="level-pill" data-level="DEBUG"><span class="pill-dot"></span>Debug<span class="pill-count"
<div id="main"> id="cnt-DEBUG">0</span></button>
<!-- Chart --> <button class="level-pill" data-level="INFO"><span class="pill-dot"></span>Info<span class="pill-count"
<div id="chart-panel"> id="cnt-INFO">0</span></button>
<div id="chart-wrap"> <button class="level-pill" data-level="WARN"><span class="pill-dot"></span>Warn<span class="pill-count"
<canvas id="intensity-chart"></canvas> id="cnt-WARN">0</span></button>
<span id="chart-hint">drag to zoom · right-click to reset</span> <button class="level-pill" data-level="ERROR"><span class="pill-dot"></span>Error<span class="pill-count"
id="cnt-ERROR">0</span></button>
<button class="level-pill" data-level="FATAL"><span class="pill-dot"></span>Fatal<span class="pill-count"
id="cnt-FATAL">0</span></button>
</div>
<div class="filter-actions">
<div id="time-badge">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.4" />
<path d="M8 5v3l2 2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
</svg>
<span id="time-badge-label">time range</span>
<span class="tx-close"></span>
</div>
<button id="clear-filters">Clear filters</button>
</div> </div>
</div> </div>
<!-- Empty state / Table area --> <!-- Main -->
<div id="empty-state"> <div id="main">
<svg width="48" height="48" viewBox="0 0 48 48" fill="none"> <!-- Chart -->
<rect x="6" y="6" width="36" height="36" rx="6" stroke="currentColor" stroke-width="1.5" <div id="chart-panel">
stroke-dasharray="5 3" /> <div id="chart-wrap">
<path d="M16 20h16M16 24h10M16 28h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" /> <canvas id="intensity-chart"></canvas>
</svg> <span id="chart-hint">drag to zoom · right-click to reset</span>
<div class="es-title">No log file loaded</div> </div>
<div class="es-sub">Drag & drop a log file anywhere on the page or click below to browse</div> </div>
<button class="es-btn" onclick="document.getElementById('file-input').click()">Select log file</button>
</div>
<div id="table-area" style="display:none"> <!-- Empty state / Table area -->
<div id="dt-wrap"> <div id="empty-state">
<table id="log-table"> <svg width="48" height="48" viewBox="0 0 48 48" fill="none">
<thead> <rect x="6" y="6" width="36" height="36" rx="6" stroke="currentColor" stroke-width="1.5"
<tr> stroke-dasharray="5 3" />
<th class="col-time" data-col="time">Time</th> <path d="M16 20h16M16 24h10M16 28h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<th class="col-level" data-col="level">Level</th> </svg>
<th class="col-msg" data-col="message">Message</th> <div class="es-title">No log file loaded</div>
</tr> <div class="es-sub">Drag & drop a log file anywhere on the page or click below to browse</div>
</thead> <button class="es-btn" onclick="document.getElementById('file-input').click()">Select log file</button>
<tbody id="log-tbody"></tbody> </div>
</table>
<div id="table-area" style="display:none">
<div id="dt-wrap">
<table id="log-table">
<thead>
<tr>
<th class="col-time" data-col="time">Time</th>
<th class="col-level" data-col="level">Level</th>
<th class="col-msg" data-col="message">Message</th>
</tr>
</thead>
<tbody id="log-tbody"></tbody>
</table>
</div>
</div>
<!-- Status bar -->
<div id="status-bar" style="display:none">
<span class="status-count" id="status-showing">0 entries</span>
<span class="sb-sep">·</span>
<span id="status-total">0 total</span>
<span class="sb-sep">·</span>
<span id="status-file">no file</span>
</div> </div>
</div> </div>
<!-- Status bar --> <div class="loading-overlay" id="loader">
<div id="status-bar" style="display:none"> <div class="spinner"></div>
<span class="status-count" id="status-showing">0 entries</span> <p>Loading Data...</p>
<span class="sb-sep">·</span>
<span id="status-total">0 total</span>
<span class="sb-sep">·</span>
<span id="status-file">no file</span>
</div> </div>
</div> </div>
@@ -804,6 +862,7 @@
let expandedRows = new Set(); let expandedRows = new Set();
let chart = null; let chart = null;
let chartBuckets = []; let chartBuckets = [];
const msg_len_limit = 100;
// ---- Demo data generator ---- // ---- Demo data generator ----
function generateDemoLogs() { function generateDemoLogs() {
@@ -864,7 +923,7 @@
]; ];
let i = 0; let i = 0;
debugger //debugger
while (i < lines.length) { while (i < lines.length) {
const line = lines[i].trim(); const line = lines[i].trim();
let matched = false; let matched = false;
@@ -899,7 +958,14 @@
j++; j++;
} }
i = j - 1; i = j - 1;
logs.push({ time, level: levelStr, message: msg.trim(), props: null, exception: exception || null }); logs.push({
time,
level: levelStr,
message: msg.length > msg_len_limit ? msg.slice(0, msg_len_limit) + "..." : msg.trim(),
fullmessage: msg.length > msg_len_limit ? msg : null,
props: null,
exception: exception || null
});
matched = true; matched = true;
break; break;
} }
@@ -932,7 +998,12 @@
const usedKeys = new Set([...timeKeys, ...levelKeys, ...msgKeys].filter(k => obj[k] !== undefined)); const usedKeys = new Set([...timeKeys, ...levelKeys, ...msgKeys].filter(k => obj[k] !== undefined));
const props = Object.fromEntries(Object.entries(obj).filter(([k]) => !usedKeys.has(k))); const props = Object.fromEntries(Object.entries(obj).filter(([k]) => !usedKeys.has(k)));
const exception = obj.exception || obj.error || obj.stack || null; const exception = obj.exception || obj.error || obj.stack || null;
logs.push({ time, level: ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'].includes(level) ? level : 'INFO', message: String(msgVal), props: Object.keys(props).length ? props : null, exception: exception ? String(exception) : null }); logs.push({
time,
level: ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'].includes(level) ? level : 'INFO',
message: String(msgVal), props: Object.keys(props).length ? props : null,
exception: exception ? String(exception) : null
});
} catch { } } catch { }
} }
return logs; return logs;
@@ -1016,7 +1087,39 @@
x: { x: {
type: 'category', type: 'category',
stacked: true, stacked: true,
ticks: { display: false }, ticks: {
// Grouping: Limit the number of labels shown to prevent overlapping
maxTicksLimit: 15,
// Formatting
// callback: function (val, index) {
// // Assuming your data labels are Date objects or Date strings
// const date = new Date(this.getLabelForValue(val));
// // Returns "HH:mm:ss" in local time
// return date.toLocaleTimeString('sv-SE');
// }
callback: function (val, index) {
const labels = this.chart.data.labels;
const date = new Date(labels[index]);
// Check if the first and last labels are on the same day
const firstDate = new Date(labels[0]).toDateString();
const lastDate = new Date(labels[labels.length - 1]).toDateString();
const isSameDay = firstDate === lastDate;
// Format based on the result
if (isSameDay) {
// Returns: HH:mm:ss
return date.toLocaleTimeString('sv-SE');
} else {
// Returns: DD/MM HH:mm:ss
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const time = date.toLocaleTimeString('sv-SE');
return `${day}/${month} ${time}`;
}
}
},
grid: { color: '#ffffff08', drawBorder: false }, grid: { color: '#ffffff08', drawBorder: false },
border: { color: '#ffffff10' } border: { color: '#ffffff10' }
}, },
@@ -1032,6 +1135,7 @@
canvas.addEventListener('contextmenu', e => { canvas.addEventListener('contextmenu', e => {
e.preventDefault(); e.preventDefault();
showLoading()
clearTimeFilter(); clearTimeFilter();
}); });
} }
@@ -1043,19 +1147,25 @@
// ---- Filtering ---- // ---- Filtering ----
function applyTimeFilter(start, end) { function applyTimeFilter(start, end) {
showLoading()
timeFilter = { start, end }; timeFilter = { start, end };
const startD = new Date(start), endD = new Date(end); const startD = new Date(start), endD = new Date(end);
document.getElementById('time-badge-label').textContent = document.getElementById('time-badge-label').textContent =
`${fmtShort(startD)} ${fmtShort(endD)}`; `${fmtShort(startD)} ${fmtShort(endD)}`;
document.getElementById('time-badge').classList.add('visible'); document.getElementById('time-badge').classList.add('visible');
applyFilters(); applyFilters();
updateFilteredLevelCounts();
hideLoading()
} }
function clearTimeFilter() { function clearTimeFilter() {
showLoading()
timeFilter = null; timeFilter = null;
document.getElementById('time-badge').classList.remove('visible'); document.getElementById('time-badge').classList.remove('visible');
if (chart) chart.resetZoom(); if (chart) chart.resetZoom();
applyFilters(); applyFilters();
updateLevelCounts();
hideLoading()
} }
function applyFilters() { function applyFilters() {
@@ -1103,9 +1213,9 @@
tr.className = 'fade-in'; tr.className = 'fade-in';
tr.dataset.idx = idx; tr.dataset.idx = idx;
tr.innerHTML = ` tr.innerHTML = `
<td class="col-time">${fmtTime(log.time)}</td> <td class="col-time">${fmtTimeLocal(log.time)}</td>
<td class="col-level"><span class="lv lv-${log.level}">${log.level}</span></td> <td class="col-level"><span class="lv lv-${log.level}">${log.level}</span></td>
<td class="col-msg">${esc(log.message)}${log.exception || log.props ? ' <span style="color:var(--text3);font-size:10px">▸ details</span>' : ''}</td> <td class="col-msg">${esc(log.message)}${log.exception || log.props || log.fullmessage ? ' <span style="color:var(--text3);font-size:10px">▸ details</span>' : ''}</td>
`; `;
tr.addEventListener('click', () => toggleDetail(tr, log, idx)); tr.addEventListener('click', () => toggleDetail(tr, log, idx));
tbody.appendChild(tr); tbody.appendChild(tr);
@@ -1129,6 +1239,9 @@
const detailTr = document.createElement('tr'); const detailTr = document.createElement('tr');
detailTr.className = 'row-detail'; detailTr.className = 'row-detail';
let sections = ''; let sections = '';
if (log.fullmessage) {
sections += `<div class="detail-section"><div class="detail-label">Full Message</div><div class="detail-val">${esc(log.fullmessage)}</div></div>`;
}
if (log.exception) { if (log.exception) {
sections += `<div class="detail-section"><div class="detail-label">Exception / Stack Trace</div><div class="detail-val" style="color:var(--error)">${esc(log.exception)}</div></div>`; sections += `<div class="detail-section"><div class="detail-label">Exception / Stack Trace</div><div class="detail-val" style="color:var(--error)">${esc(log.exception)}</div></div>`;
} }
@@ -1149,6 +1262,14 @@
}); });
} }
function updateFilteredLevelCounts() {
const counts = { TRACE: 0, DEBUG: 0, INFO: 0, WARN: 0, ERROR: 0, FATAL: 0 };
filteredLogs.forEach(l => { if (counts[l.level] !== undefined) counts[l.level]++; });
Object.entries(counts).forEach(([lv, cnt]) => {
document.getElementById('cnt-' + lv).textContent = cnt.toLocaleString();
});
}
// ---- Status bar ---- // ---- Status bar ----
function updateStatus() { function updateStatus() {
const s = document.getElementById('status-showing'); const s = document.getElementById('status-showing');
@@ -1161,6 +1282,9 @@
function fmtTime(d) { function fmtTime(d) {
return d.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ''); return d.toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
} }
function fmtTimeLocal(d) {
return d.toLocaleString('sv-SE').replace('T', ' ').replace(/\.\d+Z$/, '');
}
function fmtShort(d) { function fmtShort(d) {
return d.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); return d.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
} }
@@ -1171,6 +1295,7 @@
// ---- Load file ---- // ---- Load file ----
function loadFile(file) { function loadFile(file) {
//debugger //debugger
showLoading();
const reader = new FileReader(); const reader = new FileReader();
reader.onload = e => { reader.onload = e => {
const text = e.target.result; const text = e.target.result;
@@ -1248,6 +1373,17 @@
buildChart(allLogs); buildChart(allLogs);
renderTable(); renderTable();
updateStatus(); updateStatus();
hideLoading();
}
const loader = document.getElementById('loader');
function showLoading() {
if (!loader.classList.contains('is-active')) {
loader.classList.add('is-active');
}
}
function hideLoading() {
loader.classList.remove('is-active');
} }
// ---- Events ---- // ---- Events ----
@@ -1274,6 +1410,7 @@
document.getElementById('time-badge').addEventListener('click', () => clearTimeFilter()); document.getElementById('time-badge').addEventListener('click', () => clearTimeFilter());
document.getElementById('clear-filters').addEventListener('click', () => { document.getElementById('clear-filters').addEventListener('click', () => {
//showLoading()
levelFilter = null; levelFilter = null;
document.getElementById('search').value = ''; document.getElementById('search').value = '';
document.querySelectorAll('.level-pill').forEach(p => p.classList.remove('active')); document.querySelectorAll('.level-pill').forEach(p => p.classList.remove('active'));
@@ -1314,7 +1451,7 @@
}); });
// Load demo on start // Load demo on start
loadDemo(); //loadDemo();
</script> </script>
</body> </body>