Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
057c235
[O2B-1502] Filter model setup boilerplate code.
Houwie7000 Nov 26, 2025
a7a9eda
[O2B-1502] Add filter button on LHC-fills overview page.
Houwie7000 Nov 26, 2025
2648f1c
[O2B-1502] Added stable beams only to filter
Houwie7000 Nov 26, 2025
094e6c5
[O2B-1502] Filtering with Stable Beams Only works, radioButton elemen…
Houwie7000 Nov 26, 2025
da7ff37
[O2B-1502] Doc fixes
Houwie7000 Nov 26, 2025
ee72512
[O2B-1502] Increase timeout of detailsForSimulationPass test. Local m…
Houwie7000 Nov 26, 2025
96a04c0
[O2B-1502] Potential fix for test failure.
Houwie7000 Nov 28, 2025
6fa4034
Revert "[O2B-1502] Potential fix for test failure."
Houwie7000 Dec 1, 2025
17ea048
Merge branch 'main' into feature/O2B-1502/filtering-panel-lhc-fills-f…
graduta Dec 5, 2025
695622b
[O2B-1502] Processed feedback
Houwie7000 Dec 8, 2025
87bee89
[O2B-1502] Git failed to detect rename. Ran: git mv RadioButton.js ra…
Houwie7000 Dec 8, 2025
5a18d9e
[O2B-1502] Added test import
Houwie7000 Dec 8, 2025
4c530ef
Revert "[O2B-1502] Processed feedback"
Houwie7000 Dec 10, 2025
51b50d9
[O2B-1502] Cherry pick previous feedback changes
Houwie7000 Dec 10, 2025
c0c8559
[O2B-1502] Integrated stable beam only filter into filtermodel.
Houwie7000 Dec 10, 2025
9934e56
[O2B-1502] fixed stable beam default value
Houwie7000 Dec 10, 2025
f247a6f
[O2B-1502] Fixed logic and type
Houwie7000 Dec 11, 2025
9b67281
[O2B-1502] Don't set any defaults in the filter as it will conflict w…
Houwie7000 Dec 11, 2025
ea0880f
[O2B-1502] Code cleanup
Houwie7000 Dec 11, 2025
46d4ae8
[O2B-1502] minor changes, processed feedback
Houwie7000 Dec 15, 2025
91e350c
[O2B-1502] Removed duplicate function due to override
Houwie7000 Dec 15, 2025
e95c847
[O2B-1503] Added front end fill number filter
Houwie7000 Nov 27, 2025
5077fec
[O2B-1503] fillNumbers work, todo ranges
Houwie7000 Nov 28, 2025
9130c87
[O2B-1503] ranges accepted by fill numbers filter
Houwie7000 Nov 28, 2025
0d0986e
[O2B-1503] Added/fixed test lhc-fill overview
Houwie7000 Nov 28, 2025
804cd4c
[O2B-1503] doc change
Houwie7000 Nov 28, 2025
2f5932a
[O2B-1503] JSDoc enhancements. Extracted duplicate functions to utils…
Houwie7000 Dec 15, 2025
1e3f503
[O2B-1503] placeholder text changed
Houwie7000 Dec 15, 2025
fd055ca
Merge branch 'main' into feature/O2B-1503/lhcfills-fill-numbers-filter
Houwie7000 Dec 17, 2025
8e82b34
[O2B-1503] Processed feedback, added tests
Houwie7000 Dec 18, 2025
e5bbc05
[O2B-1503] Added test for splitStringToStringsTrimmed()
Houwie7000 Dec 18, 2025
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
4 changes: 4 additions & 0 deletions lib/domain/dtos/filters/LhcFillsFilterDto.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
* or submit itself to any jurisdiction.
*/
const Joi = require('joi');
const { validateRange } = require('../../../utilities/rangeUtils');

exports.LhcFillsFilterDto = Joi.object({
hasStableBeams: Joi.boolean(),
fillNumbers: Joi.string().trim().custom(validateRange).messages({
'any.invalid': '{{#message}}',
}),
});
30 changes: 1 addition & 29 deletions lib/domain/dtos/filters/RunFilterDto.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const { IntegerComparisonDto, FloatComparisonDto } = require('./NumericalCompari
const { RUN_CALIBRATION_STATUS } = require('../../enums/RunCalibrationStatus.js');
const { RUN_DEFINITIONS } = require('../../enums/RunDefinition.js');
const { singleRunsCollectionCustomCheck } = require('../utils.js');
const { validateRange } = require('../../../utilities/rangeUtils.js');

const DetectorsFilterDto = Joi.object({
operator: Joi.string().valid('or', 'and', 'none').required(),
Expand All @@ -30,35 +31,6 @@ const EorReasonFilterDto = Joi.object({
description: Joi.string(),
});

/**
* Validates run numbers ranges to not exceed 100 runs
*
* @param {*} value The value to validate
* @param {*} helpers The helpers object
* @returns {Object} The value if validation passes
*/
const validateRange = (value, helpers) => {
const MAX_RANGE_SIZE = 100;

const runNumbers = value.split(',').map((runNumber) => runNumber.trim());

for (const runNumber of runNumbers) {
if (runNumber.includes('-')) {
const [start, end] = runNumber.split('-').map((n) => parseInt(n, 10));
if (Number.isNaN(start) || Number.isNaN(end) || start > end) {
return helpers.error('any.invalid', { message: `Invalid range: ${runNumber}` });
}
const rangeSize = end - start + 1;

if (rangeSize > MAX_RANGE_SIZE) {
return helpers.error('any.invalid', { message: `Given range exceeds max size of ${MAX_RANGE_SIZE} runs: ${runNumber}` });
}
}
}

return value;
};

exports.RunFilterDto = Joi.object({
runNumbers: Joi.string().trim().custom(validateRange).messages({
'any.invalid': '{{#message}}',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @license
* Copyright CERN and copyright holders of ALICE Trg. This software is
* distributed under the terms of the GNU General Public License v3 (GPL
* Version 3), copied verbatim in the file "COPYING".
*
* See http://alice-Trg.web.cern.ch/license for full licensing information.
*
* 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.
*/

import { rawTextFilter } from '../common/filters/rawTextFilter.js';

/**
* Component to filter LHC-fills by fill number
*
* @param {RawTextFilterModel} filterModel the filter model
* @returns {Component} the text field
*/
export const fillNumberFilter = (filterModel) => rawTextFilter(
filterModel,
{ classes: ['w-100', 'fill-numbers-filter'], placeholder: 'e.g. 11392, 11383, 7625' },
);
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { infologgerLinksComponents } from '../../../components/common/externalLi
import { formatBeamType } from '../../../utilities/formatting/formatBeamType.js';
import { frontLink } from '../../../components/common/navigation/frontLink.js';
import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js';
import { fillNumberFilter } from '../../../components/Filters/LhcFillsFilter/fillNumberFilter.js';

/**
* List of active columns for a lhc fills table
Expand All @@ -49,6 +50,7 @@ export const lhcFillsActiveColumns = {
),
],
),
filter: (lhcFillModel) => fillNumberFilter(lhcFillModel.filteringModel.get('fillNumbers')),
profiles: {
lhcFill: true,
environment: true,
Expand Down
2 changes: 2 additions & 0 deletions lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import { buildUrl } from '/js/src/index.js';
import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js';
import { StableBeamFilterModel } from '../../../components/Filters/LhcFillsFilter/StableBeamFilterModel.js';
import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js';
import { OverviewPageModel } from '../../../models/OverviewModel.js';
import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js';

Expand All @@ -32,6 +33,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel {
super();

this._filteringModel = new FilteringModel({
fillNumbers: new RawTextFilterModel(),
hasStableBeams: new StableBeamFilterModel(),
});

Expand Down
18 changes: 17 additions & 1 deletion lib/usecases/lhcFill/GetAllLhcFillsUseCase.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const {
const { lhcFillAdapter } = require('../../database/adapters/index.js');
const { ApiConfig } = require('../../config/index.js');
const { RunDefinition } = require('../../domain/enums/RunDefinition.js');
const { unpackNumberRange } = require('../../utilities/rangeUtils.js');
const { splitStringToStringsTrimmed } = require('../../utilities/stringUtils.js');

/**
* GetAllLhcFillsUseCase
Expand All @@ -38,14 +40,28 @@ class GetAllLhcFillsUseCase {
const { filter, page = {} } = query;
const { limit = ApiConfig.pagination.limit, offset = 0 } = page;

const SEARCH_ITEMS_SEPARATOR = ',';

const queryBuilder = new QueryBuilder();

if (filter) {
const { hasStableBeams } = filter;
const { hasStableBeams, fillNumbers } = filter;
if (hasStableBeams) {
// For now, if a stableBeamsStart is present, then a beam is stable
queryBuilder.where('stableBeamsStart').not().is(null);
}

if (fillNumbers) {
const fillNumberCriteria = splitStringToStringsTrimmed(fillNumbers, SEARCH_ITEMS_SEPARATOR);

const finalFillnumberList = Array.from(unpackNumberRange(fillNumberCriteria));

// Check that the final fill numbers list contains at least one valid fill number
if (finalFillnumberList.length > 0) {
finalFillnumberList.length === 1 ? queryBuilder.where('fillNumber').is(finalFillnumberList[0])
: queryBuilder.where('fillNumber').oneOf(...finalFillnumberList);
}
}
}

const { count, rows } = await TransactionHelper.provide(async () => {
Expand Down
26 changes: 4 additions & 22 deletions lib/usecases/run/GetAllRunsUseCase.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const { BadParameterError } = require('../../server/errors/BadParameterError');
const { gaqService } = require('../../server/services/qualityControlFlag/GaqService.js');
const { qcFlagSummaryService } = require('../../server/services/qualityControlFlag/QcFlagSummaryService.js');
const { DetectorType } = require('../../domain/enums/DetectorTypes.js');
const { unpackNumberRange } = require('../../utilities/rangeUtils.js');
const { splitStringToStringsTrimmed } = require('../../utilities/stringUtils.js');

/**
* GetAllRunsUseCase
Expand Down Expand Up @@ -83,29 +85,9 @@ class GetAllRunsUseCase {
} = filter;

if (runNumbers) {
const runNumberCriteria = runNumbers.split(SEARCH_ITEMS_SEPARATOR)
.map((runNumbers) => runNumbers.trim())
.filter(Boolean);

const runNumberSet = new Set();

runNumberCriteria.forEach((runNumber) => {
if (runNumber.includes('-')) {
const [start, end] = runNumber.split('-').map((n) => parseInt(n, 10));
if (!Number.isNaN(start) && !Number.isNaN(end)) {
for (let i = start; i <= end; i++) {
runNumberSet.add(i);
}
}
} else {
const parsedRunNumber = parseInt(runNumber, 10);
if (!Number.isNaN(parsedRunNumber)) {
runNumberSet.add(parsedRunNumber);
}
}
});
const runNumberCriteria = splitStringToStringsTrimmed(runNumbers, SEARCH_ITEMS_SEPARATOR);

const finalRunNumberList = Array.from(runNumberSet);
const finalRunNumberList = Array.from(unpackNumberRange(runNumberCriteria));

// Check that the final run numbers list contains at least one valid run number
if (finalRunNumberList.length > 0) {
Expand Down
79 changes: 79 additions & 0 deletions lib/utilities/rangeUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @license
* Copyright CERN and copyright holders of ALICE O2. This software is
* distributed under the terms of the GNU General Public License v3 (GPL
* Version 3), copied verbatim in the file "COPYING".
*
* See http://alice-o2.web.cern.ch/license for full licensing information.
*
* 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.
*/

/**
* Validates numbers ranges to not exceed 100 entities
* Expects a string containing comma seperated number values.
*
* @param {string} value The value to validate
* @param {*} helpers The helpers object
* @returns {Object} The value if validation passes
*/
export const validateRange = (value, helpers) => {
const MAX_RANGE_SIZE = 100;

const numbers = value.split(',').map((number) => number.trim());

for (const number of numbers) {
if (number.includes('-')) {
// Check if '-' occurs more than once in this part of the range
if (number.lastIndexOf('-') !== number.indexOf('-')) {
return helpers.error('any.invalid', { message: `Invalid range: ${number}` });
}
const [start, end] = number.split('-').map((n) => Number(n));
if (Number.isNaN(start) || Number.isNaN(end) || start > end) {
return helpers.error('any.invalid', { message: `Invalid range: ${number}` });
}
const rangeSize = end - start + 1;

if (rangeSize > MAX_RANGE_SIZE) {
return helpers.error('any.invalid', { message: `Given range exceeds max size of ${MAX_RANGE_SIZE} range: ${number}` });
}
} else {
// Prevent non-numeric input.
if (isNaN(number)) {
return helpers.error('any.invalid', { message: `Invalid number: ${number}` });
}
}
}

return value;
};

/**
* Unpacks a given string containing number ranges.
* E.G. input: 5,7-9 => output: 5,7,8,9
* @param {string[]} numbersRanges numbers that may or may not contain ranges.
* @param {string} rangeSplitter string used to indicate and unpack a range.
* @returns {Set<Number>} set containing the unpacked range.
*/
export function unpackNumberRange(numbersRanges, rangeSplitter = '-') {
// Set to prevent duplicate values.
const resultNumbers = new Set();

numbersRanges.forEach((number) => {
if (number.includes(rangeSplitter)) {
const [start, end] = number.split(rangeSplitter).map((n) => parseInt(n, 10));
if (!Number.isNaN(start) && !Number.isNaN(end)) {
for (let i = start; i <= end; i++) {
resultNumbers.add(Number(i));
}
}
} else {
if (!isNaN(number)) {
resultNumbers.add(Number(number));
}
}
});
return resultNumbers;
}
12 changes: 12 additions & 0 deletions lib/utilities/stringUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ const snakeToCamel = (snake) => snake.toLowerCase()
*/
const snakeToPascal = (snake) => ucFirst(snakeToCamel(snake));

/**
* Split the received string to an array of trimmed strings.
* Boolean trick: https://michaeluloth.com/javascript-filter-boolean/
* @param {string} stringCollection String containing other strings withing split by seperator.
* @param {string} stringSeperator Used to seperate the stringCollection.
*/
const splitStringToStringsTrimmed = (stringCollection, stringSeperator = ',') => stringCollection.split(stringSeperator)
.map((string) => string.trim())
.filter(Boolean);

exports.ucFirst = ucFirst;

exports.lcFirst = lcFirst;
Expand All @@ -73,3 +83,5 @@ exports.pascalToSnake = pascalToSnake;
exports.snakeToCamel = snakeToCamel;

exports.snakeToPascal = snakeToPascal;

exports.splitStringToStringsTrimmed = splitStringToStringsTrimmed;
2 changes: 1 addition & 1 deletion test/api/runs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ module.exports = () => {
expect(response.status).to.equal(400);
const { errors: [error] } = response.body;
expect(error.title).to.equal('Invalid Attribute');
expect(error.detail).to.equal(`Given range exceeds max size of ${MAX_RANGE_SIZE} runs: ${runNumberRange}`);
expect(error.detail).to.equal(`Given range exceeds max size of ${MAX_RANGE_SIZE} range: ${runNumberRange}`);
});

it('should return 400 if the calibration status filter is invalid', async () => {
Expand Down
60 changes: 60 additions & 0 deletions test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,64 @@ module.exports = () => {
expect(lhcFill.stableBeamsStart).to.not.be.null;
});
});

// Fill number filter tests

it('should only contain specified fill number', async () => {
getAllLhcFillsDto.query = { filter: { hasStableBeams: true, fillNumbers: '6' } };
const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto);
expect(lhcFills).to.be.an('array').and.lengthOf(1)

lhcFills.forEach((lhcFill) => {
expect(lhcFill.fillNumber).to.equal(6)
});
})

it('should only contain specified fill numbers', async () => {
getAllLhcFillsDto.query = { filter: { hasStableBeams: true, fillNumbers: '6,3' } };
const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto);


expect(lhcFills).to.be.an('array').and.lengthOf(2)

lhcFills.forEach((lhcFill) => {
expect(lhcFill.fillNumber).oneOf([6,3])
});
})

it('should only contain specified fill numbers, range', async () => {
getAllLhcFillsDto.query = { filter: { hasStableBeams: true, fillNumbers: '1-3,6' } };
const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto);


expect(lhcFills).to.be.an('array').and.lengthOf(4)

lhcFills.forEach((lhcFill) => {
expect(lhcFill.fillNumber).oneOf([1,2,3,6])
});
})

it('should only contain specified fill numbers, whitespace', async () => {
getAllLhcFillsDto.query = { filter: { hasStableBeams: true, fillNumbers: ' 6 , 3 ' } };
const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto);


expect(lhcFills).to.be.an('array').and.lengthOf(2)

lhcFills.forEach((lhcFill) => {
expect(lhcFill.fillNumber).oneOf([6,3])
});
})

it('should only contain specified fill numbers, comma misplacement', async () => {
getAllLhcFillsDto.query = { filter: { hasStableBeams: true, fillNumbers: ',6,3,' } };
const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto);


expect(lhcFills).to.be.an('array').and.lengthOf(2)

lhcFills.forEach((lhcFill) => {
expect(lhcFill.fillNumber).oneOf([6,3])
});
})
};
2 changes: 2 additions & 0 deletions test/lib/utilities/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
const cacheAsyncFunctionTest = require('./cacheAsyncFunction.test.js');
const deepmerge = require('./deepmerge.test.js');
const isPromise = require('./isPromise.test.js');
const rangeUtilsTest = require('./rangeUtils.test.js');
const stringUtilsTest = require('./stringUtils.test.js');

module.exports = () => {
describe('cacheFunction', cacheAsyncFunctionTest);
describe('deepmerge', deepmerge);
describe('isPromise', isPromise);
describe('stringUtils', stringUtilsTest);
describe('rangeUtils', rangeUtilsTest)
};
Loading
Loading