Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions InfoLogger/public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,24 @@
.table-logs-header td { padding: 0.3rem 0.2rem; }

.table-logs-content { width: 100%; border-collapse: collapse; }
.table-logs-content td {}

.table-logs-header td,
.table-logs-content td { font-size: var(--log-font-size); }

td,
th { max-width: 0; /* allow ellipsis on tables */ vertical-align: top; }

.cell { line-height: var(--row-height); font-size: 1rem; padding: 0rem 0.2rem; font-weight: 100; }
.cell-bordered { border-left: 1px solid rgb(170, 170, 170); }
.cell-content { display: flex; justify-content: space-between; align-items: center; max-width: 100%; }
.cell-text {
.cell {
position: relative;
line-height: var(--row-height);
font-size: 1rem;
padding: 0rem 0.2rem;
font-weight: 100;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.cell-bordered { border-left: 1px solid rgb(170, 170, 170); }

.cell-xs { width: 2rem; }
.cell-s { width: 4rem; }
Expand All @@ -74,9 +75,14 @@ th { max-width: 0; /* allow ellipsis on tables */ vertical-align: top; }
.row-hover:hover { background-color: rgba(0, 0, 0, .075); }
.row-selected, .row-selected:hover { background-color: #007bff; color: white; }

/* context menu hint */
/* context menu hint - out of flow and unselectable so it is never part of a copied selection */
.cell-context-menu-hint {
display: none;
/* position absolutely within the cell */
position: absolute;
top: 50%;
right: 0.2rem;
transform: translateY(-50%);
font-size: 0.80rem;
font-weight: bold;
color: var(--color-black);
Expand All @@ -85,6 +91,8 @@ th { max-width: 0; /* allow ellipsis on tables */ vertical-align: top; }
padding: 0 3px;
line-height: 1;
cursor: pointer;
user-select: none;
-webkit-user-select: none;
}
.cell:hover .cell-context-menu-hint { display: block; }
.cell-context-menu-hint:hover { background-color: rgba(0, 0, 0, .15); }
Expand Down
21 changes: 10 additions & 11 deletions InfoLogger/public/log/tableLogsContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,16 @@ const cellWithContextMenu = (model, row, field, content, extraClasses = '', extr
...extraAttrs,
oncontextmenu: hasContent ? openContextMenu : null,
}, [
h('.cell-content', [
h('.cell-text', content),
hasContent && h(
'span.cell-context-menu-hint',
{
onclick: openContextMenu,
title: 'Right-click also opens this menu',
},
'⋮',
),
]),
// content sits directly in the <td> so that it can be selected/copied without new lines
content,
hasContent && h(
'span.cell-context-menu-hint',
{
onclick: openContextMenu,
title: 'Right-click also opens this menu',
},
'⋮',
),
]);
};

Expand Down
24 changes: 9 additions & 15 deletions InfoLogger/test/public/log-context-menu-mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ describe('Cell Context Menu', async () => {
}, filledRow, emptyRow);

await page.waitForFunction(() => {
const cells = Array.from(document.querySelectorAll('.cell-text'));
return cells.some((cell) => cell.textContent.trim() === 'ctx-host-01')
&& cells.some((cell) => cell.textContent.trim() === 'ctx-message-01');
const cells = Array.from(document.querySelectorAll('td.cell'));
return cells.some((cell) => cell.textContent.includes('ctx-host-01'))
&& cells.some((cell) => cell.textContent.includes('ctx-message-01'));
});
});

Expand All @@ -94,8 +94,8 @@ describe('Cell Context Menu', async () => {
describe('Menu visibility', async () => {
it('should show context menu on right-click', async () => {
await page.evaluate(() => {
const hostNameCell = Array.from(document.querySelectorAll('.cell-text'))
.find((cell) => cell.textContent.trim() === 'ctx-host-01');
const hostNameCell = Array.from(document.querySelectorAll('td.cell'))
.find((cell) => cell.textContent.includes('ctx-host-01'));
hostNameCell.dispatchEvent(new MouseEvent('contextmenu', {
bubbles: true,
cancelable: true,
Expand Down Expand Up @@ -164,8 +164,8 @@ describe('Cell Context Menu', async () => {

// Dispatch actual right-click event on the cell to trigger the context menu and row selection
await page.evaluate(() => {
const cell = Array.from(document.querySelectorAll('.cell-text'))
.find((cell) => cell.textContent.trim() === 'ctx-message-01');
const cell = Array.from(document.querySelectorAll('td.cell'))
.find((cell) => cell.textContent.includes('ctx-message-01'));
cell.dispatchEvent(new MouseEvent('contextmenu', {
bubbles: true,
cancelable: true,
Expand All @@ -183,10 +183,7 @@ describe('Cell Context Menu', async () => {
it('should not open context menu on right-click of empty cell', async () => {
await page.evaluate(() => {
const emptyCell = Array.from(document.querySelectorAll('td.cell'))
.find((cell) => {
const textEl = cell.querySelector('.cell-text');
return textEl && textEl.textContent.trim() === '';
});
.find((cell) => cell.textContent.trim() === '');
emptyCell.dispatchEvent(new MouseEvent('contextmenu', {
bubbles: true, cancelable: true, clientX: 100, clientY: 120, button: 2,
}));
Expand Down Expand Up @@ -748,10 +745,7 @@ describe('Cell Context Menu', async () => {
it('should not render hint on cells with empty content', async () => {
const emptyHints = await page.evaluate(() => {
const emptyCells = Array.from(document.querySelectorAll('td.cell'))
.filter((cell) => {
const textEl = cell.querySelector('.cell-text');
return textEl && textEl.textContent.trim() === '';
});
.filter((cell) => cell.textContent.trim() === '');
return emptyCells.filter((cell) => cell.querySelector('.cell-context-menu-hint')).length;
});

Expand Down
32 changes: 32 additions & 0 deletions InfoLogger/test/public/query-mode-mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

const assert = require('assert');
const test = require('../mocha-index');
const { injectLogs, waitForTextInElement } = require('../utils/utils');

const TEXT_FILTER_VALUE_BY_OPERATOR = {
since: '2026-01-01T00:00:00.000Z',
Expand Down Expand Up @@ -139,6 +140,37 @@ describe('Query Mode test-suite', async () => {
}
});

it('should copy multiple rows in the correct format', async () => {
await injectLogs(page, [
{ severity: 'I', message: 'info log', timestamp: Date.now() },
{ severity: 'E', message: 'error log', timestamp: Date.now() },
{ severity: 'W', message: 'warning log', timestamp: Date.now() },
]);
await waitForTextInElement(page, '.table-logs-content tbody tr:first-child', 'info log');

// select the first two rows entirely, as a user dragging across them would
const copied = await page.evaluate(() => {
const rows = document.querySelectorAll('.table-logs-content tbody tr');
const range = document.createRange();
range.setStartBefore(rows[0].querySelector('td:first-child'));
range.setEndAfter(rows[1].querySelector('td:last-child'));

const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);

// what the browser puts on the clipboard as text/plain for this selection
return selection.toString();
});

const lines = copied.split('\n').filter((line) => line.trim() !== '');

assert.strictEqual(lines.length, 2, `selection should be one line per row, got:\n${copied}`);
assert.ok(lines[0].includes('info log'), 'first line should hold the first row message');
assert.ok(lines[1].includes('error log'), 'second line should hold the second row message');
assert.ok(!copied.includes('⋮'), 'the context menu hint should not be part of the copied text');
});

describe('no-text-filter confirmation dialog', () => {
let textFilterOperators;

Expand Down
33 changes: 1 addition & 32 deletions InfoLogger/test/public/status-bar-mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

const assert = require('assert');
const test = require('../mocha-index');
const { injectLogs, waitForTextInElement } = require('../utils/utils');

/**
* Helper function to get the counts of each severity type from the status bar.
Expand All @@ -33,38 +34,6 @@ async function getSeverityCounts(page) {
});
}

/**
* Helper function to inject logs into the model and trigger a re-render.
* @param {Page} page - puppeteer page
* @param {Array<{severity: string}>} logs - array of log objects to inject
*/
async function injectLogs(page, logs) {
await page.evaluate((logs) => {
window.model.log.list = logs;
window.model.log.resetStats();
window.model.log.list.forEach((log) => window.model.log.addStats(log));
window.model.notify();
}, logs);
}

/**
* Helper to wait until an element's text includes the expected substring.
* @param {Page} page - puppeteer page
* @param {string} selector - CSS selector
* @param {string} text - substring to wait for
*/
async function waitForTextInElement(page, selector, text) {
await page.waitForFunction(
(sel, txt) => {
const el = document.querySelector(sel);
return el && el.textContent.includes(txt);
},
{ timeout: 2000 },
selector,
text,
);
}

describe('Status Bar test-suite', async () => {
const AUTOSCROLL_SELECTOR = '#status-bar-application-options label[title*="Scroll down"] input';
const INSPECTOR_SELECTOR = '#status-bar-application-options label[title*="Show details"] input';
Expand Down
50 changes: 50 additions & 0 deletions InfoLogger/test/utils/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* In applying this license CERN does not waive the privileges and immunities
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/

/**
* Helper function to inject logs into the model and trigger a re-render.
* @param {Page} page - puppeteer page
* @param {Array<{severity: string}>} logs - array of log objects to inject
*/
async function injectLogs(page, logs) {
await page.evaluate((logs) => {
window.model.log.list = logs;
window.model.log.resetStats();
window.model.log.list.forEach((log) => window.model.log.addStats(log));
window.model.notify();
}, logs);
};

/**
* Helper to wait until an element's text includes the expected substring.
* @param {Page} page - puppeteer page
* @param {string} selector - CSS selector
* @param {string} text - substring to wait for
*/
async function waitForTextInElement(page, selector, text) {
await page.waitForFunction(
(sel, txt) => {
const el = document.querySelector(sel);
return el && el.textContent.includes(txt);
},
{ timeout: 2000 },
selector,
text,
);
}

module.exports = {
injectLogs,
waitForTextInElement,
};