diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index 8d0ab51242..5b525c15c6 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -19,6 +19,10 @@ exports.LhcFillsFilterDto = Joi.object({ fillNumbers: Joi.string().trim().custom(validateRange).messages({ 'any.invalid': '{{#message}}', }), + runDuration: Joi.string().trim().min(8).max(8).custom(validateTime).messages({ + 'any.invalid': '{{#message}}', + }), + runDurationOperator: Joi.string().trim().min(1).max(2), beamDuration: { limit: Joi.string().trim().min(8).max(8).custom(validateTime).messages({ 'any.invalid': '{{#message}}', diff --git a/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js b/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js new file mode 100644 index 0000000000..a00e326c48 --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js @@ -0,0 +1,31 @@ +/** + * @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 { comparisonOperatorFilter } from '../common/filters/comparisonOperatorFilter.js'; +import { rawTextFilter } from '../common/filters/rawTextFilter.js'; + +/** + * Component to filter LHC-fills by run duration + * + * @param {TextComparisonFilterModel} runDurationFilterModel runDurationFilterModel + * @returns {Component} the text field + */ +export const runDurationFilter = (runDurationFilterModel) => { + const amountFilter = rawTextFilter( + runDurationFilterModel.operandInputModel, + { classes: ['w-100', 'run-duration-filter'], placeholder: 'e.g 16:14:15 (HH:MM:SS)' }, + ); + + return comparisonOperatorFilter(amountFilter, runDurationFilterModel.operatorSelectionModel.value, (value) => + runDurationFilterModel.operatorSelectionModel.select(value)); +}; diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 8d0ef13e1e..4b0edeb0b7 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 { beamDurationFilter } from '../../../components/Filters/LhcFillsFilter/beamDurationFilter.js'; +import { runDurationFilter } from '../../../components/Filters/LhcFillsFilter/runDurationFilter.js'; /** * List of active columns for a lhc fills table @@ -141,6 +142,7 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-8', format: (duration) => formatDuration(duration), + filter: (lhcFillModel) => runDurationFilter(lhcFillModel.filteringModel.get('runDuration')), }, efficiency: { name: 'Fill Efficiency', diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 274d711193..9bdb5f68f4 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -38,6 +38,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { this._filteringModel = new FilteringModel({ fillNumbers: new RawTextFilterModel(), beamDuration: new TextComparisonFilterModel(), + runDuration: new TextComparisonFilterModel(), hasStableBeams: new StableBeamFilterModel(), }); diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 30321645e2..eb26bc552c 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -44,8 +44,10 @@ class GetAllLhcFillsUseCase { const queryBuilder = new QueryBuilder(); + let associatedStatisticsRequired = false; + if (filter) { - const { hasStableBeams, fillNumbers, beamDuration } = filter; + const { hasStableBeams, fillNumbers, beamDuration, runDurationOperator, runDuration } = filter; if (hasStableBeams) { // For now, if a stableBeamsStart is present, then a beam is stable queryBuilder.where('stableBeamsStart').not().is(null); @@ -62,6 +64,15 @@ class GetAllLhcFillsUseCase { : queryBuilder.where('fillNumber').oneOf(...finalFillnumberList); } } + + // Run duration filter and corresponding operator. + if (runDuration !== null && runDuration !== undefined && runDurationOperator) { + associatedStatisticsRequired = true; + // 00:00:00 aka 0 value is saved in the DB as null + runDuration === 0 ? queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator(runDurationOperator, null) + : queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator(runDurationOperator, runDuration); + } + // Beam duration filter, limit and corresponding operator. if (beamDuration?.limit !== undefined && beamDuration?.operator) { const beamDurationLimit = Number(beamDuration.limit) === 0 ? null : beamDuration.limit; @@ -75,6 +86,11 @@ class GetAllLhcFillsUseCase { where: { definition: RunDefinition.PHYSICS }, required: false, }); + queryBuilder.include({ + association: 'statistics', + required: associatedStatisticsRequired, + }); + queryBuilder.orderBy('fillNumber', 'desc'); queryBuilder.limit(limit); queryBuilder.offset(offset); diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index f0f9c89cae..3e0a324747 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -102,7 +102,6 @@ module.exports = () => { }) // Beam duration filter tests - it('should only contain specified stable beam durations, < 12:00:00', async () => { getAllLhcFillsDto.query = { filter: { beamDuration: {limit: '43200', operator: '<'} } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); @@ -160,4 +159,65 @@ module.exports = () => { expect(lhcFill.stableBeamsDuration).equals(null) }); }) + + it('should only contain specified total run duration, > 04:00:00', async () => { + getAllLhcFillsDto.query = { filter: { runDuration: '14400', runDurationOperator: '>' } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + + expect(lhcFills).to.be.an('array').and.lengthOf(1) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.statistics.runsCoverage).greaterThan(14400) + }); + }) + + it('should only contain specified total run duration, >= 05:00:00', async () => { + getAllLhcFillsDto.query = { filter: { runDuration: '18000', runDurationOperator: '>=' } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + + expect(lhcFills).to.be.an('array').and.lengthOf(1) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.statistics.runsCoverage).greaterThan(18000) + }); + }) + + it('should only contain specified total run duration, = 05:00:00', async () => { + getAllLhcFillsDto.query = { filter: { runDuration: '18000', runDurationOperator: '=' } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + + expect(lhcFills).to.be.an('array').and.lengthOf(1) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.statistics.runsCoverage).greaterThan(18000) + }); + }) + + it('should only contain specified total run duration, = 00:00:00', async () => { + // Tests the usecase's ability to replace the request for 0 to a request for null. + getAllLhcFillsDto.query = { filter: { runDuration: 0, runDurationOperator: '=' } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + + expect(lhcFills).to.be.an('array').and.lengthOf(4) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.statistics.runsCoverage).equals(0) + }); + }) + + it('should only contain specified total run duration, <= 05:00:00', async () => { + getAllLhcFillsDto.query = { filter: { runDuration: '18000', runDurationOperator: '<=' } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + + expect(lhcFills).to.be.an('array').and.lengthOf(1) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.statistics.runsCoverage).greaterThan(18000) + }); + }) + + it('should only contain specified total run duration, < 06:30:59', async () => { + getAllLhcFillsDto.query = { filter: { runDuration: '23459', runDurationOperator: '<' } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + + expect(lhcFills).to.be.an('array').and.lengthOf(1) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.statistics.runsCoverage).greaterThan(23459) + }); + }) }; diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 1b69518047..c67a1ebb3c 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -270,7 +270,9 @@ module.exports = () => { const filterSBExpect = { selector: '.stableBeams-filter .w-30', value: 'Stable Beams Only' }; const filterFillNRExpect = {selector: 'div.items-baseline:nth-child(1) > div:nth-child(1)', value: 'Fill #'} const filterSBDurationExpect = {selector: 'div.items-baseline:nth-child(3) > div:nth-child(1)', value: 'SB Duration'} - const filterSBDurationPlaceholderExpect = {selector: 'input.w-100:nth-child(2)', value: 'e.g 16:14:15 (HH:MM:SS)'} + const filterSBDurationPlaceholderExpect = {selector: '.beam-duration-filter', value: 'e.g 16:14:15 (HH:MM:SS)'} + const filterRunDurationExpect = {selector: 'div.flex-row:nth-child(4) > div:nth-child(1)', value: 'Total runs duration'} + const filterRunDurationPlaceholderExpect = {selector: '.run-duration-filter', value: 'e.g 16:14:15 (HH:MM:SS)'} await goToPage(page, 'lhc-fill-overview'); @@ -280,6 +282,8 @@ module.exports = () => { await expectInnerText(page, filterFillNRExpect.selector, filterFillNRExpect.value); await expectInnerText(page, filterSBDurationExpect.selector, filterSBDurationExpect.value); await expectAttributeValue(page, filterSBDurationPlaceholderExpect.selector, 'placeholder', filterSBDurationPlaceholderExpect.value); + await expectInnerText(page, filterRunDurationExpect.selector, filterRunDurationExpect.value); + await expectAttributeValue(page, filterRunDurationPlaceholderExpect.selector, 'placeholder', filterRunDurationPlaceholderExpect.value); }); it('should successfully un-apply Stable Beam filter menu', async () => {