From b3685b13c675d4e73a42c14af56dc747fa762dae Mon Sep 17 00:00:00 2001 From: George Raduta Date: Wed, 29 Jul 2026 09:59:02 +0200 Subject: [PATCH 1/5] Extract BeamTypeDto in reusable component --- lib/domain/dtos/common/BeamTypeDto.js | 23 ++++++++++++++++++++ lib/domain/dtos/filters/LhcFillsFilterDto.js | 10 ++------- lib/domain/dtos/filters/RunFilterDto.js | 2 ++ 3 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 lib/domain/dtos/common/BeamTypeDto.js diff --git a/lib/domain/dtos/common/BeamTypeDto.js b/lib/domain/dtos/common/BeamTypeDto.js new file mode 100644 index 0000000000..dc2d401113 --- /dev/null +++ b/lib/domain/dtos/common/BeamTypeDto.js @@ -0,0 +1,23 @@ +/** + * @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. + */ + +const Joi = require('joi'); +const { validateBeamTypes, BEAM_TYPE_INVALID } = require('../../../utilities/beamTypeUtils'); + +exports.BeamTypesDto = Joi.string() + .trim() + .custom(validateBeamTypes) + .messages({ + [BEAM_TYPE_INVALID]: '{{#message}}', + 'string.base': 'Beam type must be a string', + }); diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index 3fe9578388..52249ce8b0 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -12,7 +12,7 @@ */ const Joi = require('joi'); const { validateRange, RANGE_INVALID } = require('../../../utilities/rangeUtils'); -const { validateBeamTypes, BEAM_TYPE_INVALID } = require('../../../utilities/beamTypeUtils'); +const { BeamTypesDto } = require('../common/BeamTypeDto.js'); const { validateTimeDuration } = require('../../../utilities/validateTime'); const { FromToFilterDto } = require('./FromToFilterDto.js'); @@ -27,11 +27,5 @@ exports.LhcFillsFilterDto = Joi.object({ stableBeamsStart: FromToFilterDto, stableBeamsEnd: FromToFilterDto, schemeName: Joi.string().trim().max(64), - beamTypes: Joi.string() - .trim() - .custom(validateBeamTypes) - .messages({ - [BEAM_TYPE_INVALID]: '{{#message}}', - 'string.base': 'Beam type must be a string', - }), + beamTypes: BeamTypesDto, }); diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index e67106d9f0..726ab4220f 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -12,6 +12,7 @@ */ const Joi = require('joi'); const { CustomJoi } = require('../CustomJoi.js'); +const { BeamTypesDto } = require('../common/BeamTypeDto.js'); const { FromToFilterDto } = require('./FromToFilterDto.js'); const { RUN_QUALITIES } = require('../../enums/RunQualities.js'); const { IntegerComparisonDto, FloatComparisonDto } = require('./NumericalComparisonDto.js'); @@ -39,6 +40,7 @@ exports.RunFilterDto = Joi.object({ 'string.pattern.base': 'Beam modes "{{#value}}" must contain only uppercase letters and single spaces between words.', })), + beamTypes: BeamTypesDto, runNumbers: Joi.string().trim().custom(validateRange).messages({ [RANGE_INVALID]: '{{#message}}', 'string.base': 'Run numbers must be comma-separated numbers or ranges (e.g. 12,15-18)', From 326c8c147fb402beb4f95ff82e78ed95ec90a7d9 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Wed, 29 Jul 2026 11:34:01 +0200 Subject: [PATCH 2/5] Add filtering of GetAllRuns by beam type --- lib/usecases/run/GetAllRunsUseCase.js | 10 ++++ test/api/runs.test.js | 48 +++++++++++++++++++ .../usecases/run/GetAllRunsUseCase.test.js | 35 ++++++++++++++ 3 files changed, 93 insertions(+) diff --git a/lib/usecases/run/GetAllRunsUseCase.js b/lib/usecases/run/GetAllRunsUseCase.js index ae0b14d071..813b5b66fe 100644 --- a/lib/usecases/run/GetAllRunsUseCase.js +++ b/lib/usecases/run/GetAllRunsUseCase.js @@ -84,6 +84,7 @@ class GetAllRunsUseCase { gaq, detectorsQcNotBadFraction, beamModes, + beamTypes, } = filter; if (runNumbers) { @@ -119,6 +120,15 @@ class GetAllRunsUseCase { filteringQueryBuilder.where('lhcBeamMode').oneOf(...beamModes); } + if (beamTypes) { + const beamTypesList = splitStringToStringsTrimmed(beamTypes, SEARCH_ITEMS_SEPARATOR); + filteringQueryBuilder.include({ + association: 'lhcFill', + where: { beamType: { [Op.in]: beamTypesList } }, + required: true, + }); + } + if (eorReason) { const eorReasonTypeWhere = {}; if (eorReason.category) { diff --git a/test/api/runs.test.js b/test/api/runs.test.js index e3272b6cef..6f10812686 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -160,6 +160,54 @@ module.exports = () => { expect(runs).to.lengthOf(6); }); + it('should successfully filter with single beamType', async () => { + const beamType = 'p-p'; + const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamType}`); + + expect(response.status).to.equal(200); + const { data: runs } = response.body; + + expect(runs).to.be.an('array'); + expect(runs).to.have.lengthOf.greaterThan(0); + expect(runs.every(({ lhcFill }) => lhcFill?.beamType === beamType)).to.be.true; + }); + + it('should successfully filter with single beamType containing spaces around hyphen', async () => { + const beamType = 'NE10 - NE10'; + const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamType}`); + + expect(response.status).to.equal(200); + const { data: runs } = response.body; + + expect(runs).to.be.an('array'); + expect(runs).to.have.lengthOf.greaterThan(0); + expect(runs.every(({ lhcFill }) => lhcFill?.beamType === beamType)).to.be.true; + }); + + it('should successfully filter with multiple beamTypes', async () => { + const beamTypes = ['p-p', 'Pb-Pb']; + const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamTypes.join(',')}`); + + expect(response.status).to.equal(200); + const { data: runs } = response.body; + + expect(runs).to.be.an('array'); + expect(runs).to.have.lengthOf.greaterThan(0); + expect(runs.every(({ lhcFill }) => beamTypes.includes(lhcFill?.beamType))).to.be.true; + }); + + it('should return 400 if beamTypes filter has the incorrect format', async () => { + const beamTypeString = 'DOES NOT EXIST'; + const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamTypeString}`); + + expect(response.status).to.equal(400); + + const { errors: [error] } = response.body; + + expect(error.title).to.equal('Invalid Attribute'); + expect(error.detail).to.equal(`Invalid beam type format: ${beamTypeString}`); + }); + it('should return 400 if beamModes filter has the incorrect format', async () => { const beamModeString = '*THERE\'S NON LETTERS IN HERE'; const response = await request(server).get(`/api/runs?filter[beamModes]=${beamModeString}`); diff --git a/test/lib/usecases/run/GetAllRunsUseCase.test.js b/test/lib/usecases/run/GetAllRunsUseCase.test.js index febeae02aa..bd24b4cc4a 100644 --- a/test/lib/usecases/run/GetAllRunsUseCase.test.js +++ b/test/lib/usecases/run/GetAllRunsUseCase.test.js @@ -209,6 +209,41 @@ module.exports = () => { } }); + it('should successfully filter on beamTypes', async () => { + const singleBeamType = 'p-p'; + const singleBeamTypeWithSpaces = 'NE10 - NE10'; + const multipleBeamTypes = 'p-p,Pb-Pb'; + const nonExistentBeamType = 'DOES-NOT-EXIST'; + + getAllRunsDto.query = { filter: { beamTypes: singleBeamType }, page: { limit: 200 } }; + { + const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto); + expect(runs).to.have.lengthOf.greaterThan(0); + expect(runs.every(({ lhcFill }) => lhcFill?.beamType === singleBeamType)).to.be.true; + } + + getAllRunsDto.query = { filter: { beamTypes: singleBeamTypeWithSpaces }, page: { limit: 200 } }; + { + const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto); + expect(runs).to.have.lengthOf.greaterThan(0); + expect(runs.every(({ lhcFill }) => lhcFill?.beamType === singleBeamTypeWithSpaces)).to.be.true; + } + + getAllRunsDto.query = { filter: { beamTypes: multipleBeamTypes }, page: { limit: 200 } }; + { + const acceptedBeamTypes = multipleBeamTypes.split(','); + const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto); + expect(runs).to.have.lengthOf.greaterThan(0); + expect(runs.every(({ lhcFill }) => acceptedBeamTypes.includes(lhcFill?.beamType))).to.be.true; + } + + getAllRunsDto.query = { filter: { beamTypes: nonExistentBeamType } }; + { + const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto); + expect(runs).to.have.lengthOf(0); + } + }); + it('should successfully filter on run definition', async () => { const PHYSICS_COUNT = 7; const COSMICS_COUNT = 2; From a349432d02614777310880bc3226c3b3d80e2386 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Wed, 29 Jul 2026 11:34:26 +0200 Subject: [PATCH 3/5] Add front-end widget in run-overview to filter by beam type --- lib/public/views/Runs/ActiveColumns/runsActiveColumns.js | 2 ++ lib/public/views/Runs/Overview/RunsOverviewModel.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 9992a93407..1027a2ce3a 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -44,6 +44,7 @@ import { numericalComparisonFilter } from '../../../components/Filters/common/fi import { checkboxes } from '../../../components/Filters/common/filters/checkboxFilter.js'; import radioButtonFilter from '../../../components/Filters/common/filters/radioButtonFilter.js'; import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; +import { beamTypeFilter } from '../../../components/Filters/LhcFillsFilter/beamTypeFilter.js'; /** * List of active columns for a generic runs table @@ -587,6 +588,7 @@ export const runsActiveColumns = { pdpBeamType: { name: 'PDP Beam Type', visible: false, + filter: (runsOverviewModel) => beamTypeFilter(runsOverviewModel.filteringModel.get('beamTypes')), }, readoutCfgUri: { name: 'Readout Config URI', diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index c6912513b6..af658e36ad 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -35,6 +35,7 @@ import { beamModesProvider } from '../../../services/beamModes/beamModesProvider import { RadioButtonFilterModel } from '../../../components/Filters/common/RadioButtonFilterModel.js'; import { SelectionModel } from '../../../components/common/selection/SelectionModel.js'; import { TRIGGER_VALUES } from '../../../domain/enums/TriggerValue.js'; +import { BeamTypeFilterModel } from '../../../components/Filters/LhcFillsFilter/BeamTypeFilterModel.js'; /** * Model representing handlers for runs page @@ -95,6 +96,7 @@ export class RunsOverviewModel extends FilterableOverviewPageModel { dcs: new RadioButtonFilterModel([{ label: 'ANY' }, { label: 'ON', value: true }, { label: 'OFF', value: false }]), epn: new RadioButtonFilterModel([{ label: 'ANY' }, { label: 'ON', value: true }, { label: 'OFF', value: false }]), triggerValues: new SelectionModel({ availableOptions: TRIGGER_VALUES.map((value) => ({ label: value, value })) }), + beamTypes: new BeamTypeFilterModel(), }, ); From 81b629932f2d378f9f2efc9cc3ff84bdd323d96f Mon Sep 17 00:00:00 2001 From: George Raduta Date: Wed, 29 Jul 2026 12:13:57 +0200 Subject: [PATCH 4/5] Fix tests --- test/api/runs.test.js | 12 ------------ test/lib/usecases/run/GetAllRunsUseCase.test.js | 8 -------- 2 files changed, 20 deletions(-) diff --git a/test/api/runs.test.js b/test/api/runs.test.js index 6f10812686..c45a898634 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -172,18 +172,6 @@ module.exports = () => { expect(runs.every(({ lhcFill }) => lhcFill?.beamType === beamType)).to.be.true; }); - it('should successfully filter with single beamType containing spaces around hyphen', async () => { - const beamType = 'NE10 - NE10'; - const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamType}`); - - expect(response.status).to.equal(200); - const { data: runs } = response.body; - - expect(runs).to.be.an('array'); - expect(runs).to.have.lengthOf.greaterThan(0); - expect(runs.every(({ lhcFill }) => lhcFill?.beamType === beamType)).to.be.true; - }); - it('should successfully filter with multiple beamTypes', async () => { const beamTypes = ['p-p', 'Pb-Pb']; const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamTypes.join(',')}`); diff --git a/test/lib/usecases/run/GetAllRunsUseCase.test.js b/test/lib/usecases/run/GetAllRunsUseCase.test.js index bd24b4cc4a..7d4db513e5 100644 --- a/test/lib/usecases/run/GetAllRunsUseCase.test.js +++ b/test/lib/usecases/run/GetAllRunsUseCase.test.js @@ -211,7 +211,6 @@ module.exports = () => { it('should successfully filter on beamTypes', async () => { const singleBeamType = 'p-p'; - const singleBeamTypeWithSpaces = 'NE10 - NE10'; const multipleBeamTypes = 'p-p,Pb-Pb'; const nonExistentBeamType = 'DOES-NOT-EXIST'; @@ -222,13 +221,6 @@ module.exports = () => { expect(runs.every(({ lhcFill }) => lhcFill?.beamType === singleBeamType)).to.be.true; } - getAllRunsDto.query = { filter: { beamTypes: singleBeamTypeWithSpaces }, page: { limit: 200 } }; - { - const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto); - expect(runs).to.have.lengthOf.greaterThan(0); - expect(runs.every(({ lhcFill }) => lhcFill?.beamType === singleBeamTypeWithSpaces)).to.be.true; - } - getAllRunsDto.query = { filter: { beamTypes: multipleBeamTypes }, page: { limit: 200 } }; { const acceptedBeamTypes = multipleBeamTypes.split(','); From 49923a2a7c98b27b5d21159a9911a9314a1653f8 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Thu, 30 Jul 2026 09:25:37 +0200 Subject: [PATCH 5/5] Add filter with correct name --- lib/public/views/Runs/ActiveColumns/runsActiveColumns.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 1027a2ce3a..f121d791ad 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -588,6 +588,10 @@ export const runsActiveColumns = { pdpBeamType: { name: 'PDP Beam Type', visible: false, + }, + beamType: { + name: 'Beam Type', + visible: false, filter: (runsOverviewModel) => beamTypeFilter(runsOverviewModel.filteringModel.get('beamTypes')), }, readoutCfgUri: {