Skip to content
18 changes: 18 additions & 0 deletions InfoLogger/public/constants/text-filter-operators.const.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @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.
*/
export const TEXT_FILTER_OPERATORS = Object.freeze(['since', 'until', 'match', 'exclude']);
8 changes: 8 additions & 0 deletions InfoLogger/public/log/Log.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,14 @@ 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 (!window.confirm('No date or text filters set.'
+ ' This will return a large amount of data. Execute query anyway?')) {
return;
}
}

this.queryResult = RemoteData.loading();
this.notify();

Expand Down
11 changes: 11 additions & 0 deletions InfoLogger/public/logFilter/LogFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/

import { Observable } from '/js/src/index.js';
import { TEXT_FILTER_OPERATORS } from '../constants/text-filter-operators.const.js';

/**
* @typedef Criteria
Expand All @@ -22,7 +23,7 @@
*/

/**
* @typedef Criteria * @type {Array.<Criteria>}

Check warning on line 26 in InfoLogger/public/logFilter/LogFilter.js

View workflow job for this annotation

GitHub Actions / Tests on ubuntu-latest

Unexpected inline JSDoc tag. Did you mean to use {@type}, \@type, or `@type`?

Check warning on line 26 in InfoLogger/public/logFilter/LogFilter.js

View workflow job for this annotation

GitHub Actions / Tests & coverage on ubuntu-latest

Unexpected inline JSDoc tag. Did you mean to use {@type}, \@type, or `@type`?
*/

/**
Expand Down Expand Up @@ -142,10 +143,20 @@
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() {
return Object.values(this.criterias).some((criteria) =>
TEXT_FILTER_OPERATORS.some((operator) => criteria[operator]?.trim()));
}

/**
* Generates a function to filter a log passed as argument to it
* Output of function is boolean.
* @returns {Function.<WebSocketMessage, boolean>} - function to filter logs

Check warning on line 159 in InfoLogger/public/logFilter/LogFilter.js

View workflow job for this annotation

GitHub Actions / Tests on ubuntu-latest

Prefer a more specific type to `Function`

Check warning on line 159 in InfoLogger/public/logFilter/LogFilter.js

View workflow job for this annotation

GitHub Actions / Tests & coverage on ubuntu-latest

Prefer a more specific type to `Function`
*/
toStringifyFunction() {
/**
Expand Down
98 changes: 98 additions & 0 deletions InfoLogger/test/public/query-mode-mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,74 @@
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 {string} [options.textFilterOperator] - operator to set before querying
* @returns {Promise<{confirmCalls: number, postCalls: number}>}
*/
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,
textFilterOperator,
textFilterValueByOperator,
textFilterFieldByOperator,
}) => {
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 (textFilterOperator) {
window.model.log.filter.setCriteria(
textFilterFieldByOperator[textFilterOperator],
textFilterOperator,
textFilterValueByOperator[textFilterOperator],
);
}
await window.model.log.query();

return { confirmCalls, postCalls };
}, {
confirmReturn,
textFilterOperator,
textFilterValueByOperator: TEXT_FILTER_VALUE_BY_OPERATOR,
textFilterFieldByOperator: TEXT_FILTER_FIELD_BY_OPERATOR,
});

describe('Query Mode test-suite', async () => {
let page;

Expand All @@ -34,4 +102,34 @@ describe('Query Mode test-suite', async () => {
// code failed, so it is a successful test
}
});

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);
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 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}"`);
}
});
});
});
Loading