From 36bdd8d648d87e8016934ded21f3cce983bc9c00 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Tue, 2 Dec 2025 17:40:35 +0100 Subject: [PATCH 1/4] [O2B-1506] Frontend run duration filter code --- .../LhcFillsFilter/runDurationFilter.js | 32 +++++++++++++++++++ .../ActiveColumns/lhcFillsActiveColumns.js | 6 ++++ .../Overview/LhcFillsOverviewModel.js | 26 ++++++++++++++- 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js 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..3b5c4779bb 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 @@ -139,6 +140,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), + ), }, runsCoverage: { name: 'Total runs duration', 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; } /** From b7fe81091a68fc4572a65fc39a66c683528a6234 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 4 Dec 2025 14:14:21 +0100 Subject: [PATCH 2/4] [O2B-1506] Run duration filter tests + backend --- lib/domain/dtos/filters/LhcFillsFilterDto.js | 4 ++ .../ActiveColumns/lhcFillsActiveColumns.js | 10 ++-- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 16 +++++- .../lhcFill/GetAllLhcFillsUseCase.test.js | 51 +++++++++++++++++++ test/public/lhcFills/overview.test.js | 6 ++- 5 files changed, 80 insertions(+), 7 deletions(-) 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/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 3b5c4779bb..1a0cd8aa9d 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -140,17 +140,17 @@ 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), - ), }, runsCoverage: { name: 'Total runs duration', 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/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index d994661202..71488d2836 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,13 @@ class GetAllLhcFillsUseCase { queryBuilder.where('fillNumber').oneOf(...finalFillnumberList); } } + + // Run duration filter and corresponding operator. + if (runDuration && runDurationOperator) { + associatedStatisticsRequired = true; + 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 +83,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..4c7965da15 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -160,4 +160,55 @@ 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.runDuration).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.runDuration).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.runDuration).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.runDuration).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.runDuration).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 () => { From 0751b574d3860c6d9a2441ad80446f9ae8347d62 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 8 Dec 2025 15:57:32 +0100 Subject: [PATCH 3/4] [O2B-1506] Fixed GehAllLhcFillsUseCase run duration tests --- .../usecases/lhcFill/GetAllLhcFillsUseCase.test.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index 4c7965da15..8140ce7a1d 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); @@ -167,7 +166,7 @@ module.exports = () => { expect(lhcFills).to.be.an('array').and.lengthOf(1) lhcFills.forEach((lhcFill) => { - expect(lhcFill.runDuration).greaterThan(14400) + expect(lhcFill.statistics.runsCoverage).greaterThan(14400) }); }) @@ -177,7 +176,7 @@ module.exports = () => { expect(lhcFills).to.be.an('array').and.lengthOf(1) lhcFills.forEach((lhcFill) => { - expect(lhcFill.runDuration).greaterThan(18000) + expect(lhcFill.statistics.runsCoverage).greaterThan(18000) }); }) @@ -187,7 +186,7 @@ module.exports = () => { expect(lhcFills).to.be.an('array').and.lengthOf(1) lhcFills.forEach((lhcFill) => { - expect(lhcFill.runDuration).greaterThan(18000) + expect(lhcFill.statistics.runsCoverage).greaterThan(18000) }); }) @@ -198,7 +197,7 @@ module.exports = () => { expect(lhcFills).to.be.an('array').and.lengthOf(1) lhcFills.forEach((lhcFill) => { - expect(lhcFill.runDuration).greaterThan(18000) + expect(lhcFill.statistics.runsCoverage).greaterThan(18000) }); }) @@ -208,7 +207,7 @@ module.exports = () => { expect(lhcFills).to.be.an('array').and.lengthOf(1) lhcFills.forEach((lhcFill) => { - expect(lhcFill.runDuration).greaterThan(23459) + expect(lhcFill.statistics.runsCoverage).greaterThan(23459) }); }) }; From 6f77e0cee3342188e074c7762838400917c63934 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Tue, 16 Dec 2025 10:35:49 +0100 Subject: [PATCH 4/4] [O2B-1506] Fixed 00:00:00 filter, added test to cover condition --- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 6 ++++-- .../lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 71488d2836..e961548817 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -65,9 +65,11 @@ class GetAllLhcFillsUseCase { } // Run duration filter and corresponding operator. - if (runDuration && runDurationOperator) { + if (runDuration !== null && runDuration !== undefined && runDurationOperator) { associatedStatisticsRequired = true; - queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator(runDurationOperator, runDuration); + // 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. diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index 8140ce7a1d..6c317588ff 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -190,6 +190,16 @@ module.exports = () => { }); }) + 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: '<=' } };