Skip to content

Commit 4d0e2a0

Browse files
authored
[O2B-1503] LHCFills page, fill-number filter added (#2033)
* Add filter by fill number to LHCFills page * extract code to improve reusability for `validateRange` moved out of RunFilterDTO to utilities to reuse in other DTO's
1 parent 5081ae9 commit 4d0e2a0

15 files changed

Lines changed: 376 additions & 54 deletions

File tree

lib/domain/dtos/filters/LhcFillsFilterDto.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
* or submit itself to any jurisdiction.
1212
*/
1313
const Joi = require('joi');
14+
const { validateRange } = require('../../../utilities/rangeUtils');
1415

1516
exports.LhcFillsFilterDto = Joi.object({
1617
hasStableBeams: Joi.boolean(),
18+
fillNumbers: Joi.string().trim().custom(validateRange).messages({
19+
'any.invalid': '{{#message}}',
20+
}),
1721
});

lib/domain/dtos/filters/RunFilterDto.js

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const { IntegerComparisonDto, FloatComparisonDto } = require('./NumericalCompari
1818
const { RUN_CALIBRATION_STATUS } = require('../../enums/RunCalibrationStatus.js');
1919
const { RUN_DEFINITIONS } = require('../../enums/RunDefinition.js');
2020
const { singleRunsCollectionCustomCheck } = require('../utils.js');
21+
const { validateRange } = require('../../../utilities/rangeUtils.js');
2122

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

33-
/**
34-
* Validates run numbers ranges to not exceed 100 runs
35-
*
36-
* @param {*} value The value to validate
37-
* @param {*} helpers The helpers object
38-
* @returns {Object} The value if validation passes
39-
*/
40-
const validateRange = (value, helpers) => {
41-
const MAX_RANGE_SIZE = 100;
42-
43-
const runNumbers = value.split(',').map((runNumber) => runNumber.trim());
44-
45-
for (const runNumber of runNumbers) {
46-
if (runNumber.includes('-')) {
47-
const [start, end] = runNumber.split('-').map((n) => parseInt(n, 10));
48-
if (Number.isNaN(start) || Number.isNaN(end) || start > end) {
49-
return helpers.error('any.invalid', { message: `Invalid range: ${runNumber}` });
50-
}
51-
const rangeSize = end - start + 1;
52-
53-
if (rangeSize > MAX_RANGE_SIZE) {
54-
return helpers.error('any.invalid', { message: `Given range exceeds max size of ${MAX_RANGE_SIZE} runs: ${runNumber}` });
55-
}
56-
}
57-
}
58-
59-
return value;
60-
};
61-
6234
exports.RunFilterDto = Joi.object({
6335
runNumbers: Joi.string().trim().custom(validateRange).messages({
6436
'any.invalid': '{{#message}}',
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @license
3+
* Copyright CERN and copyright holders of ALICE Trg. This software is
4+
* distributed under the terms of the GNU General Public License v3 (GPL
5+
* Version 3), copied verbatim in the file "COPYING".
6+
*
7+
* See http://alice-Trg.web.cern.ch/license for full licensing information.
8+
*
9+
* In applying this license CERN does not waive the privileges and immunities
10+
* granted to it by virtue of its status as an Intergovernmental Organization
11+
* or submit itself to any jurisdiction.
12+
*/
13+
14+
import { rawTextFilter } from '../common/filters/rawTextFilter.js';
15+
16+
/**
17+
* Component to filter LHC-fills by fill number
18+
*
19+
* @param {RawTextFilterModel} filterModel the filter model
20+
* @returns {Component} the text field
21+
*/
22+
export const fillNumberFilter = (filterModel) => rawTextFilter(
23+
filterModel,
24+
{ classes: ['w-100', 'fill-numbers-filter'], placeholder: 'e.g. 11392, 11383, 7625' },
25+
);

lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { infologgerLinksComponents } from '../../../components/common/externalLi
2424
import { formatBeamType } from '../../../utilities/formatting/formatBeamType.js';
2525
import { frontLink } from '../../../components/common/navigation/frontLink.js';
2626
import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js';
27+
import { fillNumberFilter } from '../../../components/Filters/LhcFillsFilter/fillNumberFilter.js';
2728

2829
/**
2930
* List of active columns for a lhc fills table
@@ -49,6 +50,7 @@ export const lhcFillsActiveColumns = {
4950
),
5051
],
5152
),
53+
filter: (lhcFillModel) => fillNumberFilter(lhcFillModel.filteringModel.get('fillNumbers')),
5254
profiles: {
5355
lhcFill: true,
5456
environment: true,

lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import { buildUrl } from '/js/src/index.js';
1515
import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js';
1616
import { StableBeamFilterModel } from '../../../components/Filters/LhcFillsFilter/StableBeamFilterModel.js';
17+
import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js';
1718
import { OverviewPageModel } from '../../../models/OverviewModel.js';
1819
import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js';
1920

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

3435
this._filteringModel = new FilteringModel({
36+
fillNumbers: new RawTextFilterModel(),
3537
hasStableBeams: new StableBeamFilterModel(),
3638
});
3739

lib/usecases/lhcFill/GetAllLhcFillsUseCase.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const {
2222
const { lhcFillAdapter } = require('../../database/adapters/index.js');
2323
const { ApiConfig } = require('../../config/index.js');
2424
const { RunDefinition } = require('../../domain/enums/RunDefinition.js');
25+
const { unpackNumberRange } = require('../../utilities/rangeUtils.js');
26+
const { splitStringToStringsTrimmed } = require('../../utilities/stringUtils.js');
2527

2628
/**
2729
* GetAllLhcFillsUseCase
@@ -38,14 +40,28 @@ class GetAllLhcFillsUseCase {
3840
const { filter, page = {} } = query;
3941
const { limit = ApiConfig.pagination.limit, offset = 0 } = page;
4042

43+
const SEARCH_ITEMS_SEPARATOR = ',';
44+
4145
const queryBuilder = new QueryBuilder();
4246

4347
if (filter) {
44-
const { hasStableBeams } = filter;
48+
const { hasStableBeams, fillNumbers } = filter;
4549
if (hasStableBeams) {
4650
// For now, if a stableBeamsStart is present, then a beam is stable
4751
queryBuilder.where('stableBeamsStart').not().is(null);
4852
}
53+
54+
if (fillNumbers) {
55+
const fillNumberCriteria = splitStringToStringsTrimmed(fillNumbers, SEARCH_ITEMS_SEPARATOR);
56+
57+
const finalFillnumberList = Array.from(unpackNumberRange(fillNumberCriteria));
58+
59+
// Check that the final fill numbers list contains at least one valid fill number
60+
if (finalFillnumberList.length > 0) {
61+
finalFillnumberList.length === 1 ? queryBuilder.where('fillNumber').is(finalFillnumberList[0])
62+
: queryBuilder.where('fillNumber').oneOf(...finalFillnumberList);
63+
}
64+
}
4965
}
5066

5167
const { count, rows } = await TransactionHelper.provide(async () => {

lib/usecases/run/GetAllRunsUseCase.js

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const { BadParameterError } = require('../../server/errors/BadParameterError');
2323
const { gaqService } = require('../../server/services/qualityControlFlag/GaqService.js');
2424
const { qcFlagSummaryService } = require('../../server/services/qualityControlFlag/QcFlagSummaryService.js');
2525
const { DetectorType } = require('../../domain/enums/DetectorTypes.js');
26+
const { unpackNumberRange } = require('../../utilities/rangeUtils.js');
27+
const { splitStringToStringsTrimmed } = require('../../utilities/stringUtils.js');
2628

2729
/**
2830
* GetAllRunsUseCase
@@ -83,29 +85,9 @@ class GetAllRunsUseCase {
8385
} = filter;
8486

8587
if (runNumbers) {
86-
const runNumberCriteria = runNumbers.split(SEARCH_ITEMS_SEPARATOR)
87-
.map((runNumbers) => runNumbers.trim())
88-
.filter(Boolean);
89-
90-
const runNumberSet = new Set();
91-
92-
runNumberCriteria.forEach((runNumber) => {
93-
if (runNumber.includes('-')) {
94-
const [start, end] = runNumber.split('-').map((n) => parseInt(n, 10));
95-
if (!Number.isNaN(start) && !Number.isNaN(end)) {
96-
for (let i = start; i <= end; i++) {
97-
runNumberSet.add(i);
98-
}
99-
}
100-
} else {
101-
const parsedRunNumber = parseInt(runNumber, 10);
102-
if (!Number.isNaN(parsedRunNumber)) {
103-
runNumberSet.add(parsedRunNumber);
104-
}
105-
}
106-
});
88+
const runNumberCriteria = splitStringToStringsTrimmed(runNumbers, SEARCH_ITEMS_SEPARATOR);
10789

108-
const finalRunNumberList = Array.from(runNumberSet);
90+
const finalRunNumberList = Array.from(unpackNumberRange(runNumberCriteria));
10991

11092
// Check that the final run numbers list contains at least one valid run number
11193
if (finalRunNumberList.length > 0) {

lib/utilities/rangeUtils.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* @license
3+
* Copyright CERN and copyright holders of ALICE O2. This software is
4+
* distributed under the terms of the GNU General Public License v3 (GPL
5+
* Version 3), copied verbatim in the file "COPYING".
6+
*
7+
* See http://alice-o2.web.cern.ch/license for full licensing information.
8+
*
9+
* In applying this license CERN does not waive the privileges and immunities
10+
* granted to it by virtue of its status as an Intergovernmental Organization
11+
* or submit itself to any jurisdiction.
12+
*/
13+
14+
/**
15+
* Validates numbers ranges to not exceed 100 entities
16+
* Expects a string containing comma seperated number values.
17+
*
18+
* @param {string} value The value to validate
19+
* @param {*} helpers The helpers object
20+
* @returns {Object} The value if validation passes
21+
*/
22+
export const validateRange = (value, helpers) => {
23+
const MAX_RANGE_SIZE = 100;
24+
25+
const numbers = value.split(',').map((number) => number.trim());
26+
27+
for (const number of numbers) {
28+
if (number.includes('-')) {
29+
// Check if '-' occurs more than once in this part of the range
30+
if (number.lastIndexOf('-') !== number.indexOf('-')) {
31+
return helpers.error('any.invalid', { message: `Invalid range: ${number}` });
32+
}
33+
const [start, end] = number.split('-').map((n) => Number(n));
34+
if (Number.isNaN(start) || Number.isNaN(end) || start > end) {
35+
return helpers.error('any.invalid', { message: `Invalid range: ${number}` });
36+
}
37+
const rangeSize = end - start + 1;
38+
39+
if (rangeSize > MAX_RANGE_SIZE) {
40+
return helpers.error('any.invalid', { message: `Given range exceeds max size of ${MAX_RANGE_SIZE} range: ${number}` });
41+
}
42+
} else {
43+
// Prevent non-numeric input.
44+
if (isNaN(number)) {
45+
return helpers.error('any.invalid', { message: `Invalid number: ${number}` });
46+
}
47+
}
48+
}
49+
50+
return value;
51+
};
52+
53+
/**
54+
* Unpacks a given string containing number ranges.
55+
* E.G. input: 5,7-9 => output: 5,7,8,9
56+
* @param {string[]} numbersRanges numbers that may or may not contain ranges.
57+
* @param {string} rangeSplitter string used to indicate and unpack a range.
58+
* @returns {Set<Number>} set containing the unpacked range.
59+
*/
60+
export function unpackNumberRange(numbersRanges, rangeSplitter = '-') {
61+
// Set to prevent duplicate values.
62+
const resultNumbers = new Set();
63+
64+
numbersRanges.forEach((number) => {
65+
if (number.includes(rangeSplitter)) {
66+
const [start, end] = number.split(rangeSplitter).map((n) => parseInt(n, 10));
67+
if (!Number.isNaN(start) && !Number.isNaN(end)) {
68+
for (let i = start; i <= end; i++) {
69+
resultNumbers.add(Number(i));
70+
}
71+
}
72+
} else {
73+
if (!isNaN(number)) {
74+
resultNumbers.add(Number(number));
75+
}
76+
}
77+
});
78+
return resultNumbers;
79+
}

lib/utilities/stringUtils.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ const snakeToCamel = (snake) => snake.toLowerCase()
6262
*/
6363
const snakeToPascal = (snake) => ucFirst(snakeToCamel(snake));
6464

65+
/**
66+
* Split the received string to an array of trimmed strings.
67+
* Boolean trick: https://michaeluloth.com/javascript-filter-boolean/
68+
* @param {string} stringCollection String containing other strings withing split by seperator.
69+
* @param {string} stringSeperator Used to seperate the stringCollection.
70+
*/
71+
const splitStringToStringsTrimmed = (stringCollection, stringSeperator = ',') => stringCollection.split(stringSeperator)
72+
.map((string) => string.trim())
73+
.filter(Boolean);
74+
6575
exports.ucFirst = ucFirst;
6676

6777
exports.lcFirst = lcFirst;
@@ -73,3 +83,5 @@ exports.pascalToSnake = pascalToSnake;
7383
exports.snakeToCamel = snakeToCamel;
7484

7585
exports.snakeToPascal = snakeToPascal;
86+
87+
exports.splitStringToStringsTrimmed = splitStringToStringsTrimmed;

test/api/runs.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ module.exports = () => {
166166
expect(response.status).to.equal(400);
167167
const { errors: [error] } = response.body;
168168
expect(error.title).to.equal('Invalid Attribute');
169-
expect(error.detail).to.equal(`Given range exceeds max size of ${MAX_RANGE_SIZE} runs: ${runNumberRange}`);
169+
expect(error.detail).to.equal(`Given range exceeds max size of ${MAX_RANGE_SIZE} range: ${runNumberRange}`);
170170
});
171171

172172
it('should return 400 if the calibration status filter is invalid', async () => {

0 commit comments

Comments
 (0)