Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3f14a80
Refactor filter button components into reusable
graduta May 6, 2026
cd8c76d
Provide filters from model instead of hardcoded
graduta May 6, 2026
fdfab13
Add util function for checking if user has shifter but no admin role
graduta May 6, 2026
28efc39
On URL load, ensure shifters use only OPS level
graduta May 6, 2026
caa7eec
Fix eslint warnings in changed files
graduta May 6, 2026
9031c35
Extract access util function
graduta May 7, 2026
78c22c0
Add tests for shifter based roles
graduta May 7, 2026
b631069
Add documentation for role-based
graduta May 7, 2026
8c6e7d6
Include newly added test suite
graduta May 7, 2026
6337802
Fix eslint in mocha-index
graduta May 7, 2026
78b27f3
Improve newly added consts
graduta May 7, 2026
38e4d4b
Merge branch 'dev' into feature/ILG/OGUI-1892-disable-level-buttons-f…
graduta May 7, 2026
4ea774e
Merge branch 'dev' into feature/ILG/OGUI-1892-disable-level-buttons-f…
graduta May 8, 2026
746724d
Merge branch 'dev' of github.com:AliceO2Group/WebUi into feature/ILG/…
graduta May 8, 2026
328e93e
Use const instead of hard-coded string
graduta May 8, 2026
49ac19f
Merge branch 'feature/ILG/OGUI-1892-disable-level-buttons-for-shifter…
graduta May 8, 2026
d2cbf11
Add constraints to filter logic to be applied per user role
graduta May 20, 2026
921590a
Display label rather than index (null)
graduta May 20, 2026
fb9b317
Merge branch 'dev' of github.com:AliceO2Group/WebUi into feature/ILG/…
graduta May 20, 2026
b5ff6e2
Update tests for edge cases
graduta May 20, 2026
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
6 changes: 6 additions & 0 deletions InfoLogger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

- [InfoLogger GUI (ILG)](#infologger-gui-ilg)
- [Interface User Guide](#interface-user-guide)
- [Shifter based role](#shifter-based-role)
- [Requirements](#requirements)
- [Installation](#installation)
- [Development database installation](#development-database-installation)
- [Dummy InfoLogger test server](#dummy-infologger-test-server)
- [InfoLogger insights](#infologger-insights)
- [Continuous Integration Workflows](#continuous-integration-workflows)
Expand Down Expand Up @@ -34,6 +36,10 @@ It interfaces with the system using two modes:
- Use arrows keys to navigate quickly between logs
- Download the logs in a file via the top left download icon

### Shifter based role

If the authenticated user is defined as having one of the access roles 'shifter' but does **not** have 'admin', then the UI should restrict the levels by which the user can filter messages. More specifically, the shifters are only allowed to filter messages by `Ops` (Operations)

## Requirements
- `nodejs` >= `16.x`
- InfoLogger MariaDB database for Query mode
Expand Down
6 changes: 5 additions & 1 deletion InfoLogger/public/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import {
Observable, WebSocketClient, QueryRouter,
Loader, RemoteData, sessionService, Notification,
} from '/js/src/index.js';
import { callRateLimiter, setBrowserTabTitle } from './common/utils.js';
import { callRateLimiter, setBrowserTabTitle, hasShifterButNoAdminRole } from './common/utils.js';
import { ConfigurationService } from './services/ConfigurationService.js';
import { InfoLoggerLevel } from './constants/infologger-level.const.js';
import { MODE } from './constants/mode.const.js';
import Log from './log/Log.js';
import Table from './table/Table.js';
Expand All @@ -45,6 +46,9 @@ export default class Model extends Observable {

this.log = new Log(this);
this.log.bubbleTo(this);
if (hasShifterButNoAdminRole(this.session.access)) {
this.log.filter.setConstraint('level', 'max', InfoLoggerLevel.OPS.index);
}

this.table = new Table(this);
this.table.bubbleTo(this);
Expand Down
32 changes: 31 additions & 1 deletion InfoLogger/public/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
* or submit itself to any jurisdiction.
*/

import { Role } from './../constants/role.const.js';
import { INFOLOGGER_LEVEL_LIST, InfoLoggerLevel } from './../constants/infologger-level.const.js';

/**
* Limit the number of calls to `fn` to 1 per `time` maximum.
* First call is immediate if `time` have been waited already.
* All other calls before end of `time` window will lead to 1 exececution at the end of window.
* @param {string} fn - function to be called
* @param {string} time - ms
* @returns {Function} lambda function to be called to call `fn`
* @returns {void} lambda function to be called to call `fn`
* @example
* let f = callRateLimiter((arg) => console.log('called', arg), 1000);
* 00:00:00 f(1);f(2);f(3);f(4);
Expand Down Expand Up @@ -60,3 +63,30 @@ export function setBrowserTabTitle(title = undefined) {
document.title = title;
}
}

/**
* Method to check if the user has only shifter role and not admin role
* @param {string[]} access - array of user roles email groups affiliation
* @returns {boolean} true if the user has only shifter role and not admin role, false otherwise
*/
export function hasShifterButNoAdminRole(access = []) {
return access.includes(Role.SHIFTER) && !access.includes(Role.ADMIN);
}

/**
* Method to return filter levels allowed for filtering based on current user role email groups affiliation
* * Shifters are only allowed to filter by Ops level
* @param {string[]} access - array of user roles email groups affiliation
* @returns {{label: string, index:number}[]} - filter levels allowed for filtering
*/
export function getFilterLevelsAllowed(access = []) {
Comment thread
isaachilly marked this conversation as resolved.
return hasShifterButNoAdminRole(access)
? INFOLOGGER_LEVEL_LIST.map((level) => ({
...level,
available: level.label === InfoLoggerLevel.OPS.label,
}))
: INFOLOGGER_LEVEL_LIST.map((level) => ({
...level,
available: true,
}));
}
45 changes: 45 additions & 0 deletions InfoLogger/public/constants/infologger-level.const.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* @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.
*/

/**
* Object containing the different levels of logs that can be displayed in the application,
* with their label and index as in the database
* These values are as per InfoLogger defined levels:
* {@link https://github.com/AliceO2Group/InfoLogger/blob/master/doc/README.md}
*/
export const InfoLoggerLevel = Object.freeze({
OPS: {
label: 'Ops',
index: 1,
},
SUPPORT: {
label: 'Support',
index: 6,
},
DEVEL: {
label: 'Devel',
index: 11,
},
TRACE: {
label: 'Trace',
index: null,
},
});

/**
* Array containing the different levels of logs that can be displayed in the application,
* with their label and index as in the database, used for iterating over the levels in the UI
* These values are as per InfoLogger defined levels:
*/
export const INFOLOGGER_LEVEL_LIST = Object.values(InfoLoggerLevel);
22 changes: 22 additions & 0 deletions InfoLogger/public/constants/role.const.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @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.
*/

/**
* Object containing the different roles that a user can have in the application, used for checking
* permissions and access levels. These roles are defined in CERN Application Service
*/
export const Role = Object.freeze({
SHIFTER: 'shifter',
ADMIN: 'admin',
});
53 changes: 49 additions & 4 deletions InfoLogger/public/logFilter/LogFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import { TEXT_FILTER_OPERATORS } from '../constants/text-filter-operators.const.
*/

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

/**
Expand All @@ -43,6 +44,7 @@ export default class LogFilter extends Observable {

this.model = model;

this._constraints = {};
this.resetCriteria();
}

Expand Down Expand Up @@ -72,9 +74,12 @@ export default class LogFilter extends Observable {
case 'min':
this.criterias[field]['$min'] = parseInt(value, 10);
break;
case 'max':
this.criterias[field]['$max'] = parseInt(value, 10);
case 'max': {
const effectiveValue = this._clampToConstraint(field, operator, value);
this.criterias[field][operator] = effectiveValue;
this.criterias[field]['$max'] = effectiveValue !== null ? parseInt(effectiveValue, 10) : null;
break;
}
case 'match':
this.criterias[field]['$match'] = value ? value : null;
break;
Expand All @@ -95,6 +100,37 @@ export default class LogFilter extends Observable {
}
}

/**
* Set a hard constraint on a field+operator that persists through resetCriteria.
* Any value set via setCriteria that exceeds this constraint will be clamped to it.
* @param {string} field - field name like 'level'
* @param {string} operator - operator like 'max'
* @param {string|number} value - constraint value
*/
setConstraint(field, operator, value) {
if (!this._constraints[field]) {
this._constraints[field] = {};
}
this._constraints[field][operator] = value;
this.setCriteria(field, operator, value);
}

/**
* Returns the value clamped to the active constraint for the given field+operator, if any.
* For now only `max` operator is supported, but this can be extended in the future if needed.
* @param {string} field - field name
* @param {string} operator - operator name
* @param {string|number} value - value to clamp
* @returns {string|number} clamped value
*/
_clampToConstraint(field, operator, value) {
const constraint = this._constraints?.[field]?.[operator];
if (operator === 'max' && constraint !== undefined && (value === null || value > constraint)) {
return constraint;
}
return value;
}

/**
* Exports all filled filters inputs
* @returns {object} minimal filter object
Expand Down Expand Up @@ -156,7 +192,7 @@ export default class LogFilter extends Observable {
/**
* 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
* @returns {void.<WebSocketMessage, boolean>} - function to filter logs
*/
toStringifyFunction() {
/**
Expand Down Expand Up @@ -396,6 +432,15 @@ export default class LogFilter extends Observable {
$max: null, // 0, 1, 6, 11, 21
},
};
for (const [field, operators] of Object.entries(this._constraints)) {
for (const [operator, value] of Object.entries(operators)) {
const effectiveValue = this._clampToConstraint(field, operator, value);
this.criterias[field][operator] = effectiveValue;
if (operator === 'max') {
this.criterias[field]['$max'] = effectiveValue !== null ? parseInt(effectiveValue, 10) : null;
}
}
}
this.notify();
}
}
Loading
Loading