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
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,7 +17,7 @@ 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 { MODE } from './constants/mode.const.js';
import Log from './log/Log.js';
Expand Down Expand Up @@ -311,12 +311,16 @@ export default class Model extends Observable {

/**
* Delegates sub-model actions depending new location of the page
* If user is shifter but not admin, set Ops as maximum level for filtering
*/
handleLocationChange() {
const { params } = this.router;
if (params) {
this.parseLocation(params);
}
if (hasShifterButNoAdminRole(this.session.access)) {
this.log.filter.setCriteria('level', 'max', 1);
}
}

/**
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 = []) {
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',
});
5 changes: 3 additions & 2 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 Down Expand Up @@ -156,7 +157,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
147 changes: 76 additions & 71 deletions InfoLogger/public/logFilter/commandFilters.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,92 +14,97 @@

import { h } from '/js/src/index.js';

const LIMIT_LEVELS = [
{ label: '100k', value: 100000 },
{ label: '500k', value: 500000 },
{ label: '1M', value: 1000000 },
];
const SEVERITIES_ALLOWED = [
{ label: 'Debug', value: 'D' },
{ label: 'Info', value: 'I' },
{ label: 'Warn', value: 'W' },
{ label: 'Error', value: 'E' },
{ label: 'Fatal', value: 'F' },
];

/**
* Filtering main options, in toolbar, top-right.
* - severity
* - level
* - limit
* - reset
* @param {Model} model - root model of the application
* @param {Log} logModel - log model of the application
* @param {{label: string, index:number}[]} filterLevelsAllowed - levels allowed for filtering
* @returns {vnode} - the view of filters panel
*/
export default (model) => [
export default (logModel, filterLevelsAllowed) => [
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),
'.btn-group',
SEVERITIES_ALLOWED.map(({ label, value }) => _selectableButtonComponent(
label,
{
id: `severity-${value}`,
title: `Match severity ${label.toLowerCase()}`,
isActive: logModel.filter.criterias.severity.in.includes(value),
onclick: () => logModel.setCriteria('severity', 'in', value),
},
)),
),
];

/**
* Makes a button to toggle severity
* @param {Model} model - root model of the application
* @param {string} label - button's label
* @param {string} title - button's title on mouse over
* @param {string} value - a char to represent severity: W E F or I, can be many with spaces like 'W E'
* @returns {vnode} - the button to toggle severity
*/
const buttonSeverity = (model, label, title, value) => h('button.btn', {
className: model.log.filter.criterias.severity.in.includes(value) ? 'active' : '',
onclick: (e) => {
model.log.setCriteria('severity', 'in', value);
e.target.blur(); // remove focus so user can 'enter' without actually toggle again the button
},
title: title,
}, label);
h(
'.btn-group',
filterLevelsAllowed.map(({ label, index, available }) => _selectableButtonComponent(
label,
{
id: `level-${index}`,
title: available ? `Filter level ≤ ${index}` : `You don't have access to level ${label}`,
isActive: logModel.filter.criterias.level.max === index,
onclick: () => logModel.setCriteria('level', 'max', index),
disabled: !available,
},
)),
),

h(
'.btn-group',
LIMIT_LEVELS.map(({ label, value }) =>
_selectableButtonComponent(
label,
{
id: `limit-${value}`,
title: `Keep only ${value / 1000}k logs in the view`,
isActive: logModel.limit === value,
onclick: () => logModel.setLimit(value),
},
)),
),

_selectableButtonComponent(
'Reset filters',
{
title: 'Reset date, time, matches, excludes, log levels',
isActive: false,
onclick: () => logModel.filter.resetCriteria(),
},
),
];

/**
* Makes a button to set filtering level (shifter, debug, etc) with number
* @param {Model} model - root model of the application
* Component representing the creation of a button for filtering header
* @param {string} label - button's label
* @param {number} value - maximum level of filtering, from 1 to 21
* @param {object} options - options for the button
* @param {string} options.id - button's id
* @param {string} options.title - button's title on mouse over
* @param {boolean} options.isActive - whether the button is active
* @param {void} options.onclick - function to call when button is clicked
* @param {boolean} options.disabled - whether the button is disabled
* @returns {vnode} - component representing the creation of a button for filtering
*/
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 _selectableButtonComponent = (label, { id, title, isActive, onclick, disabled }) => h('button.btn', {
id,
className: [isActive ? 'active' : '', disabled ? 'disabled' : ''].join(' '),
onclick,
title,
disabled,

/**
* 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
*/
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);

/**
* Makes a button to reset filters
* @param {Model} model - root model of the application
* @returns {vnode} - component representing the creation of a button to reset filters
*/
const buttonReset = (model) => h('button.btn', {
onclick: () => model.log.filter.resetCriteria(),
title: 'Reset date, time, matches, excludes, log levels',
}, 'Reset filters');
13 changes: 6 additions & 7 deletions InfoLogger/public/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import tableLogsContent from './log/tableLogsContent.js';
import tableLogsScrollMap from './log/tableLogsScrollMap.js';
import aboutComponent from './about/about.component.js';
import errorComponent from './common/errorComponent.js';
import { getFilterLevelsAllowed } from './common/utils.js';

/**
* Main view of the application
Expand All @@ -34,14 +35,12 @@ export default (model) => [
notification(model.notification),
h('.flex-column absolute-fill', [
h('.shadow-level2', [
h('header.p1.flex-row.f7', [
h('', commandLogs(model)),
h('header.p1.flex-wrap.flex-row.f7.g2', [
h('.flex-row', commandLogs(model)),
h(
'.flex-grow',
{
style: 'display: flex; flex-direction:row-reverse;',
},
commandFilters(model),
'.flex-row.g3',
{ style: 'margin-left: auto;' },
commandFilters(model.log, getFilterLevelsAllowed(model.session.access)),
),
]),
h('header.f7', tableFilters(model)),
Expand Down
Loading
Loading