diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index 537d5a95e2..7c5d6853f7 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -23,4 +23,8 @@ exports.LhcFillsFilterDto = Joi.object({ 'any.invalid': '{{#message}}', }), beamDurationOperator: Joi.string().trim().min(1).max(2), + runDuration: Joi.string().trim().min(8).max(8).custom(validateTime).messages({ + 'any.invalid': '{{#message}}', + }), + runDurationOperator: Joi.string().trim().min(1).max(2), }); diff --git a/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js b/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js new file mode 100644 index 0000000000..c4d3cca920 --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js @@ -0,0 +1,32 @@ +/** + * @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 {rawTextFilter} runDurationFilterModel runDurationFilterModel + * @param {string} runDurationOperator run duration operator value + * @param {(string) => undefined} runDurationOperatorUpdate run duration operator setter function + * @returns {Component} the text field + */ +export const runDurationFilter = (runDurationFilterModel, runDurationOperator, runDurationOperatorUpdate) => { + const amountFilter = rawTextFilter( + runDurationFilterModel, + { classes: ['w-100', 'run-duration-filter'], placeholder: 'e.g 16:14:15 (HH:MM:SS)' }, + ); + + return comparisonOperatorFilter(amountFilter, runDurationOperator, (value) => runDurationOperatorUpdate(value)); +}; diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index b60220adb0..1a0cd8aa9d 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 @@ -145,6 +146,11 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-8', format: (duration) => formatDuration(duration), + filter: (lhcFillModel) => runDurationFilter( + lhcFillModel.filteringModel.get('runDuration'), + lhcFillModel.getRunDurationOperator(), + (value) => lhcFillModel.setRunDurationOperator(value), + ), }, efficiency: { name: 'Fill Efficiency', diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 5dc83a4365..9837d58473 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -19,6 +19,7 @@ import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; const defaultBeamDurationOperator = '='; +const defaultRunDurationOperator = '='; /** * Model for the LHC fills overview page @@ -37,10 +38,12 @@ export class LhcFillsOverviewModel extends OverviewPageModel { this._filteringModel = new FilteringModel({ fillNumbers: new RawTextFilterModel(), beamDuration: new RawTextFilterModel(), + runDuration: new RawTextFilterModel(), hasStableBeams: new StableBeamFilterModel(), }); this._beamDurationOperator = defaultBeamDurationOperator; + this._runDurationOperator = defaultRunDurationOperator; this._filteringModel.observe(() => this._applyFilters(true)); this._filteringModel.visualChange$.bubbleTo(this); @@ -71,10 +74,29 @@ export class LhcFillsOverviewModel extends OverviewPageModel { ...this._filteringModel.get('beamDuration').isEmpty === false && { 'filter[beamDurationOperator]': this._beamDurationOperator, }, + ...this._filteringModel.get('runDuration').isEmpty === false && { + 'filter[runDurationOperator]': this._runDurationOperator, + }, }; return buildUrl('/api/lhcFills', params); } + /** + * Setter function for runDurationOperator + */ + setRunDurationOperator(runDurationOperator) { + this._runDurationOperator = runDurationOperator; + this._applyFilters(); + this.notify(); + } + + /** + * Run duration operator getter + */ + getRunDurationOperator() { + return this._runDurationOperator; + } + /** * Beam duration operator setter */ @@ -109,6 +131,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { resetFiltering(fetch = true) { this._filteringModel.reset(); this._beamDurationOperator = defaultBeamDurationOperator; + this._runDurationOperator = defaultRunDurationOperator; if (fetch) { this._applyFilters(true); @@ -121,7 +144,8 @@ export class LhcFillsOverviewModel extends OverviewPageModel { */ isAnyFilterActive() { return this._filteringModel.isAnyFilterActive() - || this._beamDurationOperator !== defaultBeamDurationOperator; + || this._beamDurationOperator !== defaultBeamDurationOperator + || this._runDurationOperator !== defaultRunDurationOperator; } /** diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index d994661202..e961548817 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, beamDurationOperator, beamDuration } = filter; + const { hasStableBeams, fillNumbers, beamDurationOperator, beamDuration, runDurationOperator, runDuration } = filter; if (hasStableBeams) { // For now, if a stableBeamsStart is present, then a beam is stable queryBuilder.where('stableBeamsStart').not().is(null); @@ -61,6 +63,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 and corresponding operator. if (beamDuration !== null && beamDuration !== undefined && beamDurationOperator) { beamDuration === 0 ? queryBuilder.where('stableBeamsDuration').applyOperator(beamDurationOperator, null) @@ -74,6 +85,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 46cdc64a62..6c317588ff 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: '43200', beamDurationOperator: '<' } }; 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 () => {