diff --git a/lib/database/utilities/QueryBuilder.js b/lib/database/utilities/QueryBuilder.js index a422c86bd6..80f5d5f25b 100644 --- a/lib/database/utilities/QueryBuilder.js +++ b/lib/database/utilities/QueryBuilder.js @@ -88,7 +88,7 @@ class WhereQueryBuilder { } /** - * Sets an **OR** match filter using the provided values. + * Sets an **IN** match filter using the provided values. * * @param {...any} values The required values. * @returns {QueryBuilder} The current QueryBuilder instance. @@ -408,6 +408,7 @@ class WhereAssociationQueryBuilder extends WhereQueryBuilder { /** * Sets the operation. + * If an WhereAssociation is already set it will convert to an OR condition * * @param {Object} operation The Sequelize operation to use as where filter. * @returns {QueryBuilder} The current QueryBuilder instance. diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index 5c71b5bd4a..ee53e64bf2 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -22,4 +22,5 @@ exports.LhcFillsFilterDto = Joi.object({ }), runDuration: validateTimeDuration, beamDuration: validateTimeDuration, + beamType: Joi.string(), }); diff --git a/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js new file mode 100644 index 0000000000..534e35ab1b --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js @@ -0,0 +1,36 @@ +/** + * @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 { beamTypesProvider } from '../../../services/beamTypes/beamTypesProvider.js'; +import { SelectionFilterModel } from '../common/filters/SelectionFilterModel.js'; + +/** + * Beam type filter model + */ +export class BeamTypeFilterModel extends SelectionFilterModel { + /** + * Constructor + */ + constructor() { + super({ multiple: true, allowEmpty: true }); + + beamTypesProvider.items$.observe(() => { + beamTypesProvider.items$.getCurrent().apply({ + Success: (types) => { + const beamTypes = types.map((type) => ({ value: String(type.beam_type) })); + this._selectionModel.setAvailableOptions(beamTypes); + }, + }); + }); + } +} diff --git a/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js new file mode 100644 index 0000000000..7872734704 --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js @@ -0,0 +1,26 @@ +/** + * @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. + */ + +import { checkboxes } from '../common/filters/checkboxFilter.js'; + +/** + * Renders a list of checkboxes that lets the user look for beam types + * + * @param {BeamTypeFilterModel} beamTypeFilterModel beamTypeFilterModel + * @return {Component} the filter + */ +export const beamTypeFilter = (beamTypeFilterModel) => + checkboxes( + beamTypeFilterModel.selectionModel, + { selector: 'beam-types' }, + ); diff --git a/lib/public/services/beamTypes/beamTypesProvider.js b/lib/public/services/beamTypes/beamTypesProvider.js new file mode 100644 index 0000000000..3b268c9597 --- /dev/null +++ b/lib/public/services/beamTypes/beamTypesProvider.js @@ -0,0 +1,30 @@ +/** + * @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. + */ + +import { getRemoteData } from '../../utilities/fetch/getRemoteData.js'; +import { RemoteDataProvider } from '../RemoteDataProvider.js'; + +/** + * Service class to fetch beams types from the backend + */ +export class BeamTypesProvider extends RemoteDataProvider { + /** + * @inheritDoc + */ + async getRemoteData() { + const { data } = await getRemoteData('/api/lhcFills/beamTypes'); + return data; + } +} + +export const beamTypesProvider = new BeamTypesProvider(); diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 277bcb6752..83cddcbe6e 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -26,6 +26,7 @@ import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; import { fillNumberFilter } from '../../../components/Filters/LhcFillsFilter/fillNumberFilter.js'; import { durationFilter } from '../../../components/Filters/LhcFillsFilter/durationFilter.js'; +import { beamTypeFilter } from '../../../components/Filters/LhcFillsFilter/beamTypeFilter.js'; /** * List of active columns for a lhc fills table @@ -161,6 +162,7 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-8', format: (value) => formatBeamType(value), + filter: (lhcFillModel) => beamTypeFilter(lhcFillModel.filteringModel.get('beamType')), }, collidingBunches: { name: 'Colliding bunches', diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index a3a64d138a..389be9ef13 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -17,6 +17,7 @@ import { StableBeamFilterModel } from '../../../components/Filters/LhcFillsFilte import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; +import { BeamTypeFilterModel } from '../../../components/Filters/LhcFillsFilter/BeamTypeFilterModel.js'; import { TextComparisonFilterModel } from '../../../components/Filters/common/filters/TextComparisonFilterModel.js'; /** @@ -33,11 +34,14 @@ export class LhcFillsOverviewModel extends OverviewPageModel { constructor(stableBeamsOnly = false) { super(); + this._beamTypes = []; + this._filteringModel = new FilteringModel({ fillNumbers: new RawTextFilterModel(), beamDuration: new TextComparisonFilterModel(), runDuration: new TextComparisonFilterModel(), hasStableBeams: new StableBeamFilterModel(), + beamType: new BeamTypeFilterModel(), }); this._filteringModel.observe(() => this._applyFilters()); @@ -70,6 +74,13 @@ export class LhcFillsOverviewModel extends OverviewPageModel { return buildUrl('/api/lhcFills', params); } + /** + * Getter + */ + getBeamTypes() { + return this._beamTypes; + } + /** * Returns all filtering, sorting and pagination settings to their default values * @param {boolean} [fetch = true] whether to refetch all data after filters have been reset diff --git a/lib/server/controllers/lhcFill.controller.js b/lib/server/controllers/lhcFill.controller.js index 6624fed3a2..3637bf190b 100644 --- a/lib/server/controllers/lhcFill.controller.js +++ b/lib/server/controllers/lhcFill.controller.js @@ -35,6 +35,7 @@ const { ApiConfig } = require('../../config/index.js'); const { DtoFactory } = require('../../domain/dtos/DtoFactory.js'); const Joi = require('joi'); const { logService } = require('../services/log/LogService.js'); +const { getAllBeamTypes } = require('../services/beam/getAllBeamTypes.js'); const { updateExpressResponseFromNativeError } = require('../express/updateExpressResponseFromNativeError.js'); const { lhcFillService } = require('../services/lhcFill/LhcFillService.js'); const { runToHttpView } = require('./runsToHttpView.js'); @@ -256,6 +257,30 @@ const getAllLhcFillsWithStableBeamsEndedInPeriodHandler = async (request, respon } }; +/** + * Retrieve a list of unique beam types + * + * @param {Object} _request The *request* object represents the HTTP request and has properties for the request query + * string, parameters, body, HTTP headers, and so on. + * @param {Object} response The *response* object represents the HTTP response that an Express app sends when it gets + * an HTTP request. + * @param {NextFunction} _next The *next* object represents the next middleware function which is used to pass control to + * the next middleware function. + * @returns {undefined} + */ +const listBeamTypes = async (_request, response, _next) => { + try { + const beamTypes = await getAllBeamTypes(); + if (beamTypes && beamTypes.length > 0) { + response.status(200).json({ data: beamTypes }); + } else { + response.status(204).json({ data: [] }); + } + } catch { + response.status(502).json({ errors: ['Unable to retrieve list of beam types'] }); + } +}; + module.exports = { createLhcFill, listLhcFills, @@ -265,4 +290,5 @@ module.exports = { getLhcFillById, getAllLhcFillLogsHandler, getAllLhcFillsWithStableBeamsEndedInPeriodHandler, + listBeamTypes, }; diff --git a/lib/server/routers/lhcFills.router.js b/lib/server/routers/lhcFills.router.js index 7a99cfe85f..2b33cedb8c 100644 --- a/lib/server/routers/lhcFills.router.js +++ b/lib/server/routers/lhcFills.router.js @@ -24,6 +24,11 @@ module.exports = { method: 'post', controller: LhcFillsController.createLhcFill, }, + { + method: 'get', + path: 'beamTypes', + controller: LhcFillsController.listBeamTypes, + }, { path: 'stable-beams-ended-within', method: 'get', diff --git a/lib/server/services/beam/getAllBeamTypes.js b/lib/server/services/beam/getAllBeamTypes.js new file mode 100644 index 0000000000..da4334e954 --- /dev/null +++ b/lib/server/services/beam/getAllBeamTypes.js @@ -0,0 +1,34 @@ +/** + * @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 { repositories: { LhcFillRepository }, sequelize } = require('../../../database'); +const { Op } = require('sequelize'); + +/** + * Return the a list of unique beam types which is built from the runs data + * + * @returns {Promise} Promise resolving with the list of unique beam types + */ +exports.getAllBeamTypes = async () => { + const beamTypes = await LhcFillRepository.findAll({ + attributes: [[sequelize.fn('DISTINCT', sequelize.col('beam_type')), 'beam_type']], + where: { + beam_type: { + [Op.ne]: null, + }, + }, + order: [['beam_type', 'ASC']], + raw: true, + }); + return beamTypes; +}; diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index e0f0e9db55..a06e751c63 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -47,7 +47,7 @@ class GetAllLhcFillsUseCase { let associatedStatisticsRequired = false; if (filter) { - const { hasStableBeams, fillNumbers, beamDuration, runDuration } = filter; + const { hasStableBeams, fillNumbers, beamDuration, runDuration, beamType } = filter; if (hasStableBeams) { // For now, if a stableBeamsStart is present, then a beam is stable queryBuilder.where('stableBeamsStart').not().is(null); @@ -85,6 +85,12 @@ class GetAllLhcFillsUseCase { if (beamDuration?.limit !== undefined && beamDuration?.operator) { queryBuilder.where('stableBeamsDuration').applyOperator(beamDuration.operator, beamDuration.limit); } + + // Beams type. + if (beamType) { + const beamTypes = beamType.split(','); + queryBuilder.where('beamType').oneOf(beamTypes); + } } const { count, rows } = await TransactionHelper.provide(async () => { diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index 8557945f9e..34b5b4fb2d 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -519,6 +519,58 @@ module.exports = () => { done(); }); }); + + it('should return 200 and an LHCFill array for beam types filter, correct', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamType]=Pb-Pb') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(1); + expect(res.body.data[0].fillNumber).to.equal(3); + + done(); + }); + }); + + it('should return 200 and an LHCFill array for beam types filter, multiple correct', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamType]=Pb-Pb,p-p,p-Pb') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(4); + expect(res.body.data[0].fillNumber).to.equal(4); + + done(); + }); + }); + + // API accepts filters that do not exist, this is because it does not affect the results + it('should return 200 for beam types filter, one wrong', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamType]=Pb-Pb,Jasper-Jasper,p-p,p-Pb') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(4); + expect(res.body.data[0].fillNumber).to.equal(4); + + done(); + }); + }); }); describe('POST /api/lhcFills', () => { it('should return 201 if valid data is provided', async () => { diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index dc0f317999..dd4c09f2d9 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -254,4 +254,29 @@ module.exports = () => { expect(lhcFill.statistics.runsCoverage).greaterThan(23459) }); }) + + it('should only contain specified beam types, {p-p, PROTON-PROTON, Pb-Pb}', async () => { + const beamTypes = ['p-p', ' PROTON-PROTON', 'Pb-Pb'] + + getAllLhcFillsDto.query = { filter: { beamType: beamTypes.join(',') } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + + expect(lhcFills).to.be.an('array').and.lengthOf(4) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.beamType).oneOf(beamTypes) + }); + }) + + it('should only contain specified beam types, {p-p, PROTON-PROTON, Pb-Pb, null}', async () => { + let beamTypes = ['p-p', ' PROTON-PROTON', 'Pb-Pb', 'null'] + + getAllLhcFillsDto.query = { filter: { beamType: beamTypes.join(',') } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + + expect(lhcFills).to.be.an('array').and.lengthOf(4) + + lhcFills.forEach((lhcFill) => { + expect(lhcFill.beamType).oneOf(beamTypes) + }); + }) }; diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 47e320b01b..14f541a956 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -275,6 +275,7 @@ module.exports = () => { const filterRunDurationExpect = {selector: 'div.flex-row:nth-child(4) > div:nth-child(1)', value: 'Total runs duration'} const filterRunDurationPlaceholderExpect = {selector: '#run-duration-filter-operand', value: 'e.g 16:14:15 (HH:MM:SS)'}; const filterSBDurationOperatorExpect = { value: true }; + const filterBeamTypeExpect = {selector: 'div.flex-row:nth-child(5) > div:nth-child(1)', value: 'Beam Type'} await goToPage(page, 'lhc-fill-overview'); @@ -288,6 +289,7 @@ module.exports = () => { await expectAttributeValue(page, filterSBDurationPlaceholderExpect.selector, 'placeholder', filterSBDurationPlaceholderExpect.value); await expectInnerText(page, filterRunDurationExpect.selector, filterRunDurationExpect.value); await expectAttributeValue(page, filterRunDurationPlaceholderExpect.selector, 'placeholder', filterRunDurationPlaceholderExpect.value); + await expectInnerText(page, filterBeamTypeExpect.selector, filterBeamTypeExpect.value); }); it('should successfully un-apply Stable Beam filter menu', async () => { @@ -333,4 +335,16 @@ module.exports = () => { await fillInput(page, filterRunDurationOperand, '00:00:00', ['change']); await waitForTableLength(page, 5); }); + + it('should successfully apply beam types filter', async () => { + const filterBeamTypeP_Pb = '#beam-types-checkbox-p-Pb'; + const filterBeamTypePb_Pb = '#beam-types-checkbox-Pb-Pb'; + await goToPage(page, 'lhc-fill-overview'); + await waitForTableLength(page, 5); + await openFilteringPanel(page); + await pressElement(page, filterBeamTypeP_Pb); + await waitForTableLength(page, 1); + await pressElement(page, filterBeamTypePb_Pb); + await waitForTableLength(page, 2); + }); };