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)', diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 9992a93407..f121d791ad 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 @@ -588,6 +589,11 @@ export const runsActiveColumns = { name: 'PDP Beam Type', visible: false, }, + beamType: { + name: 'Beam Type', + visible: false, + filter: (runsOverviewModel) => beamTypeFilter(runsOverviewModel.filteringModel.get('beamTypes')), + }, readoutCfgUri: { name: 'Readout Config URI', visible: false, 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(), }, ); 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..c45a898634 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -160,6 +160,42 @@ 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 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..7d4db513e5 100644 --- a/test/lib/usecases/run/GetAllRunsUseCase.test.js +++ b/test/lib/usecases/run/GetAllRunsUseCase.test.js @@ -209,6 +209,33 @@ module.exports = () => { } }); + it('should successfully filter on beamTypes', async () => { + const singleBeamType = 'p-p'; + 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: 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;