Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a12fac16c8 |
No files matched your search
@@ -64,6 +64,12 @@ Severity badges `err`/`crit` and `warning` use the colors of the `error` and `wa
|
|||||||
|
|
||||||
Shortcuts: `/` focuses the search box, `Esc` clears it. Clicking a row shows all its fields.
|
Shortcuts: `/` focuses the search box, `Esc` clears it. Clicking a row shows all its fields.
|
||||||
|
|
||||||
|
The column headers stay visible while scrolling. Drag the edge of a header (received, message
|
||||||
|
time, severity, host, app) to resize the column, double-click it to go back to the automatic
|
||||||
|
width; the message takes the remaining space. Widths are remembered by the browser
|
||||||
|
(**Settings > Interface > Reset column widths** restores them all). On phones the list keeps its
|
||||||
|
two-line layout without columns.
|
||||||
|
|
||||||
## Timeline
|
## Timeline
|
||||||
|
|
||||||
The timeline above the list shows the volume of logs per interval, counted by **reception
|
The timeline above the list shows the volume of logs per interval, counted by **reception
|
||||||
|
|||||||
+70
@@ -126,6 +126,10 @@ const I18N = {
|
|||||||
hDivision: (s) => `interval ${s}`, hCapped: 'enlarged',
|
hDivision: (s) => `interval ${s}`, hCapped: 'enlarged',
|
||||||
hCappedTitle: 'The chosen division would exceed 300 intervals over this range: it has been enlarged.',
|
hCappedTitle: 'The chosen division would exceed 300 intervals over this range: it has been enlarged.',
|
||||||
hUnzoom: '× Reset zoom', unitDay: 'd',
|
hUnzoom: '× Reset zoom', unitDay: 'd',
|
||||||
|
colRcv: 'Received', colMt: 'Message time', colSev: 'Severity', colHost: 'Host', colApp: 'App', colMsg: 'Message',
|
||||||
|
colGrip: 'Drag to resize, double-click for the automatic width',
|
||||||
|
resetCols: 'Reset column widths', colsReset: 'Column widths reset',
|
||||||
|
colsHelp: 'Drag the edge of a column header in the log list to resize it (remembered by this browser).',
|
||||||
},
|
},
|
||||||
fr: {
|
fr: {
|
||||||
locale: 'fr-FR',
|
locale: 'fr-FR',
|
||||||
@@ -250,6 +254,10 @@ const I18N = {
|
|||||||
hDivision: (s) => `intervalle ${s}`, hCapped: 'élargi',
|
hDivision: (s) => `intervalle ${s}`, hCapped: 'élargi',
|
||||||
hCappedTitle: 'La division choisie dépasserait 300 intervalles sur cette plage : elle a été élargie.',
|
hCappedTitle: 'La division choisie dépasserait 300 intervalles sur cette plage : elle a été élargie.',
|
||||||
hUnzoom: '× Annuler le zoom', unitDay: 'j',
|
hUnzoom: '× Annuler le zoom', unitDay: 'j',
|
||||||
|
colRcv: 'Réception', colMt: 'Heure message', colSev: 'Sévérité', colHost: 'Hôte', colApp: 'App', colMsg: 'Message',
|
||||||
|
colGrip: 'Glisser pour redimensionner, double-clic pour la largeur automatique',
|
||||||
|
resetCols: 'Réinitialiser les colonnes', colsReset: 'Largeurs de colonnes réinitialisées',
|
||||||
|
colsHelp: 'Glissez le bord d\'un en-tête de colonne de la liste pour la redimensionner (mémorisé par ce navigateur).',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -762,6 +770,7 @@ function renderList() {
|
|||||||
addFacetValues(state.rows);
|
addFacetValues(state.rows);
|
||||||
list.innerHTML = state.rows.map((r) => rowHTML(r, false)).join('');
|
list.innerHTML = state.rows.map((r) => rowHTML(r, false)).join('');
|
||||||
bindRows(list.firstElementChild, state.rows);
|
bindRows(list.firstElementChild, state.rows);
|
||||||
|
$('#table').hidden = !state.rows.length;
|
||||||
$('#empty').hidden = state.rows.length > 0 || !$('#error').hidden;
|
$('#empty').hidden = state.rows.length > 0 || !$('#error').hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -778,6 +787,7 @@ function prependRows(rows) {
|
|||||||
list.insertAdjacentHTML('afterbegin', rows.map((r) => rowHTML(r, true)).join(''));
|
list.insertAdjacentHTML('afterbegin', rows.map((r) => rowHTML(r, true)).join(''));
|
||||||
bindRows(list.firstElementChild, rows);
|
bindRows(list.firstElementChild, rows);
|
||||||
state.rows = rows.concat(state.rows);
|
state.rows = rows.concat(state.rows);
|
||||||
|
$('#table').hidden = false;
|
||||||
while (state.rows.length > MAX_ROWS) {
|
while (state.rows.length > MAX_ROWS) {
|
||||||
state.rows.pop();
|
state.rows.pop();
|
||||||
list.lastElementChild?.remove();
|
list.lastElementChild?.remove();
|
||||||
@@ -858,6 +868,66 @@ list.addEventListener('keydown', (ev) => {
|
|||||||
if (ev.key === 'Enter' && ev.target.classList.contains('row')) ev.target.click();
|
if (ev.key === 'Enter' && ev.target.classList.contains('row')) ev.target.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ================= Column widths ================= */
|
||||||
|
|
||||||
|
// Resizable columns of the log list: width in px per column, remembered per
|
||||||
|
// browser (logstream.cols.<name>); no stored width = automatic default (CSS).
|
||||||
|
const COLS = { rcv: [60, 420], mt: [60, 420], sev: [36, 200], host: [50, 600], app: [40, 600] }; // [min, max]
|
||||||
|
const clampCol = (k, w) => Math.min(COLS[k][1], Math.max(COLS[k][0], Math.round(w)));
|
||||||
|
|
||||||
|
function applyColWidths() {
|
||||||
|
const table = $('#table');
|
||||||
|
for (const k of Object.keys(COLS)) {
|
||||||
|
const w = parseInt(store.get('cols.' + k, ''), 10);
|
||||||
|
if (w > 0) table.style.setProperty('--col-' + k, clampCol(k, w) + 'px');
|
||||||
|
else table.style.removeProperty('--col-' + k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#listHead').addEventListener('pointerdown', (ev) => {
|
||||||
|
const grip = ev.target.closest('.grip');
|
||||||
|
if (!grip || ev.button !== 0) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
const k = grip.parentElement.dataset.col;
|
||||||
|
const x0 = ev.clientX;
|
||||||
|
const w0 = grip.parentElement.getBoundingClientRect().width;
|
||||||
|
const table = $('#table');
|
||||||
|
grip.setPointerCapture(ev.pointerId);
|
||||||
|
grip.classList.add('active');
|
||||||
|
document.body.classList.add('col-resizing');
|
||||||
|
const move = (e) => table.style.setProperty('--col-' + k, clampCol(k, w0 + e.clientX - x0) + 'px');
|
||||||
|
const end = () => {
|
||||||
|
grip.removeEventListener('pointermove', move);
|
||||||
|
grip.removeEventListener('pointerup', end);
|
||||||
|
grip.removeEventListener('pointercancel', end);
|
||||||
|
grip.classList.remove('active');
|
||||||
|
document.body.classList.remove('col-resizing');
|
||||||
|
const w = parseInt(table.style.getPropertyValue('--col-' + k), 10);
|
||||||
|
if (w > 0 && w !== Math.round(w0)) store.set('cols.' + k, String(w));
|
||||||
|
};
|
||||||
|
grip.addEventListener('pointermove', move);
|
||||||
|
grip.addEventListener('pointerup', end);
|
||||||
|
grip.addEventListener('pointercancel', end);
|
||||||
|
});
|
||||||
|
// Double-click on a handle: back to the automatic width.
|
||||||
|
$('#listHead').addEventListener('dblclick', (ev) => {
|
||||||
|
const grip = ev.target.closest('.grip');
|
||||||
|
if (!grip) return;
|
||||||
|
store.set('cols.' + grip.parentElement.dataset.col, '');
|
||||||
|
applyColWidths();
|
||||||
|
});
|
||||||
|
$('#resetCols').addEventListener('click', () => {
|
||||||
|
for (const k of Object.keys(COLS)) store.set('cols.' + k, '');
|
||||||
|
applyColWidths();
|
||||||
|
toast(t('colsReset'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// The header sticks right under the top bar, whose height changes with the layout.
|
||||||
|
new ResizeObserver(([e]) => {
|
||||||
|
document.documentElement.style.setProperty('--topbar-h', Math.ceil(e.target.getBoundingClientRect().height) + 'px');
|
||||||
|
}).observe($('.topbar'));
|
||||||
|
applyColWidths();
|
||||||
|
|
||||||
/* ================= Timeline (histogram) ================= */
|
/* ================= Timeline (histogram) ================= */
|
||||||
|
|
||||||
// Settings > Interface > Timeline, remembered per browser; the first value is the default.
|
// Settings > Interface > Timeline, remembered per browser; the first value is the default.
|
||||||
|
|||||||
@@ -95,7 +95,17 @@
|
|||||||
<div id="error" class="error-banner" hidden></div>
|
<div id="error" class="error-banner" hidden></div>
|
||||||
<button id="newPill" class="pill" type="button" hidden></button>
|
<button id="newPill" class="pill" type="button" hidden></button>
|
||||||
|
|
||||||
|
<div id="table" class="table" hidden>
|
||||||
|
<div id="listHead" class="list-head" role="presentation">
|
||||||
|
<span data-col="rcv"><span class="lbl" data-i18n="colRcv">Received</span><i class="grip" data-i18n-title="colGrip"></i></span>
|
||||||
|
<span data-col="mt"><span class="lbl" data-i18n="colMt">Message time</span><i class="grip" data-i18n-title="colGrip"></i></span>
|
||||||
|
<span data-col="sev"><span class="lbl" data-i18n="colSev">Severity</span><i class="grip" data-i18n-title="colGrip"></i></span>
|
||||||
|
<span data-col="host"><span class="lbl" data-i18n="colHost">Host</span><i class="grip" data-i18n-title="colGrip"></i></span>
|
||||||
|
<span data-col="app"><span class="lbl" data-i18n="colApp">App</span><i class="grip" data-i18n-title="colGrip"></i></span>
|
||||||
|
<span data-col="msg"><span class="lbl" data-i18n="colMsg">Message</span></span>
|
||||||
|
</div>
|
||||||
<main id="list" class="list"></main>
|
<main id="list" class="list"></main>
|
||||||
|
</div>
|
||||||
<div id="empty" class="empty" hidden>
|
<div id="empty" class="empty" hidden>
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h10M4 18h7"/></svg>
|
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h10M4 18h7"/></svg>
|
||||||
<p><strong data-i18n="emptyTitle">No matching logs.</strong></p>
|
<p><strong data-i18n="emptyTitle">No matching logs.</strong></p>
|
||||||
@@ -248,6 +258,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="fontPreview" class="list preview-list" aria-hidden="true"></div>
|
<div id="fontPreview" class="list preview-list" aria-hidden="true"></div>
|
||||||
<p class="muted small" data-i18n="fontHelp"></p>
|
<p class="muted small" data-i18n="fontHelp"></p>
|
||||||
|
<div class="cols-row">
|
||||||
|
<button id="resetCols" class="btn ghost" type="button" data-i18n="resetCols">Reset column widths</button>
|
||||||
|
<span class="muted small" data-i18n="colsHelp"></span>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="set-section">
|
<section class="set-section">
|
||||||
<h3 data-i18n="histoTitle">Timeline</h3>
|
<h3 data-i18n="histoTitle">Timeline</h3>
|
||||||
|
|||||||
+42
-4
@@ -296,6 +296,39 @@ body.busy .progress::after {
|
|||||||
.list { margin: 6px 20px 0; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
.list { margin: 6px 20px 0; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
||||||
.list:empty { display: none; }
|
.list:empty { display: none; }
|
||||||
|
|
||||||
|
/* Columns: received, message time, severity, host, app, message. Widths come from
|
||||||
|
--col-* (set on #table when a column has been resized, see app.js), shared by
|
||||||
|
the header and every row through subgrid so that the columns line up. */
|
||||||
|
.table {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: var(--col-rcv, max-content) var(--col-mt, max-content) var(--col-sev, 4.6rem)
|
||||||
|
var(--col-host, minmax(5rem, 9rem)) var(--col-app, minmax(4rem, 8rem)) minmax(0, 1fr);
|
||||||
|
column-gap: 12px;
|
||||||
|
margin: 6px 20px 0; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
overflow: clip; /* rounded corners without breaking the sticky header (hidden would) */
|
||||||
|
}
|
||||||
|
.table[hidden] { display: none; }
|
||||||
|
.table .list {
|
||||||
|
display: grid; grid-template-columns: subgrid; grid-column: 1 / -1;
|
||||||
|
margin: 0; border: 0; border-radius: 0; background: none; overflow: visible;
|
||||||
|
}
|
||||||
|
.table .row { grid-template-columns: subgrid; grid-column: 1 / -1; }
|
||||||
|
.list-head {
|
||||||
|
display: grid; grid-template-columns: subgrid; grid-column: 1 / -1;
|
||||||
|
position: sticky; top: var(--topbar-h, 0px); z-index: 5;
|
||||||
|
padding: 0 14px 0 11px; border-left: 3px solid transparent;
|
||||||
|
background: var(--panel-2); border-bottom: 1px solid var(--border);
|
||||||
|
font-size: 10.5px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; color: var(--muted);
|
||||||
|
}
|
||||||
|
.list-head > span { position: relative; min-width: 0; padding: 4px 0; }
|
||||||
|
/* Labels never widen a column (contain: inline-size), they are cut instead */
|
||||||
|
.list-head .lbl { display: block; contain: inline-size; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: inherit; }
|
||||||
|
.list-head .grip { position: absolute; top: 0; bottom: 0; right: -9px; width: 6px; z-index: 1; cursor: col-resize; touch-action: none; }
|
||||||
|
.list-head .grip::after { content: ""; position: absolute; left: 2px; top: 25%; bottom: 25%; border-left: 1px solid var(--border); }
|
||||||
|
.list-head .grip:hover::after, .list-head .grip.active::after { top: 0; bottom: 0; border-left: 2px solid var(--accent); left: 2px; }
|
||||||
|
body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-select: none; -webkit-user-select: none; }
|
||||||
|
.cols-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; margin-top: 12px; }
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: max-content max-content 4.6rem minmax(5rem, 9rem) minmax(4rem, 8rem) 1fr;
|
grid-template-columns: max-content max-content 4.6rem minmax(5rem, 9rem) minmax(4rem, 8rem) 1fr;
|
||||||
@@ -313,7 +346,8 @@ body.busy .progress::after {
|
|||||||
.row.new { animation: flash 1.2s ease-out; }
|
.row.new { animation: flash 1.2s ease-out; }
|
||||||
@keyframes flash { from { background: var(--accent-soft); } to { background: transparent; } }
|
@keyframes flash { from { background: var(--accent-soft); } to { background: transparent; } }
|
||||||
|
|
||||||
.row time { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
.row > * { min-width: 0; }
|
||||||
|
.row time { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.row time.rcv { color: var(--text); }
|
.row time.rcv { color: var(--text); }
|
||||||
.row time .ms { opacity: .6; }
|
.row time .ms { opacity: .6; }
|
||||||
.row .host, .row .app { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.row .host, .row .app { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
@@ -322,7 +356,7 @@ body.busy .progress::after {
|
|||||||
.row .msg { white-space: pre-wrap; word-break: break-word; min-width: 0; }
|
.row .msg { white-space: pre-wrap; word-break: break-word; min-width: 0; }
|
||||||
|
|
||||||
.sev {
|
.sev {
|
||||||
justify-self: start;
|
justify-self: start; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
|
||||||
font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
|
font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
|
||||||
padding: 1px 6px; border-radius: 5px;
|
padding: 1px 6px; border-radius: 5px;
|
||||||
color: var(--sev); background: color-mix(in srgb, var(--sev) 14%, transparent);
|
color: var(--sev); background: color-mix(in srgb, var(--sev) 14%, transparent);
|
||||||
@@ -593,8 +627,12 @@ input.switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2
|
|||||||
#count { flex: 1 1 50%; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
#count { flex: 1 1 50%; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.histo { padding: 0 16px 6px; }
|
.histo { padding: 0 16px 6px; }
|
||||||
.h-leg { display: none; }
|
.h-leg { display: none; }
|
||||||
.list, .error-banner { margin-left: 16px; margin-right: 16px; }
|
.list, .error-banner, .table { margin-left: 16px; margin-right: 16px; }
|
||||||
.row {
|
/* Phones: no columns, two lines per log (layout below); the widths do not apply */
|
||||||
|
.table { display: block; }
|
||||||
|
.table .list { display: block; margin: 0; }
|
||||||
|
.list-head { display: none; }
|
||||||
|
.row, .table .row {
|
||||||
grid-template-columns: auto auto 1fr;
|
grid-template-columns: auto auto 1fr;
|
||||||
grid-template-areas: "rcv sev host" "msg msg msg";
|
grid-template-areas: "rcv sev host" "msg msg msg";
|
||||||
gap: 3px 8px; padding: 8px 12px 8px 10px;
|
gap: 3px 8px; padding: 8px 12px 8px 10px;
|
||||||
|
|||||||
Reference in new issue
Block a user