From 9f3725cda0fdc073700c5ae37fda663034eae46c Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 8 May 2026 10:08:50 +0200 Subject: [PATCH 1/6] [OGUI-1577] Warn user before querying with no text filters set Adds a method to the filter model which returns true if the user has typed a value in any of the since/until/match/exclude inputs. Before executing a query, the query handler now calls this method and shows a confirmation dialog if no text filters are set. This prevents accidentally making an unnecessary unbounded query. Severity and level filters are intentionally excluded from the check as even when selected, the query can still be very large, so they do not count as meaningful constraints. --- InfoLogger/public/log/Log.js | 11 +++++++++++ InfoLogger/public/logFilter/LogFilter.js | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/InfoLogger/public/log/Log.js b/InfoLogger/public/log/Log.js index 26f82ed7f..ffa076d86 100644 --- a/InfoLogger/public/log/Log.js +++ b/InfoLogger/public/log/Log.js @@ -325,6 +325,17 @@ export default class Log extends Observable { if (!this.model.frameworkInfo.isSuccess() || !this.model.frameworkInfo.payload.mysql.status.ok) { throw new Error('Query service is not available'); } + + if (!this.filter.hasActiveTextFilters()) { + // If the browser does not support the confirm dialog, execute the query anyway + const shouldExecuteQueryWithoutFilters = typeof window.confirm === 'function' + ? window.confirm('No date or text filters set. This will return a large amount of data. Execute query anyway?') + : true; + if (!shouldExecuteQueryWithoutFilters) { + return; + } + } + this.queryResult = RemoteData.loading(); this.notify(); diff --git a/InfoLogger/public/logFilter/LogFilter.js b/InfoLogger/public/logFilter/LogFilter.js index 502b10def..0aac1de8b 100644 --- a/InfoLogger/public/logFilter/LogFilter.js +++ b/InfoLogger/public/logFilter/LogFilter.js @@ -142,6 +142,17 @@ export default class LogFilter extends Observable { this.notify(); } + /** + * Check whether at least one text filter is set by the user. + * Only text filters use the since/until and match/exclude fields. + * @returns {boolean} true if at least one text filter has a value + */ + hasActiveTextFilters() { + const textOperators = ['since', 'until', 'match', 'exclude']; + return Object.values(this.criterias).some((criteria) => + textOperators.some((operator) => criteria[operator]?.trim())); + } + /** * Generates a function to filter a log passed as argument to it * Output of function is boolean. From d2e0ac38cc3b2e2bcac1177f4d9feb6ad627a41d Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 8 May 2026 10:22:52 +0200 Subject: [PATCH 2/6] [OGUI-1577] Add query-mode tests for no text filters confirmation dialog Adds three tests covering the confirmation dialog behaviour for the no-text-filters warning. --- InfoLogger/test/public/query-mode-mocha.js | 60 ++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/InfoLogger/test/public/query-mode-mocha.js b/InfoLogger/test/public/query-mode-mocha.js index 42888c7f9..d5e58bc14 100644 --- a/InfoLogger/test/public/query-mode-mocha.js +++ b/InfoLogger/test/public/query-mode-mocha.js @@ -17,6 +17,46 @@ const assert = require('assert'); const test = require('../mocha-index'); +/** + * Runs model.log.query() in the browser context with mocked dependencies. + * @param {Page} page - puppeteer page + * @param {object} options + * @param {boolean} options.confirmReturn - value window.confirm will return + * @param {boolean} [options.setTextFilter=false] - if true, sets a message match filter before querying + * @returns {Promise<{confirmCalls: number, postCalls: number}>} + */ +const runQueryWithMocks = (page, { confirmReturn, setTextFilter = false }) => + // Sets up mocks for confirmation dialog and post request, needs to be run in the browser context + page.evaluate(async ({ confirmReturn, setTextFilter }) => { + let confirmCalls = 0; + let postCalls = 0; + + window.confirm = () => { + confirmCalls += 1; + return confirmReturn; + }; + + window.model.loader.post = async () => { + postCalls += 1; + return { ok: true, result: { rows: [] } }; + }; + + // Mock the frameworkInfo to make the query method think the query service is available in its check + window.model.frameworkInfo = { + isSuccess: () => true, + payload: { mysql: { status: { ok: true } } }, + }; + + // Default state of filters includes no text filters + window.model.log.filter.resetCriteria(); + if (setTextFilter) { + window.model.log.filter.setCriteria('message', 'match', 'some-message'); + } + await window.model.log.query(); + + return { confirmCalls, postCalls }; + }, { confirmReturn, setTextFilter }); + describe('Query Mode test-suite', async () => { let page; @@ -34,4 +74,24 @@ describe('Query Mode test-suite', async () => { // code failed, so it is a successful test } }); + + describe('no-text-filter confirmation dialog', () => { + it('should ask for confirmation when no text filters are set and not execute query when user cancels', async () => { + const result = await runQueryWithMocks(page, { confirmReturn: false }); + assert.strictEqual(result.confirmCalls, 1); + assert.strictEqual(result.postCalls, 0); + }); + + it('should execute the query when no text filters are set but user confirms the dialog anyway', async () => { + const result = await runQueryWithMocks(page, { confirmReturn: true }); + assert.strictEqual(result.confirmCalls, 1); + assert.strictEqual(result.postCalls, 1); + }); + + it('should not ask for confirmation when at least one text filter is set', async () => { + const result = await runQueryWithMocks(page, { confirmReturn: true, setTextFilter: true }); + assert.strictEqual(result.confirmCalls, 0); + assert.strictEqual(result.postCalls, 1); + }); + }); }); From 8d738b494b793a1603af0ae146371213d0b53020 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 8 May 2026 11:12:23 +0200 Subject: [PATCH 3/6] [OGUI-1577] Remove unneeded check if confirmation dialog is supported Removed unneeded check and made if statement cleaner. --- InfoLogger/public/log/Log.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/InfoLogger/public/log/Log.js b/InfoLogger/public/log/Log.js index ffa076d86..a7f615841 100644 --- a/InfoLogger/public/log/Log.js +++ b/InfoLogger/public/log/Log.js @@ -327,11 +327,8 @@ export default class Log extends Observable { } if (!this.filter.hasActiveTextFilters()) { - // If the browser does not support the confirm dialog, execute the query anyway - const shouldExecuteQueryWithoutFilters = typeof window.confirm === 'function' - ? window.confirm('No date or text filters set. This will return a large amount of data. Execute query anyway?') - : true; - if (!shouldExecuteQueryWithoutFilters) { + if (!window.confirm('No date or text filters set.' + + ' This will return a large amount of data. Execute query anyway?')) { return; } } From 1ebf57648f3666c7062ea00a9770bc84f9ad275e Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 8 May 2026 11:22:24 +0200 Subject: [PATCH 4/6] [OGUI-1577] Extract text filter operators into constant --- InfoLogger/public/constants/text-filter-operators.const.js | 4 ++++ InfoLogger/public/logFilter/LogFilter.js | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 InfoLogger/public/constants/text-filter-operators.const.js diff --git a/InfoLogger/public/constants/text-filter-operators.const.js b/InfoLogger/public/constants/text-filter-operators.const.js new file mode 100644 index 000000000..c2ea3050b --- /dev/null +++ b/InfoLogger/public/constants/text-filter-operators.const.js @@ -0,0 +1,4 @@ +/** + * Operators used with the text filters. + */ +export const TEXT_FILTER_OPERATORS = Object.freeze(['since', 'until', 'match', 'exclude']); diff --git a/InfoLogger/public/logFilter/LogFilter.js b/InfoLogger/public/logFilter/LogFilter.js index 0aac1de8b..2410dcb00 100644 --- a/InfoLogger/public/logFilter/LogFilter.js +++ b/InfoLogger/public/logFilter/LogFilter.js @@ -13,6 +13,7 @@ */ import { Observable } from '/js/src/index.js'; +import { TEXT_FILTER_OPERATORS } from '../constants/text-filter-operators.const.js'; /** * @typedef Criteria @@ -148,9 +149,8 @@ export default class LogFilter extends Observable { * @returns {boolean} true if at least one text filter has a value */ hasActiveTextFilters() { - const textOperators = ['since', 'until', 'match', 'exclude']; return Object.values(this.criterias).some((criteria) => - textOperators.some((operator) => criteria[operator]?.trim())); + TEXT_FILTER_OPERATORS.some((operator) => criteria[operator]?.trim())); } /** From 6966d204463f4339baca0373c88f13b85ec9e15c Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 8 May 2026 11:24:55 +0200 Subject: [PATCH 5/6] [OGUI-1577] Extend confirmation dialog test to cover all text filter operators --- InfoLogger/test/public/query-mode-mocha.js | 58 ++++++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/InfoLogger/test/public/query-mode-mocha.js b/InfoLogger/test/public/query-mode-mocha.js index d5e58bc14..36fd85cad 100644 --- a/InfoLogger/test/public/query-mode-mocha.js +++ b/InfoLogger/test/public/query-mode-mocha.js @@ -17,17 +17,36 @@ const assert = require('assert'); const test = require('../mocha-index'); +const TEXT_FILTER_VALUE_BY_OPERATOR = { + since: '2026-01-01T00:00:00.000Z', + until: '2026-01-01T00:00:00.000Z', + match: 'some-message', + exclude: 'some-message', +}; + +const TEXT_FILTER_FIELD_BY_OPERATOR = { + since: 'timestamp', + until: 'timestamp', + match: 'message', + exclude: 'message', +}; + /** * Runs model.log.query() in the browser context with mocked dependencies. * @param {Page} page - puppeteer page * @param {object} options * @param {boolean} options.confirmReturn - value window.confirm will return - * @param {boolean} [options.setTextFilter=false] - if true, sets a message match filter before querying + * @param {string} [options.textFilterOperator] - operator to set before querying * @returns {Promise<{confirmCalls: number, postCalls: number}>} */ -const runQueryWithMocks = (page, { confirmReturn, setTextFilter = false }) => +const runQueryWithMocks = (page, { confirmReturn, textFilterOperator }) => // Sets up mocks for confirmation dialog and post request, needs to be run in the browser context - page.evaluate(async ({ confirmReturn, setTextFilter }) => { + page.evaluate(async ({ + confirmReturn, + textFilterOperator, + textFilterValueByOperator, + textFilterFieldByOperator, + }) => { let confirmCalls = 0; let postCalls = 0; @@ -49,13 +68,22 @@ const runQueryWithMocks = (page, { confirmReturn, setTextFilter = false }) => // Default state of filters includes no text filters window.model.log.filter.resetCriteria(); - if (setTextFilter) { - window.model.log.filter.setCriteria('message', 'match', 'some-message'); + if (textFilterOperator) { + window.model.log.filter.setCriteria( + textFilterFieldByOperator[textFilterOperator], + textFilterOperator, + textFilterValueByOperator[textFilterOperator], + ); } await window.model.log.query(); return { confirmCalls, postCalls }; - }, { confirmReturn, setTextFilter }); + }, { + confirmReturn, + textFilterOperator, + textFilterValueByOperator: TEXT_FILTER_VALUE_BY_OPERATOR, + textFilterFieldByOperator: TEXT_FILTER_FIELD_BY_OPERATOR, + }); describe('Query Mode test-suite', async () => { let page; @@ -76,6 +104,14 @@ describe('Query Mode test-suite', async () => { }); describe('no-text-filter confirmation dialog', () => { + let textFilterOperators; + + before(async () => { + ({ + TEXT_FILTER_OPERATORS: textFilterOperators, + } = await import('../../public/constants/text-filter-operators.const.js')); + }); + it('should ask for confirmation when no text filters are set and not execute query when user cancels', async () => { const result = await runQueryWithMocks(page, { confirmReturn: false }); assert.strictEqual(result.confirmCalls, 1); @@ -88,10 +124,12 @@ describe('Query Mode test-suite', async () => { assert.strictEqual(result.postCalls, 1); }); - it('should not ask for confirmation when at least one text filter is set', async () => { - const result = await runQueryWithMocks(page, { confirmReturn: true, setTextFilter: true }); - assert.strictEqual(result.confirmCalls, 0); - assert.strictEqual(result.postCalls, 1); + it('should not ask for confirmation for each active text filter operator', async () => { + for (const operator of textFilterOperators) { + const result = await runQueryWithMocks(page, { confirmReturn: true, textFilterOperator: operator }); + assert.strictEqual(result.confirmCalls, 0, `expected no confirm dialog for operator "${operator}"`); + assert.strictEqual(result.postCalls, 1, `expected query execution for operator "${operator}"`); + } }); }); }); From 912b171508df8b2d95e64883530e8b05288c4d46 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 8 May 2026 11:34:16 +0200 Subject: [PATCH 6/6] [OGUI-1577] Add missing copyright header for new constant file --- .../constants/text-filter-operators.const.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/InfoLogger/public/constants/text-filter-operators.const.js b/InfoLogger/public/constants/text-filter-operators.const.js index c2ea3050b..3aa6587f3 100644 --- a/InfoLogger/public/constants/text-filter-operators.const.js +++ b/InfoLogger/public/constants/text-filter-operators.const.js @@ -1,3 +1,17 @@ +/** + * @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. + */ + /** * Operators used with the text filters. */