From 26c79e20471765772daaf131dda6aefada8a9f37 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 29 May 2026 15:02:26 +0200 Subject: [PATCH 01/13] Improve testing for locally multiple in a row runs --- InfoLogger/test/mocha-index.js | 3 ++- InfoLogger/test/test-config.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/InfoLogger/test/mocha-index.js b/InfoLogger/test/mocha-index.js index aa2ab23b6..4082e7e2e 100644 --- a/InfoLogger/test/mocha-index.js +++ b/InfoLogger/test/mocha-index.js @@ -34,13 +34,14 @@ describe('InfoLogger', function () { let subprocess; // web-server runs into a subprocess let subprocessOutput = ''; let ilgServer; - this.timeout(30000); this.slow(1000); const baseUrl = `http://${config.http.hostname}:${config.http.port}/`; before(async () => { + await fs.copyFile(testDbSourcePath, testDbRunningPath); + // Add error handlers for uncaught errors process.on('unhandledRejection', (error) => { console.error('[Test Setup] Unhandled Promise Rejection at:', new Date().toISOString()); diff --git a/InfoLogger/test/test-config.js b/InfoLogger/test/test-config.js index 885acf844..a697de6ea 100644 --- a/InfoLogger/test/test-config.js +++ b/InfoLogger/test/test-config.js @@ -33,5 +33,5 @@ module.exports = { expiration: '60s', maxAge: '2', }, - dbFile: './test/testdb.json', + dbFile: './test/testdb-running.json', }; From 1a808f31efc92d09bb7292df1434fbc52c575623 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 5 Jun 2026 13:50:00 +0200 Subject: [PATCH 02/13] Remove complexity added to the tests --- InfoLogger/test/mocha-index.js | 2 -- InfoLogger/test/test-config.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/InfoLogger/test/mocha-index.js b/InfoLogger/test/mocha-index.js index 4082e7e2e..4c060f24b 100644 --- a/InfoLogger/test/mocha-index.js +++ b/InfoLogger/test/mocha-index.js @@ -40,8 +40,6 @@ describe('InfoLogger', function () { const baseUrl = `http://${config.http.hostname}:${config.http.port}/`; before(async () => { - await fs.copyFile(testDbSourcePath, testDbRunningPath); - // Add error handlers for uncaught errors process.on('unhandledRejection', (error) => { console.error('[Test Setup] Unhandled Promise Rejection at:', new Date().toISOString()); diff --git a/InfoLogger/test/test-config.js b/InfoLogger/test/test-config.js index a697de6ea..885acf844 100644 --- a/InfoLogger/test/test-config.js +++ b/InfoLogger/test/test-config.js @@ -33,5 +33,5 @@ module.exports = { expiration: '60s', maxAge: '2', }, - dbFile: './test/testdb-running.json', + dbFile: './test/testdb.json', }; From 5d33cdbd5169d99e727c9fe8731772300426197b Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 5 Jun 2026 14:14:31 +0200 Subject: [PATCH 03/13] Adjust HTML tags and CSS so that header fits 1 line on 13.5" screens --- InfoLogger/public/log/commandLogs.js | 76 ++++++++++--------- InfoLogger/public/logFilter/commandFilters.js | 44 +++++------ InfoLogger/public/view.js | 14 +--- 3 files changed, 62 insertions(+), 72 deletions(-) diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index df3dd3020..81d121df8 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -36,39 +36,41 @@ let liveButtonIcon = iconMediaPlay(); */ export const commandLogs = (model) => [ userActionsDropdown(model), - h('div.btn-group.mh3', interactionModesGroupButton(model)), - h('button.btn.mh3', { onclick: () => model.log.empty(), style: 'font-weight: bold' }, 'Clear'), - h('button.btn', { - disabled: !model.log.list.length, - onclick: () => model.log.firstError(), - title: 'Go to first error/fatal (ALT + left arrow)', - }, '|←'), - ' ', - h('button.btn', { - disabled: !model.log.list.length, - onclick: () => model.log.previousError(), - title: 'Go to previous error/fatal (left arrow)', - }, '←'), - ' ', - h('button.btn', { - disabled: !model.log.list.length, - onclick: () => model.log.nextError(), - title: 'Go to next error/fatal (left arrow)', - }, '→'), - ' ', - h('button.btn', { - disabled: !model.log.list.length, - onclick: () => model.log.lastError(), - title: 'Go to last error/fatal (ALT + right arrow)', - }, '→|'), - ' ', - h('button.btn', { - disabled: !model.log.list.length, - onclick: () => model.log.goToLastItem(), - title: 'Go to last log message (ALT + down arrow)', - }, '↓'), - downloadButtonGroup(model.log), - zoomButtonGroup(model.zoom), + h('', interactionModesGroupButton(model)), + h('', h('button.btn', { onclick: () => model.log.empty(), style: 'font-weight: bold' }, 'Clear')), + h('.btn-group', [ + h('button.btn', { + disabled: !model.log.list.length, + onclick: () => model.log.firstError(), + title: 'Go to first error/fatal (ALT + left arrow)', + }, '|←'), + ' ', + h('button.btn', { + disabled: !model.log.list.length, + onclick: () => model.log.previousError(), + title: 'Go to previous error/fatal (left arrow)', + }, '←'), + ' ', + h('button.btn', { + disabled: !model.log.list.length, + onclick: () => model.log.nextError(), + title: 'Go to next error/fatal (left arrow)', + }, '→'), + ' ', + h('button.btn', { + disabled: !model.log.list.length, + onclick: () => model.log.lastError(), + title: 'Go to last error/fatal (ALT + right arrow)', + }, '→|'), + ' ', + h('button.btn', { + disabled: !model.log.list.length, + onclick: () => model.log.goToLastItem(), + title: 'Go to last log message (ALT + down arrow)', + }, '↓'), + ]), + h('', downloadButtonGroup(model.log)), + h('', zoomButtonGroup(model.zoom)), ]; /** @@ -82,12 +84,12 @@ const interactionModesGroupButton = (model) => { return frameworkInfo.match({ NotAsked: () => h('button.btn', { disabled: true }, ''), Loading: () => h('button.btn', { disabled: true, className: 'loading' }, 'Loading'), - Failure: () => [], + Failure: () => null, Success: (frameworkInfo) => - [ + h('.btn-group', [ queryButton(model, frameworkInfo), liveButton(model, frameworkInfo), - ], + ]), }); }; @@ -194,7 +196,7 @@ const saveUserProfileMenuItem = (model) => */ const downloadButtonGroup = (logModel) => h('.dropdown', { class: logModel.download.isVisible ? 'dropdown-open' : '' }, [ - h('button.btn.mh3', { + h('button.btn', { onclick: () => { if (!logModel.download.isVisible) { logModel.generateLogDownloadContent(); diff --git a/InfoLogger/public/logFilter/commandFilters.js b/InfoLogger/public/logFilter/commandFilters.js index 210563363..c310e1a3d 100644 --- a/InfoLogger/public/logFilter/commandFilters.js +++ b/InfoLogger/public/logFilter/commandFilters.js @@ -24,31 +24,25 @@ import { h } from '/js/src/index.js'; * @returns {vnode} - the view of filters panel */ export default (model) => [ - h( - '', - h('.btn-group', [ - buttonSeverity(model, 'Debug', 'Match severity debug', 'D'), - buttonSeverity(model, 'Info', 'Match severity info', 'I'), - buttonSeverity(model, 'Warn', 'Match severity warnings', 'W'), - buttonSeverity(model, 'Error', 'Match severity errors', 'E'), - buttonSeverity(model, 'Fatal', 'Match severity fatal', 'F'), - ]), - h('span.mh3'), - h('.btn-group', [ - buttonFilterLevel(model, 'Ops', 1), - buttonFilterLevel(model, 'Support', 6), - buttonFilterLevel(model, 'Devel', 11), - buttonFilterLevel(model, 'Trace', null), // 21 - ]), - h('span.mh3'), - h('.btn-group', [ - buttonLogLimit(model, '100k', 100000), - buttonLogLimit(model, '500k', 500000), - buttonLogLimit(model, '1M', 1000000), - ]), - h('span.mh3'), - buttonReset(model), - ), + h('.btn-group', [ + buttonSeverity(model, 'Debug', 'Match severity debug', 'D'), + buttonSeverity(model, 'Info', 'Match severity info', 'I'), + buttonSeverity(model, 'Warn', 'Match severity warnings', 'W'), + buttonSeverity(model, 'Error', 'Match severity errors', 'E'), + buttonSeverity(model, 'Fatal', 'Match severity fatal', 'F'), + ]), + h('.btn-group', [ + buttonFilterLevel(model, 'Ops', 1), + buttonFilterLevel(model, 'Support', 6), + buttonFilterLevel(model, 'Devel', 11), + buttonFilterLevel(model, 'Trace', null), // 21 + ]), + h('.btn-group', [ + buttonLogLimit(model, '100k', 100000), + buttonLogLimit(model, '500k', 500000), + buttonLogLimit(model, '1M', 1000000), + ]), + buttonReset(model), ]; /** diff --git a/InfoLogger/public/view.js b/InfoLogger/public/view.js index dc45e9efa..7fd49bba9 100644 --- a/InfoLogger/public/view.js +++ b/InfoLogger/public/view.js @@ -36,19 +36,13 @@ export default (model) => [ cellContextMenu(model), h('.flex-column absolute-fill', [ h('.shadow-level2', [ - h('header.p1.flex-row.f7', [ - h('', commandLogs(model)), - h( - '.flex-grow', - { - style: 'display: flex; flex-direction:row-reverse;', - }, - commandFilters(model), - ), + h('header.p1.flex-row.f7.g1.justify-between', [ + h('.flex-row.g2', commandLogs(model)), + h('.flex-row.g2', commandFilters(model)), ]), h('header.f7', tableFilters(model)), ]), - h('div.flex-grow.flex-row.shadow-level0.logs-container', [ + h('.flex-grow.flex-row.shadow-level0.logs-container', [ aboutComponent(model), logsTable(model), inspectorSide(model), From fe399ebb151d432d020afb0e19900b45931f9291 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 5 Jun 2026 14:17:12 +0200 Subject: [PATCH 04/13] Remove forced spacing between arrow buttons --- InfoLogger/public/log/commandLogs.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/InfoLogger/public/log/commandLogs.js b/InfoLogger/public/log/commandLogs.js index 81d121df8..06ca8932f 100644 --- a/InfoLogger/public/log/commandLogs.js +++ b/InfoLogger/public/log/commandLogs.js @@ -44,25 +44,21 @@ export const commandLogs = (model) => [ onclick: () => model.log.firstError(), title: 'Go to first error/fatal (ALT + left arrow)', }, '|←'), - ' ', h('button.btn', { disabled: !model.log.list.length, onclick: () => model.log.previousError(), title: 'Go to previous error/fatal (left arrow)', }, '←'), - ' ', h('button.btn', { disabled: !model.log.list.length, onclick: () => model.log.nextError(), title: 'Go to next error/fatal (left arrow)', }, '→'), - ' ', h('button.btn', { disabled: !model.log.list.length, onclick: () => model.log.lastError(), title: 'Go to last error/fatal (ALT + right arrow)', }, '→|'), - ' ', h('button.btn', { disabled: !model.log.list.length, onclick: () => model.log.goToLastItem(), From 01bf121e5604d20d39c357d946195dba2e6525fa Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 5 Jun 2026 14:41:51 +0200 Subject: [PATCH 05/13] Replace in-line buttons with select component for level and limit --- InfoLogger/public/logFilter/commandFilters.js | 78 +++++++++++-------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/InfoLogger/public/logFilter/commandFilters.js b/InfoLogger/public/logFilter/commandFilters.js index c310e1a3d..18132501e 100644 --- a/InfoLogger/public/logFilter/commandFilters.js +++ b/InfoLogger/public/logFilter/commandFilters.js @@ -31,17 +31,8 @@ export default (model) => [ buttonSeverity(model, 'Error', 'Match severity errors', 'E'), buttonSeverity(model, 'Fatal', 'Match severity fatal', 'F'), ]), - h('.btn-group', [ - buttonFilterLevel(model, 'Ops', 1), - buttonFilterLevel(model, 'Support', 6), - buttonFilterLevel(model, 'Devel', 11), - buttonFilterLevel(model, 'Trace', null), // 21 - ]), - h('.btn-group', [ - buttonLogLimit(model, '100k', 100000), - buttonLogLimit(model, '500k', 500000), - buttonLogLimit(model, '1M', 1000000), - ]), + selectFilterLevel(model.log), + selectLogLimit(model.log), buttonReset(model), ]; @@ -67,30 +58,55 @@ const buttonSeverity = (model, label, title, value) => { }; /** - * Makes a button to set filtering level (shifter, debug, etc) with number - * @param {Model} model - root model of the application - * @param {string} label - button's label - * @param {number} value - maximum level of filtering, from 1 to 21 - * @returns {vnode} - component representing the creation of a button for filtering + * Build a vnode for a select component with given options and onchange callback + * @param {string} title - tooltip shown on hover + * @param {string} id - id of the element + * @param {Array<{label: string, value: string, selected: boolean}>} options - list of options to render + * @param {void} onchange - callback receiving the raw string value from the select + * @returns {vnode} - component representing the select element */ -const buttonFilterLevel = (model, label, value) => h('button.btn', { - className: model.log.filter.criterias.level.max === value ? 'active' : '', - onclick: () => model.log.setCriteria('level', 'max', value), - title: `Filter level ≤ ${value}`, -}, label); +const selectBtn = (title, id, options, onchange) => h( + 'select.select-btn', + { + id, + title, + onchange: (e) => onchange(e.target.value), + }, + options.map(({ label, value, selected }) => h('option', { value, selected }, label)), +); /** - * Makes a button to set log limit, maximum logs in memory - * @param {Model} model - root model of the application - * @param {string} label - button's label - * @param {number} limit - how much logs to keep in memory - * @returns {vnode} - component representing the creation of a button for log limit + * Makes a select to set filtering level (Ops, Support, Devel, Trace) + * @param {Log} logModel - log model of the application + * @returns {vnode} - component representing the selection of a log level filter + */ +const selectFilterLevel = (logModel) => selectBtn( + 'Filter by log level', + 'filter-level', + [ + { label: 'Ops', value: '1', selected: logModel.filter.criterias.level.max === 1 }, + { label: 'Support', value: '6', selected: logModel.filter.criterias.level.max === 6 }, + { label: 'Devel', value: '11', selected: logModel.filter.criterias.level.max === 11 }, + { label: 'Trace', value: '', selected: logModel.filter.criterias.level.max === null }, + ], + (value) => logModel.setCriteria('level', 'max', value === '' ? null : parseInt(value, 10)), +); + +/** + * Makes a select to set the maximum number of logs to keep in memory + * @param {Log} logModel - log model of the application + * @returns {vnode} - component representing the selection of a log limit */ -const buttonLogLimit = (model, label, limit) => h('button.btn', { - className: model.log.limit === limit ? 'active' : '', - onclick: () => model.log.setLimit(limit), - title: `Keep only ${label} logs in the view`, -}, label); +const selectLogLimit = (logModel) => selectBtn( + 'Maximum logs to keep in the view', + 'log-limit', + [ + { label: '100k', value: '100000', selected: logModel.limit === 100000 }, + { label: '500k', value: '500000', selected: logModel.limit === 500000 }, + { label: '1M', value: '1000000', selected: logModel.limit === 1000000 }, + ], + (value) => logModel.setLimit(parseInt(value, 10)), +); /** * Makes a button to reset filters From a37c6bbf7811616d2e6aab63e369f758031ae95d Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 5 Jun 2026 14:42:02 +0200 Subject: [PATCH 06/13] Add styling for select to be similar to existing buttons --- InfoLogger/public/app.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InfoLogger/public/app.css b/InfoLogger/public/app.css index 5a76d735c..5a39bc785 100644 --- a/InfoLogger/public/app.css +++ b/InfoLogger/public/app.css @@ -124,6 +124,9 @@ footer { border-top: 1px solid var(--color-gray); } .btn:hover { background-color: #a0a0a0; color: var(--color-white); } .btn:active, .btn.active, .dropdown-open > .btn { background-color: #c0c0c0; } +.select-btn { background-color: #e0e0e0; color: #404040; border: 0; border-radius: .25rem; padding: 0em 0.5em; font-size: 1em; font-family: inherit; cursor: pointer; } +.select-btn:hover { background-color: #a0a0a0; color: var(--color-white); } + .text-area-for-message:focus { width: 50%; height: 10rem !important; right: 0; position: absolute; } a.disabled { pointer-events: none; cursor: default; } From 715a50029a3b18e4a169ff8fc38a7960e6f5faed Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 5 Jun 2026 14:45:05 +0200 Subject: [PATCH 07/13] Add tests for new select component for limit and level --- .../test/public/log-filter-actions-mocha.js | 148 +++++++++++++----- 1 file changed, 113 insertions(+), 35 deletions(-) diff --git a/InfoLogger/test/public/log-filter-actions-mocha.js b/InfoLogger/test/public/log-filter-actions-mocha.js index 32a4afa87..cf71d231e 100644 --- a/InfoLogger/test/public/log-filter-actions-mocha.js +++ b/InfoLogger/test/public/log-filter-actions-mocha.js @@ -10,9 +10,8 @@ * 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. -*/ + */ -/* eslint-disable max-len */ const assert = require('assert'); const test = require('../mocha-index'); @@ -26,8 +25,8 @@ describe('Filter actions test-suite', async () => { }); // "physicist" is not a distinct stored profile; the server returns defaultCriterias for any name - it('should succesfully load a page with profile in the URI', async function() { - await page.goto(baseUrl + "?profile=physicist", {waitUntil: 'networkidle0'}); + it('should succesfully load a page with profile in the URI', async () => { + await page.goto(`${baseUrl}?profile=physicist`, { waitUntil: 'networkidle0' }); const location = await page.evaluate(() => window.location); const search = decodeURIComponent(location.search); @@ -37,60 +36,58 @@ describe('Filter actions test-suite', async () => { it('should update column headers based on profile when passed in the URI', async () => { const expectedColumns = { - date: {size: 'cell-m', visible: false}, - time: {size: 'cell-m', visible: true}, - hostname: {size: 'cell-m', visible: true}, - rolename: {size: 'cell-m', visible: false}, - pid: {size: 'cell-s', visible: false}, - username: {size: 'cell-m', visible: false}, - system: {size: 'cell-s', visible: true}, - facility: {size: 'cell-m', visible: true}, - detector: {size: 'cell-s', visible: true}, - partition: {size: 'cell-m', visible: true}, - run: {size: 'cell-s', visible: true}, - errcode: {size: 'cell-s', visible: false}, - errline: {size: 'cell-s', visible: false}, - errsource: {size: 'cell-m', visible: false}, - message: {size: 'cell-xl', visible: true} + date: { size: 'cell-m', visible: false }, + time: { size: 'cell-m', visible: true }, + hostname: { size: 'cell-m', visible: true }, + rolename: { size: 'cell-m', visible: false }, + pid: { size: 'cell-s', visible: false }, + username: { size: 'cell-m', visible: false }, + system: { size: 'cell-s', visible: true }, + facility: { size: 'cell-m', visible: true }, + detector: { size: 'cell-s', visible: true }, + partition: { size: 'cell-m', visible: true }, + run: { size: 'cell-s', visible: true }, + errcode: { size: 'cell-s', visible: false }, + errline: { size: 'cell-s', visible: false }, + errsource: { size: 'cell-m', visible: false }, + message: { size: 'cell-xl', visible: true }, }; - const columns = await page.evaluate(() => { - return window.model.table.colsHeader; - }); + const columns = await page.evaluate(() => window.model.table.colsHeader); assert.deepStrictEqual(columns, expectedColumns); }); it('should update filters based on profile when passed in the URI', async () => { - // for now check if the filters are reset once the profile is passed + // for now check if the filters are reset once the profile is passed const expectedParams = '?q={%22severity%22:{%22in%22:%22I%20W%20E%20F%22},%22level%22:{%22max%22:1}}'; const searchParams = await page.evaluate(() => { - const params = {profile: 'physicist'}; + const params = { profile: 'physicist' }; window.model.parseLocation(params); return window.location.search; }); - await page.waitForFunction(`window.model.notification.state === 'shown'`); - await page.waitForFunction(`window.model.notification.type === 'success'`); - await page.waitForFunction(`window.model.notification.message === "The profile PHYSICIST was loaded successfully"`); + await page.waitForFunction('window.model.notification.state === \'shown\''); + await page.waitForFunction('window.model.notification.type === \'success\''); + await page.waitForFunction('window.model.notification.message === "The profile PHYSICIST was loaded successfully"'); assert.strictEqual(searchParams, expectedParams); }); it('should reset filters and show warning message when profile and filters are passed', async () => { // wait until the previous notification is hidden - await page.waitForFunction(`window.model.notification.state === 'hidden'`); + await page.waitForFunction('window.model.notification.state === \'hidden\''); const expectedParams = '?q={%22severity%22:{%22in%22:%22I%20W%20E%20F%22},%22level%22:{%22max%22:1}}'; const searchParams = await page.evaluate(() => { - const params = {profile: "physicist", q: '"severity":{"in":"I W E F"}}'}; + const params = { profile: 'physicist', q: '"severity":{"in":"I W E F"}}' }; window.model.parseLocation(params); return window.location.search; }); - await page.waitForFunction(`window.model.notification.state === 'shown'`); - await page.waitForFunction(`window.model.notification.type === 'warning'`); - await page.waitForFunction(`window.model.notification.message === "URL can contain only filters or profile, not both"`); + await page.waitForFunction('window.model.notification.state === \'shown\''); + await page.waitForFunction('window.model.notification.type === \'warning\''); + await page.waitForFunction('window.model.notification.message === "URL can contain only filters or profile, not both"'); assert.strictEqual(searchParams, expectedParams); }); @@ -111,11 +108,11 @@ describe('Filter actions test-suite', async () => { // CI/CD runs on Chromium so this assertion is based on Chromium's JSON engine's error message assert.strictEqual( locationAndNotification.notification.message, - 'Invalid URL filter format: Expected \',\' or \'}\' after property value in JSON at position 27 (line 1 column 28)'); + 'Invalid URL filter format: Expected \',\' or \'}\' after property value in JSON at position 27 (line 1 column 28)', + ); }); it('should update URI with new encoded "match" criteria', async () => { - /* eslint-disable max-len */ const decodedParams = '?q={"hostname":{"match":"\\"%ald_qdip01%"},"severity":{"in":"I W E F"}}'; const expectedParams = '?q={%22hostname%22:{%22match%22:%22%5C%22%25ald_qdip01%25%22},%22severity%22:{%22in%22:%22I%20W%20E%20F%22}}'; const searchParams = await page.evaluate(() => { @@ -129,7 +126,6 @@ describe('Filter actions test-suite', async () => { }); it('should update URI with new encoded "exclude" criteria', async () => { - /* eslint-disable max-len */ const decodedParams = '?q={"hostname":{"exclude":"\\"%ald_qdip01%"},"severity":{"in":"I W E F"}}'; const expectedParams = '?q={%22hostname%22:{%22exclude%22:%22%5C%22%25ald_qdip01%25%22},%22severity%22:{%22in%22:%22I%20W%20E%20F%22}}'; const searchParams = await page.evaluate(() => { @@ -310,4 +306,86 @@ describe('Filter actions test-suite', async () => { }); }); }); + + describe('Level filter select', async () => { + it('should build a select components options for level to filter by', async () => { + const options = await page.evaluate(() => { + const select = document.getElementById('filter-level'); + return select ? Array.from(select.options) + .map((element) => ({ value: element.value, text: element.text })) : null; + }); + + assert.deepStrictEqual(options, [ + { value: '1', text: 'Ops' }, + { value: '6', text: 'Support' }, + { value: '11', text: 'Devel' }, + { value: '', text: 'Trace' }, + ]); + }); + + it('should mark the currently active level option as selected', async () => { + await page.evaluate(() => model.log.filter.setCriteria('level', 'max', 6)); + await page.waitForFunction(() => document.getElementById('filter-level')?.value === '6'); + + const selectedValue = await page.evaluate(() => document.getElementById('filter-level')?.value); + assert.strictEqual(selectedValue, '6'); + }); + + it('should update level criteria when an option is selected', async () => { + const level = await page.evaluate(() => { + const select = document.getElementById('filter-level'); + select.value = '11'; + select.dispatchEvent(new Event('change')); + return window.model.log.filter.criterias.level.max; + }); + + assert.strictEqual(level, 11); + }); + + it('should set level to null when Trace option is selected', async () => { + const level = await page.evaluate(() => { + const select = document.getElementById('filter-level'); + select.value = ''; + select.dispatchEvent(new Event('change')); + return window.model.log.filter.criterias.level.max; + }); + + assert.strictEqual(level, null); + }); + }); + + describe('Log limit select', async () => { + it('should display a select component for limit to filter by', async () => { + const options = await page.evaluate(() => { + const select = document.getElementById('log-limit'); + return select ? Array.from(select.options) + .map((element) => ({ value: element.value, text: element.text })) : null; + }); + + assert.deepStrictEqual(options, [ + { value: '100000', text: '100k' }, + { value: '500000', text: '500k' }, + { value: '1000000', text: '1M' }, + ]); + }); + + it('should mark the currently active limit option as selected', async () => { + await page.evaluate(() => window.model.log.setLimit(500000)); + await page.waitForFunction(() => document.getElementById('log-limit')?.value === '500000'); + + const selectedValue = await page.evaluate(() => document.getElementById('log-limit')?.value); + assert.strictEqual(selectedValue, '500000'); + }); + + it('should update log limit when an option is selected', async () => { + const limit = await page.evaluate(() => { + const select = document.getElementById('log-limit'); + select.value = '1000000'; + select.dispatchEvent(new Event('change')); + return window.model.log.limit; + }); + + assert.strictEqual(limit, 1000000); + }); + }); }); From 186ac738460df938dc245672a06b2f245531923c Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 5 Jun 2026 14:48:06 +0200 Subject: [PATCH 08/13] Add a bit more spacing --- InfoLogger/public/view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InfoLogger/public/view.js b/InfoLogger/public/view.js index 7fd49bba9..33cf83426 100644 --- a/InfoLogger/public/view.js +++ b/InfoLogger/public/view.js @@ -37,8 +37,8 @@ export default (model) => [ h('.flex-column absolute-fill', [ h('.shadow-level2', [ h('header.p1.flex-row.f7.g1.justify-between', [ - h('.flex-row.g2', commandLogs(model)), - h('.flex-row.g2', commandFilters(model)), + h('.flex-row.g3', commandLogs(model)), + h('.flex-row.g3', commandFilters(model)), ]), h('header.f7', tableFilters(model)), ]), From 62f7395329ba6211a6c8cce1d3b3fb2e930e9b1a Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 5 Jun 2026 14:59:09 +0200 Subject: [PATCH 09/13] Add label to describe the options list --- InfoLogger/public/logFilter/commandFilters.js | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/InfoLogger/public/logFilter/commandFilters.js b/InfoLogger/public/logFilter/commandFilters.js index 18132501e..2f7f8dc2d 100644 --- a/InfoLogger/public/logFilter/commandFilters.js +++ b/InfoLogger/public/logFilter/commandFilters.js @@ -63,17 +63,24 @@ const buttonSeverity = (model, label, title, value) => { * @param {string} id - id of the element * @param {Array<{label: string, value: string, selected: boolean}>} options - list of options to render * @param {void} onchange - callback receiving the raw string value from the select + * @param {string} [optGroupLabel] - optional non-selectable group header shown inside the dropdown * @returns {vnode} - component representing the select element */ -const selectBtn = (title, id, options, onchange) => h( - 'select.select-btn', - { - id, - title, - onchange: (e) => onchange(e.target.value), - }, - options.map(({ label, value, selected }) => h('option', { value, selected }, label)), -); +const selectBtn = (title, id, options, onchange, optGroupLabel = null) => { + const optionsMap = options.map(({ label, value, selected }) => h('option', { value, selected }, label)); + return h( + 'select.select-btn', + { + id, + title, + onchange: (e) => onchange(e.target.value), + }, + optGroupLabel + ? h('optgroup', { label: optGroupLabel }, optionsMap) + : optionsMap, + + ); +}; /** * Makes a select to set filtering level (Ops, Support, Devel, Trace) @@ -90,6 +97,7 @@ const selectFilterLevel = (logModel) => selectBtn( { label: 'Trace', value: '', selected: logModel.filter.criterias.level.max === null }, ], (value) => logModel.setCriteria('level', 'max', value === '' ? null : parseInt(value, 10)), + 'Log Level', ); /** @@ -106,6 +114,7 @@ const selectLogLimit = (logModel) => selectBtn( { label: '1M', value: '1000000', selected: logModel.limit === 1000000 }, ], (value) => logModel.setLimit(parseInt(value, 10)), + 'Buffer Limit', ); /** From 090e372389c82a640c844b78d066e51fab9f3cbc Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 5 Jun 2026 15:03:27 +0200 Subject: [PATCH 10/13] Remove forgotten extra new line --- InfoLogger/public/logFilter/commandFilters.js | 1 - 1 file changed, 1 deletion(-) diff --git a/InfoLogger/public/logFilter/commandFilters.js b/InfoLogger/public/logFilter/commandFilters.js index 2f7f8dc2d..1fd7245e3 100644 --- a/InfoLogger/public/logFilter/commandFilters.js +++ b/InfoLogger/public/logFilter/commandFilters.js @@ -78,7 +78,6 @@ const selectBtn = (title, id, options, onchange, optGroupLabel = null) => { optGroupLabel ? h('optgroup', { label: optGroupLabel }, optionsMap) : optionsMap, - ); }; From 25da966bfe59d8d06d85d81b688281880bb78bfb Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 5 Jun 2026 15:04:20 +0200 Subject: [PATCH 11/13] Remove unintended new line --- InfoLogger/test/mocha-index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/InfoLogger/test/mocha-index.js b/InfoLogger/test/mocha-index.js index 4c060f24b..aa2ab23b6 100644 --- a/InfoLogger/test/mocha-index.js +++ b/InfoLogger/test/mocha-index.js @@ -34,6 +34,7 @@ describe('InfoLogger', function () { let subprocess; // web-server runs into a subprocess let subprocessOutput = ''; let ilgServer; + this.timeout(30000); this.slow(1000); From 10df052e908167306d9b355adb2f53e83e4b1cc0 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 5 Jun 2026 16:26:25 +0200 Subject: [PATCH 12/13] Add variables for buttons to share hex values --- InfoLogger/public/app.css | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/InfoLogger/public/app.css b/InfoLogger/public/app.css index 5a39bc785..d6e22f641 100644 --- a/InfoLogger/public/app.css +++ b/InfoLogger/public/app.css @@ -14,6 +14,11 @@ /* make the sidebar big enough to contain inspector */ +:root { + --color-gray-button-background-hover: #a0a0a0; + --color-gray-button-background: #e0e0e0; + --color-gray-button: #404040; +} .sidebar, .sidebar .sidebar-content { width: 20rem; } @@ -120,12 +125,12 @@ footer { border-top: 1px solid var(--color-gray); } .query-item:active { background-color: var(--color-gray-dark); color: var(--color-black); } .query-item.selected { background-color: var(--color-primary); color: var(--color-white); } -.btn { background-color: #e0e0e0; color: #404040; } -.btn:hover { background-color: #a0a0a0; color: var(--color-white); } -.btn:active, .btn.active, .dropdown-open > .btn { background-color: #c0c0c0; } +.btn { background-color: var(--color-gray-button-background); color: var(--color-gray-button); } +.btn:hover { background-color: var(--color-gray-button-background-hover); color: var(--color-white); } +.btn:active, .btn.active, .dropdown-open > .btn { background-color: var(--color-gray-button-active); } -.select-btn { background-color: #e0e0e0; color: #404040; border: 0; border-radius: .25rem; padding: 0em 0.5em; font-size: 1em; font-family: inherit; cursor: pointer; } -.select-btn:hover { background-color: #a0a0a0; color: var(--color-white); } +.select-btn { background-color: var(--color-gray-button-background); color: var(--color-gray-button); border: 0; border-radius: .25rem; padding: 0em 0.5em; font-size: 1em; font-family: inherit; cursor: pointer; } +.select-btn:hover { background-color: var(--color-gray-button-background-hover); color: var(--color-white); } .text-area-for-message:focus { width: 50%; height: 10rem !important; right: 0; position: absolute; } a.disabled { pointer-events: none; cursor: default; } From 3f0c06d5db9f5298b3e44cb8adaecffecabcd407 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 5 Jun 2026 16:41:59 +0200 Subject: [PATCH 13/13] Add forgotten color variable --- InfoLogger/public/app.css | 1 + 1 file changed, 1 insertion(+) diff --git a/InfoLogger/public/app.css b/InfoLogger/public/app.css index d6e22f641..9d2e934a9 100644 --- a/InfoLogger/public/app.css +++ b/InfoLogger/public/app.css @@ -18,6 +18,7 @@ --color-gray-button-background-hover: #a0a0a0; --color-gray-button-background: #e0e0e0; --color-gray-button: #404040; + --color-gray-button-active: #c0c0c0; } .sidebar, .sidebar .sidebar-content { width: 20rem; }