From 057c23588aee890905d701174cb0bed25bf9eb3e Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 26 Nov 2025 10:58:57 +0100 Subject: [PATCH 01/76] [O2B-1502] Filter model setup boilerplate code. --- .../Overview/LhcFillsOverviewModel.js | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 99d95cb271..7be2d8695d 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -11,6 +11,7 @@ * or submit itself to any jurisdiction. */ +import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; @@ -28,7 +29,13 @@ export class LhcFillsOverviewModel extends OverviewPageModel { constructor(stableBeamsOnly = false) { super(); + this._filteringModel = new FilteringModel({}); this._stableBeamsOnly = stableBeamsOnly; + + this._filteringModel.observe(() => this._applyFilters(true)); + this._filteringModel.visualChange$.bubbleTo(this); + + this.reset(false); } /** @@ -79,6 +86,57 @@ export class LhcFillsOverviewModel extends OverviewPageModel { return this._stableBeamsOnly; } + /** + * 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 + * @return {void} + */ + reset(fetch = true) { + super.reset(); + this.resetFiltering(fetch); + } + + /** + * Reset all filtering models + * @param {boolean} fetch Whether to refetch all data after filters have been reset + * @return {void} + */ + resetFiltering(fetch = true) { + this._filteringModel.reset(); + + if (fetch) { + this._applyFilters(true); + } + } + + /** + * Checks if any filter value has been modified from their default (empty) + * @return {Boolean} If any filter is active + */ + isAnyFilterActive() { + return this._filteringModel.isAnyFilterActive(); + } + + /** + * Return the filtering model + * + * @return {FilteringModel} the filtering model + */ + get filteringModel() { + return this._filteringModel; + } + + /** + * Returns the list of URL params corresponding to the currently applied filter + * + * @return {Object} the URL params + * + * @private + */ + _getFilterQueryParams() { + return {}; + } + /** * Apply the current filtering and update the remote data list * From a7a9eda5085ee58d82f17c4d494d0dd00607a67a Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 26 Nov 2025 11:06:30 +0100 Subject: [PATCH 02/76] [O2B-1502] Add filter button on LHC-fills overview page. --- lib/public/views/LhcFills/Overview/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/public/views/LhcFills/Overview/index.js b/lib/public/views/LhcFills/Overview/index.js index 32c12b6b4f..513e915a2c 100644 --- a/lib/public/views/LhcFills/Overview/index.js +++ b/lib/public/views/LhcFills/Overview/index.js @@ -19,6 +19,7 @@ import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplay import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { switchInput } from '../../../components/common/form/switchInput.js'; +import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; const TABLEROW_HEIGHT = 53.3; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -63,6 +64,7 @@ const showLhcFillsTable = (lhcFillsOverviewModel) => { return [ h('.flex-row.header-container.g2.pv2', [ frontLink(h('button.btn.btn-primary', 'Statistics'), 'statistics'), + filtersPanelPopover(lhcFillsOverviewModel, lhcFillsActiveColumns), toggleStableBeamOnlyFilter(lhcFillsOverviewModel), ]), h('.w-100.flex-column', [ From 2648f1ce2036b079434ac443e361b1ef0bb8da71 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 26 Nov 2025 12:09:23 +0100 Subject: [PATCH 03/76] [O2B-1502] Added stable beams only to filter --- .../LhcFillsFilter/stableBeamFilter.js | 27 +++++++++++++++++++ .../ActiveColumns/lhcFillsActiveColumns.js | 7 +++++ .../Overview/LhcFillsOverviewModel.js | 7 +++-- lib/public/views/LhcFills/Overview/index.js | 15 +---------- test/public/lhcFills/overview.test.js | 10 ++++++- 5 files changed, 49 insertions(+), 17 deletions(-) create mode 100644 lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js diff --git a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js new file mode 100644 index 0000000000..b0c6aea092 --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js @@ -0,0 +1,27 @@ +/** + * @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 { switchInput } from '../../common/form/switchInput.js'; + +/** + * Display a toggle switch to display stable beams only + * + * @param {LhcFillsOverviewModel} lhcFillsOverviewModel the overview model + * @returns {Component} the toggle switch + */ +export const toggleStableBeamOnlyFilter = (lhcFillsOverviewModel) => { + const isStableBeamsOnly = lhcFillsOverviewModel.getStableBeamsOnly(); + return switchInput(isStableBeamsOnly, (newState) => { + lhcFillsOverviewModel.setStableBeamsFilter(newState); + }, { labelAfter: 'STABLE BEAM ONLY' }); +}; diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 5efda8b1cb..4b03b1da2b 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -23,6 +23,7 @@ import { buttonLinkWithDropdown } from '../../../components/common/selection/inf import { infologgerLinksComponents } from '../../../components/common/externalLinks/infologgerLinksComponents.js'; import { formatBeamType } from '../../../utilities/formatting/formatBeamType.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; +import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; /** * List of active columns for a lhc fills table @@ -84,6 +85,12 @@ export const lhcFillsActiveColumns = { }, }, }, + stableBeams: { + name: 'Stable beams', + visible: false, + format: (boolean) => boolean ? 'On' : 'Off', + filter: toggleStableBeamOnlyFilter, + }, stableBeamsDuration: { name: 'SB Duration', visible: true, diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 7be2d8695d..da0bb6c698 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -82,7 +82,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * * @return {boolean} true if the stable beams filter is active */ - isStableBeamsOnly() { + getStableBeamsOnly() { return this._stableBeamsOnly; } @@ -104,6 +104,8 @@ export class LhcFillsOverviewModel extends OverviewPageModel { resetFiltering(fetch = true) { this._filteringModel.reset(); + this._stableBeamsOnly = true; + if (fetch) { this._applyFilters(true); } @@ -114,7 +116,8 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @return {Boolean} If any filter is active */ isAnyFilterActive() { - return this._filteringModel.isAnyFilterActive(); + return this._filteringModel.isAnyFilterActive() + || this._stableBeamsOnly == false; } /** diff --git a/lib/public/views/LhcFills/Overview/index.js b/lib/public/views/LhcFills/Overview/index.js index 513e915a2c..5de64d5989 100644 --- a/lib/public/views/LhcFills/Overview/index.js +++ b/lib/public/views/LhcFills/Overview/index.js @@ -18,26 +18,13 @@ import { lhcFillsActiveColumns } from '../ActiveColumns/lhcFillsActiveColumns.js import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; -import { switchInput } from '../../../components/common/form/switchInput.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; +import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; const TABLEROW_HEIGHT = 53.3; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; const PAGE_USED_HEIGHT = 230; -/** - * Display a toggle switch to display stable beams only - * - * @param {LhcFillsOverviewModel} lhcFillsOverviewModel the overview model - * @returns {Component} the toggle switch - */ -export const toggleStableBeamOnlyFilter = (lhcFillsOverviewModel) => { - const isStableBeamsOnly = lhcFillsOverviewModel.isStableBeamsOnly(); - return switchInput(isStableBeamsOnly, (newState) => { - lhcFillsOverviewModel.setStableBeamsFilter(newState); - }, { labelAfter: 'STABLE BEAM ONLY' }); -}; - /** * The function to load the lhcFills overview * @param {Model} model The overall model object. diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 7ca7935b92..3bd12c3991 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -163,7 +163,7 @@ module.exports = () => { await page.waitForSelector(`body > div:nth-child(3) > div:nth-child(1)`); await expectInnerText(page, `#copy-6 > div:nth-child(1)`, 'Copy Fill Number') - await expectLink(page, 'body > div:nth-child(3) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > a:nth-child(3)', { + await expectLink(page, 'body > div:nth-child(4) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > a:nth-child(3)', { href: `http://localhost:4000/?page=log-create&lhcFillNumbers=6`, innerText: ' Add log to this fill' }) // disable the popover @@ -264,6 +264,14 @@ module.exports = () => { await expectInnerText(page, efficiencyExpect.selector, efficiencyExpect.value); }); + it('should successfully display filter elements', async () => { + const efficiencyExpect = { selector: 'tbody tr:nth-child(1) td:nth-child(8)', value: '41.67%' }; + + await goToPage(page, 'lhc-fill-overview'); + + await expectInnerText(page, beamTypeExpect.selector, beamTypeExpect.value); + }); + it('should successfully toggle to stable beam only', async () => { await waitForTableLength(page, 5); await pressElement(page, '.slider.round'); From 094e6c55d275f3f151abf2027a94dbeb2ff63397 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 26 Nov 2025 15:25:45 +0100 Subject: [PATCH 04/76] [O2B-1502] Filtering with Stable Beams Only works, radioButton element extracted and reused. --- .../LhcFillsFilter/stableBeamFilter.js | 31 ++++++++-- .../components/Filters/RunsFilter/dcs.js | 48 ++++++++-------- .../components/Filters/RunsFilter/ddflp.js | 48 ++++++++-------- .../components/Filters/RunsFilter/epn.js | 48 ++++++++-------- .../common/form/inputs/RadioButton.js | 57 +++++++++++++++++++ .../ActiveColumns/lhcFillsActiveColumns.js | 4 +- .../Overview/LhcFillsOverviewModel.js | 11 ---- test/public/lhcFills/overview.test.js | 21 +++++-- 8 files changed, 172 insertions(+), 96 deletions(-) create mode 100644 lib/public/components/common/form/inputs/RadioButton.js diff --git a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js index b0c6aea092..d1cb8608d2 100644 --- a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js @@ -11,17 +11,40 @@ * or submit itself to any jurisdiction. */ +import { h } from '/js/src/index.js'; import { switchInput } from '../../common/form/switchInput.js'; +import radiobutton from '../../common/form/inputs/RadioButton.js'; /** * Display a toggle switch to display stable beams only * * @param {LhcFillsOverviewModel} lhcFillsOverviewModel the overview model + * @param radioButtonMode * @returns {Component} the toggle switch */ -export const toggleStableBeamOnlyFilter = (lhcFillsOverviewModel) => { +export const toggleStableBeamOnlyFilter = (lhcFillsOverviewModel, radioButtonMode = false) => { const isStableBeamsOnly = lhcFillsOverviewModel.getStableBeamsOnly(); - return switchInput(isStableBeamsOnly, (newState) => { - lhcFillsOverviewModel.setStableBeamsFilter(newState); - }, { labelAfter: 'STABLE BEAM ONLY' }); + const name = 'stableBeamsOnlyRadio'; + const label1 = 'OFF'; + const label2 = 'ON'; + if (radioButtonMode) { + return h('.form-group-header.flex-row.w-100', [ + radiobutton({ + label: label1, + isChecked: isStableBeamsOnly === false, + action: () => lhcFillsOverviewModel.setStableBeamsFilter(false), + name: name, + }), + radiobutton({ + label: label2, + isChecked: isStableBeamsOnly === true, + action: () => lhcFillsOverviewModel.setStableBeamsFilter(true), + name: name, + }), + ]); + } else { + return switchInput(isStableBeamsOnly, (newState) => { + lhcFillsOverviewModel.setStableBeamsFilter(newState); + }, { labelAfter: 'STABLE BEAM ONLY' }); + } }; diff --git a/lib/public/components/Filters/RunsFilter/dcs.js b/lib/public/components/Filters/RunsFilter/dcs.js index 4486b98390..807567d2f2 100644 --- a/lib/public/components/Filters/RunsFilter/dcs.js +++ b/lib/public/components/Filters/RunsFilter/dcs.js @@ -11,6 +11,7 @@ * or submit itself to any jurisdiction. */ +import radiobutton from '../../common/form/inputs/RadioButton.js'; import { h } from '/js/src/index.js'; /** @@ -20,33 +21,30 @@ import { h } from '/js/src/index.js'; */ const dcsOperationRadioButtons = (runModel) => { const state = runModel.getDcsFilterOperation(); + const name = 'dcsFilterRadio'; + const label1 = 'ANY'; + const label2 = 'OFF'; + const label3 = 'ON'; return h('.form-group-header.flex-row.w-100', [ - radioButton('ANY', state === '', () => runModel.removeDcs()), - radioButton('OFF', state === false, () => runModel.setDcsFilterOperation(false)), - radioButton('ON', state === true, () => runModel.setDcsFilterOperation(true)), + radiobutton({ + label: label1, + isChecked: state === '', + action: () => runModel.removeDcs(), + name: name, + }), + radiobutton({ + label: label2, + isChecked: state === false, + action: () => runModel.setDcsFilterOperation(false), + name: name, + }), + radiobutton({ + label: label3, + isChecked: state === true, + action: () => runModel.setDcsFilterOperation(true), + name: name, + }), ]); }; -/** - * Build a radio button with its configuration and actions - * @param {string} label - label to be displayed to the user for radio button - * @param {boolean} isChecked - is radio button selected or not - * @param {Function} action - action to be followed on user click - * @return {vnode} - radio button with label associated - */ -const radioButton = (label, isChecked, action) => h('.w-33.form-check', [ - h('input.form-check-input', { - onchange: action, - type: 'radio', - id: `dcsFilterRadio${label}`, - name: 'dcsFilterRadio', - value: label, - checked: isChecked, - }, ''), - h('label.form-check-label', { - style: 'cursor: pointer;', - for: `dcsFilterRadio${label}`, - }, label), -]); - export default dcsOperationRadioButtons; diff --git a/lib/public/components/Filters/RunsFilter/ddflp.js b/lib/public/components/Filters/RunsFilter/ddflp.js index 34e11b02a8..00fdfc67d1 100644 --- a/lib/public/components/Filters/RunsFilter/ddflp.js +++ b/lib/public/components/Filters/RunsFilter/ddflp.js @@ -11,6 +11,7 @@ * or submit itself to any jurisdiction. */ +import radioButton from '../../common/form/inputs/RadioButton.js'; import { h } from '/js/src/index.js'; /** @@ -20,33 +21,30 @@ import { h } from '/js/src/index.js'; */ const ddflpOperationRadioButtons = (runModel) => { const state = runModel.getDdflpFilterOperation(); + const name = 'ddFlpFilterRadio'; + const label1 = 'ANY'; + const label2 = 'OFF'; + const label3 = 'ON'; return h('.form-group-header.flex-row.w-100', [ - radioButton('ANY', state === '', () => runModel.removeDdflp()), - radioButton('OFF', state === false, () => runModel.setDdflpFilterOperation(false)), - radioButton('ON', state === true, () => runModel.setDdflpFilterOperation(true)), + radioButton({ + label: label1, + isChecked: state === '', + action: () => runModel.removeDdflp(), + name: name, + }), + radioButton({ + label: label2, + isChecked: state === false, + action: () => runModel.setDdflpFilterOperation(false), + name: name, + }), + radioButton({ + label: label3, + isChecked: state === true, + action: () => runModel.setDdflpFilterOperation(true), + name: name, + }), ]); }; -/** - * Build a radio button with its configuration and actions - * @param {string} label - label to be displayed to the user for radio button - * @param {boolean} isChecked - is radio button selected or not - * @param {Function} action - action to be followed on user click - * @return {vnode} - radio button with label associated - */ -const radioButton = (label, isChecked, action) => h('.w-33.form-check', [ - h('input.form-check-input', { - onchange: action, - type: 'radio', - id: `ddFlpFilterRadio${label}`, - name: 'ddFlpFilterRadio', - value: label, - checked: isChecked, - }, ''), - h('label.form-check-label', { - style: 'cursor: pointer;', - for: `ddFlpFilterRadio${label}`, - }, label), -]); - export default ddflpOperationRadioButtons; diff --git a/lib/public/components/Filters/RunsFilter/epn.js b/lib/public/components/Filters/RunsFilter/epn.js index 63f1a0f760..f103ca34dd 100644 --- a/lib/public/components/Filters/RunsFilter/epn.js +++ b/lib/public/components/Filters/RunsFilter/epn.js @@ -11,6 +11,7 @@ * or submit itself to any jurisdiction. */ +import radiobutton from '../../common/form/inputs/RadioButton.js'; import { h } from '/js/src/index.js'; /** @@ -20,33 +21,30 @@ import { h } from '/js/src/index.js'; */ const epnOperationRadioButtons = (runModel) => { const state = runModel.getEpnFilterOperation(); + const name = 'epnFilterRadio'; + const label1 = 'ANY'; + const label2 = 'OFF'; + const label3 = 'ON'; return h('.form-group-header.flex-row.w-100', [ - radioButton('ANY', state === '', () => runModel.removeEpn()), - radioButton('OFF', state === false, () => runModel.setEpnFilterOperation(false)), - radioButton('ON', state === true, () => runModel.setEpnFilterOperation(true)), + radiobutton({ + label: label1, + isChecked: state === '', + action: () => runModel.removeEpn(), + name: name, + }), + radiobutton({ + label: label2, + isChecked: state === false, + action: () => runModel.setEpnFilterOperation(false), + name: name, + }), + radiobutton({ + label: label3, + isChecked: state === true, + action: () => runModel.setEpnFilterOperation(true), + name: name, + }), ]); }; -/** - * Build a radio button with its configuration and actions - * @param {string} label - label to be displayed to the user for radio button - * @param {boolean} isChecked - is radio button selected or not - * @param {Function} action - action to be followed on user click - * @return {vnode} - radio button with label associated - */ -const radioButton = (label, isChecked, action) => h('.w-33.form-check', [ - h('input.form-check-input', { - onchange: action, - type: 'radio', - id: `epnFilterRadio${label}`, - name: 'epnFilterRadio', - value: label, - checked: isChecked, - }, ''), - h('label.form-check-label', { - style: 'cursor: pointer;', - for: `epnFilterRadio${label}`, - }, label), -]); - export default epnOperationRadioButtons; diff --git a/lib/public/components/common/form/inputs/RadioButton.js b/lib/public/components/common/form/inputs/RadioButton.js new file mode 100644 index 0000000000..4546d76af0 --- /dev/null +++ b/lib/public/components/common/form/inputs/RadioButton.js @@ -0,0 +1,57 @@ +/** + * @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 { h } from '/js/src/index.js'; + +/** + * @typedef radioButtonConfig - configration object for radioButton. + * + * @property {string} label - label to be displayed to the user for radio button + * @property {boolean} isChecked - is radio button selected or not + * @property {function} action - action to be followed on user click + * @property {string} id - id of the radiobutton element + * @property {string} name - name of the radiobutton element + * @property {string} style - label style property + */ + +/** + * Build a radio button with its configuration and actions + * @param {radioButtonConfig} configuration - configration object for radioButton. + * @return {vnode} - radio button with associated label. + */ +const radiobutton = (configuration = {}) => { + const { + label = 'radio', + isChecked = false, + action = () => { }, + name = 'value', + id = `${name}${label}`, + style = 'cursor: pointer;', + } = configuration; + return h('.w-33.form-check', [ + h('input.form-check-input', { + onchange: action, + type: 'radio', + id: id, + name: name, + value: label, + checked: isChecked, + }, ''), + h('label.form-check-label', { + style: style, + for: `${name}${label}`, + }, label), + ]); +}; + +export default radiobutton; diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 4b03b1da2b..3e5be046bf 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -86,10 +86,10 @@ export const lhcFillsActiveColumns = { }, }, stableBeams: { - name: 'Stable beams', + name: 'Stable Beams Only', visible: false, format: (boolean) => boolean ? 'On' : 'Off', - filter: toggleStableBeamOnlyFilter, + filter: (lhcFillModel) => toggleStableBeamOnlyFilter(lhcFillModel, true), }, stableBeamsDuration: { name: 'SB Duration', diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index da0bb6c698..d83f87329d 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -129,17 +129,6 @@ export class LhcFillsOverviewModel extends OverviewPageModel { return this._filteringModel; } - /** - * Returns the list of URL params corresponding to the currently applied filter - * - * @return {Object} the URL params - * - * @private - */ - _getFilterQueryParams() { - return {}; - } - /** * Apply the current filtering and update the remote data list * diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 3bd12c3991..66542ba2ac 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -31,6 +31,8 @@ const { expect } = chai; const percentageRegex = new RegExp(/\d{1,2}.\d{2}%/); const durationRegex = new RegExp(/\d{2}:\d{2}:\d{2}/); +const filterButtonSellector= '#openFilterToggle'; + const defaultViewPort = { width: 700, height: 763, @@ -265,14 +267,25 @@ module.exports = () => { }); it('should successfully display filter elements', async () => { - const efficiencyExpect = { selector: 'tbody tr:nth-child(1) td:nth-child(8)', value: '41.67%' }; - + const filterSBExpect = { selector: '.w-30', value: 'Stable Beams Only' }; await goToPage(page, 'lhc-fill-overview'); + await pressElement(page, filterButtonSellector); + await expectInnerText(page, filterSBExpect.selector, filterSBExpect.value); + }); - await expectInnerText(page, beamTypeExpect.selector, beamTypeExpect.value); + + it('should successfully un-apply Stable Beam filter menu', async () => { + const filterButtonSBOnlySellector= '#stableBeamsOnlyRadioOFF'; + const filterSBExpect = { selector: '.w-30', value: 'Stable Beams Only' }; + await goToPage(page, 'lhc-fill-overview'); + await waitForTableLength(page, 5); + await pressElement(page, filterButtonSellector); + await pressElement(page, filterButtonSBOnlySellector); + await waitForTableLength(page, 6); }); - it('should successfully toggle to stable beam only', async () => { + it('should successfully turn off stable beam only from header', async () => { + await goToPage(page, 'lhc-fill-overview'); await waitForTableLength(page, 5); await pressElement(page, '.slider.round'); await waitForTableLength(page, 6); From da7ff37b0cf41eb40ebcec369a7bad137e019d19 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 26 Nov 2025 15:45:41 +0100 Subject: [PATCH 05/76] [O2B-1502] Doc fixes --- .../components/Filters/LhcFillsFilter/stableBeamFilter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js index d1cb8608d2..5a489c7221 100644 --- a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js @@ -16,10 +16,10 @@ import { switchInput } from '../../common/form/switchInput.js'; import radiobutton from '../../common/form/inputs/RadioButton.js'; /** - * Display a toggle switch to display stable beams only + * Display a toggle switch or radio buttons to filter stable beams only * * @param {LhcFillsOverviewModel} lhcFillsOverviewModel the overview model - * @param radioButtonMode + * @param {boolean} radioButtonMode define whether or not to return radio buttons or a switch. * @returns {Component} the toggle switch */ export const toggleStableBeamOnlyFilter = (lhcFillsOverviewModel, radioButtonMode = false) => { From ee7251290c4f082cfa2dc9105f4a54020745c628 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 26 Nov 2025 16:30:01 +0100 Subject: [PATCH 06/76] [O2B-1502] Increase timeout of detailsForSimulationPass test. Local machine hitting timout threshold --- test/public/qcFlags/detailsForSimulationPass.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/public/qcFlags/detailsForSimulationPass.test.js b/test/public/qcFlags/detailsForSimulationPass.test.js index e02d902345..641d817fac 100644 --- a/test/public/qcFlags/detailsForSimulationPass.test.js +++ b/test/public/qcFlags/detailsForSimulationPass.test.js @@ -149,9 +149,9 @@ module.exports = () => { await page.waitForSelector('#delete:not([disabled])'); await expectInnerText(page, '#qc-flag-details-verified', 'Verified:\nNo'); - await page.waitForSelector('#submit', { hidden: true, timeout: 250 }); - await page.waitForSelector('#cancel-verification', { hidden: true, timeout: 250 }); - await page.waitForSelector('#verification-comment', { hidden: true, timeout: 250 }); + await page.waitForSelector('#submit', { hidden: true, timeout: 350 }); + await page.waitForSelector('#cancel-verification', { hidden: true, timeout: 350 }); + await page.waitForSelector('#verification-comment', { hidden: true, timeout: 350 }); await pressElement(page, 'button#verify-qc-flag'); await page.waitForSelector('#verification-comment'); @@ -159,9 +159,9 @@ module.exports = () => { await page.waitForSelector('#submit'); await pressElement(page, 'button#cancel-verification'); - await page.waitForSelector('#submit', { hidden: true, timeout: 250 }); - await page.waitForSelector('#cancel-verification', { hidden: true, timeout: 250 }); - await page.waitForSelector('#verification-comment', { hidden: true, timeout: 250 }); + await page.waitForSelector('#submit', { hidden: true, timeout: 350 }); + await page.waitForSelector('#cancel-verification', { hidden: true, timeout: 350 }); + await page.waitForSelector('#verification-comment', { hidden: true, timeout: 350 }); await pressElement(page, 'button#verify-qc-flag'); await pressElement(page, '#verification-comment ~ .CodeMirror'); From 96a04c070ab087aca802f5f0e8c3aae3fba9ad63 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Fri, 28 Nov 2025 10:53:12 +0100 Subject: [PATCH 07/76] [O2B-1502] Potential fix for test failure. --- test/public/runs/runsPerDataPass.overview.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/public/runs/runsPerDataPass.overview.test.js b/test/public/runs/runsPerDataPass.overview.test.js index 090384e0f6..98110e5b86 100644 --- a/test/public/runs/runsPerDataPass.overview.test.js +++ b/test/public/runs/runsPerDataPass.overview.test.js @@ -232,7 +232,7 @@ module.exports = () => { it('can set how many runs are available per page', async () => { await navigateToRunsPerDataPass(page, 1, 3, 4); const amountSelectorId = '#amountSelector'; - const amountSelectorButtonSelector = `${amountSelectorId} button`; + const amountSelectorButtonSelector = `${amountSelectorId} > button:nth-child(1)`; await pressElement(page, amountSelectorButtonSelector); const amountSelectorDropdown = await page.$(`${amountSelectorId} .dropup-menu`); From 6fa403422af75571a9d5644a9166a422f8aed212 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 1 Dec 2025 11:18:42 +0100 Subject: [PATCH 08/76] Revert "[O2B-1502] Potential fix for test failure." This reverts commit 96a04c070ab087aca802f5f0e8c3aae3fba9ad63. --- test/public/runs/runsPerDataPass.overview.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/public/runs/runsPerDataPass.overview.test.js b/test/public/runs/runsPerDataPass.overview.test.js index 98110e5b86..090384e0f6 100644 --- a/test/public/runs/runsPerDataPass.overview.test.js +++ b/test/public/runs/runsPerDataPass.overview.test.js @@ -232,7 +232,7 @@ module.exports = () => { it('can set how many runs are available per page', async () => { await navigateToRunsPerDataPass(page, 1, 3, 4); const amountSelectorId = '#amountSelector'; - const amountSelectorButtonSelector = `${amountSelectorId} > button:nth-child(1)`; + const amountSelectorButtonSelector = `${amountSelectorId} button`; await pressElement(page, amountSelectorButtonSelector); const amountSelectorDropdown = await page.$(`${amountSelectorId} .dropup-menu`); From 695622bd4f510ed10492f95827c2cf60ccad5729 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 8 Dec 2025 13:41:18 +0100 Subject: [PATCH 09/76] [O2B-1502] Processed feedback --- lib/domain/dtos/filters/LhcFillsFilterDto.js | 2 +- .../LhcFillsFilter/BeamsModeFilterModel.js | 53 +++++++++++++++++++ .../LhcFillsFilter/stableBeamFilter.js | 18 +++---- .../components/Filters/RunsFilter/dcs.js | 26 ++++----- .../components/Filters/RunsFilter/ddflp.js | 20 +++---- .../components/Filters/RunsFilter/epn.js | 26 ++++----- .../common/form/inputs/RadioButton.js | 36 +++++++------ .../ActiveColumns/lhcFillsActiveColumns.js | 4 +- .../Overview/LhcFillsOverviewModel.js | 35 +++--------- lib/public/views/LhcFills/Overview/index.js | 4 +- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 13 +++-- .../lhcFill/GetAllLhcFillsUseCase.test.js | 3 +- test/public/lhcFills/overview.test.js | 13 +++-- 13 files changed, 147 insertions(+), 106 deletions(-) create mode 100644 lib/public/components/Filters/LhcFillsFilter/BeamsModeFilterModel.js diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index c42dadf229..712cab8069 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -13,5 +13,5 @@ const Joi = require('joi'); exports.LhcFillsFilterDto = Joi.object({ - hasStableBeams: Joi.boolean(), + beamsMode: Joi.string(), }); diff --git a/lib/public/components/Filters/LhcFillsFilter/BeamsModeFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/BeamsModeFilterModel.js new file mode 100644 index 0000000000..4398ebd911 --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/BeamsModeFilterModel.js @@ -0,0 +1,53 @@ +import { BeamModes } from '../../../domain/enums/BeamModes.js'; +import { SelectionFilterModel } from '../common/filters/SelectionFilterModel.js'; + +/** + * Beams mode filter model. + */ +export class BeamsModeFilterModel extends SelectionFilterModel { + /** + * Constructor + */ + constructor() { + super({ availableOptions: [{ value: BeamModes.STABLE_BEAMS }] }); + } + + /** + * Returns true if the current filter is stable beams only + * + * @returns {boolean} true if filter is stable beams only + */ + isStableBeamsOnly() { + const selectedOptions = this._selectionModel.selected; + return selectedOptions.length === 1 && selectedOptions[0] === BeamModes.STABLE_BEAMS; + } + + /** + * Sets the current filter to be stable beams only. + * @param {boolean} value wether to have stable beams only true or false + */ + setStableBeamsOnly(value) { + switch (value) { + case true: + this._selectionModel.selectedOptions = []; + this._selectionModel.select(BeamModes.STABLE_BEAMS); + break; + case false: + this.reset(); + this.notify(); + break; + default: + break; + } + } + + /** + * Empty the filter + */ + setEmpty() { + if (!this.isEmpty) { + this.reset(); + this.notify(); + } + } +}; diff --git a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js index 5a489c7221..2766a60aa4 100644 --- a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js @@ -13,38 +13,38 @@ import { h } from '/js/src/index.js'; import { switchInput } from '../../common/form/switchInput.js'; -import radiobutton from '../../common/form/inputs/RadioButton.js'; +import { radioButton } from '../../common/form/inputs/radioButton.js'; /** * Display a toggle switch or radio buttons to filter stable beams only * - * @param {LhcFillsOverviewModel} lhcFillsOverviewModel the overview model + * @param {BeamsModeFilterModel} beamsModeFilterModel beamsModeFilterModel * @param {boolean} radioButtonMode define whether or not to return radio buttons or a switch. * @returns {Component} the toggle switch */ -export const toggleStableBeamOnlyFilter = (lhcFillsOverviewModel, radioButtonMode = false) => { - const isStableBeamsOnly = lhcFillsOverviewModel.getStableBeamsOnly(); +export const stableBeamFilter = (beamsModeFilterModel, radioButtonMode = false) => { + const isStableBeamsOnly = beamsModeFilterModel.isStableBeamsOnly(); const name = 'stableBeamsOnlyRadio'; const label1 = 'OFF'; const label2 = 'ON'; if (radioButtonMode) { return h('.form-group-header.flex-row.w-100', [ - radiobutton({ + radioButton({ label: label1, isChecked: isStableBeamsOnly === false, - action: () => lhcFillsOverviewModel.setStableBeamsFilter(false), + action: () => beamsModeFilterModel.setStableBeamsOnly(false), name: name, }), - radiobutton({ + radioButton({ label: label2, isChecked: isStableBeamsOnly === true, - action: () => lhcFillsOverviewModel.setStableBeamsFilter(true), + action: () => beamsModeFilterModel.setStableBeamsOnly(true), name: name, }), ]); } else { return switchInput(isStableBeamsOnly, (newState) => { - lhcFillsOverviewModel.setStableBeamsFilter(newState); + beamsModeFilterModel.setStableBeamsOnly(newState); }, { labelAfter: 'STABLE BEAM ONLY' }); } }; diff --git a/lib/public/components/Filters/RunsFilter/dcs.js b/lib/public/components/Filters/RunsFilter/dcs.js index 807567d2f2..590eb81b78 100644 --- a/lib/public/components/Filters/RunsFilter/dcs.js +++ b/lib/public/components/Filters/RunsFilter/dcs.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import radiobutton from '../../common/form/inputs/RadioButton.js'; +import { radioButton } from '../../common/form/inputs/radioButton.js'; import { h } from '/js/src/index.js'; /** @@ -22,27 +22,27 @@ import { h } from '/js/src/index.js'; const dcsOperationRadioButtons = (runModel) => { const state = runModel.getDcsFilterOperation(); const name = 'dcsFilterRadio'; - const label1 = 'ANY'; - const label2 = 'OFF'; - const label3 = 'ON'; + const labelAny = 'ANY'; + const labelOff = 'OFF'; + const labelOn = 'ON'; return h('.form-group-header.flex-row.w-100', [ - radiobutton({ - label: label1, + radioButton({ + label: labelAny, isChecked: state === '', action: () => runModel.removeDcs(), - name: name, + name, }), - radiobutton({ - label: label2, + radioButton({ + label: labelOff, isChecked: state === false, action: () => runModel.setDcsFilterOperation(false), - name: name, + name, }), - radiobutton({ - label: label3, + radioButton({ + label: labelOn, isChecked: state === true, action: () => runModel.setDcsFilterOperation(true), - name: name, + name, }), ]); }; diff --git a/lib/public/components/Filters/RunsFilter/ddflp.js b/lib/public/components/Filters/RunsFilter/ddflp.js index 00fdfc67d1..74bf28f4ba 100644 --- a/lib/public/components/Filters/RunsFilter/ddflp.js +++ b/lib/public/components/Filters/RunsFilter/ddflp.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import radioButton from '../../common/form/inputs/RadioButton.js'; +import { radioButton } from '../../common/form/inputs/radioButton.js'; import { h } from '/js/src/index.js'; /** @@ -22,27 +22,27 @@ import { h } from '/js/src/index.js'; const ddflpOperationRadioButtons = (runModel) => { const state = runModel.getDdflpFilterOperation(); const name = 'ddFlpFilterRadio'; - const label1 = 'ANY'; - const label2 = 'OFF'; - const label3 = 'ON'; + const labelAny = 'ANY'; + const labelOff = 'OFF'; + const labelOn = 'ON'; return h('.form-group-header.flex-row.w-100', [ radioButton({ - label: label1, + label: labelAny, isChecked: state === '', action: () => runModel.removeDdflp(), - name: name, + name, }), radioButton({ - label: label2, + label: labelOff, isChecked: state === false, action: () => runModel.setDdflpFilterOperation(false), - name: name, + name, }), radioButton({ - label: label3, + label: labelOn, isChecked: state === true, action: () => runModel.setDdflpFilterOperation(true), - name: name, + name, }), ]); }; diff --git a/lib/public/components/Filters/RunsFilter/epn.js b/lib/public/components/Filters/RunsFilter/epn.js index f103ca34dd..5e639d8afb 100644 --- a/lib/public/components/Filters/RunsFilter/epn.js +++ b/lib/public/components/Filters/RunsFilter/epn.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import radiobutton from '../../common/form/inputs/RadioButton.js'; +import { radioButton } from '../../common/form/inputs/radioButton.js'; import { h } from '/js/src/index.js'; /** @@ -22,27 +22,27 @@ import { h } from '/js/src/index.js'; const epnOperationRadioButtons = (runModel) => { const state = runModel.getEpnFilterOperation(); const name = 'epnFilterRadio'; - const label1 = 'ANY'; - const label2 = 'OFF'; - const label3 = 'ON'; + const labelAny = 'ANY'; + const labelOff = 'OFF'; + const labelOn = 'ON'; return h('.form-group-header.flex-row.w-100', [ - radiobutton({ - label: label1, + radioButton({ + label: labelAny, isChecked: state === '', action: () => runModel.removeEpn(), - name: name, + name, }), - radiobutton({ - label: label2, + radioButton({ + label: labelOff, isChecked: state === false, action: () => runModel.setEpnFilterOperation(false), - name: name, + name, }), - radiobutton({ - label: label3, + radioButton({ + label: labelOn, isChecked: state === true, action: () => runModel.setEpnFilterOperation(true), - name: name, + name, }), ]); }; diff --git a/lib/public/components/common/form/inputs/RadioButton.js b/lib/public/components/common/form/inputs/RadioButton.js index 4546d76af0..8f0c159a59 100644 --- a/lib/public/components/common/form/inputs/RadioButton.js +++ b/lib/public/components/common/form/inputs/RadioButton.js @@ -14,44 +14,48 @@ import { h } from '/js/src/index.js'; /** - * @typedef radioButtonConfig - configration object for radioButton. + * @typedef RadioButtonConfigStyle + * @property {string} labelStyle - value for the label's style property. + * @property {string} radioButtonStyle - value for the radio button's element styling. + */ + +/** + * @typedef RadioButtonConfig - configration object for radioButton. * * @property {string} label - label to be displayed to the user for radio button * @property {boolean} isChecked - is radio button selected or not - * @property {function} action - action to be followed on user click + * @property {function()} action - action to be followed on user click * @property {string} id - id of the radiobutton element * @property {string} name - name of the radiobutton element - * @property {string} style - label style property + * @property {RadioButtonConfigStyle} style - label style property */ /** * Build a radio button with its configuration and actions - * @param {radioButtonConfig} configuration - configration object for radioButton. + * @param {RadioButtonConfig} configuration - configuration object for radioButton. * @return {vnode} - radio button with associated label. */ -const radiobutton = (configuration = {}) => { +export const radioButton = (configuration = {}) => { const { - label = 'radio', + label = '', isChecked = false, action = () => { }, - name = 'value', + name = '', id = `${name}${label}`, - style = 'cursor: pointer;', + style = { labelStyle: 'cursor: pointer;', radioButtonStyle: '.w-33' }, } = configuration; - return h('.w-33.form-check', [ + return h(`${style.radioButtonStyle}.form-check`, [ h('input.form-check-input', { onchange: action, type: 'radio', - id: id, - name: name, + id, + name, value: label, checked: isChecked, - }, ''), + }), h('label.form-check-label', { - style: style, - for: `${name}${label}`, + style: style.labelStyle, + for: id, }, label), ]); }; - -export default radiobutton; diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 3e5be046bf..029135d03f 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -23,7 +23,7 @@ import { buttonLinkWithDropdown } from '../../../components/common/selection/inf import { infologgerLinksComponents } from '../../../components/common/externalLinks/infologgerLinksComponents.js'; import { formatBeamType } from '../../../utilities/formatting/formatBeamType.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; -import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; +import { stableBeamFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; /** * List of active columns for a lhc fills table @@ -89,7 +89,7 @@ export const lhcFillsActiveColumns = { name: 'Stable Beams Only', visible: false, format: (boolean) => boolean ? 'On' : 'Off', - filter: (lhcFillModel) => toggleStableBeamOnlyFilter(lhcFillModel, true), + filter: (lhcFillModel) => stableBeamFilter(lhcFillModel.filteringModel.get('beamsMode'), true), }, stableBeamsDuration: { name: 'SB Duration', diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index d83f87329d..7227f735dc 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -12,6 +12,7 @@ */ import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; +import { BeamsModeFilterModel } from '../../../components/Filters/LhcFillsFilter/BeamsModeFilterModel.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; @@ -29,13 +30,15 @@ export class LhcFillsOverviewModel extends OverviewPageModel { constructor(stableBeamsOnly = false) { super(); - this._filteringModel = new FilteringModel({}); - this._stableBeamsOnly = stableBeamsOnly; + this._filteringModel = new FilteringModel({ + beamsMode: new BeamsModeFilterModel(), + }); this._filteringModel.observe(() => this._applyFilters(true)); this._filteringModel.visualChange$.bubbleTo(this); this.reset(false); + this._filteringModel.get('beamsMode').setStableBeamsOnly(stableBeamsOnly); } /** @@ -61,31 +64,10 @@ export class LhcFillsOverviewModel extends OverviewPageModel { async getLoadParameters() { return { ...await super.getLoadParameters(), - 'filter[hasStableBeams]': this._stableBeamsOnly, + ...{ filter: this.filteringModel.normalized }, }; } - /** - * Sets the stable beams filter - * - * @param {boolean} stableBeamsOnly the new stable beams filter value - * @return {void} - */ - setStableBeamsFilter(stableBeamsOnly) { - this._stableBeamsOnly = stableBeamsOnly; - this._applyFilters(); - this.notify(); - } - - /** - * Checks if the stable beams filter is set - * - * @return {boolean} true if the stable beams filter is active - */ - getStableBeamsOnly() { - return this._stableBeamsOnly; - } - /** * 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 @@ -104,8 +86,6 @@ export class LhcFillsOverviewModel extends OverviewPageModel { resetFiltering(fetch = true) { this._filteringModel.reset(); - this._stableBeamsOnly = true; - if (fetch) { this._applyFilters(true); } @@ -116,8 +96,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @return {Boolean} If any filter is active */ isAnyFilterActive() { - return this._filteringModel.isAnyFilterActive() - || this._stableBeamsOnly == false; + return this._filteringModel.isAnyFilterActive(); } /** diff --git a/lib/public/views/LhcFills/Overview/index.js b/lib/public/views/LhcFills/Overview/index.js index 5de64d5989..fdc99de72c 100644 --- a/lib/public/views/LhcFills/Overview/index.js +++ b/lib/public/views/LhcFills/Overview/index.js @@ -19,7 +19,7 @@ import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplay import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; -import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; +import { stableBeamFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; const TABLEROW_HEIGHT = 53.3; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -52,7 +52,7 @@ const showLhcFillsTable = (lhcFillsOverviewModel) => { h('.flex-row.header-container.g2.pv2', [ frontLink(h('button.btn.btn-primary', 'Statistics'), 'statistics'), filtersPanelPopover(lhcFillsOverviewModel, lhcFillsActiveColumns), - toggleStableBeamOnlyFilter(lhcFillsOverviewModel), + stableBeamFilter(lhcFillsOverviewModel.filteringModel.get('beamsMode')), ]), h('.w-100.flex-column', [ table(lhcFillsOverviewModel.items, lhcFillsActiveColumns, null, { tableClasses: '.table-sm' }), diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index dd5cc41f2c..ae3904ca95 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -41,10 +41,15 @@ class GetAllLhcFillsUseCase { const queryBuilder = new QueryBuilder(); if (filter) { - const { hasStableBeams } = filter; - if (hasStableBeams) { - // For now, if a stableBeamsStart is present, then a beam is stable - queryBuilder.where('stableBeamsStart').not().is(null); + const { beamsMode } = filter; + if (beamsMode) { + switch (beamsMode) { + case 'STABLE BEAMS': + queryBuilder.where('stableBeamsStart').not().is(null); + break; + default: + break; + } } } diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index 089420a321..cf3b009437 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -15,6 +15,7 @@ const { environment: { GetAllEnvironmentsUseCase } } = require('../../../../lib/ const { lhcFill: { GetAllLhcFillsUseCase } } = require('../../../../lib/usecases/index.js'); const { dtos: { GetAllLhcFillsDto } } = require('../../../../lib/domain/index.js'); const chai = require('chai'); +const { BeamModes } = require('../../../../lib/public/domain/enums/BeamModes.js'); const { expect } = chai; @@ -31,7 +32,7 @@ module.exports = () => { }); it('should only containing lhc fills with stable beams', async () => { - getAllLhcFillsDto.query = { filter: { hasStableBeams: true } }; + getAllLhcFillsDto.query = { filter: { beamsMode: BeamModes.STABLE_BEAMS } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); expect(lhcFills).to.be.an('array'); diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 66542ba2ac..f4b299f2e7 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -31,8 +31,6 @@ const { expect } = chai; const percentageRegex = new RegExp(/\d{1,2}.\d{2}%/); const durationRegex = new RegExp(/\d{2}:\d{2}:\d{2}/); -const filterButtonSellector= '#openFilterToggle'; - const defaultViewPort = { width: 700, height: 763, @@ -269,18 +267,19 @@ module.exports = () => { it('should successfully display filter elements', async () => { const filterSBExpect = { selector: '.w-30', value: 'Stable Beams Only' }; await goToPage(page, 'lhc-fill-overview'); - await pressElement(page, filterButtonSellector); + // Open the filtering panel + await openFilteringPanel(page); await expectInnerText(page, filterSBExpect.selector, filterSBExpect.value); }); it('should successfully un-apply Stable Beam filter menu', async () => { - const filterButtonSBOnlySellector= '#stableBeamsOnlyRadioOFF'; - const filterSBExpect = { selector: '.w-30', value: 'Stable Beams Only' }; + const filterButtonSBOnlySelector= '#stableBeamsOnlyRadioOFF'; await goToPage(page, 'lhc-fill-overview'); await waitForTableLength(page, 5); - await pressElement(page, filterButtonSellector); - await pressElement(page, filterButtonSBOnlySellector); + // Open the filtering panel + await openFilteringPanel(page); + await pressElement(page, filterButtonSBOnlySelector); await waitForTableLength(page, 6); }); From 87bee8968bc76fb5b2426498a19933785a6c1607 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 8 Dec 2025 13:49:39 +0100 Subject: [PATCH 10/76] [O2B-1502] Git failed to detect rename. Ran: git mv RadioButton.js radioButton.js --- .../common/form/inputs/{RadioButton.js => radioButton.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/public/components/common/form/inputs/{RadioButton.js => radioButton.js} (100%) diff --git a/lib/public/components/common/form/inputs/RadioButton.js b/lib/public/components/common/form/inputs/radioButton.js similarity index 100% rename from lib/public/components/common/form/inputs/RadioButton.js rename to lib/public/components/common/form/inputs/radioButton.js From 5a18d9eb3738501e4fb47bb73fec532aba4271fd Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 8 Dec 2025 13:53:46 +0100 Subject: [PATCH 11/76] [O2B-1502] Added test import --- test/public/lhcFills/overview.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index f4b299f2e7..9c5c7b0ac9 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -23,6 +23,7 @@ const { expectInnerText, waitForTableLength, expectLink, + openFilteringPanel, } = require('../defaults.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); From 4c530ef1d52b1477303d99610f96b36241162298 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 10 Dec 2025 13:58:23 +0100 Subject: [PATCH 12/76] Revert "[O2B-1502] Processed feedback" This reverts commit 695622bd4f510ed10492f95827c2cf60ccad5729. --- lib/domain/dtos/filters/LhcFillsFilterDto.js | 2 +- .../LhcFillsFilter/BeamsModeFilterModel.js | 53 ------------------- .../LhcFillsFilter/stableBeamFilter.js | 18 +++---- .../components/Filters/RunsFilter/dcs.js | 26 ++++----- .../components/Filters/RunsFilter/ddflp.js | 20 +++---- .../components/Filters/RunsFilter/epn.js | 26 ++++----- .../common/form/inputs/radioButton.js | 36 ++++++------- .../ActiveColumns/lhcFillsActiveColumns.js | 4 +- .../Overview/LhcFillsOverviewModel.js | 35 +++++++++--- lib/public/views/LhcFills/Overview/index.js | 4 +- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 13 ++--- .../lhcFill/GetAllLhcFillsUseCase.test.js | 3 +- test/public/lhcFills/overview.test.js | 13 ++--- 13 files changed, 106 insertions(+), 147 deletions(-) delete mode 100644 lib/public/components/Filters/LhcFillsFilter/BeamsModeFilterModel.js diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index 712cab8069..c42dadf229 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -13,5 +13,5 @@ const Joi = require('joi'); exports.LhcFillsFilterDto = Joi.object({ - beamsMode: Joi.string(), + hasStableBeams: Joi.boolean(), }); diff --git a/lib/public/components/Filters/LhcFillsFilter/BeamsModeFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/BeamsModeFilterModel.js deleted file mode 100644 index 4398ebd911..0000000000 --- a/lib/public/components/Filters/LhcFillsFilter/BeamsModeFilterModel.js +++ /dev/null @@ -1,53 +0,0 @@ -import { BeamModes } from '../../../domain/enums/BeamModes.js'; -import { SelectionFilterModel } from '../common/filters/SelectionFilterModel.js'; - -/** - * Beams mode filter model. - */ -export class BeamsModeFilterModel extends SelectionFilterModel { - /** - * Constructor - */ - constructor() { - super({ availableOptions: [{ value: BeamModes.STABLE_BEAMS }] }); - } - - /** - * Returns true if the current filter is stable beams only - * - * @returns {boolean} true if filter is stable beams only - */ - isStableBeamsOnly() { - const selectedOptions = this._selectionModel.selected; - return selectedOptions.length === 1 && selectedOptions[0] === BeamModes.STABLE_BEAMS; - } - - /** - * Sets the current filter to be stable beams only. - * @param {boolean} value wether to have stable beams only true or false - */ - setStableBeamsOnly(value) { - switch (value) { - case true: - this._selectionModel.selectedOptions = []; - this._selectionModel.select(BeamModes.STABLE_BEAMS); - break; - case false: - this.reset(); - this.notify(); - break; - default: - break; - } - } - - /** - * Empty the filter - */ - setEmpty() { - if (!this.isEmpty) { - this.reset(); - this.notify(); - } - } -}; diff --git a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js index 2766a60aa4..5a489c7221 100644 --- a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js @@ -13,38 +13,38 @@ import { h } from '/js/src/index.js'; import { switchInput } from '../../common/form/switchInput.js'; -import { radioButton } from '../../common/form/inputs/radioButton.js'; +import radiobutton from '../../common/form/inputs/RadioButton.js'; /** * Display a toggle switch or radio buttons to filter stable beams only * - * @param {BeamsModeFilterModel} beamsModeFilterModel beamsModeFilterModel + * @param {LhcFillsOverviewModel} lhcFillsOverviewModel the overview model * @param {boolean} radioButtonMode define whether or not to return radio buttons or a switch. * @returns {Component} the toggle switch */ -export const stableBeamFilter = (beamsModeFilterModel, radioButtonMode = false) => { - const isStableBeamsOnly = beamsModeFilterModel.isStableBeamsOnly(); +export const toggleStableBeamOnlyFilter = (lhcFillsOverviewModel, radioButtonMode = false) => { + const isStableBeamsOnly = lhcFillsOverviewModel.getStableBeamsOnly(); const name = 'stableBeamsOnlyRadio'; const label1 = 'OFF'; const label2 = 'ON'; if (radioButtonMode) { return h('.form-group-header.flex-row.w-100', [ - radioButton({ + radiobutton({ label: label1, isChecked: isStableBeamsOnly === false, - action: () => beamsModeFilterModel.setStableBeamsOnly(false), + action: () => lhcFillsOverviewModel.setStableBeamsFilter(false), name: name, }), - radioButton({ + radiobutton({ label: label2, isChecked: isStableBeamsOnly === true, - action: () => beamsModeFilterModel.setStableBeamsOnly(true), + action: () => lhcFillsOverviewModel.setStableBeamsFilter(true), name: name, }), ]); } else { return switchInput(isStableBeamsOnly, (newState) => { - beamsModeFilterModel.setStableBeamsOnly(newState); + lhcFillsOverviewModel.setStableBeamsFilter(newState); }, { labelAfter: 'STABLE BEAM ONLY' }); } }; diff --git a/lib/public/components/Filters/RunsFilter/dcs.js b/lib/public/components/Filters/RunsFilter/dcs.js index 590eb81b78..807567d2f2 100644 --- a/lib/public/components/Filters/RunsFilter/dcs.js +++ b/lib/public/components/Filters/RunsFilter/dcs.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import { radioButton } from '../../common/form/inputs/radioButton.js'; +import radiobutton from '../../common/form/inputs/RadioButton.js'; import { h } from '/js/src/index.js'; /** @@ -22,27 +22,27 @@ import { h } from '/js/src/index.js'; const dcsOperationRadioButtons = (runModel) => { const state = runModel.getDcsFilterOperation(); const name = 'dcsFilterRadio'; - const labelAny = 'ANY'; - const labelOff = 'OFF'; - const labelOn = 'ON'; + const label1 = 'ANY'; + const label2 = 'OFF'; + const label3 = 'ON'; return h('.form-group-header.flex-row.w-100', [ - radioButton({ - label: labelAny, + radiobutton({ + label: label1, isChecked: state === '', action: () => runModel.removeDcs(), - name, + name: name, }), - radioButton({ - label: labelOff, + radiobutton({ + label: label2, isChecked: state === false, action: () => runModel.setDcsFilterOperation(false), - name, + name: name, }), - radioButton({ - label: labelOn, + radiobutton({ + label: label3, isChecked: state === true, action: () => runModel.setDcsFilterOperation(true), - name, + name: name, }), ]); }; diff --git a/lib/public/components/Filters/RunsFilter/ddflp.js b/lib/public/components/Filters/RunsFilter/ddflp.js index 74bf28f4ba..00fdfc67d1 100644 --- a/lib/public/components/Filters/RunsFilter/ddflp.js +++ b/lib/public/components/Filters/RunsFilter/ddflp.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import { radioButton } from '../../common/form/inputs/radioButton.js'; +import radioButton from '../../common/form/inputs/RadioButton.js'; import { h } from '/js/src/index.js'; /** @@ -22,27 +22,27 @@ import { h } from '/js/src/index.js'; const ddflpOperationRadioButtons = (runModel) => { const state = runModel.getDdflpFilterOperation(); const name = 'ddFlpFilterRadio'; - const labelAny = 'ANY'; - const labelOff = 'OFF'; - const labelOn = 'ON'; + const label1 = 'ANY'; + const label2 = 'OFF'; + const label3 = 'ON'; return h('.form-group-header.flex-row.w-100', [ radioButton({ - label: labelAny, + label: label1, isChecked: state === '', action: () => runModel.removeDdflp(), - name, + name: name, }), radioButton({ - label: labelOff, + label: label2, isChecked: state === false, action: () => runModel.setDdflpFilterOperation(false), - name, + name: name, }), radioButton({ - label: labelOn, + label: label3, isChecked: state === true, action: () => runModel.setDdflpFilterOperation(true), - name, + name: name, }), ]); }; diff --git a/lib/public/components/Filters/RunsFilter/epn.js b/lib/public/components/Filters/RunsFilter/epn.js index 5e639d8afb..f103ca34dd 100644 --- a/lib/public/components/Filters/RunsFilter/epn.js +++ b/lib/public/components/Filters/RunsFilter/epn.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import { radioButton } from '../../common/form/inputs/radioButton.js'; +import radiobutton from '../../common/form/inputs/RadioButton.js'; import { h } from '/js/src/index.js'; /** @@ -22,27 +22,27 @@ import { h } from '/js/src/index.js'; const epnOperationRadioButtons = (runModel) => { const state = runModel.getEpnFilterOperation(); const name = 'epnFilterRadio'; - const labelAny = 'ANY'; - const labelOff = 'OFF'; - const labelOn = 'ON'; + const label1 = 'ANY'; + const label2 = 'OFF'; + const label3 = 'ON'; return h('.form-group-header.flex-row.w-100', [ - radioButton({ - label: labelAny, + radiobutton({ + label: label1, isChecked: state === '', action: () => runModel.removeEpn(), - name, + name: name, }), - radioButton({ - label: labelOff, + radiobutton({ + label: label2, isChecked: state === false, action: () => runModel.setEpnFilterOperation(false), - name, + name: name, }), - radioButton({ - label: labelOn, + radiobutton({ + label: label3, isChecked: state === true, action: () => runModel.setEpnFilterOperation(true), - name, + name: name, }), ]); }; diff --git a/lib/public/components/common/form/inputs/radioButton.js b/lib/public/components/common/form/inputs/radioButton.js index 8f0c159a59..4546d76af0 100644 --- a/lib/public/components/common/form/inputs/radioButton.js +++ b/lib/public/components/common/form/inputs/radioButton.js @@ -14,48 +14,44 @@ import { h } from '/js/src/index.js'; /** - * @typedef RadioButtonConfigStyle - * @property {string} labelStyle - value for the label's style property. - * @property {string} radioButtonStyle - value for the radio button's element styling. - */ - -/** - * @typedef RadioButtonConfig - configration object for radioButton. + * @typedef radioButtonConfig - configration object for radioButton. * * @property {string} label - label to be displayed to the user for radio button * @property {boolean} isChecked - is radio button selected or not - * @property {function()} action - action to be followed on user click + * @property {function} action - action to be followed on user click * @property {string} id - id of the radiobutton element * @property {string} name - name of the radiobutton element - * @property {RadioButtonConfigStyle} style - label style property + * @property {string} style - label style property */ /** * Build a radio button with its configuration and actions - * @param {RadioButtonConfig} configuration - configuration object for radioButton. + * @param {radioButtonConfig} configuration - configration object for radioButton. * @return {vnode} - radio button with associated label. */ -export const radioButton = (configuration = {}) => { +const radiobutton = (configuration = {}) => { const { - label = '', + label = 'radio', isChecked = false, action = () => { }, - name = '', + name = 'value', id = `${name}${label}`, - style = { labelStyle: 'cursor: pointer;', radioButtonStyle: '.w-33' }, + style = 'cursor: pointer;', } = configuration; - return h(`${style.radioButtonStyle}.form-check`, [ + return h('.w-33.form-check', [ h('input.form-check-input', { onchange: action, type: 'radio', - id, - name, + id: id, + name: name, value: label, checked: isChecked, - }), + }, ''), h('label.form-check-label', { - style: style.labelStyle, - for: id, + style: style, + for: `${name}${label}`, }, label), ]); }; + +export default radiobutton; diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 029135d03f..3e5be046bf 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -23,7 +23,7 @@ import { buttonLinkWithDropdown } from '../../../components/common/selection/inf import { infologgerLinksComponents } from '../../../components/common/externalLinks/infologgerLinksComponents.js'; import { formatBeamType } from '../../../utilities/formatting/formatBeamType.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; -import { stableBeamFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; +import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; /** * List of active columns for a lhc fills table @@ -89,7 +89,7 @@ export const lhcFillsActiveColumns = { name: 'Stable Beams Only', visible: false, format: (boolean) => boolean ? 'On' : 'Off', - filter: (lhcFillModel) => stableBeamFilter(lhcFillModel.filteringModel.get('beamsMode'), true), + filter: (lhcFillModel) => toggleStableBeamOnlyFilter(lhcFillModel, true), }, stableBeamsDuration: { name: 'SB Duration', diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 7227f735dc..d83f87329d 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -12,7 +12,6 @@ */ import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; -import { BeamsModeFilterModel } from '../../../components/Filters/LhcFillsFilter/BeamsModeFilterModel.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; @@ -30,15 +29,13 @@ export class LhcFillsOverviewModel extends OverviewPageModel { constructor(stableBeamsOnly = false) { super(); - this._filteringModel = new FilteringModel({ - beamsMode: new BeamsModeFilterModel(), - }); + this._filteringModel = new FilteringModel({}); + this._stableBeamsOnly = stableBeamsOnly; this._filteringModel.observe(() => this._applyFilters(true)); this._filteringModel.visualChange$.bubbleTo(this); this.reset(false); - this._filteringModel.get('beamsMode').setStableBeamsOnly(stableBeamsOnly); } /** @@ -64,10 +61,31 @@ export class LhcFillsOverviewModel extends OverviewPageModel { async getLoadParameters() { return { ...await super.getLoadParameters(), - ...{ filter: this.filteringModel.normalized }, + 'filter[hasStableBeams]': this._stableBeamsOnly, }; } + /** + * Sets the stable beams filter + * + * @param {boolean} stableBeamsOnly the new stable beams filter value + * @return {void} + */ + setStableBeamsFilter(stableBeamsOnly) { + this._stableBeamsOnly = stableBeamsOnly; + this._applyFilters(); + this.notify(); + } + + /** + * Checks if the stable beams filter is set + * + * @return {boolean} true if the stable beams filter is active + */ + getStableBeamsOnly() { + return this._stableBeamsOnly; + } + /** * 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 @@ -86,6 +104,8 @@ export class LhcFillsOverviewModel extends OverviewPageModel { resetFiltering(fetch = true) { this._filteringModel.reset(); + this._stableBeamsOnly = true; + if (fetch) { this._applyFilters(true); } @@ -96,7 +116,8 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @return {Boolean} If any filter is active */ isAnyFilterActive() { - return this._filteringModel.isAnyFilterActive(); + return this._filteringModel.isAnyFilterActive() + || this._stableBeamsOnly == false; } /** diff --git a/lib/public/views/LhcFills/Overview/index.js b/lib/public/views/LhcFills/Overview/index.js index fdc99de72c..5de64d5989 100644 --- a/lib/public/views/LhcFills/Overview/index.js +++ b/lib/public/views/LhcFills/Overview/index.js @@ -19,7 +19,7 @@ import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplay import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; -import { stableBeamFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; +import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; const TABLEROW_HEIGHT = 53.3; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -52,7 +52,7 @@ const showLhcFillsTable = (lhcFillsOverviewModel) => { h('.flex-row.header-container.g2.pv2', [ frontLink(h('button.btn.btn-primary', 'Statistics'), 'statistics'), filtersPanelPopover(lhcFillsOverviewModel, lhcFillsActiveColumns), - stableBeamFilter(lhcFillsOverviewModel.filteringModel.get('beamsMode')), + toggleStableBeamOnlyFilter(lhcFillsOverviewModel), ]), h('.w-100.flex-column', [ table(lhcFillsOverviewModel.items, lhcFillsActiveColumns, null, { tableClasses: '.table-sm' }), diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index ae3904ca95..dd5cc41f2c 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -41,15 +41,10 @@ class GetAllLhcFillsUseCase { const queryBuilder = new QueryBuilder(); if (filter) { - const { beamsMode } = filter; - if (beamsMode) { - switch (beamsMode) { - case 'STABLE BEAMS': - queryBuilder.where('stableBeamsStart').not().is(null); - break; - default: - break; - } + const { hasStableBeams } = filter; + if (hasStableBeams) { + // For now, if a stableBeamsStart is present, then a beam is stable + queryBuilder.where('stableBeamsStart').not().is(null); } } diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index cf3b009437..089420a321 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -15,7 +15,6 @@ const { environment: { GetAllEnvironmentsUseCase } } = require('../../../../lib/ const { lhcFill: { GetAllLhcFillsUseCase } } = require('../../../../lib/usecases/index.js'); const { dtos: { GetAllLhcFillsDto } } = require('../../../../lib/domain/index.js'); const chai = require('chai'); -const { BeamModes } = require('../../../../lib/public/domain/enums/BeamModes.js'); const { expect } = chai; @@ -32,7 +31,7 @@ module.exports = () => { }); it('should only containing lhc fills with stable beams', async () => { - getAllLhcFillsDto.query = { filter: { beamsMode: BeamModes.STABLE_BEAMS } }; + getAllLhcFillsDto.query = { filter: { hasStableBeams: true } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); expect(lhcFills).to.be.an('array'); diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 9c5c7b0ac9..bb93b78f55 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -32,6 +32,8 @@ const { expect } = chai; const percentageRegex = new RegExp(/\d{1,2}.\d{2}%/); const durationRegex = new RegExp(/\d{2}:\d{2}:\d{2}/); +const filterButtonSellector= '#openFilterToggle'; + const defaultViewPort = { width: 700, height: 763, @@ -268,19 +270,18 @@ module.exports = () => { it('should successfully display filter elements', async () => { const filterSBExpect = { selector: '.w-30', value: 'Stable Beams Only' }; await goToPage(page, 'lhc-fill-overview'); - // Open the filtering panel - await openFilteringPanel(page); + await pressElement(page, filterButtonSellector); await expectInnerText(page, filterSBExpect.selector, filterSBExpect.value); }); it('should successfully un-apply Stable Beam filter menu', async () => { - const filterButtonSBOnlySelector= '#stableBeamsOnlyRadioOFF'; + const filterButtonSBOnlySellector= '#stableBeamsOnlyRadioOFF'; + const filterSBExpect = { selector: '.w-30', value: 'Stable Beams Only' }; await goToPage(page, 'lhc-fill-overview'); await waitForTableLength(page, 5); - // Open the filtering panel - await openFilteringPanel(page); - await pressElement(page, filterButtonSBOnlySelector); + await pressElement(page, filterButtonSellector); + await pressElement(page, filterButtonSBOnlySellector); await waitForTableLength(page, 6); }); From 51b50d92d7316d97f327f2211b73f6837698f733 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 10 Dec 2025 14:28:55 +0100 Subject: [PATCH 13/76] [O2B-1502] Cherry pick previous feedback changes --- .../LhcFillsFilter/stableBeamFilter.js | 6 ++-- .../components/Filters/RunsFilter/dcs.js | 26 +++++++------- .../components/Filters/RunsFilter/ddflp.js | 20 +++++------ .../components/Filters/RunsFilter/epn.js | 26 +++++++------- .../common/form/inputs/radioButton.js | 36 ++++++++++--------- test/public/lhcFills/overview.test.js | 13 ++++--- 6 files changed, 65 insertions(+), 62 deletions(-) diff --git a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js index 5a489c7221..d6e8ce61b0 100644 --- a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js @@ -13,7 +13,7 @@ import { h } from '/js/src/index.js'; import { switchInput } from '../../common/form/switchInput.js'; -import radiobutton from '../../common/form/inputs/RadioButton.js'; +import { radioButton } from '../../common/form/inputs/radioButton.js'; /** * Display a toggle switch or radio buttons to filter stable beams only @@ -29,13 +29,13 @@ export const toggleStableBeamOnlyFilter = (lhcFillsOverviewModel, radioButtonMod const label2 = 'ON'; if (radioButtonMode) { return h('.form-group-header.flex-row.w-100', [ - radiobutton({ + radioButton({ label: label1, isChecked: isStableBeamsOnly === false, action: () => lhcFillsOverviewModel.setStableBeamsFilter(false), name: name, }), - radiobutton({ + radioButton({ label: label2, isChecked: isStableBeamsOnly === true, action: () => lhcFillsOverviewModel.setStableBeamsFilter(true), diff --git a/lib/public/components/Filters/RunsFilter/dcs.js b/lib/public/components/Filters/RunsFilter/dcs.js index 807567d2f2..590eb81b78 100644 --- a/lib/public/components/Filters/RunsFilter/dcs.js +++ b/lib/public/components/Filters/RunsFilter/dcs.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import radiobutton from '../../common/form/inputs/RadioButton.js'; +import { radioButton } from '../../common/form/inputs/radioButton.js'; import { h } from '/js/src/index.js'; /** @@ -22,27 +22,27 @@ import { h } from '/js/src/index.js'; const dcsOperationRadioButtons = (runModel) => { const state = runModel.getDcsFilterOperation(); const name = 'dcsFilterRadio'; - const label1 = 'ANY'; - const label2 = 'OFF'; - const label3 = 'ON'; + const labelAny = 'ANY'; + const labelOff = 'OFF'; + const labelOn = 'ON'; return h('.form-group-header.flex-row.w-100', [ - radiobutton({ - label: label1, + radioButton({ + label: labelAny, isChecked: state === '', action: () => runModel.removeDcs(), - name: name, + name, }), - radiobutton({ - label: label2, + radioButton({ + label: labelOff, isChecked: state === false, action: () => runModel.setDcsFilterOperation(false), - name: name, + name, }), - radiobutton({ - label: label3, + radioButton({ + label: labelOn, isChecked: state === true, action: () => runModel.setDcsFilterOperation(true), - name: name, + name, }), ]); }; diff --git a/lib/public/components/Filters/RunsFilter/ddflp.js b/lib/public/components/Filters/RunsFilter/ddflp.js index 00fdfc67d1..74bf28f4ba 100644 --- a/lib/public/components/Filters/RunsFilter/ddflp.js +++ b/lib/public/components/Filters/RunsFilter/ddflp.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import radioButton from '../../common/form/inputs/RadioButton.js'; +import { radioButton } from '../../common/form/inputs/radioButton.js'; import { h } from '/js/src/index.js'; /** @@ -22,27 +22,27 @@ import { h } from '/js/src/index.js'; const ddflpOperationRadioButtons = (runModel) => { const state = runModel.getDdflpFilterOperation(); const name = 'ddFlpFilterRadio'; - const label1 = 'ANY'; - const label2 = 'OFF'; - const label3 = 'ON'; + const labelAny = 'ANY'; + const labelOff = 'OFF'; + const labelOn = 'ON'; return h('.form-group-header.flex-row.w-100', [ radioButton({ - label: label1, + label: labelAny, isChecked: state === '', action: () => runModel.removeDdflp(), - name: name, + name, }), radioButton({ - label: label2, + label: labelOff, isChecked: state === false, action: () => runModel.setDdflpFilterOperation(false), - name: name, + name, }), radioButton({ - label: label3, + label: labelOn, isChecked: state === true, action: () => runModel.setDdflpFilterOperation(true), - name: name, + name, }), ]); }; diff --git a/lib/public/components/Filters/RunsFilter/epn.js b/lib/public/components/Filters/RunsFilter/epn.js index f103ca34dd..5e639d8afb 100644 --- a/lib/public/components/Filters/RunsFilter/epn.js +++ b/lib/public/components/Filters/RunsFilter/epn.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import radiobutton from '../../common/form/inputs/RadioButton.js'; +import { radioButton } from '../../common/form/inputs/radioButton.js'; import { h } from '/js/src/index.js'; /** @@ -22,27 +22,27 @@ import { h } from '/js/src/index.js'; const epnOperationRadioButtons = (runModel) => { const state = runModel.getEpnFilterOperation(); const name = 'epnFilterRadio'; - const label1 = 'ANY'; - const label2 = 'OFF'; - const label3 = 'ON'; + const labelAny = 'ANY'; + const labelOff = 'OFF'; + const labelOn = 'ON'; return h('.form-group-header.flex-row.w-100', [ - radiobutton({ - label: label1, + radioButton({ + label: labelAny, isChecked: state === '', action: () => runModel.removeEpn(), - name: name, + name, }), - radiobutton({ - label: label2, + radioButton({ + label: labelOff, isChecked: state === false, action: () => runModel.setEpnFilterOperation(false), - name: name, + name, }), - radiobutton({ - label: label3, + radioButton({ + label: labelOn, isChecked: state === true, action: () => runModel.setEpnFilterOperation(true), - name: name, + name, }), ]); }; diff --git a/lib/public/components/common/form/inputs/radioButton.js b/lib/public/components/common/form/inputs/radioButton.js index 4546d76af0..8f0c159a59 100644 --- a/lib/public/components/common/form/inputs/radioButton.js +++ b/lib/public/components/common/form/inputs/radioButton.js @@ -14,44 +14,48 @@ import { h } from '/js/src/index.js'; /** - * @typedef radioButtonConfig - configration object for radioButton. + * @typedef RadioButtonConfigStyle + * @property {string} labelStyle - value for the label's style property. + * @property {string} radioButtonStyle - value for the radio button's element styling. + */ + +/** + * @typedef RadioButtonConfig - configration object for radioButton. * * @property {string} label - label to be displayed to the user for radio button * @property {boolean} isChecked - is radio button selected or not - * @property {function} action - action to be followed on user click + * @property {function()} action - action to be followed on user click * @property {string} id - id of the radiobutton element * @property {string} name - name of the radiobutton element - * @property {string} style - label style property + * @property {RadioButtonConfigStyle} style - label style property */ /** * Build a radio button with its configuration and actions - * @param {radioButtonConfig} configuration - configration object for radioButton. + * @param {RadioButtonConfig} configuration - configuration object for radioButton. * @return {vnode} - radio button with associated label. */ -const radiobutton = (configuration = {}) => { +export const radioButton = (configuration = {}) => { const { - label = 'radio', + label = '', isChecked = false, action = () => { }, - name = 'value', + name = '', id = `${name}${label}`, - style = 'cursor: pointer;', + style = { labelStyle: 'cursor: pointer;', radioButtonStyle: '.w-33' }, } = configuration; - return h('.w-33.form-check', [ + return h(`${style.radioButtonStyle}.form-check`, [ h('input.form-check-input', { onchange: action, type: 'radio', - id: id, - name: name, + id, + name, value: label, checked: isChecked, - }, ''), + }), h('label.form-check-label', { - style: style, - for: `${name}${label}`, + style: style.labelStyle, + for: id, }, label), ]); }; - -export default radiobutton; diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index bb93b78f55..9c5c7b0ac9 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -32,8 +32,6 @@ const { expect } = chai; const percentageRegex = new RegExp(/\d{1,2}.\d{2}%/); const durationRegex = new RegExp(/\d{2}:\d{2}:\d{2}/); -const filterButtonSellector= '#openFilterToggle'; - const defaultViewPort = { width: 700, height: 763, @@ -270,18 +268,19 @@ module.exports = () => { it('should successfully display filter elements', async () => { const filterSBExpect = { selector: '.w-30', value: 'Stable Beams Only' }; await goToPage(page, 'lhc-fill-overview'); - await pressElement(page, filterButtonSellector); + // Open the filtering panel + await openFilteringPanel(page); await expectInnerText(page, filterSBExpect.selector, filterSBExpect.value); }); it('should successfully un-apply Stable Beam filter menu', async () => { - const filterButtonSBOnlySellector= '#stableBeamsOnlyRadioOFF'; - const filterSBExpect = { selector: '.w-30', value: 'Stable Beams Only' }; + const filterButtonSBOnlySelector= '#stableBeamsOnlyRadioOFF'; await goToPage(page, 'lhc-fill-overview'); await waitForTableLength(page, 5); - await pressElement(page, filterButtonSellector); - await pressElement(page, filterButtonSBOnlySellector); + // Open the filtering panel + await openFilteringPanel(page); + await pressElement(page, filterButtonSBOnlySelector); await waitForTableLength(page, 6); }); From c0c85592d4ed70c5fa580a1fb105fdec082cd351 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 10 Dec 2025 15:58:15 +0100 Subject: [PATCH 14/76] [O2B-1502] Integrated stable beam only filter into filtermodel. --- .../LhcFillsFilter/StableBeamFilterModel.js | 78 +++++++++++++++++++ .../LhcFillsFilter/stableBeamFilter.js | 17 ++-- .../ActiveColumns/lhcFillsActiveColumns.js | 2 +- .../Overview/LhcFillsOverviewModel.js | 34 ++------ lib/public/views/LhcFills/Overview/index.js | 2 +- 5 files changed, 94 insertions(+), 39 deletions(-) create mode 100644 lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js diff --git a/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js new file mode 100644 index 0000000000..41be45ff6a --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js @@ -0,0 +1,78 @@ +/** + * @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 { SelectionModel } from '../../common/selection/SelectionModel.js'; + +/** + * Stable beam filter filter model + * Holds true or false value + */ +export class StableBeamFilterModel extends SelectionModel { + /** + * Constructor + * @param {boolean} value if true sets the filter's starting value to be true. + */ + constructor(value = false) { + super({ availableOptions: [{ value: true }, { value: false }], + defaultSelection: [{ value: false }], + multiple: false, + allowEmpty: false }); + // Sets filter value to true if + if (value) { + this.setStableBeamsOnly(value); + } + } + + /** + * Returns true if the current filter is stable beams only + * + * @return {boolean} true if filter is stable beams only + */ + isStableBeamsOnly() { + const selectedOptions = this.selected; + return selectedOptions[0] === true; + } + + /** + * Sets the current filter to stable beams only + * + * @param {boolean} value value to set this stable beams only filter with + * @return {void} + */ + setStableBeamsOnly(value) { + if (value) { + this.select({ value: true }); + } else { + this.select({ value: false }); + } + } + + /** + * Get normalized selected option + */ + get normalized() { + return this.selected[0]; + } + + /** + * Reset the filter to default values + * + * @return {void} + */ + resetDefaults() { + if (!this.isEmpty) { + this.reset(); + this.notify(); + } + } +} diff --git a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js index d6e8ce61b0..ba34b2af1a 100644 --- a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js @@ -18,12 +18,11 @@ import { radioButton } from '../../common/form/inputs/radioButton.js'; /** * Display a toggle switch or radio buttons to filter stable beams only * - * @param {LhcFillsOverviewModel} lhcFillsOverviewModel the overview model + * @param {StableBeamFilterModel} stableBeamFilterModel the stableBeamFilterModel * @param {boolean} radioButtonMode define whether or not to return radio buttons or a switch. * @returns {Component} the toggle switch */ -export const toggleStableBeamOnlyFilter = (lhcFillsOverviewModel, radioButtonMode = false) => { - const isStableBeamsOnly = lhcFillsOverviewModel.getStableBeamsOnly(); +export const toggleStableBeamOnlyFilter = (stableBeamFilterModel, radioButtonMode = false) => { const name = 'stableBeamsOnlyRadio'; const label1 = 'OFF'; const label2 = 'ON'; @@ -31,20 +30,20 @@ export const toggleStableBeamOnlyFilter = (lhcFillsOverviewModel, radioButtonMod return h('.form-group-header.flex-row.w-100', [ radioButton({ label: label1, - isChecked: isStableBeamsOnly === false, - action: () => lhcFillsOverviewModel.setStableBeamsFilter(false), + isChecked: !stableBeamFilterModel.isStableBeamsOnly(), + action: () => stableBeamFilterModel.setStableBeamsOnly(false), name: name, }), radioButton({ label: label2, - isChecked: isStableBeamsOnly === true, - action: () => lhcFillsOverviewModel.setStableBeamsFilter(true), + isChecked: stableBeamFilterModel.isStableBeamsOnly(), + action: () => stableBeamFilterModel.setStableBeamsOnly(true), name: name, }), ]); } else { - return switchInput(isStableBeamsOnly, (newState) => { - lhcFillsOverviewModel.setStableBeamsFilter(newState); + return switchInput(stableBeamFilterModel.isStableBeamsOnly(), (newState) => { + stableBeamFilterModel.setStableBeamsOnly(newState); }, { labelAfter: 'STABLE BEAM ONLY' }); } }; diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 3e5be046bf..f575652b34 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -89,7 +89,7 @@ export const lhcFillsActiveColumns = { name: 'Stable Beams Only', visible: false, format: (boolean) => boolean ? 'On' : 'Off', - filter: (lhcFillModel) => toggleStableBeamOnlyFilter(lhcFillModel, true), + filter: (lhcFillModel) => toggleStableBeamOnlyFilter(lhcFillModel.filteringModel.get('hasStableBeams'), true), }, stableBeamsDuration: { name: 'SB Duration', diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index d83f87329d..27d994b137 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -12,6 +12,7 @@ */ import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; +import { StableBeamFilterModel } from '../../../components/Filters/LhcFillsFilter/StableBeamFilterModel.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; @@ -29,8 +30,9 @@ export class LhcFillsOverviewModel extends OverviewPageModel { constructor(stableBeamsOnly = false) { super(); - this._filteringModel = new FilteringModel({}); - this._stableBeamsOnly = stableBeamsOnly; + this._filteringModel = new FilteringModel({ + hasStableBeams: new StableBeamFilterModel(stableBeamsOnly), + }); this._filteringModel.observe(() => this._applyFilters(true)); this._filteringModel.visualChange$.bubbleTo(this); @@ -61,31 +63,10 @@ export class LhcFillsOverviewModel extends OverviewPageModel { async getLoadParameters() { return { ...await super.getLoadParameters(), - 'filter[hasStableBeams]': this._stableBeamsOnly, + ...{ filter: this.filteringModel.normalized }, }; } - /** - * Sets the stable beams filter - * - * @param {boolean} stableBeamsOnly the new stable beams filter value - * @return {void} - */ - setStableBeamsFilter(stableBeamsOnly) { - this._stableBeamsOnly = stableBeamsOnly; - this._applyFilters(); - this.notify(); - } - - /** - * Checks if the stable beams filter is set - * - * @return {boolean} true if the stable beams filter is active - */ - getStableBeamsOnly() { - return this._stableBeamsOnly; - } - /** * 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 @@ -104,8 +85,6 @@ export class LhcFillsOverviewModel extends OverviewPageModel { resetFiltering(fetch = true) { this._filteringModel.reset(); - this._stableBeamsOnly = true; - if (fetch) { this._applyFilters(true); } @@ -116,8 +95,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @return {Boolean} If any filter is active */ isAnyFilterActive() { - return this._filteringModel.isAnyFilterActive() - || this._stableBeamsOnly == false; + return this._filteringModel.isAnyFilterActive(); } /** diff --git a/lib/public/views/LhcFills/Overview/index.js b/lib/public/views/LhcFills/Overview/index.js index 5de64d5989..cfef2aebad 100644 --- a/lib/public/views/LhcFills/Overview/index.js +++ b/lib/public/views/LhcFills/Overview/index.js @@ -52,7 +52,7 @@ const showLhcFillsTable = (lhcFillsOverviewModel) => { h('.flex-row.header-container.g2.pv2', [ frontLink(h('button.btn.btn-primary', 'Statistics'), 'statistics'), filtersPanelPopover(lhcFillsOverviewModel, lhcFillsActiveColumns), - toggleStableBeamOnlyFilter(lhcFillsOverviewModel), + toggleStableBeamOnlyFilter(lhcFillsOverviewModel.filteringModel.get('hasStableBeams')), ]), h('.w-100.flex-column', [ table(lhcFillsOverviewModel.items, lhcFillsActiveColumns, null, { tableClasses: '.table-sm' }), From 9934e566fad475ca7283a94d8c23ec19bf4e4f76 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 10 Dec 2025 17:08:07 +0100 Subject: [PATCH 15/76] [O2B-1502] fixed stable beam default value --- .../LhcFillsFilter/StableBeamFilterModel.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js index 41be45ff6a..835f63588a 100644 --- a/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js +++ b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js @@ -24,13 +24,9 @@ export class StableBeamFilterModel extends SelectionModel { */ constructor(value = false) { super({ availableOptions: [{ value: true }, { value: false }], - defaultSelection: [{ value: false }], + defaultSelection: [{ value: value }], multiple: false, allowEmpty: false }); - // Sets filter value to true if - if (value) { - this.setStableBeamsOnly(value); - } } /** @@ -39,8 +35,7 @@ export class StableBeamFilterModel extends SelectionModel { * @return {boolean} true if filter is stable beams only */ isStableBeamsOnly() { - const selectedOptions = this.selected; - return selectedOptions[0] === true; + return this.current; } /** @@ -50,11 +45,7 @@ export class StableBeamFilterModel extends SelectionModel { * @return {void} */ setStableBeamsOnly(value) { - if (value) { - this.select({ value: true }); - } else { - this.select({ value: false }); - } + this.select({ value }); } /** From f247a6f86b8945e22824fe19ae547ba52daea2e6 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 11 Dec 2025 11:43:46 +0100 Subject: [PATCH 16/76] [O2B-1502] Fixed logic and type --- .../Filters/LhcFillsFilter/StableBeamFilterModel.js | 12 ++++++++++-- .../components/common/form/inputs/radioButton.js | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js index 835f63588a..27cb8c022d 100644 --- a/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js +++ b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js @@ -24,7 +24,7 @@ export class StableBeamFilterModel extends SelectionModel { */ constructor(value = false) { super({ availableOptions: [{ value: true }, { value: false }], - defaultSelection: [{ value: value }], + defaultSelection: [{ value }], multiple: false, allowEmpty: false }); } @@ -52,7 +52,15 @@ export class StableBeamFilterModel extends SelectionModel { * Get normalized selected option */ get normalized() { - return this.selected[0]; + return this.current; + } + + /** + * Overrides SelectionModel.isEmpty to respect the fact that stable beam filter cannot be empty. + * @returns {boolean} true if the current value of the filter is false. + */ + get isEmpty() { + return this.current === false; } /** diff --git a/lib/public/components/common/form/inputs/radioButton.js b/lib/public/components/common/form/inputs/radioButton.js index 8f0c159a59..32b4a80cab 100644 --- a/lib/public/components/common/form/inputs/radioButton.js +++ b/lib/public/components/common/form/inputs/radioButton.js @@ -20,7 +20,7 @@ import { h } from '/js/src/index.js'; */ /** - * @typedef RadioButtonConfig - configration object for radioButton. + * @typedef RadioButtonConfig - configuration object for radioButton. * * @property {string} label - label to be displayed to the user for radio button * @property {boolean} isChecked - is radio button selected or not From 9b672812baadd91f1b944eb2c6ce929239cabd2f Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 11 Dec 2025 12:08:54 +0100 Subject: [PATCH 17/76] [O2B-1502] Don't set any defaults in the filter as it will conflict with the query/reset logic. Just set the value afterwards --- .../Filters/LhcFillsFilter/StableBeamFilterModel.js | 4 ++-- lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js index 27cb8c022d..c7f8fa6a31 100644 --- a/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js +++ b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js @@ -22,9 +22,9 @@ export class StableBeamFilterModel extends SelectionModel { * Constructor * @param {boolean} value if true sets the filter's starting value to be true. */ - constructor(value = false) { + constructor() { super({ availableOptions: [{ value: true }, { value: false }], - defaultSelection: [{ value }], + defaultSelection: [{ value: false }], multiple: false, allowEmpty: false }); } diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 27d994b137..9a5d1227ec 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -31,13 +31,17 @@ export class LhcFillsOverviewModel extends OverviewPageModel { super(); this._filteringModel = new FilteringModel({ - hasStableBeams: new StableBeamFilterModel(stableBeamsOnly), + hasStableBeams: new StableBeamFilterModel(), }); this._filteringModel.observe(() => this._applyFilters(true)); this._filteringModel.visualChange$.bubbleTo(this); this.reset(false); + + if (stableBeamsOnly) { + this._filteringModel.get('hasStableBeams').setStableBeamsOnly(true); + } } /** From ea0880f50557031b6dfb8b8a2d0fe6dedea0c3ef Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 11 Dec 2025 17:16:15 +0100 Subject: [PATCH 18/76] [O2B-1502] Code cleanup --- .../components/Filters/LhcFillsFilter/StableBeamFilterModel.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js index c7f8fa6a31..1bc3f8aed2 100644 --- a/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js +++ b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js @@ -14,13 +14,12 @@ import { SelectionModel } from '../../common/selection/SelectionModel.js'; /** - * Stable beam filter filter model + * Stable beam filter model * Holds true or false value */ export class StableBeamFilterModel extends SelectionModel { /** * Constructor - * @param {boolean} value if true sets the filter's starting value to be true. */ constructor() { super({ availableOptions: [{ value: true }, { value: false }], From 46d4ae869469b9b7e4ae9e2efe662c848b6dd622 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 15 Dec 2025 10:11:20 +0100 Subject: [PATCH 19/76] [O2B-1502] minor changes, processed feedback --- .../components/Filters/LhcFillsFilter/stableBeamFilter.js | 8 ++++---- .../views/LhcFills/Overview/LhcFillsOverviewModel.js | 4 ++-- test/public/lhcFills/overview.test.js | 5 ++--- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js index ba34b2af1a..b4429c002c 100644 --- a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js @@ -24,18 +24,18 @@ import { radioButton } from '../../common/form/inputs/radioButton.js'; */ export const toggleStableBeamOnlyFilter = (stableBeamFilterModel, radioButtonMode = false) => { const name = 'stableBeamsOnlyRadio'; - const label1 = 'OFF'; - const label2 = 'ON'; + const labelOff = 'OFF'; + const labelOn = 'ON'; if (radioButtonMode) { return h('.form-group-header.flex-row.w-100', [ radioButton({ - label: label1, + label: labelOff, isChecked: !stableBeamFilterModel.isStableBeamsOnly(), action: () => stableBeamFilterModel.setStableBeamsOnly(false), name: name, }), radioButton({ - label: label2, + label: labelOn, isChecked: stableBeamFilterModel.isStableBeamsOnly(), action: () => stableBeamFilterModel.setStableBeamsOnly(true), name: name, diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 9a5d1227ec..bb5598a5e8 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -11,6 +11,7 @@ * or submit itself to any jurisdiction. */ +import { buildUrl } from '/js/src/index.js'; import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; import { StableBeamFilterModel } from '../../../components/Filters/LhcFillsFilter/StableBeamFilterModel.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; @@ -58,7 +59,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @inheritDoc */ getRootEndpoint() { - return '/api/lhcFills'; + return buildUrl('/api/lhcFills', { filter: this.filteringModel.normalized }); } /** @@ -67,7 +68,6 @@ export class LhcFillsOverviewModel extends OverviewPageModel { async getLoadParameters() { return { ...await super.getLoadParameters(), - ...{ filter: this.filteringModel.normalized }, }; } diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 9c5c7b0ac9..269239f2c2 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -266,15 +266,14 @@ module.exports = () => { }); it('should successfully display filter elements', async () => { - const filterSBExpect = { selector: '.w-30', value: 'Stable Beams Only' }; + const filterSBExpect = { selector: '.stableBeams-filter .w-30', value: 'Stable Beams Only' }; await goToPage(page, 'lhc-fill-overview'); // Open the filtering panel await openFilteringPanel(page); await expectInnerText(page, filterSBExpect.selector, filterSBExpect.value); }); - - it('should successfully un-apply Stable Beam filter menu', async () => { + it('should successfully un-apply Stable Beam filter menu', async () => { const filterButtonSBOnlySelector= '#stableBeamsOnlyRadioOFF'; await goToPage(page, 'lhc-fill-overview'); await waitForTableLength(page, 5); From 91e350cf546da7883aab71166713dec74e46ddc5 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 15 Dec 2025 14:10:41 +0100 Subject: [PATCH 20/76] [O2B-1502] Removed duplicate function due to override --- .../views/LhcFills/Overview/LhcFillsOverviewModel.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index bb5598a5e8..787d467fe5 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -62,15 +62,6 @@ export class LhcFillsOverviewModel extends OverviewPageModel { return buildUrl('/api/lhcFills', { filter: this.filteringModel.normalized }); } - /** - * @inheritDoc - */ - async getLoadParameters() { - return { - ...await super.getLoadParameters(), - }; - } - /** * 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 From e95c847bc7e0ca404687252b0f98de421607082a Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 27 Nov 2025 18:14:19 +0100 Subject: [PATCH 21/76] [O2B-1503] Added front end fill number filter --- .../LhcFillsFilter/fillNumberFilter.js | 25 +++++++++++++++++++ .../ActiveColumns/lhcFillsActiveColumns.js | 2 ++ .../Overview/LhcFillsOverviewModel.js | 2 ++ 3 files changed, 29 insertions(+) create mode 100644 lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js diff --git a/lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js b/lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js new file mode 100644 index 0000000000..34d65f092f --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js @@ -0,0 +1,25 @@ +/** + * @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 { rawTextFilter } from '../common/filters/rawTextFilter.js'; + +/** + * Component to filter LHC-fills by fill number + * + * @param {RawTextFilterModel} filterModel the filter model + * @returns {Component} the toggle switch + */ +export const fillNumberFilter = (filterModel) => rawTextFilter( + filterModel, + { classes: ['w-100', 'fill-numbers-filter'], placeholder: 'e.g. 6, 3, 4' }, +); diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index f575652b34..5442ed8bcc 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -24,6 +24,7 @@ import { infologgerLinksComponents } from '../../../components/common/externalLi import { formatBeamType } from '../../../utilities/formatting/formatBeamType.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; +import { fillNumberFilter } from '../../../components/Filters/LhcFillsFilter/fillNumberFilter.js'; /** * List of active columns for a lhc fills table @@ -49,6 +50,7 @@ export const lhcFillsActiveColumns = { ), ], ), + filter: (lhcFillModel) => fillNumberFilter(lhcFillModel.filteringModel.get('fillNumbers')), profiles: { lhcFill: true, environment: true, diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 787d467fe5..55a417dc66 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -14,6 +14,7 @@ import { buildUrl } from '/js/src/index.js'; import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; import { StableBeamFilterModel } from '../../../components/Filters/LhcFillsFilter/StableBeamFilterModel.js'; +import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; @@ -32,6 +33,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { super(); this._filteringModel = new FilteringModel({ + fillNumbers: new RawTextFilterModel(), hasStableBeams: new StableBeamFilterModel(), }); From 5077fec4b85052f98cf0118d6d68ba43c7252b1c Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Fri, 28 Nov 2025 13:59:43 +0100 Subject: [PATCH 22/76] [O2B-1503] fillNumbers work, todo ranges --- lib/domain/dtos/filters/LhcFillsFilterDto.js | 4 ++ lib/domain/dtos/filters/RunFilterDto.js | 30 +----------- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 11 ++++- lib/utilities/validateRange.js | 28 +++++++++++ test/api/runs.test.js | 2 +- .../lhcFill/GetAllLhcFillsUseCase.test.js | 48 +++++++++++++++++++ 6 files changed, 92 insertions(+), 31 deletions(-) create mode 100644 lib/utilities/validateRange.js diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index c42dadf229..225d3b01c5 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -11,7 +11,11 @@ * or submit itself to any jurisdiction. */ const Joi = require('joi'); +const { validateRange } = require('../../../utilities/validateRange'); exports.LhcFillsFilterDto = Joi.object({ hasStableBeams: Joi.boolean(), + fillNumbers: Joi.string().trim().custom(validateRange).messages({ + 'any.invalid': '{{#message}}', + }), }); diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index 0c8c032b63..e5513ec0a4 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -18,6 +18,7 @@ const { IntegerComparisonDto, FloatComparisonDto } = require('./NumericalCompari const { RUN_CALIBRATION_STATUS } = require('../../enums/RunCalibrationStatus.js'); const { RUN_DEFINITIONS } = require('../../enums/RunDefinition.js'); const { singleRunsCollectionCustomCheck } = require('../utils.js'); +const { validateRange } = require('../../../utilities/validateRange.js'); const DetectorsFilterDto = Joi.object({ operator: Joi.string().valid('or', 'and', 'none').required(), @@ -30,35 +31,6 @@ const EorReasonFilterDto = Joi.object({ description: Joi.string(), }); -/** - * Validates run numbers ranges to not exceed 100 runs - * - * @param {*} value The value to validate - * @param {*} helpers The helpers object - * @returns {Object} The value if validation passes - */ -const validateRange = (value, helpers) => { - const MAX_RANGE_SIZE = 100; - - const runNumbers = value.split(',').map((runNumber) => runNumber.trim()); - - for (const runNumber of runNumbers) { - if (runNumber.includes('-')) { - const [start, end] = runNumber.split('-').map((n) => parseInt(n, 10)); - if (Number.isNaN(start) || Number.isNaN(end) || start > end) { - return helpers.error('any.invalid', { message: `Invalid range: ${runNumber}` }); - } - const rangeSize = end - start + 1; - - if (rangeSize > MAX_RANGE_SIZE) { - return helpers.error('any.invalid', { message: `Given range exceeds max size of ${MAX_RANGE_SIZE} runs: ${runNumber}` }); - } - } - } - - return value; -}; - exports.RunFilterDto = Joi.object({ runNumbers: Joi.string().trim().custom(validateRange).messages({ 'any.invalid': '{{#message}}', diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index dd5cc41f2c..111aa2f968 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -41,11 +41,20 @@ class GetAllLhcFillsUseCase { const queryBuilder = new QueryBuilder(); if (filter) { - const { hasStableBeams } = filter; + const { hasStableBeams, fillNumbers } = filter; if (hasStableBeams) { // For now, if a stableBeamsStart is present, then a beam is stable queryBuilder.where('stableBeamsStart').not().is(null); } + + if (fillNumbers) { + const fillNumbersSplit = fillNumbers.split(','); + + const fillNumbersValidated = fillNumbersSplit.filter((number) => !Number.isNaN(number)); + if (fillNumbersValidated.length > 0) { + queryBuilder.where('fillNumber').oneOf(...fillNumbersValidated); + } + } } const { count, rows } = await TransactionHelper.provide(async () => { diff --git a/lib/utilities/validateRange.js b/lib/utilities/validateRange.js new file mode 100644 index 0000000000..c4a37c5de7 --- /dev/null +++ b/lib/utilities/validateRange.js @@ -0,0 +1,28 @@ +/** + * Validates numbers ranges to not exceed 100 entities + * + * @param {*} value The value to validate + * @param {*} helpers The helpers object + * @returns {Object} The value if validation passes + */ +export const validateRange = (value, helpers) => { + const MAX_RANGE_SIZE = 100; + + const numbers = value.split(',').map((runNumber) => runNumber.trim()); + + for (const number of numbers) { + if (number.includes('-')) { + const [start, end] = number.split('-').map((n) => parseInt(n, 10)); + if (Number.isNaN(start) || Number.isNaN(end) || start > end) { + return helpers.error('any.invalid', { message: `Invalid range: ${number}` }); + } + const rangeSize = end - start + 1; + + if (rangeSize > MAX_RANGE_SIZE) { + return helpers.error('any.invalid', { message: `Given range exceeds max size of ${MAX_RANGE_SIZE} range: ${number}` }); + } + } + } + + return value; +}; diff --git a/test/api/runs.test.js b/test/api/runs.test.js index 2c96ed46f5..4322040bb3 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -166,7 +166,7 @@ module.exports = () => { expect(response.status).to.equal(400); const { errors: [error] } = response.body; expect(error.title).to.equal('Invalid Attribute'); - expect(error.detail).to.equal(`Given range exceeds max size of ${MAX_RANGE_SIZE} runs: ${runNumberRange}`); + expect(error.detail).to.equal(`Given range exceeds max size of ${MAX_RANGE_SIZE} range: ${runNumberRange}`); }); it('should return 400 if the calibration status filter is invalid', async () => { diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index 089420a321..9c059cf281 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -40,4 +40,52 @@ module.exports = () => { expect(lhcFill.stableBeamsStart).to.not.be.null; }); }); + + // Fill number filter tests + + it('should only contain specified fill number', async () => { + getAllLhcFillsDto.query = { filter: { hasStableBeams: true, fillNumbers: '6' } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); + expect(lhcFills).to.be.an('array').and.lengthOf(1) + + lhcFills.forEach((lhcFill) => { + expect(lhcFill.fillNumber).to.equal(6) + }); + }) + + it('should only contain specified fill numbers', async () => { + getAllLhcFillsDto.query = { filter: { hasStableBeams: true, fillNumbers: '6,3' } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); + + + expect(lhcFills).to.be.an('array').and.lengthOf(2) + + lhcFills.forEach((lhcFill) => { + expect(lhcFill.fillNumber).oneOf([6,3]) + }); + }) + + it('should only contain specified fill numbers, whitespace', async () => { + getAllLhcFillsDto.query = { filter: { hasStableBeams: true, fillNumbers: ' 6 , 3 ' } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); + + + expect(lhcFills).to.be.an('array').and.lengthOf(2) + + lhcFills.forEach((lhcFill) => { + expect(lhcFill.fillNumber).oneOf([6,3]) + }); + }) + + it('should only contain specified fill numbers, comma misplacement', async () => { + getAllLhcFillsDto.query = { filter: { hasStableBeams: true, fillNumbers: ',6,3,' } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); + + + expect(lhcFills).to.be.an('array').and.lengthOf(2) + + lhcFills.forEach((lhcFill) => { + expect(lhcFill.fillNumber).oneOf([6,3]) + }); + }) }; From 9130c87eaa693d1a0785ef62c38386844da47f6c Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Fri, 28 Nov 2025 14:32:29 +0100 Subject: [PATCH 23/76] [O2B-1503] ranges accepted by fill numbers filter --- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 36 ++++++++++++++++--- .../lhcFill/GetAllLhcFillsUseCase.test.js | 12 +++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 111aa2f968..89ad84266c 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -38,6 +38,8 @@ class GetAllLhcFillsUseCase { const { filter, page = {} } = query; const { limit = ApiConfig.pagination.limit, offset = 0 } = page; + const SEARCH_ITEMS_SEPARATOR = ','; + const queryBuilder = new QueryBuilder(); if (filter) { @@ -48,11 +50,37 @@ class GetAllLhcFillsUseCase { } if (fillNumbers) { - const fillNumbersSplit = fillNumbers.split(','); + /* + * Split by SEARCH_ITEMS_SEPARATOR. Don't validate for only numbers + * Boolean trick: https://michaeluloth.com/javascript-filter-boolean/ + */ + const fillNumberCriteria = fillNumbers.split(SEARCH_ITEMS_SEPARATOR) + .map((runNumbers) => runNumbers.trim()) + .filter(Boolean); + + // Set to prevent duplicate values. + const fillNumberSet = new Set(); + + fillNumberCriteria.forEach((fillNumber) => { + if (fillNumber.includes('-')) { + const [start, end] = fillNumber.split('-').map((n) => parseInt(n, 10)); + if (!Number.isNaN(start) && !Number.isNaN(end)) { + for (let i = start; i <= end; i++) { + fillNumberSet.add(i); + } + } + } else { + if (!Number.isNaN(fillNumber)) { + fillNumberSet.add(Number(fillNumber)); + } + } + }); + + const finalFillnumberList = Array.from(fillNumberSet); - const fillNumbersValidated = fillNumbersSplit.filter((number) => !Number.isNaN(number)); - if (fillNumbersValidated.length > 0) { - queryBuilder.where('fillNumber').oneOf(...fillNumbersValidated); + // Check that the final fill numbers list contains at least one valid fill number + if (finalFillnumberList.length > 0) { + queryBuilder.where('fillNumber').oneOf(...finalFillnumberList); } } } diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index 9c059cf281..fdccf49678 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -65,6 +65,18 @@ module.exports = () => { }); }) + it('should only contain specified fill numbers, range', async () => { + getAllLhcFillsDto.query = { filter: { hasStableBeams: true, fillNumbers: '1-3,6' } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); + + + expect(lhcFills).to.be.an('array').and.lengthOf(4) + + lhcFills.forEach((lhcFill) => { + expect(lhcFill.fillNumber).oneOf([1,2,3,6]) + }); + }) + it('should only contain specified fill numbers, whitespace', async () => { getAllLhcFillsDto.query = { filter: { hasStableBeams: true, fillNumbers: ' 6 , 3 ' } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); From 0d0986e80da65770654c5188735a84b515f6853c Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Fri, 28 Nov 2025 16:46:18 +0100 Subject: [PATCH 24/76] [O2B-1503] Added/fixed test lhc-fill overview --- test/public/lhcFills/overview.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 269239f2c2..02b05c1591 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -267,10 +267,12 @@ module.exports = () => { it('should successfully display filter elements', async () => { 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 #'} await goToPage(page, 'lhc-fill-overview'); // Open the filtering panel await openFilteringPanel(page); await expectInnerText(page, filterSBExpect.selector, filterSBExpect.value); + await expectInnerText(page, filterFillNRExpect.selector, filterFillNRExpect.value); }); it('should successfully un-apply Stable Beam filter menu', async () => { From 804cd4cf2da2c58a08905aed037cc97f7907fa0c Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Fri, 28 Nov 2025 15:04:36 +0100 Subject: [PATCH 25/76] [O2B-1503] doc change --- .../components/Filters/LhcFillsFilter/fillNumberFilter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js b/lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js index 34d65f092f..e04d1d701d 100644 --- a/lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js @@ -17,7 +17,7 @@ import { rawTextFilter } from '../common/filters/rawTextFilter.js'; * Component to filter LHC-fills by fill number * * @param {RawTextFilterModel} filterModel the filter model - * @returns {Component} the toggle switch + * @returns {Component} the text field */ export const fillNumberFilter = (filterModel) => rawTextFilter( filterModel, From 2f5932a1e69190e50d28d6c5d7b270ce6beadf26 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 15 Dec 2025 12:11:42 +0100 Subject: [PATCH 26/76] [O2B-1503] JSDoc enhancements. Extracted duplicate functions to utils. Feedback processed. --- lib/domain/dtos/filters/LhcFillsFilterDto.js | 2 +- lib/domain/dtos/filters/RunFilterDto.js | 2 +- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 30 ++------ lib/usecases/run/GetAllRunsUseCase.js | 26 ++----- lib/utilities/rangeUtils.js | 70 +++++++++++++++++++ lib/utilities/stringUtils.js | 12 ++++ lib/utilities/validateRange.js | 28 -------- 7 files changed, 92 insertions(+), 78 deletions(-) create mode 100644 lib/utilities/rangeUtils.js delete mode 100644 lib/utilities/validateRange.js diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index 225d3b01c5..d1d2af5929 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ const Joi = require('joi'); -const { validateRange } = require('../../../utilities/validateRange'); +const { validateRange } = require('../../../utilities/rangeUtils'); exports.LhcFillsFilterDto = Joi.object({ hasStableBeams: Joi.boolean(), diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index e5513ec0a4..0feda0ddbc 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -18,7 +18,7 @@ const { IntegerComparisonDto, FloatComparisonDto } = require('./NumericalCompari const { RUN_CALIBRATION_STATUS } = require('../../enums/RunCalibrationStatus.js'); const { RUN_DEFINITIONS } = require('../../enums/RunDefinition.js'); const { singleRunsCollectionCustomCheck } = require('../utils.js'); -const { validateRange } = require('../../../utilities/validateRange.js'); +const { validateRange } = require('../../../utilities/rangeUtils.js'); const DetectorsFilterDto = Joi.object({ operator: Joi.string().valid('or', 'and', 'none').required(), diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 89ad84266c..2d83ade80f 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -22,6 +22,8 @@ const { const { lhcFillAdapter } = require('../../database/adapters/index.js'); const { ApiConfig } = require('../../config/index.js'); const { RunDefinition } = require('../../domain/enums/RunDefinition.js'); +const { unpackNumberRange } = require('../../utilities/rangeUtils.js'); +const { splitStringToStringsTrimmed } = require('../../utilities/stringUtils.js'); /** * GetAllLhcFillsUseCase @@ -50,33 +52,9 @@ class GetAllLhcFillsUseCase { } if (fillNumbers) { - /* - * Split by SEARCH_ITEMS_SEPARATOR. Don't validate for only numbers - * Boolean trick: https://michaeluloth.com/javascript-filter-boolean/ - */ - const fillNumberCriteria = fillNumbers.split(SEARCH_ITEMS_SEPARATOR) - .map((runNumbers) => runNumbers.trim()) - .filter(Boolean); + const fillNumberCriteria = splitStringToStringsTrimmed(fillNumbers, SEARCH_ITEMS_SEPARATOR); - // Set to prevent duplicate values. - const fillNumberSet = new Set(); - - fillNumberCriteria.forEach((fillNumber) => { - if (fillNumber.includes('-')) { - const [start, end] = fillNumber.split('-').map((n) => parseInt(n, 10)); - if (!Number.isNaN(start) && !Number.isNaN(end)) { - for (let i = start; i <= end; i++) { - fillNumberSet.add(i); - } - } - } else { - if (!Number.isNaN(fillNumber)) { - fillNumberSet.add(Number(fillNumber)); - } - } - }); - - const finalFillnumberList = Array.from(fillNumberSet); + const finalFillnumberList = Array.from(unpackNumberRange(fillNumberCriteria)); // Check that the final fill numbers list contains at least one valid fill number if (finalFillnumberList.length > 0) { diff --git a/lib/usecases/run/GetAllRunsUseCase.js b/lib/usecases/run/GetAllRunsUseCase.js index 77f9f0420b..d25762ad00 100644 --- a/lib/usecases/run/GetAllRunsUseCase.js +++ b/lib/usecases/run/GetAllRunsUseCase.js @@ -23,6 +23,8 @@ const { BadParameterError } = require('../../server/errors/BadParameterError'); const { gaqService } = require('../../server/services/qualityControlFlag/GaqService.js'); const { qcFlagSummaryService } = require('../../server/services/qualityControlFlag/QcFlagSummaryService.js'); const { DetectorType } = require('../../domain/enums/DetectorTypes.js'); +const { unpackNumberRange } = require('../../utilities/rangeUtils.js'); +const { splitStringToStringsTrimmed } = require('../../utilities/stringUtils.js'); /** * GetAllRunsUseCase @@ -83,29 +85,9 @@ class GetAllRunsUseCase { } = filter; if (runNumbers) { - const runNumberCriteria = runNumbers.split(SEARCH_ITEMS_SEPARATOR) - .map((runNumbers) => runNumbers.trim()) - .filter(Boolean); - - const runNumberSet = new Set(); - - runNumberCriteria.forEach((runNumber) => { - if (runNumber.includes('-')) { - const [start, end] = runNumber.split('-').map((n) => parseInt(n, 10)); - if (!Number.isNaN(start) && !Number.isNaN(end)) { - for (let i = start; i <= end; i++) { - runNumberSet.add(i); - } - } - } else { - const parsedRunNumber = parseInt(runNumber, 10); - if (!Number.isNaN(parsedRunNumber)) { - runNumberSet.add(parsedRunNumber); - } - } - }); + const runNumberCriteria = splitStringToStringsTrimmed(runNumbers, SEARCH_ITEMS_SEPARATOR); - const finalRunNumberList = Array.from(runNumberSet); + const finalRunNumberList = Array.from(unpackNumberRange(runNumberCriteria)); // Check that the final run numbers list contains at least one valid run number if (finalRunNumberList.length > 0) { diff --git a/lib/utilities/rangeUtils.js b/lib/utilities/rangeUtils.js new file mode 100644 index 0000000000..c6686ae899 --- /dev/null +++ b/lib/utilities/rangeUtils.js @@ -0,0 +1,70 @@ +/** + * @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. + */ + +/** + * Validates numbers ranges to not exceed 100 entities + * Expects a string containing comma seperated number values. + * + * @param {string} value The value to validate + * @param {*} helpers The helpers object + * @returns {Object} The value if validation passes + */ +export const validateRange = (value, helpers) => { + const MAX_RANGE_SIZE = 100; + + const numbers = value.split(',').map((number) => number.trim()); + + for (const number of numbers) { + if (number.includes('-')) { + const [start, end] = number.split('-').map((n) => parseInt(n, 10)); + if (Number.isNaN(start) || Number.isNaN(end) || start > end) { + return helpers.error('any.invalid', { message: `Invalid range: ${number}` }); + } + const rangeSize = end - start + 1; + + if (rangeSize > MAX_RANGE_SIZE) { + return helpers.error('any.invalid', { message: `Given range exceeds max size of ${MAX_RANGE_SIZE} range: ${number}` }); + } + } + } + + return value; +}; + +/** + * Unpacks a given string containing number ranges. + * E.G. input: 5,7-9 => output: 5,7,8,9 + * @param {string} numbersRange numbers that may or may not contain ranges. + * @param {string} rangeSplitter string used to indicate and unpack a range. + * @returns {Set} set containing the unpacked range. + */ +export function unpackNumberRange(numbersRange, rangeSplitter = '-') { + // Set to prevent duplicate values. + const resultNumbers = new Set(); + + numbersRange.forEach((number) => { + if (number.includes(rangeSplitter)) { + const [start, end] = number.split(rangeSplitter).map((n) => parseInt(n, 10)); + if (!Number.isNaN(start) && !Number.isNaN(end)) { + for (let i = start; i <= end; i++) { + resultNumbers.add(Number(i)); + } + } + } else { + if (!Number.isNaN(number)) { + resultNumbers.add(Number(number)); + } + } + }); + return resultNumbers; +} diff --git a/lib/utilities/stringUtils.js b/lib/utilities/stringUtils.js index c00f860a2d..cc302cbed4 100644 --- a/lib/utilities/stringUtils.js +++ b/lib/utilities/stringUtils.js @@ -62,6 +62,16 @@ const snakeToCamel = (snake) => snake.toLowerCase() */ const snakeToPascal = (snake) => ucFirst(snakeToCamel(snake)); +/** + * Split the received string to an array of trimmed strings. + * Boolean trick: https://michaeluloth.com/javascript-filter-boolean/ + * @param {string} stringCollection String containing other strings withing split by seperator. + * @param {string} stringSeperator Used to seperate the stringCollection. + */ +const splitStringToStringsTrimmed = (stringCollection, stringSeperator = ',') => stringCollection.split(stringSeperator) + .map((string) => string.trim()) + .filter(Boolean); + exports.ucFirst = ucFirst; exports.lcFirst = lcFirst; @@ -73,3 +83,5 @@ exports.pascalToSnake = pascalToSnake; exports.snakeToCamel = snakeToCamel; exports.snakeToPascal = snakeToPascal; + +exports.splitStringToStringsTrimmed = splitStringToStringsTrimmed; diff --git a/lib/utilities/validateRange.js b/lib/utilities/validateRange.js deleted file mode 100644 index c4a37c5de7..0000000000 --- a/lib/utilities/validateRange.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Validates numbers ranges to not exceed 100 entities - * - * @param {*} value The value to validate - * @param {*} helpers The helpers object - * @returns {Object} The value if validation passes - */ -export const validateRange = (value, helpers) => { - const MAX_RANGE_SIZE = 100; - - const numbers = value.split(',').map((runNumber) => runNumber.trim()); - - for (const number of numbers) { - if (number.includes('-')) { - const [start, end] = number.split('-').map((n) => parseInt(n, 10)); - if (Number.isNaN(start) || Number.isNaN(end) || start > end) { - return helpers.error('any.invalid', { message: `Invalid range: ${number}` }); - } - const rangeSize = end - start + 1; - - if (rangeSize > MAX_RANGE_SIZE) { - return helpers.error('any.invalid', { message: `Given range exceeds max size of ${MAX_RANGE_SIZE} range: ${number}` }); - } - } - } - - return value; -}; From 1e3f5037d8abbf6942dd3859d9557483c676ffbc Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 15 Dec 2025 12:14:02 +0100 Subject: [PATCH 27/76] [O2B-1503] placeholder text changed --- .../components/Filters/LhcFillsFilter/fillNumberFilter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js b/lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js index e04d1d701d..de13af7586 100644 --- a/lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js @@ -21,5 +21,5 @@ import { rawTextFilter } from '../common/filters/rawTextFilter.js'; */ export const fillNumberFilter = (filterModel) => rawTextFilter( filterModel, - { classes: ['w-100', 'fill-numbers-filter'], placeholder: 'e.g. 6, 3, 4' }, + { classes: ['w-100', 'fill-numbers-filter'], placeholder: 'e.g. 11392, 11383, 7625' }, ); From de7d95be2f8867075320c6f9a242531c393046f6 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Fri, 28 Nov 2025 16:17:22 +0100 Subject: [PATCH 28/76] [O2B-1505] Added beam duration filter to frontend --- .../LhcFillsFilter/beamDurationFilter.js | 32 +++++++++++++++++++ .../ActiveColumns/lhcFillsActiveColumns.js | 6 ++++ .../Overview/LhcFillsOverviewModel.js | 32 ++++++++++++++++++- 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js diff --git a/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js new file mode 100644 index 0000000000..a570b264c5 --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.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 beam duration + * + * @param {rawTextFilter} beamDurationFilterModel beamDurationFilterModel + * @param {string} beamDurationOperator beam duration operator value + * @param {(string) => undefined} beamDurationOperatorUpdate beam duration operator setter function + * @returns {Component} the text field + */ +export const beamDurationFilter = (beamDurationFilterModel, beamDurationOperator, beamDurationOperatorUpdate) => { + const amountFilter = rawTextFilter( + beamDurationFilterModel, + { classes: ['w-100', 'beam-duration-filter'], placeholder: 'e.g 16:14:15' }, + ); + + return comparisonOperatorFilter(amountFilter, beamDurationOperator, (value) => beamDurationOperatorUpdate(value)); +}; diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 5442ed8bcc..b60220adb0 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -25,6 +25,7 @@ import { formatBeamType } from '../../../utilities/formatting/formatBeamType.js' 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'; /** * List of active columns for a lhc fills table @@ -108,6 +109,11 @@ export const lhcFillsActiveColumns = { return '-'; }, + filter: (lhcFillModel) => beamDurationFilter( + lhcFillModel.filteringModel.get('beamDuration'), + lhcFillModel.getBeamDurationOperator(), + (value) => lhcFillModel.setBeamDurationOperator(value), + ), profiles: { lhcFill: true, environment: true, diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 55a417dc66..23a20052b6 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -34,9 +34,12 @@ export class LhcFillsOverviewModel extends OverviewPageModel { this._filteringModel = new FilteringModel({ fillNumbers: new RawTextFilterModel(), + beamDuration: new RawTextFilterModel(), hasStableBeams: new StableBeamFilterModel(), }); + this._beamDurationOperator = '='; + this._filteringModel.observe(() => this._applyFilters(true)); this._filteringModel.visualChange$.bubbleTo(this); @@ -64,6 +67,31 @@ export class LhcFillsOverviewModel extends OverviewPageModel { return buildUrl('/api/lhcFills', { filter: this.filteringModel.normalized }); } + /** + * + */ + setBeamDurationOperator(beamDurationOperator) { + this._beamDurationOperator = beamDurationOperator; + this._applyFilters(); + this.notify(); + } + + /** + * + */ + getBeamDurationOperator() { + return this._beamDurationOperator; + } + + /** + * Checks if the stable beams filter is set + * + * @return {boolean} true if the stable beams filter is active + */ + getStableBeamsOnly() { + return this._stableBeamsOnly; + } + /** * 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 @@ -81,6 +109,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { */ resetFiltering(fetch = true) { this._filteringModel.reset(); + this._beamDurationOperator = '='; if (fetch) { this._applyFilters(true); @@ -92,7 +121,8 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @return {Boolean} If any filter is active */ isAnyFilterActive() { - return this._filteringModel.isAnyFilterActive(); + return this._filteringModel.isAnyFilterActive() + || this._beamDurationOperator !== '='; } /** From cafcba1ccec7f36ef062f111251ae6b58a6599af Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Fri, 28 Nov 2025 16:58:45 +0100 Subject: [PATCH 29/76] [O2B-1505] added simple UI test --- test/public/lhcFills/overview.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 02b05c1591..d86867535d 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -268,11 +268,15 @@ module.exports = () => { it('should successfully display filter elements', async () => { 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'} + + await goToPage(page, 'lhc-fill-overview'); // Open the filtering panel await openFilteringPanel(page); await expectInnerText(page, filterSBExpect.selector, filterSBExpect.value); await expectInnerText(page, filterFillNRExpect.selector, filterFillNRExpect.value); + await expectInnerText(page, filterSBDurationExpect.selector, filterSBDurationExpect.value); }); it('should successfully un-apply Stable Beam filter menu', async () => { From 4c28a84628a8ab8f04fbadfe12eac1df68f012b5 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 1 Dec 2025 10:27:38 +0100 Subject: [PATCH 30/76] [O2B-1505] Filter+DTO work --- lib/domain/dtos/filters/LhcFillsFilterDto.js | 5 +++ .../LhcFillsFilter/beamDurationFilter.js | 2 +- .../Overview/LhcFillsOverviewModel.js | 16 ++++++--- lib/utilities/validateTime.js | 33 +++++++++++++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 lib/utilities/validateTime.js diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index d1d2af5929..537d5a95e2 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -12,10 +12,15 @@ */ const Joi = require('joi'); const { validateRange } = require('../../../utilities/rangeUtils'); +const { validateTime } = require('../../../utilities/validateTime'); exports.LhcFillsFilterDto = Joi.object({ hasStableBeams: Joi.boolean(), fillNumbers: Joi.string().trim().custom(validateRange).messages({ 'any.invalid': '{{#message}}', }), + beamDuration: Joi.string().trim().min(8).max(8).custom(validateTime).messages({ + 'any.invalid': '{{#message}}', + }), + beamDurationOperator: Joi.string().trim().min(1).max(2), }); diff --git a/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js index a570b264c5..32dd1587b3 100644 --- a/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js @@ -25,7 +25,7 @@ import { rawTextFilter } from '../common/filters/rawTextFilter.js'; export const beamDurationFilter = (beamDurationFilterModel, beamDurationOperator, beamDurationOperatorUpdate) => { const amountFilter = rawTextFilter( beamDurationFilterModel, - { classes: ['w-100', 'beam-duration-filter'], placeholder: 'e.g 16:14:15' }, + { classes: ['w-100', 'beam-duration-filter'], placeholder: 'e.g 16:14:15 (HH:MM:SS)' }, ); return comparisonOperatorFilter(amountFilter, beamDurationOperator, (value) => beamDurationOperatorUpdate(value)); diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 23a20052b6..afd7cac57d 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -18,6 +18,8 @@ import { RawTextFilterModel } from '../../../components/Filters/common/filters/R import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; +const defaultBeamDurationOperator = '='; + /** * Model for the LHC fills overview page * @@ -38,7 +40,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { hasStableBeams: new StableBeamFilterModel(), }); - this._beamDurationOperator = '='; + this._beamDurationOperator = defaultBeamDurationOperator; this._filteringModel.observe(() => this._applyFilters(true)); this._filteringModel.visualChange$.bubbleTo(this); @@ -64,7 +66,13 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @inheritDoc */ getRootEndpoint() { - return buildUrl('/api/lhcFills', { filter: this.filteringModel.normalized }); + const params = { + filter: this.filteringModel.normalized, + ...this._filteringModel.get('beamDuration').isEmpty === false && { + 'filter[beamDurationOperator]': this._beamDurationOperator, + }, + }; + return buildUrl('/api/lhcFills', params); } /** @@ -109,7 +117,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { */ resetFiltering(fetch = true) { this._filteringModel.reset(); - this._beamDurationOperator = '='; + this._beamDurationOperator = defaultBeamDurationOperator; if (fetch) { this._applyFilters(true); @@ -122,7 +130,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { */ isAnyFilterActive() { return this._filteringModel.isAnyFilterActive() - || this._beamDurationOperator !== '='; + || this._beamDurationOperator !== defaultBeamDurationOperator; } /** diff --git a/lib/utilities/validateTime.js b/lib/utilities/validateTime.js new file mode 100644 index 0000000000..cabd8867c8 --- /dev/null +++ b/lib/utilities/validateTime.js @@ -0,0 +1,33 @@ +/** + * Validates digital time in string format + * + * @param {*} value The time to validate + * @param {*} helpers The helpers object + * @param {boolean} transformSeconds Return value as seconds + * @returns {number|string|import("joi").ValidationError} The value if validation passes + */ +export const validateTime = (value, helpers, transformSeconds = false) => { + const timeSectionsString = value.split(':'); + let timeSeconds = 0; + let powerValue = 2; + + for (const timeSectionString of timeSectionsString) { + if (!Number.isNaN(timeSectionString)) { + const timeSection = Number(timeSectionString); + if (timeSection <= 60 && timeSection >= 0) { + if (powerValue !== 0) { + timeSeconds += timeSection * 60 ** powerValue; + } else { + timeSeconds += timeSection; + } + } else { + return helpers.error('any.invalid', { message: `Invalid time period: ${timeSection}` }); + } + } else { + return helpers.error('any.invalid', { message: `Invalid time: ${timeSectionString}` }); + } + powerValue--; + } + + return transformSeconds ? timeSeconds : value; +}; From a34340ee47da4fa442f864045009c432a307cadf Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 1 Dec 2025 11:44:23 +0100 Subject: [PATCH 31/76] [O2B-1505] Beam duration filter works, TODO testing --- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 7 ++++++- lib/utilities/validateTime.js | 7 +++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 2d83ade80f..6cfdeda35e 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -45,7 +45,7 @@ class GetAllLhcFillsUseCase { const queryBuilder = new QueryBuilder(); if (filter) { - const { hasStableBeams, fillNumbers } = filter; + const { hasStableBeams, fillNumbers, beamDurationOperator, beamDuration } = filter; if (hasStableBeams) { // For now, if a stableBeamsStart is present, then a beam is stable queryBuilder.where('stableBeamsStart').not().is(null); @@ -61,6 +61,11 @@ class GetAllLhcFillsUseCase { queryBuilder.where('fillNumber').oneOf(...finalFillnumberList); } } + + // Beam duration filter and corresponding operator. + if (beamDuration && beamDurationOperator) { + queryBuilder.where('stableBeamsDuration').applyOperator(beamDurationOperator, beamDuration); + } } const { count, rows } = await TransactionHelper.provide(async () => { diff --git a/lib/utilities/validateTime.js b/lib/utilities/validateTime.js index cabd8867c8..9c29dd7903 100644 --- a/lib/utilities/validateTime.js +++ b/lib/utilities/validateTime.js @@ -3,10 +3,9 @@ * * @param {*} value The time to validate * @param {*} helpers The helpers object - * @param {boolean} transformSeconds Return value as seconds - * @returns {number|string|import("joi").ValidationError} The value if validation passes + * @returns {number|import("joi").ValidationError} The value if validation passes, as seconds (Number) */ -export const validateTime = (value, helpers, transformSeconds = false) => { +export const validateTime = (value, helpers) => { const timeSectionsString = value.split(':'); let timeSeconds = 0; let powerValue = 2; @@ -29,5 +28,5 @@ export const validateTime = (value, helpers, transformSeconds = false) => { powerValue--; } - return transformSeconds ? timeSeconds : value; + return timeSeconds; }; From 99216525808086e809af7163c1ab3eae1e056836 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Tue, 2 Dec 2025 09:23:30 +0100 Subject: [PATCH 32/76] [O2B-1505] tests added/improv --- .../lhcFill/GetAllLhcFillsUseCase.test.js | 51 +++++++++++++++++++ test/public/lhcFills/overview.test.js | 3 ++ 2 files changed, 54 insertions(+) diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index fdccf49678..8a5d297afe 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -100,4 +100,55 @@ module.exports = () => { expect(lhcFill.fillNumber).oneOf([6,3]) }); }) + + // Beam duration filter tests + + it('should only contain specified stable beam durations, < 12:00:00', async () => { + getAllLhcFillsDto.query = { filter: { beamDuration: '43200', beamDurationOperator: '<', hasStableBeams: true } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); + expect(lhcFills).to.be.an('array').and.lengthOf(3) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.stableBeamsDuration).lessThan(43200) + }); + }); + + it('should only contain specified stable beam durations, <= 12:00:00', async () => { + getAllLhcFillsDto.query = { filter: { beamDuration: '43200', beamDurationOperator: '<=', hasStableBeams: true } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + expect(lhcFills).to.be.an('array').and.lengthOf(4) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.stableBeamsDuration).lessThanOrEqual(43200) + }); + }) + + it('should only contain specified stable beam durations, = 00:01:40', async () => { + getAllLhcFillsDto.query = { filter: { beamDuration: '100', beamDurationOperator: '=', hasStableBeams: true } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + expect(lhcFills).to.be.an('array').and.lengthOf(2) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.stableBeamsDuration).equal(100) + }); + }); + + it('should only contain specified stable beam durations, >= 00:01:40', async () => { + getAllLhcFillsDto.query = { filter: { beamDuration: '100', beamDurationOperator: '>=', hasStableBeams: true } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + console.log(lhcFills); + + expect(lhcFills).to.be.an('array').and.lengthOf(4) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.stableBeamsDuration).greaterThanOrEqual(100) + }); + }) + + it('should only contain specified stable beam durations, > 00:01:40', async () => { + getAllLhcFillsDto.query = { filter: { beamDuration: '100', beamDurationOperator: '>', hasStableBeams: true } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + console.log(lhcFills); + + expect(lhcFills).to.be.an('array').and.lengthOf(2) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.stableBeamsDuration).greaterThan(100) + }); + }) }; diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index d86867535d..06299d8dd1 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -269,6 +269,8 @@ 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 filterSBDurationOperatorExpect = {selector: 'select.form-control', value: '='} + const filterSBDurationOperatorEqualsPath = 'select.form-control > option:nth-child(3)'; await goToPage(page, 'lhc-fill-overview'); @@ -277,6 +279,7 @@ module.exports = () => { await expectInnerText(page, filterSBExpect.selector, filterSBExpect.value); await expectInnerText(page, filterFillNRExpect.selector, filterFillNRExpect.value); await expectInnerText(page, filterSBDurationExpect.selector, filterSBDurationExpect.value); + await expectInnerText(page, filterSBDurationOperatorExpect.selector, filterSBDurationOperatorExpect.value); }); it('should successfully un-apply Stable Beam filter menu', async () => { From 1c2fffbb6d139804f99219d09894b0f244ca2896 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Tue, 2 Dec 2025 17:10:03 +0100 Subject: [PATCH 33/76] [O2B-1505] Fixed tests --- .../usecases/lhcFill/GetAllLhcFillsUseCase.test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index 8a5d297afe..4a36c7ebef 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -104,7 +104,7 @@ module.exports = () => { // Beam duration filter tests it('should only contain specified stable beam durations, < 12:00:00', async () => { - getAllLhcFillsDto.query = { filter: { beamDuration: '43200', beamDurationOperator: '<', hasStableBeams: true } }; + getAllLhcFillsDto.query = { filter: { beamDuration: '43200', beamDurationOperator: '<' } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); expect(lhcFills).to.be.an('array').and.lengthOf(3) lhcFills.forEach((lhcFill) => { @@ -113,7 +113,7 @@ module.exports = () => { }); it('should only contain specified stable beam durations, <= 12:00:00', async () => { - getAllLhcFillsDto.query = { filter: { beamDuration: '43200', beamDurationOperator: '<=', hasStableBeams: true } }; + getAllLhcFillsDto.query = { filter: { beamDuration: '43200', beamDurationOperator: '<=' } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(4) lhcFills.forEach((lhcFill) => { @@ -122,16 +122,16 @@ module.exports = () => { }) it('should only contain specified stable beam durations, = 00:01:40', async () => { - getAllLhcFillsDto.query = { filter: { beamDuration: '100', beamDurationOperator: '=', hasStableBeams: true } }; + getAllLhcFillsDto.query = { filter: { beamDuration: '100', beamDurationOperator: '=' } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) - expect(lhcFills).to.be.an('array').and.lengthOf(2) + expect(lhcFills).to.be.an('array').and.lengthOf(3) lhcFills.forEach((lhcFill) => { expect(lhcFill.stableBeamsDuration).equal(100) }); }); it('should only contain specified stable beam durations, >= 00:01:40', async () => { - getAllLhcFillsDto.query = { filter: { beamDuration: '100', beamDurationOperator: '>=', hasStableBeams: true } }; + getAllLhcFillsDto.query = { filter: { beamDuration: '100', beamDurationOperator: '>=' } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) console.log(lhcFills); @@ -142,11 +142,11 @@ module.exports = () => { }) it('should only contain specified stable beam durations, > 00:01:40', async () => { - getAllLhcFillsDto.query = { filter: { beamDuration: '100', beamDurationOperator: '>', hasStableBeams: true } }; + getAllLhcFillsDto.query = { filter: { beamDuration: '100', beamDurationOperator: '>' } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) console.log(lhcFills); - expect(lhcFills).to.be.an('array').and.lengthOf(2) + expect(lhcFills).to.be.an('array').and.lengthOf(1) lhcFills.forEach((lhcFill) => { expect(lhcFill.stableBeamsDuration).greaterThan(100) }); From d40bd5c5bcbaedff02f464593681e46355a225da Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Tue, 2 Dec 2025 17:43:01 +0100 Subject: [PATCH 34/76] [O2B-1505] Cleanup, remove logs, docs --- lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js | 2 +- test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index afd7cac57d..89726180f9 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -85,7 +85,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { } /** - * + * Beam durationOperator getter */ getBeamDurationOperator() { return this._beamDurationOperator; diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index 4a36c7ebef..d9cdff2c53 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -133,7 +133,6 @@ module.exports = () => { it('should only contain specified stable beam durations, >= 00:01:40', async () => { getAllLhcFillsDto.query = { filter: { beamDuration: '100', beamDurationOperator: '>=' } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) - console.log(lhcFills); expect(lhcFills).to.be.an('array').and.lengthOf(4) lhcFills.forEach((lhcFill) => { @@ -144,7 +143,6 @@ module.exports = () => { it('should only contain specified stable beam durations, > 00:01:40', async () => { getAllLhcFillsDto.query = { filter: { beamDuration: '100', beamDurationOperator: '>' } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) - console.log(lhcFills); expect(lhcFills).to.be.an('array').and.lengthOf(1) lhcFills.forEach((lhcFill) => { From d3c1ead2540e3322b9d25c5167c163a41339b39d Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 8 Dec 2025 15:15:08 +0100 Subject: [PATCH 35/76] [O2B-1505] Fixed test --- test/public/lhcFills/overview.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 06299d8dd1..1b69518047 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -24,6 +24,7 @@ const { waitForTableLength, expectLink, openFilteringPanel, + expectAttributeValue, } = require('../defaults.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -269,8 +270,7 @@ 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 filterSBDurationOperatorExpect = {selector: 'select.form-control', value: '='} - const filterSBDurationOperatorEqualsPath = 'select.form-control > option:nth-child(3)'; + const filterSBDurationPlaceholderExpect = {selector: 'input.w-100:nth-child(2)', value: 'e.g 16:14:15 (HH:MM:SS)'} await goToPage(page, 'lhc-fill-overview'); @@ -279,7 +279,7 @@ module.exports = () => { await expectInnerText(page, filterSBExpect.selector, filterSBExpect.value); await expectInnerText(page, filterFillNRExpect.selector, filterFillNRExpect.value); await expectInnerText(page, filterSBDurationExpect.selector, filterSBDurationExpect.value); - await expectInnerText(page, filterSBDurationOperatorExpect.selector, filterSBDurationOperatorExpect.value); + await expectAttributeValue(page, filterSBDurationPlaceholderExpect.selector, 'placeholder', filterSBDurationPlaceholderExpect.value); }); it('should successfully un-apply Stable Beam filter menu', async () => { From fe5f6c7e7570095dd33b1a61b31351844b24809f Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 8 Dec 2025 16:03:48 +0100 Subject: [PATCH 36/76] [O2B-1505] Doc fixes --- lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 89726180f9..1926bbbc92 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -76,7 +76,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { } /** - * + * Beam duration operator setter */ setBeamDurationOperator(beamDurationOperator) { this._beamDurationOperator = beamDurationOperator; @@ -85,7 +85,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { } /** - * Beam durationOperator getter + * Beam duration operator getter */ getBeamDurationOperator() { return this._beamDurationOperator; From 7628bca0666b618816fc9533250269ac24f916d1 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 8 Dec 2025 16:13:53 +0100 Subject: [PATCH 37/76] [O2B-1505] remove getStableBeamsOnly --- .../views/LhcFills/Overview/LhcFillsOverviewModel.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 1926bbbc92..5dc83a4365 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -91,15 +91,6 @@ export class LhcFillsOverviewModel extends OverviewPageModel { return this._beamDurationOperator; } - /** - * Checks if the stable beams filter is set - * - * @return {boolean} true if the stable beams filter is active - */ - getStableBeamsOnly() { - return this._stableBeamsOnly; - } - /** * 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 From a5330887a97b7ce542ed42baa9b6f034a6d4280c Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Tue, 16 Dec 2025 10:08:02 +0100 Subject: [PATCH 38/76] [O2B-1505] Fixed 00:00:00 bug, added test --- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 6 +++--- .../usecases/lhcFill/GetAllLhcFillsUseCase.test.js | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 6cfdeda35e..d994661202 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -61,10 +61,10 @@ class GetAllLhcFillsUseCase { queryBuilder.where('fillNumber').oneOf(...finalFillnumberList); } } - // Beam duration filter and corresponding operator. - if (beamDuration && beamDurationOperator) { - queryBuilder.where('stableBeamsDuration').applyOperator(beamDurationOperator, beamDuration); + if (beamDuration !== null && beamDuration !== undefined && beamDurationOperator) { + beamDuration === 0 ? queryBuilder.where('stableBeamsDuration').applyOperator(beamDurationOperator, null) + : queryBuilder.where('stableBeamsDuration').applyOperator(beamDurationOperator, beamDuration); } } diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index d9cdff2c53..46cdc64a62 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -149,4 +149,15 @@ module.exports = () => { expect(lhcFill.stableBeamsDuration).greaterThan(100) }); }) + + it('should only contain specified stable beam durations, = 00:00:00', async () => { + // Tests the usecase's ability to replace the request for 0 to a request for null. + getAllLhcFillsDto.query = { filter: { hasStableBeams: true, beamDuration: 0, beamDurationOperator: '=' } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + + expect(lhcFills).to.be.an('array').and.lengthOf(1) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.stableBeamsDuration).equals(null) + }); + }) }; From 36bdd8d648d87e8016934ded21f3cce983bc9c00 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Tue, 2 Dec 2025 17:40:35 +0100 Subject: [PATCH 39/76] [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 40/76] [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 41/76] [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 42/76] [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: '<=' } }; From 618daa19a6a4468557a5daf57b58d179a98a605d Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 8 Dec 2025 16:59:05 +0100 Subject: [PATCH 43/76] [O2B-1508] Frontend Beams type filter working. --- .../Filters/LhcFillsFilter/beamsTypeFilter.js | 25 +++++++++++++++++++ lib/public/domain/enums/BeamType.js | 19 ++++++++++++++ .../ActiveColumns/lhcFillsActiveColumns.js | 2 ++ .../Overview/LhcFillsOverviewModel.js | 3 +++ 4 files changed, 49 insertions(+) create mode 100644 lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js create mode 100644 lib/public/domain/enums/BeamType.js diff --git a/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js new file mode 100644 index 0000000000..070b37f4ea --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js @@ -0,0 +1,25 @@ +/** + * @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 {SelectionFilterModel} selectionFilterModel selectionFilterModel + * @return {Component} the filter + */ +export const beamsTypeFilter = (selectionFilterModel) => checkboxes( + selectionFilterModel._selectionModel, + { selector: 'beams-types' }, +); diff --git a/lib/public/domain/enums/BeamType.js b/lib/public/domain/enums/BeamType.js new file mode 100644 index 0000000000..99fd2bb35c --- /dev/null +++ b/lib/public/domain/enums/BeamType.js @@ -0,0 +1,19 @@ +/** + * @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. + */ + +export const BeamType = Object.freeze({ + PROTON_PROTON: 'Proton-Proton', + LEAD_LEAD: 'Lead-Lead', +}); + +export const BEAM_TYPES = Object.values(BeamType); diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 1a0cd8aa9d..897b407d13 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -27,6 +27,7 @@ import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFills import { fillNumberFilter } from '../../../components/Filters/LhcFillsFilter/fillNumberFilter.js'; import { beamDurationFilter } from '../../../components/Filters/LhcFillsFilter/beamDurationFilter.js'; import { runDurationFilter } from '../../../components/Filters/LhcFillsFilter/runDurationFilter.js'; +import { beamsTypeFilter } from '../../../components/Filters/LhcFillsFilter/beamsTypeFilter.js'; /** * List of active columns for a lhc fills table @@ -170,6 +171,7 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-8', format: (value) => formatBeamType(value), + filter: (lhcFillModel) => beamsTypeFilter(lhcFillModel.filteringModel.get('beamsType')), }, collidingBunches: { name: 'Colliding bunches', diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 9837d58473..ab90525178 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -17,6 +17,8 @@ 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 { SelectionFilterModel } from '../../../components/Filters/common/filters/SelectionFilterModel.js'; +import { BEAM_TYPES } from '../../../domain/enums/BeamType.js'; const defaultBeamDurationOperator = '='; const defaultRunDurationOperator = '='; @@ -40,6 +42,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { beamDuration: new RawTextFilterModel(), runDuration: new RawTextFilterModel(), hasStableBeams: new StableBeamFilterModel(), + beamsType: new SelectionFilterModel({ availableOptions: BEAM_TYPES.map((type) => ({ value: type })) }), }); this._beamDurationOperator = defaultBeamDurationOperator; From 870124689d0b24b62c98c88c99af4bb76db6f11a Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 8 Dec 2025 17:28:20 +0100 Subject: [PATCH 44/76] [O2B-1508] Basic backend UseCase works --- lib/domain/dtos/filters/LhcFillsFilterDto.js | 1 + lib/public/domain/enums/BeamType.js | 4 ++-- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 8 +++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index 7c5d6853f7..2942844758 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -27,4 +27,5 @@ exports.LhcFillsFilterDto = Joi.object({ 'any.invalid': '{{#message}}', }), runDurationOperator: Joi.string().trim().min(1).max(2), + beamsType: Joi.string(), }); diff --git a/lib/public/domain/enums/BeamType.js b/lib/public/domain/enums/BeamType.js index 99fd2bb35c..c2266fda6f 100644 --- a/lib/public/domain/enums/BeamType.js +++ b/lib/public/domain/enums/BeamType.js @@ -12,8 +12,8 @@ */ export const BeamType = Object.freeze({ - PROTON_PROTON: 'Proton-Proton', - LEAD_LEAD: 'Lead-Lead', + PROTON_PROTON: 'PROTON-PROTON', + LEAD_LEAD: 'LEAD-LEAD', }); export const BEAM_TYPES = Object.values(BeamType); diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index e961548817..155861806c 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, beamDurationOperator, beamDuration, runDurationOperator, runDuration } = filter; + const { hasStableBeams, fillNumbers, beamDurationOperator, beamDuration, runDurationOperator, runDuration, beamsType } = filter; if (hasStableBeams) { // For now, if a stableBeamsStart is present, then a beam is stable queryBuilder.where('stableBeamsStart').not().is(null); @@ -77,6 +77,12 @@ class GetAllLhcFillsUseCase { beamDuration === 0 ? queryBuilder.where('stableBeamsDuration').applyOperator(beamDurationOperator, null) : queryBuilder.where('stableBeamsDuration').applyOperator(beamDurationOperator, beamDuration); } + + // Beams type. + if (beamsType) { + const beamTypes = beamsType.split(','); + queryBuilder.where('beamType').oneOfSubstrings(beamTypes); + } } const { count, rows } = await TransactionHelper.provide(async () => { From e64d28f6d3e13c8abc337dfaa7d67c94bbb451ec Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 10 Dec 2025 08:59:48 +0100 Subject: [PATCH 45/76] [O2B-1508] Load possible beamstypes for the beam_type filter from the backend --- lib/domain/dtos/GetAllBeamsTypesDto.js | 28 +++++++++ lib/domain/dtos/index.js | 2 + lib/{public => }/domain/enums/BeamType.js | 2 +- .../services/beamsTypes/beamsTypesProvider.js | 30 +++++++++ .../ActiveColumns/lhcFillsActiveColumns.js | 3 +- .../Overview/LhcFillsOverviewModel.js | 21 ++++++- .../controllers/beamsTypes.controller.js | 62 +++++++++++++++++++ lib/server/controllers/index.js | 2 + lib/server/routers/beamsTypes.router.js | 20 ++++++ lib/server/routers/index.js | 2 + .../beamsType/GetAllBeamsTypesUseCase.js | 41 ++++++++++++ lib/usecases/beamsType/index.js | 17 +++++ lib/usecases/index.js | 2 + 13 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 lib/domain/dtos/GetAllBeamsTypesDto.js rename lib/{public => }/domain/enums/BeamType.js (95%) create mode 100644 lib/public/services/beamsTypes/beamsTypesProvider.js create mode 100644 lib/server/controllers/beamsTypes.controller.js create mode 100644 lib/server/routers/beamsTypes.router.js create mode 100644 lib/usecases/beamsType/GetAllBeamsTypesUseCase.js create mode 100644 lib/usecases/beamsType/index.js diff --git a/lib/domain/dtos/GetAllBeamsTypesDto.js b/lib/domain/dtos/GetAllBeamsTypesDto.js new file mode 100644 index 0000000000..c4e78bfec5 --- /dev/null +++ b/lib/domain/dtos/GetAllBeamsTypesDto.js @@ -0,0 +1,28 @@ +/** + * @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 PaginationDto = require('./PaginationDto'); + +const QueryDto = Joi.object({ + page: PaginationDto, + token: Joi.string(), +}); + +const GetAllBeamsTypesDto = Joi.object({ + body: Joi.object({}), + params: Joi.object({}), + query: QueryDto, +}); + +module.exports = GetAllBeamsTypesDto; diff --git a/lib/domain/dtos/index.js b/lib/domain/dtos/index.js index 01eb348934..d1bb88492f 100644 --- a/lib/domain/dtos/index.js +++ b/lib/domain/dtos/index.js @@ -18,6 +18,7 @@ const CreateLhcFillDto = require('./CreateLhcFillDto'); const CreateLogDto = require('./CreateLogDto'); const CreateTagDto = require('./CreateTagDto'); const EntityIdDto = require('./EntityIdDto'); +const GetAllBeamsTypesDto = require('./GetAllBeamsTypesDto.js'); const GetAllEnvironmentsDto = require('./GetAllEnvironmentsDto'); const GetAllLhcFillsDto = require('./GetAllLhcFillsDto'); const GetAllLogAttachmentsDto = require('./GetAllLogAttachmentsDto'); @@ -56,6 +57,7 @@ module.exports = { CreateTagDto, EndRunDto, EntityIdDto, + GetAllBeamsTypesDto, GetAllEnvironmentsDto, GetAllLhcFillsDto, GetAllLogAttachmentsDto, diff --git a/lib/public/domain/enums/BeamType.js b/lib/domain/enums/BeamType.js similarity index 95% rename from lib/public/domain/enums/BeamType.js rename to lib/domain/enums/BeamType.js index c2266fda6f..9b7a1285b1 100644 --- a/lib/public/domain/enums/BeamType.js +++ b/lib/domain/enums/BeamType.js @@ -13,7 +13,7 @@ export const BeamType = Object.freeze({ PROTON_PROTON: 'PROTON-PROTON', - LEAD_LEAD: 'LEAD-LEAD', + LEAD_LEAD: 'PB82-PB82', }); export const BEAM_TYPES = Object.values(BeamType); diff --git a/lib/public/services/beamsTypes/beamsTypesProvider.js b/lib/public/services/beamsTypes/beamsTypesProvider.js new file mode 100644 index 0000000000..20a688d0a2 --- /dev/null +++ b/lib/public/services/beamsTypes/beamsTypesProvider.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 BeamsTypesProvider extends RemoteDataProvider { + /** + * @inheritDoc + */ + async getRemoteData() { + const { data } = await getRemoteData('/api/beamsTypes'); + return data; + } +} + +export const beamsTypesProvider = new BeamsTypesProvider(); diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 897b407d13..1b1ceca149 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -171,7 +171,8 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-8', format: (value) => formatBeamType(value), - filter: (lhcFillModel) => beamsTypeFilter(lhcFillModel.filteringModel.get('beamsType')), + filter: (lhcFillModel) => lhcFillModel.getBeamsTypes().length === 0 ? + undefined : beamsTypeFilter(lhcFillModel.filteringModel.get('beamsType')), }, collidingBunches: { name: 'Colliding bunches', diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index ab90525178..3734968576 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -18,7 +18,7 @@ import { RawTextFilterModel } from '../../../components/Filters/common/filters/R import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; import { SelectionFilterModel } from '../../../components/Filters/common/filters/SelectionFilterModel.js'; -import { BEAM_TYPES } from '../../../domain/enums/BeamType.js'; +import { beamsTypesProvider } from '../../../services/beamsTypes/beamsTypesProvider.js'; const defaultBeamDurationOperator = '='; const defaultRunDurationOperator = '='; @@ -37,12 +37,22 @@ export class LhcFillsOverviewModel extends OverviewPageModel { constructor(stableBeamsOnly = false) { super(); + this._beamsTypes = []; + this._filteringModel = new FilteringModel({ fillNumbers: new RawTextFilterModel(), beamDuration: new RawTextFilterModel(), runDuration: new RawTextFilterModel(), hasStableBeams: new StableBeamFilterModel(), - beamsType: new SelectionFilterModel({ availableOptions: BEAM_TYPES.map((type) => ({ value: type })) }), + }); + + beamsTypesProvider.items$.observe(() => { + beamsTypesProvider.items$.getCurrent().apply({ + Success: (types) => { + this._beamsTypes = types.map((type) => ({ value: type })); + this._filteringModel.put('beamsType', new SelectionFilterModel({ availableOptions: this._beamsTypes })); + }, + }); }); this._beamDurationOperator = defaultBeamDurationOperator; @@ -84,6 +94,13 @@ export class LhcFillsOverviewModel extends OverviewPageModel { return buildUrl('/api/lhcFills', params); } + /** + * Getter + */ + getBeamsTypes() { + return this._beamsTypes; + } + /** * Setter function for runDurationOperator */ diff --git a/lib/server/controllers/beamsTypes.controller.js b/lib/server/controllers/beamsTypes.controller.js new file mode 100644 index 0000000000..e67b21718c --- /dev/null +++ b/lib/server/controllers/beamsTypes.controller.js @@ -0,0 +1,62 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * 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 { + beamsType: { + GetAllBeamsTypesUseCase, + }, +} = require('../../usecases/index.js'); +const { + dtos: { + GetAllBeamsTypesDto, + }, +} = require('../../domain/index.js'); +const { dtoValidator } = require('../utilities/index.js'); +const { ApiConfig } = require('../../config/index.js'); + +/** + * Get all beams 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. + * @returns {undefined} + */ +const listBeamsTypes = async (request, response) => { + const value = await dtoValidator(GetAllBeamsTypesDto, request, response); + if (!value) { + return; + } + + const { count, beamsTypes } = await new GetAllBeamsTypesUseCase() + .execute(value); + + const { query: { page: { limit = ApiConfig.pagination.limit } = {} } } = value; + const totalPages = Math.ceil(count / limit); + + response.status(200).json({ + data: beamsTypes, + meta: { + page: { + pageCount: totalPages, + totalCount: count, + }, + }, + }); +}; + +module.exports = { + listBeamsTypes, +}; diff --git a/lib/server/controllers/index.js b/lib/server/controllers/index.js index 48184378b2..904dadaeb1 100644 --- a/lib/server/controllers/index.js +++ b/lib/server/controllers/index.js @@ -12,6 +12,7 @@ */ const AttachmentsController = require('./attachments.controller'); +const BeamsTypeController = require('./beamsTypes.controller.js'); const ConfigurationController = require('./configuration.controller.js'); const DetectorsController = require('./detectors.controller'); const EnvironmentsController = require('./environments.controller'); @@ -26,6 +27,7 @@ const CtpTriggerCountersController = require('./ctpTriggerCounters.controller'); module.exports = { AttachmentsController, + BeamsTypeController, ConfigurationController, DetectorsController, EnvironmentsController, diff --git a/lib/server/routers/beamsTypes.router.js b/lib/server/routers/beamsTypes.router.js new file mode 100644 index 0000000000..a64aa6b4f8 --- /dev/null +++ b/lib/server/routers/beamsTypes.router.js @@ -0,0 +1,20 @@ +/** + * @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 { BeamsTypeController } = require('../controllers'); + +exports.beamsTypesRouter = { + path: '/beamsTypes', + controller: BeamsTypeController.listBeamsTypes, + method: 'get', +}; diff --git a/lib/server/routers/index.js b/lib/server/routers/index.js index cb83465446..a064f5a246 100644 --- a/lib/server/routers/index.js +++ b/lib/server/routers/index.js @@ -15,6 +15,7 @@ const { deepmerge, isPromise } = require('../../utilities'); const attachmentRoute = require('./attachments.router'); const { configurationRouter } = require('./configuration.router.js'); +const { beamsTypesRouter } = require('./beamsTypes.router.js') const detectorsRoute = require('./detectors.router'); const { dplProcessRouter } = require('./dplProcess.router.js'); const environmentRoute = require('./environments.router'); @@ -40,6 +41,7 @@ const { ctpTriggerCountersRouter } = require('./ctpTriggerCounters.router.js'); const routes = [ attachmentRoute, + beamsTypesRouter, configurationRouter, detectorsRoute, dataPassesRouter, diff --git a/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js b/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js new file mode 100644 index 0000000000..51a8ddc6cc --- /dev/null +++ b/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js @@ -0,0 +1,41 @@ +/** + * @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 { ApiConfig } = require('../../config/index.js'); +const { BEAM_TYPES } = require('../../domain/enums/BeamType.js'); + +/** + * GetAllBeamsTypesUseCase + */ +class GetAllBeamsTypesUseCase { + /** + * Executes this use case. + * + * @param {Object} dto The GetAllBeamsTypes DTO which contains all request data. + * @returns {Promise} Promise object represents the result of this use case. + */ + async execute(dto = {}) { + // DTO data to be used if Database data is desired. + const { query = {} } = dto; + const { page = {} } = query; + const { limit = ApiConfig.pagination.limit, offset = 0 } = page; + + const beamsTypes = BEAM_TYPES; + return { + count: beamsTypes.length, + beamsTypes, + }; + } +} + +module.exports = GetAllBeamsTypesUseCase; diff --git a/lib/usecases/beamsType/index.js b/lib/usecases/beamsType/index.js new file mode 100644 index 0000000000..5a60a1e65f --- /dev/null +++ b/lib/usecases/beamsType/index.js @@ -0,0 +1,17 @@ +/** + * @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 GetAllBeamsTypesUseCase = require('./GetAllBeamsTypesUseCase.js'); + +module.exports = { + GetAllBeamsTypesUseCase, +}; diff --git a/lib/usecases/index.js b/lib/usecases/index.js index 2dfa7807e5..413ffe4791 100644 --- a/lib/usecases/index.js +++ b/lib/usecases/index.js @@ -12,6 +12,7 @@ */ const attachment = require('./attachment'); +const beamsType = require('./beamsType'); const environment = require('./environment'); const flp = require('./flp'); const lhcFill = require('./lhcFill'); @@ -24,6 +25,7 @@ const tag = require('./tag'); module.exports = { attachment, + beamsType, environment, flp, lhcFill, From 19269b8a3625bffd38a20402db97fe17d41a26b9 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 11 Dec 2025 17:08:44 +0100 Subject: [PATCH 46/76] [O2B-1508] Beams types filter works with data from backend. --- .../repositories/LhcFillRepository.js | 12 ++++ lib/domain/enums/BeamType.js | 19 ------ .../LhcFillsFilter/BeamsTypeFilterModel.js | 59 +++++++++++++++++++ .../Filters/LhcFillsFilter/beamsTypeFilter.js | 11 ++-- .../ActiveColumns/lhcFillsActiveColumns.js | 3 +- .../Overview/LhcFillsOverviewModel.js | 13 +--- .../beamsType/GetAllBeamsTypesUseCase.js | 17 ++---- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 2 +- 8 files changed, 86 insertions(+), 50 deletions(-) delete mode 100644 lib/domain/enums/BeamType.js create mode 100644 lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js diff --git a/lib/database/repositories/LhcFillRepository.js b/lib/database/repositories/LhcFillRepository.js index 8ed50d16cb..c74f9ffdd4 100644 --- a/lib/database/repositories/LhcFillRepository.js +++ b/lib/database/repositories/LhcFillRepository.js @@ -34,6 +34,10 @@ const getFillNumbersWithStableBeamsEndedInPeriodQuery = (period) => ` AND stable_beams_start IS NOT NULL `; +const getLhcFillDistinctBeamTypesQuery = () => ` + SELECT DISTINCT beam_type FROM bookkeeping.lhc_fills +`; + /** * Sequelize implementation of the RunRepository. */ @@ -45,6 +49,14 @@ class LhcFillRepository extends Repository { super(LhcFill); } + /** + * Return the list of LHC fills distict beam types. + * @returns {Promise} + */ + async getLhcFillDistinctBeamTypes() { + return await sequelize.query(getLhcFillDistinctBeamTypesQuery(), { type: QueryTypes.SELECT, raw: true }); + } + /** * Return the list of LHC fills numbers for fills with stable beams that ended in the given period * diff --git a/lib/domain/enums/BeamType.js b/lib/domain/enums/BeamType.js deleted file mode 100644 index 9b7a1285b1..0000000000 --- a/lib/domain/enums/BeamType.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @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. - */ - -export const BeamType = Object.freeze({ - PROTON_PROTON: 'PROTON-PROTON', - LEAD_LEAD: 'PB82-PB82', -}); - -export const BEAM_TYPES = Object.values(BeamType); diff --git a/lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js new file mode 100644 index 0000000000..389328b220 --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js @@ -0,0 +1,59 @@ +/** + * @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 { beamsTypesProvider } from '../../../services/beamsTypes/beamsTypesProvider.js'; +import { SelectionModel } from '../../common/selection/SelectionModel.js'; + +/** + * Beam type filter model + */ +export class BeamsTypeFilterModel extends SelectionModel { + /** + * Constructor + */ + constructor() { + let beamTypes = []; + super({ availableOptions: beamTypes, + defaultSelection: [], + multiple: true, + allowEmpty: true }); + + beamsTypesProvider.items$.observe(() => { + beamsTypesProvider.items$.getCurrent().apply({ + Success: (types) => { + beamTypes = types.map((type) => ({ value: String(type.beam_type) })); + this.setAvailableOptions(beamTypes); + }, + }); + }); + } + + /** + * Get normalized selected option + */ + get normalized() { + return this.selected.join(','); + } + + /** + * Reset the filter to default values + * + * @return {void} + */ + resetDefaults() { + if (!this.isEmpty) { + this.reset(); + this.notify(); + } + } +} diff --git a/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js index 070b37f4ea..94a5887d42 100644 --- a/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js @@ -16,10 +16,11 @@ import { checkboxes } from '../common/filters/checkboxFilter.js'; /** * Renders a list of checkboxes that lets the user look for beam types * - * @param {SelectionFilterModel} selectionFilterModel selectionFilterModel + * @param {BeamsTypeFilterModel} beamsTypeFilterModel beamsTypeFilterModel * @return {Component} the filter */ -export const beamsTypeFilter = (selectionFilterModel) => checkboxes( - selectionFilterModel._selectionModel, - { selector: 'beams-types' }, -); +export const beamsTypeFilter = (beamsTypeFilterModel) => + checkboxes( + beamsTypeFilterModel, + { selector: 'beams-types' }, + ); diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 1b1ceca149..897b407d13 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -171,8 +171,7 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-8', format: (value) => formatBeamType(value), - filter: (lhcFillModel) => lhcFillModel.getBeamsTypes().length === 0 ? - undefined : beamsTypeFilter(lhcFillModel.filteringModel.get('beamsType')), + filter: (lhcFillModel) => beamsTypeFilter(lhcFillModel.filteringModel.get('beamsType')), }, collidingBunches: { name: 'Colliding bunches', diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 3734968576..5689428c96 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -17,8 +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 { SelectionFilterModel } from '../../../components/Filters/common/filters/SelectionFilterModel.js'; -import { beamsTypesProvider } from '../../../services/beamsTypes/beamsTypesProvider.js'; +import { BeamsTypeFilterModel } from '../../../components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js'; const defaultBeamDurationOperator = '='; const defaultRunDurationOperator = '='; @@ -44,15 +43,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { beamDuration: new RawTextFilterModel(), runDuration: new RawTextFilterModel(), hasStableBeams: new StableBeamFilterModel(), - }); - - beamsTypesProvider.items$.observe(() => { - beamsTypesProvider.items$.getCurrent().apply({ - Success: (types) => { - this._beamsTypes = types.map((type) => ({ value: type })); - this._filteringModel.put('beamsType', new SelectionFilterModel({ availableOptions: this._beamsTypes })); - }, - }); + beamsType: new BeamsTypeFilterModel(), }); this._beamDurationOperator = defaultBeamDurationOperator; diff --git a/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js b/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js index 51a8ddc6cc..405e9b5f14 100644 --- a/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js +++ b/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js @@ -11,8 +11,7 @@ * or submit itself to any jurisdiction. */ -const { ApiConfig } = require('../../config/index.js'); -const { BEAM_TYPES } = require('../../domain/enums/BeamType.js'); +const LhcFillRepository = require('../../database/repositories/LhcFillRepository.js'); /** * GetAllBeamsTypesUseCase @@ -21,19 +20,13 @@ class GetAllBeamsTypesUseCase { /** * Executes this use case. * - * @param {Object} dto The GetAllBeamsTypes DTO which contains all request data. * @returns {Promise} Promise object represents the result of this use case. */ - async execute(dto = {}) { - // DTO data to be used if Database data is desired. - const { query = {} } = dto; - const { page = {} } = query; - const { limit = ApiConfig.pagination.limit, offset = 0 } = page; - - const beamsTypes = BEAM_TYPES; + async execute() { + const result = await LhcFillRepository.getLhcFillDistinctBeamTypes(); return { - count: beamsTypes.length, - beamsTypes, + count: result.length, + beamsTypes: result, }; } } diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 155861806c..5a6d2dfa80 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -81,7 +81,7 @@ class GetAllLhcFillsUseCase { // Beams type. if (beamsType) { const beamTypes = beamsType.split(','); - queryBuilder.where('beamType').oneOfSubstrings(beamTypes); + queryBuilder.where('beamType').oneOf(beamTypes); } } From 18ea3868a260f345e1ec0afc613b9c5ffefd78b2 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Tue, 16 Dec 2025 13:33:58 +0100 Subject: [PATCH 47/76] [O2B-1508] Handle beamtype 'null'. Added tests --- lib/database/utilities/QueryBuilder.js | 35 +++++++++++++++- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 10 ++++- .../lhcFill/GetAllLhcFillsUseCase.test.js | 40 +++++++++++++++++++ test/public/lhcFills/overview.test.js | 2 + 4 files changed, 84 insertions(+), 3 deletions(-) diff --git a/lib/database/utilities/QueryBuilder.js b/lib/database/utilities/QueryBuilder.js index 2f522902e8..ab16d35ccf 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. @@ -104,6 +104,39 @@ class WhereQueryBuilder { return this._op(operation); } + /** + * Sets an **OR** match filter using the provided values. + * Adds an **OR NULL** filter. + * If the spread returns empty array the filter becomes an **IS NULL** filter (**OR** is not valid anymore) + * + * @param {...any} values The required values. + * @returns {QueryBuilder} The current QueryBuilder instance. + */ + oneOfOrNull(...values) { + let operation; + if (this.notFlag) { + operation = values[0]?.length === 0 ? { + [Op.not]: null, + } : { + [Op.or]: { + [Op.notIn]: values, + [Op.not]: null, + }, + }; + } else { + operation = values[0]?.length === 0 ? { + [Op.is]: null, + } : { + [Op.or]: { + [Op.in]: values, + [Op.is]: null, + }, + }; + } + + return this._op(operation); + } + /** * Set a max range limit using the provided value * diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 5a6d2dfa80..958f07cbe2 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -80,8 +80,14 @@ class GetAllLhcFillsUseCase { // Beams type. if (beamsType) { - const beamTypes = beamsType.split(','); - queryBuilder.where('beamType').oneOf(beamTypes); + let beamTypes = beamsType.split(','); + // Check if 'null' is included in the request + if (beamTypes.find((type) => type.trim() === 'null') !== undefined) { + beamTypes = beamTypes.filter((type) => type.trim() !== 'null'); + queryBuilder.where('beamType').oneOfOrNull(beamTypes); + } else { + queryBuilder.where('beamType').oneOf(beamTypes); + } } } diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index 6c317588ff..c9284001f5 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -220,4 +220,44 @@ 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: { beamsType: 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, OR NULL, {p-p, PROTON-PROTON, Pb-Pb, null}', async () => { + let beamTypes = ['p-p', ' PROTON-PROTON', 'Pb-Pb', 'null'] + + getAllLhcFillsDto.query = { filter: { beamsType: beamTypes.join(',') } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + + expect(lhcFills).to.be.an('array').and.lengthOf(5) + + const nullIndex = beamTypes.findIndex((value) => value ==='null') + beamTypes[nullIndex] = null; + + lhcFills.forEach((lhcFill) => { + expect(lhcFill.beamType).oneOf(beamTypes) + }); + }) + + it('should only contain specified beam type, IS NULL, {null}', async () => { + const beamTypes = ['null'] + + getAllLhcFillsDto.query = { filter: { beamsType: beamTypes.join(',') } }; + const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) + + expect(lhcFills).to.be.an('array').and.lengthOf(1) + lhcFills.forEach((lhcFill) => { + expect(lhcFill.beamType).oneOf([null]) + }); + }) }; diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index c67a1ebb3c..adea7f85fa 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -273,6 +273,7 @@ module.exports = () => { 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)'} + const filterBeamTypeExpect = {selector: 'div.flex-row:nth-child(5) > div:nth-child(1)', value: 'Beam Type'} await goToPage(page, 'lhc-fill-overview'); @@ -284,6 +285,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 () => { From 6a47048e79c32ccadda00af3556956c93cd80ca9 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 17 Dec 2025 15:45:36 +0100 Subject: [PATCH 48/76] [O2B-1505] Processed feedback --- lib/domain/dtos/filters/LhcFillsFilterDto.js | 10 ++- .../LhcFillsFilter/beamDurationFilter.js | 11 ++- .../filters/TextComparisonFilterModel.js | 79 +++++++++++++++++++ .../views/Home/Overview/HomePageModel.js | 2 +- .../ActiveColumns/lhcFillsActiveColumns.js | 6 +- lib/public/views/LhcFills/LhcFills.js | 2 +- .../Overview/LhcFillsOverviewModel.js | 40 ++++------ lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 10 +-- lib/utilities/validateTime.js | 38 ++++----- .../lhcFill/GetAllLhcFillsUseCase.test.js | 12 +-- 10 files changed, 135 insertions(+), 75 deletions(-) create mode 100644 lib/public/components/Filters/common/filters/TextComparisonFilterModel.js diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index 537d5a95e2..8d0ab51242 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -19,8 +19,10 @@ exports.LhcFillsFilterDto = Joi.object({ fillNumbers: Joi.string().trim().custom(validateRange).messages({ 'any.invalid': '{{#message}}', }), - beamDuration: Joi.string().trim().min(8).max(8).custom(validateTime).messages({ - 'any.invalid': '{{#message}}', - }), - beamDurationOperator: Joi.string().trim().min(1).max(2), + beamDuration: { + limit: Joi.string().trim().min(8).max(8).custom(validateTime).messages({ + 'any.invalid': '{{#message}}', + }), + operator: Joi.string().trim().min(1).max(2), + }, }); diff --git a/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js index 32dd1587b3..a6b19fe87a 100644 --- a/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js @@ -17,16 +17,15 @@ import { rawTextFilter } from '../common/filters/rawTextFilter.js'; /** * Component to filter LHC-fills by beam duration * - * @param {rawTextFilter} beamDurationFilterModel beamDurationFilterModel - * @param {string} beamDurationOperator beam duration operator value - * @param {(string) => undefined} beamDurationOperatorUpdate beam duration operator setter function + * @param {TextComparisonFilterModel} beamDurationFilterModel beamDurationFilterModel * @returns {Component} the text field */ -export const beamDurationFilter = (beamDurationFilterModel, beamDurationOperator, beamDurationOperatorUpdate) => { +export const beamDurationFilter = (beamDurationFilterModel) => { const amountFilter = rawTextFilter( - beamDurationFilterModel, + beamDurationFilterModel.operandInputModel, { classes: ['w-100', 'beam-duration-filter'], placeholder: 'e.g 16:14:15 (HH:MM:SS)' }, ); - return comparisonOperatorFilter(amountFilter, beamDurationOperator, (value) => beamDurationOperatorUpdate(value)); + return comparisonOperatorFilter(amountFilter, beamDurationFilterModel.operatorSelectionModel.value, (value) => + beamDurationFilterModel.operatorSelectionModel.select(value)); }; diff --git a/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js b/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js new file mode 100644 index 0000000000..8cff2f42e4 --- /dev/null +++ b/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js @@ -0,0 +1,79 @@ +/** + * @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 { ComparisonSelectionModel } from './ComparisonSelectionModel.js'; +import { FilterModel } from '../FilterModel.js'; +import { RawTextFilterModel } from './RawTextFilterModel.js'; + +/** + * TextComparisonFilterModel + */ +export class TextComparisonFilterModel extends FilterModel { + /** + * Constructor + */ + constructor() { + super(); + + this._operatorSelectionModel = new ComparisonSelectionModel(); + this._operatorSelectionModel.visualChange$.bubbleTo(this._visualChange$); + + this._operandInputModel = new RawTextFilterModel(); + this._operandInputModel.visualChange$.bubbleTo(this._visualChange$); + this._operandInputModel.bubbleTo(this); + + this._operatorSelectionModel.observe(() => this._operandInputModel.value ? this.notify() : this._visualChange$.notify()); + } + + /** + * Return raw text filter model + * + * @return {RawTextFilterModel} operand input model + */ + get operandInputModel() { + return this._operandInputModel; + } + + /** + * Get operator selection model + * + * @return {ComparisonSelectionModel} selection model + */ + get operatorSelectionModel() { + return this._operatorSelectionModel; + } + + /** + * @inheritDoc + */ + reset() { + this._operandInputModel.reset(); + this._operatorSelectionModel.reset(); + } + + /** + * @inheritDoc + */ + get normalized() { + return { + operator: this._operatorSelectionModel.current, + limit: this._operandInputModel.value, + }; + } + + /** + * @inheritDoc + */ + get isEmpty() { + return !this._operandInputModel.value; + } +} diff --git a/lib/public/views/Home/Overview/HomePageModel.js b/lib/public/views/Home/Overview/HomePageModel.js index 40b6cfac85..fca0331fe7 100644 --- a/lib/public/views/Home/Overview/HomePageModel.js +++ b/lib/public/views/Home/Overview/HomePageModel.js @@ -32,7 +32,7 @@ export class HomePageModel extends Observable { this._logsOverviewModel = new LogsOverviewModel(model, true); this._logsOverviewModel.bubbleTo(this); - this._lhcFillsOverviewModel = new LhcFillsOverviewModel(true); + this._lhcFillsOverviewModel = new LhcFillsOverviewModel(model, true); this._lhcFillsOverviewModel.bubbleTo(this); } diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index b60220adb0..8d0ef13e1e 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -109,11 +109,7 @@ export const lhcFillsActiveColumns = { return '-'; }, - filter: (lhcFillModel) => beamDurationFilter( - lhcFillModel.filteringModel.get('beamDuration'), - lhcFillModel.getBeamDurationOperator(), - (value) => lhcFillModel.setBeamDurationOperator(value), - ), + filter: (lhcFillModel) => beamDurationFilter(lhcFillModel.filteringModel.get('beamDuration')), profiles: { lhcFill: true, environment: true, diff --git a/lib/public/views/LhcFills/LhcFills.js b/lib/public/views/LhcFills/LhcFills.js index 70b6c5eb3d..aa64a09ef0 100644 --- a/lib/public/views/LhcFills/LhcFills.js +++ b/lib/public/views/LhcFills/LhcFills.js @@ -29,7 +29,7 @@ export default class LhcFills extends Observable { this.model = model; // Sub-models - this._overviewModel = new LhcFillsOverviewModel(true); + this._overviewModel = new LhcFillsOverviewModel(model, true); this._overviewModel.bubbleTo(this); this._detailsModel = new LhcFillDetailsModel(); diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 5dc83a4365..de7530b577 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -17,6 +17,8 @@ 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 { debounce } from '../../../utilities/debounce.js'; +import { TextComparisonFilterModel } from '../../../components/Filters/common/filters/TextComparisonFilterModel.js'; const defaultBeamDurationOperator = '='; @@ -29,14 +31,15 @@ export class LhcFillsOverviewModel extends OverviewPageModel { /** * Constructor * + * @param {model} model global model * @param {boolean} [stableBeamsOnly=false] if true, overview will load stable beam only */ - constructor(stableBeamsOnly = false) { + constructor(model, stableBeamsOnly = false) { super(); this._filteringModel = new FilteringModel({ fillNumbers: new RawTextFilterModel(), - beamDuration: new RawTextFilterModel(), + beamDuration: new TextComparisonFilterModel(), hasStableBeams: new StableBeamFilterModel(), }); @@ -50,6 +53,12 @@ export class LhcFillsOverviewModel extends OverviewPageModel { if (stableBeamsOnly) { this._filteringModel.get('hasStableBeams').setStableBeamsOnly(true); } + + const updateDebounceTime = () => { + this._debouncedLoad = debounce(this.load.bind(this), model.inputDebounceTime); + }; + model.appConfiguration$.observe(() => updateDebounceTime()); + updateDebounceTime(); } /** @@ -68,29 +77,10 @@ export class LhcFillsOverviewModel extends OverviewPageModel { getRootEndpoint() { const params = { filter: this.filteringModel.normalized, - ...this._filteringModel.get('beamDuration').isEmpty === false && { - 'filter[beamDurationOperator]': this._beamDurationOperator, - }, }; return buildUrl('/api/lhcFills', params); } - /** - * Beam duration operator setter - */ - setBeamDurationOperator(beamDurationOperator) { - this._beamDurationOperator = beamDurationOperator; - this._applyFilters(); - this.notify(); - } - - /** - * Beam duration operator getter - */ - getBeamDurationOperator() { - return this._beamDurationOperator; - } - /** * 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 @@ -120,8 +110,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @return {Boolean} If any filter is active */ isAnyFilterActive() { - return this._filteringModel.isAnyFilterActive() - || this._beamDurationOperator !== defaultBeamDurationOperator; + return this._filteringModel.isAnyFilterActive(); } /** @@ -135,11 +124,12 @@ export class LhcFillsOverviewModel extends OverviewPageModel { /** * Apply the current filtering and update the remote data list + * @param {boolean} now if true, filtering will be applied now without debouncing * * @return {void} */ - _applyFilters() { + _applyFilters(now = false) { this._pagination.currentPage = 1; - this.load(); + now ? this.load() : this._debouncedLoad(true); } } diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index d994661202..38c3d33382 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -45,7 +45,7 @@ class GetAllLhcFillsUseCase { const queryBuilder = new QueryBuilder(); if (filter) { - const { hasStableBeams, fillNumbers, beamDurationOperator, beamDuration } = filter; + const { hasStableBeams, fillNumbers, beamDuration } = filter; if (hasStableBeams) { // For now, if a stableBeamsStart is present, then a beam is stable queryBuilder.where('stableBeamsStart').not().is(null); @@ -61,10 +61,10 @@ class GetAllLhcFillsUseCase { queryBuilder.where('fillNumber').oneOf(...finalFillnumberList); } } - // Beam duration filter and corresponding operator. - if (beamDuration !== null && beamDuration !== undefined && beamDurationOperator) { - beamDuration === 0 ? queryBuilder.where('stableBeamsDuration').applyOperator(beamDurationOperator, null) - : queryBuilder.where('stableBeamsDuration').applyOperator(beamDurationOperator, beamDuration); + // Beam duration filter, limit and corresponding operator. + if (beamDuration?.limit !== undefined && beamDuration?.operator) { + const beamDurationLimit = Number(beamDuration.limit) === 0 ? null : beamDuration.limit; + queryBuilder.where('stableBeamsDuration').applyOperator(beamDuration.operator, beamDurationLimit); } } diff --git a/lib/utilities/validateTime.js b/lib/utilities/validateTime.js index 9c29dd7903..f584e7b750 100644 --- a/lib/utilities/validateTime.js +++ b/lib/utilities/validateTime.js @@ -1,32 +1,26 @@ +import Joi from 'joi'; + /** * Validates digital time in string format * - * @param {*} value The time to validate + * @param {*} incomingValue The time to validate * @param {*} helpers The helpers object * @returns {number|import("joi").ValidationError} The value if validation passes, as seconds (Number) */ -export const validateTime = (value, helpers) => { - const timeSectionsString = value.split(':'); - let timeSeconds = 0; - let powerValue = 2; +export const validateTime = (incomingValue, helpers) => { + // Checks for valid time format. + const { error, value } = Joi.string().pattern(/^\d{2}:[0-5]\d:[0-5]\d$/).validate(incomingValue); - for (const timeSectionString of timeSectionsString) { - if (!Number.isNaN(timeSectionString)) { - const timeSection = Number(timeSectionString); - if (timeSection <= 60 && timeSection >= 0) { - if (powerValue !== 0) { - timeSeconds += timeSection * 60 ** powerValue; - } else { - timeSeconds += timeSection; - } - } else { - return helpers.error('any.invalid', { message: `Invalid time period: ${timeSection}` }); - } - } else { - return helpers.error('any.invalid', { message: `Invalid time: ${timeSectionString}` }); - } - powerValue--; + if (error !== undefined) { + return helpers.error('any.invalid', { message: `Validation error: ${error?.message ?? 'failed to validate time'}` }); } - return timeSeconds; + // Extract time to seconds... + const [hoursStr, minutesStr, secondsStr] = value.split(':'); + + const hours = Number(hoursStr); + const minutes = Number(minutesStr); + const seconds = Number(secondsStr); + + return hours * 3600 + minutes * 60 + seconds; }; diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index 46cdc64a62..f0f9c89cae 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -104,7 +104,7 @@ module.exports = () => { // Beam duration filter tests it('should only contain specified stable beam durations, < 12:00:00', async () => { - getAllLhcFillsDto.query = { filter: { beamDuration: '43200', beamDurationOperator: '<' } }; + getAllLhcFillsDto.query = { filter: { beamDuration: {limit: '43200', operator: '<'} } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto); expect(lhcFills).to.be.an('array').and.lengthOf(3) lhcFills.forEach((lhcFill) => { @@ -113,7 +113,7 @@ module.exports = () => { }); it('should only contain specified stable beam durations, <= 12:00:00', async () => { - getAllLhcFillsDto.query = { filter: { beamDuration: '43200', beamDurationOperator: '<=' } }; + getAllLhcFillsDto.query = { filter: { beamDuration: {limit: '43200', operator: '<='} } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(4) lhcFills.forEach((lhcFill) => { @@ -122,7 +122,7 @@ module.exports = () => { }) it('should only contain specified stable beam durations, = 00:01:40', async () => { - getAllLhcFillsDto.query = { filter: { beamDuration: '100', beamDurationOperator: '=' } }; + getAllLhcFillsDto.query = { filter: { beamDuration: {limit: '100', operator: '='} } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(3) lhcFills.forEach((lhcFill) => { @@ -131,7 +131,7 @@ module.exports = () => { }); it('should only contain specified stable beam durations, >= 00:01:40', async () => { - getAllLhcFillsDto.query = { filter: { beamDuration: '100', beamDurationOperator: '>=' } }; + getAllLhcFillsDto.query = { filter: { beamDuration: {limit: '100', operator: '>='} } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(4) @@ -141,7 +141,7 @@ module.exports = () => { }) it('should only contain specified stable beam durations, > 00:01:40', async () => { - getAllLhcFillsDto.query = { filter: { beamDuration: '100', beamDurationOperator: '>' } }; + getAllLhcFillsDto.query = { filter: { beamDuration: {limit: '100', operator: '>'} } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(1) @@ -152,7 +152,7 @@ module.exports = () => { it('should only contain specified stable beam durations, = 00:00:00', async () => { // Tests the usecase's ability to replace the request for 0 to a request for null. - getAllLhcFillsDto.query = { filter: { hasStableBeams: true, beamDuration: 0, beamDurationOperator: '=' } }; + getAllLhcFillsDto.query = { filter: { hasStableBeams: true, beamDuration: {limit: '0', operator: '='} } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(1) From 8e82b346f43ac7d4500c3789efd905928038ffba Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 18 Dec 2025 13:08:24 +0100 Subject: [PATCH 49/76] [O2B-1503] Processed feedback, added tests --- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 3 +- lib/utilities/rangeUtils.js | 19 ++- test/lib/utilities/index.js | 2 + test/lib/utilities/rangeUtils.test.js | 159 ++++++++++++++++++ 4 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 test/lib/utilities/rangeUtils.test.js diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 2d83ade80f..360b396968 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -58,7 +58,8 @@ class GetAllLhcFillsUseCase { // Check that the final fill numbers list contains at least one valid fill number if (finalFillnumberList.length > 0) { - queryBuilder.where('fillNumber').oneOf(...finalFillnumberList); + finalFillnumberList.length === 1 ? queryBuilder.where('fillNumber').is(finalFillnumberList[0]) + : queryBuilder.where('fillNumber').oneOf(...finalFillnumberList); } } } diff --git a/lib/utilities/rangeUtils.js b/lib/utilities/rangeUtils.js index c6686ae899..4cc9a385de 100644 --- a/lib/utilities/rangeUtils.js +++ b/lib/utilities/rangeUtils.js @@ -26,7 +26,11 @@ export const validateRange = (value, helpers) => { for (const number of numbers) { if (number.includes('-')) { - const [start, end] = number.split('-').map((n) => parseInt(n, 10)); + // Check if '-' occurs more than once in this part of the range + if (number.lastIndexOf('-') !== number.indexOf('-')) { + return helpers.error('any.invalid', { message: `Invalid range: ${number}` }); + } + const [start, end] = number.split('-').map((n) => Number(n)); if (Number.isNaN(start) || Number.isNaN(end) || start > end) { return helpers.error('any.invalid', { message: `Invalid range: ${number}` }); } @@ -35,6 +39,11 @@ export const validateRange = (value, helpers) => { if (rangeSize > MAX_RANGE_SIZE) { return helpers.error('any.invalid', { message: `Given range exceeds max size of ${MAX_RANGE_SIZE} range: ${number}` }); } + } else { + // Prevent non-numeric input. + if (isNaN(number)) { + return helpers.error('any.invalid', { message: `Invalid number: ${number}` }); + } } } @@ -44,15 +53,15 @@ export const validateRange = (value, helpers) => { /** * Unpacks a given string containing number ranges. * E.G. input: 5,7-9 => output: 5,7,8,9 - * @param {string} numbersRange numbers that may or may not contain ranges. + * @param {string[]} numbersRanges numbers that may or may not contain ranges. * @param {string} rangeSplitter string used to indicate and unpack a range. * @returns {Set} set containing the unpacked range. */ -export function unpackNumberRange(numbersRange, rangeSplitter = '-') { +export function unpackNumberRange(numbersRanges, rangeSplitter = '-') { // Set to prevent duplicate values. const resultNumbers = new Set(); - numbersRange.forEach((number) => { + numbersRanges.forEach((number) => { if (number.includes(rangeSplitter)) { const [start, end] = number.split(rangeSplitter).map((n) => parseInt(n, 10)); if (!Number.isNaN(start) && !Number.isNaN(end)) { @@ -61,7 +70,7 @@ export function unpackNumberRange(numbersRange, rangeSplitter = '-') { } } } else { - if (!Number.isNaN(number)) { + if (!isNaN(number)) { resultNumbers.add(Number(number)); } } diff --git a/test/lib/utilities/index.js b/test/lib/utilities/index.js index f074095e0c..cc8b2202ed 100644 --- a/test/lib/utilities/index.js +++ b/test/lib/utilities/index.js @@ -14,6 +14,7 @@ const cacheAsyncFunctionTest = require('./cacheAsyncFunction.test.js'); const deepmerge = require('./deepmerge.test.js'); const isPromise = require('./isPromise.test.js'); +const rangeUtilsTest = require('./rangeUtils.test.js'); const stringUtilsTest = require('./stringUtils.test.js'); module.exports = () => { @@ -21,4 +22,5 @@ module.exports = () => { describe('deepmerge', deepmerge); describe('isPromise', isPromise); describe('stringUtils', stringUtilsTest); + describe('rangeUtils', rangeUtilsTest) }; diff --git a/test/lib/utilities/rangeUtils.test.js b/test/lib/utilities/rangeUtils.test.js new file mode 100644 index 0000000000..509db85a4f --- /dev/null +++ b/test/lib/utilities/rangeUtils.test.js @@ -0,0 +1,159 @@ +/** + * @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 Sinon = require('sinon'); +const { validateRange, unpackNumberRange } = require('../../../lib/utilities/rangeUtils.js'); +const { expect } = require('chai'); + +module.exports = () => { + describe('validateRange()', () => { + let helpers; + + beforeEach(() => { + helpers = { + error: Sinon.stub() + }; + }); + + it('returns the original value for a single valid number', () => { + const input = '5'; + const result = validateRange(input, helpers); + expect(result).to.equal(input); + }); + + it('returns the original value, accepts 0', () => { + const input = '0,1'; + const result = validateRange(input, helpers); + expect(result).to.equal(input); + }); + + it('returns the original value for multiple valid numbers', () => { + const input = '1, 2,3, 10 '; + const result = validateRange(input, helpers); + expect(result).to.equal(input); + }); + + it('accepts a valid range', () => { + const input = '7-9'; + const result = validateRange(input, helpers); + expect(result).to.equal(input); + }); + + it('accepts numbers and ranges together', () => { + const input = '5,7-9,12'; + const result = validateRange(input, helpers); + expect(result).to.equal(input); + }); + + it('accepts numbers and ranges overlap', () => { + const input = '1-6,2,3,4,5,6'; + const result = validateRange(input, helpers); + expect(result).to.equal(input); + }); + + it('rejects non-numeric input', () => { + const input = '5,a,7'; + validateRange(input, helpers); + expect(helpers.error.calledOnce).to.be.true; + expect(helpers.error.firstCall.args[0]).to.equal('any.invalid'); + expect(helpers.error.firstCall.args[1]).to.deep.equal({ message: 'Invalid number: a' }); + }); + + it('rejects range with non-numeric input', () => { + const input = '3-a'; + validateRange(input, helpers); + expect(helpers.error.calledOnce).to.be.true; + expect(helpers.error.firstCall.args[1]).to.deep.equal({ message: 'Invalid range: 3-a' }); + }); + + it('rejects range where Start > End', () => { + const input = '6-5'; + validateRange(input, helpers); + expect(helpers.error.calledOnce).to.be.true; + expect(helpers.error.firstCall.args[1]).to.deep.equal({ message: 'Invalid range: 6-5' }); + }); + + // Allowed, technically a valid range + it('accepts range where Start === End', () => { + const input = '5-5'; + const result = validateRange(input, helpers); + expect(result).to.equal(input); + }); + + it('rejects range containing more than one `-`', () => { + const input = '1-2-3'; + validateRange(input, helpers); + expect(helpers.error.calledOnce).to.be.true; + expect(helpers.error.firstCall.args[1]).to.deep.equal({ message: 'Invalid range: 1-2-3' }); + }); + + it('rejects range containing more than one `-`, at end', () => { + const input = '1-2-'; + validateRange(input, helpers); + expect(helpers.error.calledOnce).to.be.true; + expect(helpers.error.firstCall.args[1]).to.deep.equal({ message: 'Invalid range: 1-2-' }); + }); + + // MAX_RANGE_SIZE = 100, should this change, also change this test... + it('rejects a range that exceeds MAX_RANGE_SIZE', () => { + const input = '1-101'; + validateRange(input, helpers); + expect(helpers.error.calledOnce).to.be.true; + expect(helpers.error.firstCall.args[1]).to.deep.equal({ message: 'Given range exceeds max size of 100 range: 1-101' }); + }); + + it('handles whitespace around inputs', () => { + const input = ' 2 , 4-6 , 9 '; + const result = validateRange(input, helpers); + expect(result).to.equal(input); + }); + }); + + describe('unpackNumberRange()', () => { + it('unpacks single numbers, duplicate', () => { + const input = ['5', '10', '5']; + const result = unpackNumberRange(input); + expect(Array.from(result)).to.deep.equal([5, 10]); + }); + + it('unpacks range', () => { + const input = ['7-9']; + const result = unpackNumberRange(input); + expect(Array.from(result)).to.deep.equal([7, 8, 9]); + }); + + it('unpacks mixed numbers and ranges', () => { + const input = ['5', '7-9', '9', '3-4']; + const result = unpackNumberRange(input); + expect(Array.from(result)).to.deep.equal([5, 7, 8, 9, 3, 4]); + }); + + it('ignores any non-numeric inputs', () => { + const input = ['5', 'x', '2-3', 'a-b', '4-a']; + const result = unpackNumberRange(input); + expect(Array.from(result)).to.deep.equal([5, 2, 3]); + }); + + it('accepts/uses a range splitter', () => { + const input = ['8..10', '12']; + const result = unpackNumberRange(input, '..'); + expect(Array.from(result)).to.deep.equal([8, 9, 10, 12]); + }); + + // Also allowed right now... + it('returns empty set if nothing is given', () => { + const result = unpackNumberRange([]); + expect(result.size).to.equal(0); + }); + }); +}; From e5bbc05f1a212ca15eedc079fa0a8b4b279a234f Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 18 Dec 2025 13:25:42 +0100 Subject: [PATCH 50/76] [O2B-1503] Added test for splitStringToStringsTrimmed() --- test/lib/utilities/stringUtils.test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/lib/utilities/stringUtils.test.js b/test/lib/utilities/stringUtils.test.js index edbd95fbe9..fb06df6d4c 100644 --- a/test/lib/utilities/stringUtils.test.js +++ b/test/lib/utilities/stringUtils.test.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -const { snakeToCamel, pascalToSnake, ucFirst, lcFirst, snakeToPascal } = require('../../../lib/utilities/stringUtils.js'); +const { snakeToCamel, pascalToSnake, ucFirst, lcFirst, snakeToPascal, splitStringToStringsTrimmed } = require('../../../lib/utilities/stringUtils.js'); const { expect } = require('chai'); module.exports = () => { @@ -61,6 +61,11 @@ module.exports = () => { expect(snakeToCamel('SNAKE')).to.equal('snake'); }); + it('should successfully split string into array of strings', () => { + expect(splitStringToStringsTrimmed('one , two, three ')).to.deep.equal(['one', 'two', 'three']); + expect(splitStringToStringsTrimmed('one . two. three ', '.')).to.deep.equal(['one', 'two', 'three']); + }); + it('should successfully convert snake_case string to PascalCase', () => { expect(snakeToPascal('this_is_snake_case')).to.equal('ThisIsSnakeCase'); expect(snakeToPascal('_this_is_snake_case')).to.equal('ThisIsSnakeCase'); From f11347378c4c5a01d0650ecbe0786e9a52bb929a Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 5 Jan 2026 09:10:32 +0100 Subject: [PATCH 51/76] [O2B-1505] Remove old code --- lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index de7530b577..274d711193 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -20,8 +20,6 @@ import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsT import { debounce } from '../../../utilities/debounce.js'; import { TextComparisonFilterModel } from '../../../components/Filters/common/filters/TextComparisonFilterModel.js'; -const defaultBeamDurationOperator = '='; - /** * Model for the LHC fills overview page * @@ -43,8 +41,6 @@ export class LhcFillsOverviewModel extends OverviewPageModel { hasStableBeams: new StableBeamFilterModel(), }); - this._beamDurationOperator = defaultBeamDurationOperator; - this._filteringModel.observe(() => this._applyFilters(true)); this._filteringModel.visualChange$.bubbleTo(this); @@ -98,7 +94,6 @@ export class LhcFillsOverviewModel extends OverviewPageModel { */ resetFiltering(fetch = true) { this._filteringModel.reset(); - this._beamDurationOperator = defaultBeamDurationOperator; if (fetch) { this._applyFilters(true); From d75c03eae760f83bb926d92f84f2c5e83c22f62f Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Mon, 5 Jan 2026 09:18:45 +0100 Subject: [PATCH 52/76] [O2-1506] Process feedback from upstream --- .../LhcFillsFilter/runDurationFilter.js | 11 ++++---- .../ActiveColumns/lhcFillsActiveColumns.js | 6 +---- .../Overview/LhcFillsOverviewModel.js | 27 ++----------------- 3 files changed, 8 insertions(+), 36 deletions(-) diff --git a/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js b/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js index c4d3cca920..a00e326c48 100644 --- a/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js @@ -17,16 +17,15 @@ 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 + * @param {TextComparisonFilterModel} runDurationFilterModel runDurationFilterModel * @returns {Component} the text field */ -export const runDurationFilter = (runDurationFilterModel, runDurationOperator, runDurationOperatorUpdate) => { +export const runDurationFilter = (runDurationFilterModel) => { const amountFilter = rawTextFilter( - runDurationFilterModel, + runDurationFilterModel.operandInputModel, { classes: ['w-100', 'run-duration-filter'], placeholder: 'e.g 16:14:15 (HH:MM:SS)' }, ); - return comparisonOperatorFilter(amountFilter, runDurationOperator, (value) => runDurationOperatorUpdate(value)); + 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 5c84a73c54..4b0edeb0b7 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -142,11 +142,7 @@ 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), - ), + 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 e90b82a088..9bdb5f68f4 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -19,7 +19,6 @@ import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; import { debounce } from '../../../utilities/debounce.js'; import { TextComparisonFilterModel } from '../../../components/Filters/common/filters/TextComparisonFilterModel.js'; -const defaultRunDurationOperator = '='; /** * Model for the LHC fills overview page @@ -39,10 +38,9 @@ export class LhcFillsOverviewModel extends OverviewPageModel { this._filteringModel = new FilteringModel({ fillNumbers: new RawTextFilterModel(), beamDuration: new TextComparisonFilterModel(), - runDuration: new RawTextFilterModel(), + runDuration: new TextComparisonFilterModel(), hasStableBeams: new StableBeamFilterModel(), }); - this._runDurationOperator = defaultRunDurationOperator; this._filteringModel.observe(() => this._applyFilters(true)); this._filteringModel.visualChange$.bubbleTo(this); @@ -76,29 +74,10 @@ export class LhcFillsOverviewModel extends OverviewPageModel { getRootEndpoint() { const params = { filter: this.filteringModel.normalized, - ...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; - } - /** * 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 @@ -116,7 +95,6 @@ export class LhcFillsOverviewModel extends OverviewPageModel { */ resetFiltering(fetch = true) { this._filteringModel.reset(); - this._runDurationOperator = defaultRunDurationOperator; if (fetch) { this._applyFilters(true); @@ -128,8 +106,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @return {Boolean} If any filter is active */ isAnyFilterActive() { - return this._filteringModel.isAnyFilterActive() - || this._runDurationOperator !== defaultRunDurationOperator; + return this._filteringModel.isAnyFilterActive(); } /** From bd1b836772f3501e103eb7723c2dd0e30d5c7a31 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Tue, 20 Jan 2026 16:04:22 +0100 Subject: [PATCH 53/76] [O2B-1505] Processed feedback, timeValidator JOI updated, debounce removed, SBDuration filter frontend fixed --- lib/domain/dtos/filters/LhcFillsFilterDto.js | 9 +--- .../LhcFillsFilter/beamDurationFilter.js | 6 +-- .../filters/TextComparisonFilterModel.js | 4 +- .../Filters/common/filters/rawTextFilter.js | 3 +- .../Overview/LhcFillsOverviewModel.js | 15 ++---- lib/utilities/validateTime.js | 52 ++++++++++++------- test/public/lhcFills/overview.test.js | 9 ++-- 7 files changed, 53 insertions(+), 45 deletions(-) diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index 8d0ab51242..9db91e46d1 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -12,17 +12,12 @@ */ const Joi = require('joi'); const { validateRange } = require('../../../utilities/rangeUtils'); -const { validateTime } = require('../../../utilities/validateTime'); +const { validateTimeDuration } = require('../../../utilities/validateTime'); exports.LhcFillsFilterDto = Joi.object({ hasStableBeams: Joi.boolean(), fillNumbers: Joi.string().trim().custom(validateRange).messages({ 'any.invalid': '{{#message}}', }), - beamDuration: { - limit: Joi.string().trim().min(8).max(8).custom(validateTime).messages({ - 'any.invalid': '{{#message}}', - }), - operator: Joi.string().trim().min(1).max(2), - }, + beamDuration: validateTimeDuration, }); diff --git a/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js index a6b19fe87a..8417d3be21 100644 --- a/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js @@ -23,9 +23,9 @@ import { rawTextFilter } from '../common/filters/rawTextFilter.js'; export const beamDurationFilter = (beamDurationFilterModel) => { const amountFilter = rawTextFilter( beamDurationFilterModel.operandInputModel, - { classes: ['w-100', 'beam-duration-filter'], placeholder: 'e.g 16:14:15 (HH:MM:SS)' }, + { id: 'beam-duration-filter-operand', classes: ['w-100', 'beam-duration-filter'], placeholder: 'e.g 16:14:15 (HH:MM:SS)' }, ); - return comparisonOperatorFilter(amountFilter, beamDurationFilterModel.operatorSelectionModel.value, (value) => - beamDurationFilterModel.operatorSelectionModel.select(value)); + return comparisonOperatorFilter(amountFilter, beamDurationFilterModel.operatorSelectionModel.current, (value) => + beamDurationFilterModel.operatorSelectionModel.select(value), { id: 'beam-duration-filter-operator' }); }; diff --git a/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js b/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js index 8cff2f42e4..113809ca9c 100644 --- a/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js +++ b/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js @@ -26,12 +26,12 @@ export class TextComparisonFilterModel extends FilterModel { this._operatorSelectionModel = new ComparisonSelectionModel(); this._operatorSelectionModel.visualChange$.bubbleTo(this._visualChange$); + // Unless the filter contains a value don't apply the filters. + this._operatorSelectionModel.observe(() => this._operandInputModel.value ? this.notify() : this._visualChange$.notify()); this._operandInputModel = new RawTextFilterModel(); this._operandInputModel.visualChange$.bubbleTo(this._visualChange$); this._operandInputModel.bubbleTo(this); - - this._operatorSelectionModel.observe(() => this._operandInputModel.value ? this.notify() : this._visualChange$.notify()); } /** diff --git a/lib/public/components/Filters/common/filters/rawTextFilter.js b/lib/public/components/Filters/common/filters/rawTextFilter.js index aa72387767..7cf39199e1 100644 --- a/lib/public/components/Filters/common/filters/rawTextFilter.js +++ b/lib/public/components/Filters/common/filters/rawTextFilter.js @@ -23,11 +23,12 @@ import { h } from '/js/src/index.js'; * @return {Component} the filter */ export const rawTextFilter = (filterModel, configuration) => { - const { classes = [], placeholder = '' } = configuration || {}; + const { classes = [], placeholder = '', id = '' } = configuration || {}; return h( 'input', { type: 'text', + id: id, class: classes.join(' '), value: filterModel.value, placeholder: placeholder, diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 274d711193..18d14c693f 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -17,7 +17,6 @@ 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 { debounce } from '../../../utilities/debounce.js'; import { TextComparisonFilterModel } from '../../../components/Filters/common/filters/TextComparisonFilterModel.js'; /** @@ -41,7 +40,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { hasStableBeams: new StableBeamFilterModel(), }); - this._filteringModel.observe(() => this._applyFilters(true)); + this._filteringModel.observe(() => this._applyFilters()); this._filteringModel.visualChange$.bubbleTo(this); this.reset(false); @@ -49,12 +48,6 @@ export class LhcFillsOverviewModel extends OverviewPageModel { if (stableBeamsOnly) { this._filteringModel.get('hasStableBeams').setStableBeamsOnly(true); } - - const updateDebounceTime = () => { - this._debouncedLoad = debounce(this.load.bind(this), model.inputDebounceTime); - }; - model.appConfiguration$.observe(() => updateDebounceTime()); - updateDebounceTime(); } /** @@ -96,7 +89,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { this._filteringModel.reset(); if (fetch) { - this._applyFilters(true); + this._applyFilters(); } } @@ -123,8 +116,8 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * * @return {void} */ - _applyFilters(now = false) { + _applyFilters() { this._pagination.currentPage = 1; - now ? this.load() : this._debouncedLoad(true); + this.load(); } } diff --git a/lib/utilities/validateTime.js b/lib/utilities/validateTime.js index f584e7b750..9b3010e086 100644 --- a/lib/utilities/validateTime.js +++ b/lib/utilities/validateTime.js @@ -1,26 +1,42 @@ -import Joi from 'joi'; - /** - * Validates digital time in string format + * @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. * - * @param {*} incomingValue The time to validate - * @param {*} helpers The helpers object - * @returns {number|import("joi").ValidationError} The value if validation passes, as seconds (Number) + * 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. */ -export const validateTime = (incomingValue, helpers) => { - // Checks for valid time format. - const { error, value } = Joi.string().pattern(/^\d{2}:[0-5]\d:[0-5]\d$/).validate(incomingValue); +import Joi from 'joi'; - if (error !== undefined) { - return helpers.error('any.invalid', { message: `Validation error: ${error?.message ?? 'failed to validate time'}` }); - } +const joiTimeDurationErrorText = 'Invalid duration value'; +/** + * Transform digital time in string format + * + * @param {*} incomingValue The time to transform + * @returns {number} The value as seconds (Number) + */ +export const transformTime = (incomingValue) => { // Extract time to seconds... - const [hoursStr, minutesStr, secondsStr] = value.split(':'); - - const hours = Number(hoursStr); - const minutes = Number(minutesStr); - const seconds = Number(secondsStr); + const [hoursStr, minutesStr, secondsStr] = incomingValue.split(':'); - return hours * 3600 + minutes * 60 + seconds; + return Number(hoursStr) * 3600 + Number(minutesStr) * 60 + Number(secondsStr); }; + +/** + * Joi object that validates time duration filters. + * This is for duration, not a point in time. 10000:59:59 is valid. + * The operator is also validated. + */ +export const validateTimeDuration = Joi.object({ + limit: Joi.string().trim().pattern(/^\d+:[0-5]?\d:[0-5]?\d$/).custom(transformTime).messages({ + 'string.pattern.base': joiTimeDurationErrorText, + 'string.base': joiTimeDurationErrorText, + 'any.invalid': joiTimeDurationErrorText, + }), + operator: Joi.string().trim().min(1).max(2), +}); diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 1b69518047..284c42fec0 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -268,14 +268,17 @@ module.exports = () => { it('should successfully display filter elements', async () => { 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 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 filterSBDurationOperatorExpect = { value: true }; await goToPage(page, 'lhc-fill-overview'); // Open the filtering panel await openFilteringPanel(page); + // Note: expectAttributeValue does not work here. + expect(await page.evaluate(() => document.querySelector('#beam-duration-filter-operator > option:nth-child(3)').selected)).to.equal(filterSBDurationOperatorExpect.value); await expectInnerText(page, filterSBExpect.selector, filterSBExpect.value); await expectInnerText(page, filterFillNRExpect.selector, filterFillNRExpect.value); await expectInnerText(page, filterSBDurationExpect.selector, filterSBDurationExpect.value); From a04fac2bfdfec69d49a71d4d4ebdc98aaf9dfb36 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Tue, 20 Jan 2026 16:53:15 +0100 Subject: [PATCH 54/76] [O2B-1506] Improvements from upstream integrated --- lib/domain/dtos/filters/LhcFillsFilterDto.js | 5 +---- .../Filters/LhcFillsFilter/runDurationFilter.js | 6 +++--- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 8 ++++---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index bdededc9a4..f2664d0cc7 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -19,9 +19,6 @@ 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), + runDuration: validateTimeDuration, beamDuration: validateTimeDuration, }); diff --git a/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js b/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js index a00e326c48..5fd185ae45 100644 --- a/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js @@ -23,9 +23,9 @@ import { rawTextFilter } from '../common/filters/rawTextFilter.js'; export const runDurationFilter = (runDurationFilterModel) => { const amountFilter = rawTextFilter( runDurationFilterModel.operandInputModel, - { classes: ['w-100', 'run-duration-filter'], placeholder: 'e.g 16:14:15 (HH:MM:SS)' }, + { id: 'run-duration-filter-operand', 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)); + return comparisonOperatorFilter(amountFilter, runDurationFilterModel.operatorSelectionModel.current, (value) => + runDurationFilterModel.operatorSelectionModel.select(value), { id: 'run-duration-filter-operator' }); }; diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index eb26bc552c..9b22ee85ac 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, runDurationOperator, runDuration } = filter; + const { hasStableBeams, fillNumbers, beamDuration, runDuration } = filter; if (hasStableBeams) { // For now, if a stableBeamsStart is present, then a beam is stable queryBuilder.where('stableBeamsStart').not().is(null); @@ -66,11 +66,11 @@ class GetAllLhcFillsUseCase { } // Run duration filter and corresponding operator. - if (runDuration !== null && runDuration !== undefined && runDurationOperator) { + if (runDuration?.limit !== undefined && runDuration?.operator) { 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); + const runDurationLimit = Number(runDuration.limit) === 0 ? null : runDuration.limit; + queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator(runDuration.operator, runDurationLimit); } // Beam duration filter, limit and corresponding operator. From ff540af47aa9b191b36ce1153ac5da59a2d34a2e Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Tue, 20 Jan 2026 17:29:07 +0100 Subject: [PATCH 55/76] [O2B-1505] Fixed validateTime error handling, removed unused code, TODO validate tests --- .../dtos/filters/NumericalComparisonDto.js | 2 ++ lib/public/views/LhcFills/LhcFills.js | 2 +- .../Overview/LhcFillsOverviewModel.js | 2 +- lib/utilities/validateTime.js | 25 +++++++++++++------ 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/domain/dtos/filters/NumericalComparisonDto.js b/lib/domain/dtos/filters/NumericalComparisonDto.js index d71a7858e9..f0d5802125 100644 --- a/lib/domain/dtos/filters/NumericalComparisonDto.js +++ b/lib/domain/dtos/filters/NumericalComparisonDto.js @@ -24,3 +24,5 @@ exports.FloatComparisonDto = Joi.object({ operator: Joi.string().valid(...NUMERICAL_COMPARISON_OPERATORS), limit: Joi.number().min(0), }); + +exports.NUMERICAL_COMPARISON_OPERATORS = NUMERICAL_COMPARISON_OPERATORS; diff --git a/lib/public/views/LhcFills/LhcFills.js b/lib/public/views/LhcFills/LhcFills.js index aa64a09ef0..70b6c5eb3d 100644 --- a/lib/public/views/LhcFills/LhcFills.js +++ b/lib/public/views/LhcFills/LhcFills.js @@ -29,7 +29,7 @@ export default class LhcFills extends Observable { this.model = model; // Sub-models - this._overviewModel = new LhcFillsOverviewModel(model, true); + this._overviewModel = new LhcFillsOverviewModel(true); this._overviewModel.bubbleTo(this); this._detailsModel = new LhcFillDetailsModel(); diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 18d14c693f..78f8272535 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -31,7 +31,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @param {model} model global model * @param {boolean} [stableBeamsOnly=false] if true, overview will load stable beam only */ - constructor(model, stableBeamsOnly = false) { + constructor(stableBeamsOnly = false) { super(); this._filteringModel = new FilteringModel({ diff --git a/lib/utilities/validateTime.js b/lib/utilities/validateTime.js index 9b3010e086..3b9349883b 100644 --- a/lib/utilities/validateTime.js +++ b/lib/utilities/validateTime.js @@ -11,20 +11,30 @@ * or submit itself to any jurisdiction. */ import Joi from 'joi'; +import { NUMERICAL_COMPARISON_OPERATORS } from '../domain/dtos/filters/NumericalComparisonDto.js'; const joiTimeDurationErrorText = 'Invalid duration value'; /** * Transform digital time in string format * - * @param {*} incomingValue The time to transform - * @returns {number} The value as seconds (Number) + * @param {string} incomingValue The time to transform + * @param {*} helpers The Joi helpers object + * @returns {number|import("joi").ValidationError} The value if transformation passes, as seconds (Number) */ -export const transformTime = (incomingValue) => { - // Extract time to seconds... - const [hoursStr, minutesStr, secondsStr] = incomingValue.split(':'); +export const transformTime = (incomingValue, helpers) => { + try { + // Extract time to seconds... + const [hoursStr, minutesStr, secondsStr] = incomingValue.split(':'); - return Number(hoursStr) * 3600 + Number(minutesStr) * 60 + Number(secondsStr); + const hours = Number(hoursStr); + const minutes = Number(minutesStr); + const seconds = Number(secondsStr); + + return hours * 3600 + minutes * 60 + seconds; + } catch (error) { + return helpers.error('any.invalid', { message: `Validation error: ${error?.message ?? 'failed to transform time'}` }); + } }; /** @@ -36,7 +46,6 @@ export const validateTimeDuration = Joi.object({ limit: Joi.string().trim().pattern(/^\d+:[0-5]?\d:[0-5]?\d$/).custom(transformTime).messages({ 'string.pattern.base': joiTimeDurationErrorText, 'string.base': joiTimeDurationErrorText, - 'any.invalid': joiTimeDurationErrorText, }), - operator: Joi.string().trim().min(1).max(2), + operator: Joi.string().valid(...NUMERICAL_COMPARISON_OPERATORS), }); From 9e4e68c3b42b10e1fcead57ec3ba0452476f8810 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 21 Jan 2026 12:05:07 +0100 Subject: [PATCH 56/76] [O2B-1505] processed feedback, empty id, code deduplication, bad doc --- .../Filters/common/filters/TextComparisonFilterModel.js | 7 +++---- .../components/Filters/common/filters/rawTextFilter.js | 2 +- .../views/LhcFills/Overview/LhcFillsOverviewModel.js | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js b/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js index 113809ca9c..b6510f8fae 100644 --- a/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js +++ b/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js @@ -26,12 +26,11 @@ export class TextComparisonFilterModel extends FilterModel { this._operatorSelectionModel = new ComparisonSelectionModel(); this._operatorSelectionModel.visualChange$.bubbleTo(this._visualChange$); - // Unless the filter contains a value don't apply the filters. - this._operatorSelectionModel.observe(() => this._operandInputModel.value ? this.notify() : this._visualChange$.notify()); this._operandInputModel = new RawTextFilterModel(); - this._operandInputModel.visualChange$.bubbleTo(this._visualChange$); - this._operandInputModel.bubbleTo(this); + // Unless the filter contains a value don't apply the filters. + this._operatorSelectionModel.observe(() => this._operandInputModel.value ? this.notify() : this._visualChange$.notify()); + this._addSubmodel(this._operandInputModel); } /** diff --git a/lib/public/components/Filters/common/filters/rawTextFilter.js b/lib/public/components/Filters/common/filters/rawTextFilter.js index 7cf39199e1..3cf2078103 100644 --- a/lib/public/components/Filters/common/filters/rawTextFilter.js +++ b/lib/public/components/Filters/common/filters/rawTextFilter.js @@ -23,7 +23,7 @@ import { h } from '/js/src/index.js'; * @return {Component} the filter */ export const rawTextFilter = (filterModel, configuration) => { - const { classes = [], placeholder = '', id = '' } = configuration || {}; + const { classes = [], placeholder = '', id } = configuration || {}; return h( 'input', { diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 78f8272535..fae9b894f8 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -112,7 +112,6 @@ export class LhcFillsOverviewModel extends OverviewPageModel { /** * Apply the current filtering and update the remote data list - * @param {boolean} now if true, filtering will be applied now without debouncing * * @return {void} */ From 0af9bdf39952bd7d5e30780d71c6f3c1b022e88f Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 21 Jan 2026 14:26:52 +0100 Subject: [PATCH 57/76] [O2B-1505] API + UI tests added --- test/api/lhcFills.test.js | 218 ++++++++++++++++++++++++++ test/public/lhcFills/overview.test.js | 13 ++ 2 files changed, 231 insertions(+) diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index ab57c3f380..95bcde95ba 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -35,6 +35,224 @@ module.exports = () => { done(); }); }); + + it('should return 200 and an LHCFill array for stablebeams only filter', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[hasStableBeams]=true') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(5); + + done(); + }); + }); + + it('should return 200 and an LHCFill array for stablebeams duration filter, = 12:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamDuration][operator]==&filter[beamDuration][limit]=12:00:00') + .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(6); + + done(); + }); + }); + + it('should return 200 for stablebeams duration filter, = 00:9:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamDuration][operator]==&filter[beamDuration][limit]=00:9:00') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(0); + + done(); + }); + }); + + it('should return 200 for stablebeams duration filter, = 00:00:9', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamDuration][operator]==&filter[beamDuration][limit]=00:00:9') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(0); + + done(); + }); + }); + + it('should return 200 for stablebeams duration filter, = 999999:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamDuration][operator]==&filter[beamDuration][limit]=999999:00:00') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(0); + + done(); + }); + }); + + + it('should return 200 for stablebeams duration filter, = 999999:0:0', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamDuration][operator]==&filter[beamDuration][limit]=999999:0:0') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(0); + + done(); + }); + }); + + + + it('should return 400 for wrong stablebeams duration filter, = 44:60:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamDuration][operator]==&filter[beamDuration][limit]=44:60:00') + .expect(400) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.errors[0].title).to.equal('Invalid Attribute'); + + done(); + }); + }); + + it('should return 400 for wrong stablebeams duration filter, = 44:00:60', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamDuration][operator]==&filter[beamDuration][limit]=44:00:60') + .expect(400) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.errors[0].title).to.equal('Invalid Attribute'); + + done(); + }); + }); + + it('should return 400 for wrong stablebeams duration filter, = -44:30:15', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamDuration][operator]==&filter[beamDuration][limit]=-44:30:15') + .expect(400) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.errors[0].title).to.equal('Invalid Attribute'); + + done(); + }); + }); + + + it('should return 200 and an LHCFill array for stablebeams duration filter, < 12:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamDuration][operator]=<&filter[beamDuration][limit]=12:00:00') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(3); + expect(res.body.data[0].fillNumber).to.equal(3); + + done(); + }); + }); + + it('should return 200 and an LHCFill array for stablebeams duration filter, <= 12:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamDuration][operator]=<=&filter[beamDuration][limit]=12:00:00') + .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(6); + + done(); + }); + }); + + it('should return 200 and an LHCFill array for stablebeams duration filter, >= 12:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamDuration][operator]=>=&filter[beamDuration][limit]=12:00:00') + .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(6); + + done(); + }); + }); + + it('should return 200 and an LHCFill array for stablebeams duration filter, > 12:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamDuration][operator]=>&filter[beamDuration][limit]=12:00:00') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(0); + + done(); + }); + }); + + // }); describe('POST /api/lhcFills', () => { it('should return 201 if valid data is provided', async () => { diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 284c42fec0..83ad6d996c 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -25,6 +25,7 @@ const { expectLink, openFilteringPanel, expectAttributeValue, + fillInput, } = require('../defaults.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -301,4 +302,16 @@ module.exports = () => { await pressElement(page, '.slider.round'); await waitForTableLength(page, 6); }); + + it('should successfully apply beam duration filter', async () => { + const filterSBDurationOperator= '#beam-duration-filter-operator'; + const filterSBDurationOperand= '#beam-duration-filter-operand'; + await goToPage(page, 'lhc-fill-overview'); + await waitForTableLength(page, 5); + // Open the filtering panel + await openFilteringPanel(page); + await page.select(filterSBDurationOperator, '>='); + await fillInput(page, filterSBDurationOperand, '00:01:40', ['change']); + await waitForTableLength(page, 4); + }); }; From 45acea263cec30cdd3c621c60d9518d7530e5fc4 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 21 Jan 2026 14:34:18 +0100 Subject: [PATCH 58/76] [O2B-1505] disable 00:00:00 conversion to null for DB --- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 30321645e2..d270572fbe 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -64,8 +64,7 @@ class GetAllLhcFillsUseCase { } // Beam duration filter, limit and corresponding operator. if (beamDuration?.limit !== undefined && beamDuration?.operator) { - const beamDurationLimit = Number(beamDuration.limit) === 0 ? null : beamDuration.limit; - queryBuilder.where('stableBeamsDuration').applyOperator(beamDuration.operator, beamDurationLimit); + queryBuilder.where('stableBeamsDuration').applyOperator(beamDuration.operator, beamDuration.limit); } } From 4a4ed6d415aff65c7dc0a4d8810a0232e528883d Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 21 Jan 2026 14:49:19 +0100 Subject: [PATCH 59/76] [O2B-1505] typo removed --- test/api/lhcFills.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index 95bcde95ba..2983e4dd86 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -251,8 +251,6 @@ module.exports = () => { done(); }); }); - - // }); describe('POST /api/lhcFills', () => { it('should return 201 if valid data is provided', async () => { From eca4ffa30f089d2598575c345c4a72a0767f5b1f Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 21 Jan 2026 15:05:19 +0100 Subject: [PATCH 60/76] [1506] WIP testing, usecase fixing --- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 2 +- test/api/lhcFills.test.js | 230 ++++++++++++++++++ 2 files changed, 231 insertions(+), 1 deletion(-) diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index f1fd90d615..6873d5999d 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -68,7 +68,7 @@ class GetAllLhcFillsUseCase { // Run duration filter and corresponding operator. if (runDuration?.limit !== undefined && runDuration?.operator) { associatedStatisticsRequired = true; - // 00:00:00 aka 0 value is saved in the DB as null + // 00:00:00 aka 0 value is saved in the DB as null (bookkeeping.fill_statistics.runs_coverage) const runDurationLimit = Number(runDuration.limit) === 0 ? null : runDuration.limit; queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator(runDuration.operator, runDurationLimit); } diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index 2983e4dd86..8dbe802949 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -251,6 +251,236 @@ module.exports = () => { done(); }); }); + + + + + + + + + + // RUNS + + it('should return 200 and an LHCFill array for runs duration filter, = 05:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]==&filter[runDuration][limit]=05:00:00') + .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(6); + + done(); + }); + }); + + it('should return 200 and an LHCFill array for runs duration filter, = 5:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]==&filter[runDuration][limit]=5:00:00') + .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(6); + + done(); + }); + }); + + it('should return 200 for runs duration filter, = 00:9:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]==&filter[runDuration][limit]=00:9:00') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(0); + + done(); + }); + }); + + it('should return 200 for runs duration filter, = 00:00:9', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]==&filter[runDuration][limit]=00:00:9') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(0); + + done(); + }); + }); + + it('should return 200 for runs duration filter, = 999999:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]==&filter[runDuration][limit]=999999:00:00') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(0); + + done(); + }); + }); + + + it('should return 200 for runs duration filter, = 999999:0:0', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]==&filter[runDuration][limit]=999999:0:0') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(0); + + done(); + }); + }); + + + + it('should return 400 for wrong runs duration filter, = 44:60:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]==&filter[runDuration][limit]=44:60:00') + .expect(400) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.errors[0].title).to.equal('Invalid Attribute'); + + done(); + }); + }); + + it('should return 400 for wrong runs duration filter, = 44:00:60', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]==&filter[runDuration][limit]=44:00:60') + .expect(400) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.errors[0].title).to.equal('Invalid Attribute'); + + done(); + }); + }); + + it('should return 400 for wrong runs duration filter, = -44:30:15', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]==&filter[runDuration][limit]=-44:30:15') + .expect(400) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.errors[0].title).to.equal('Invalid Attribute'); + + done(); + }); + }); + + + it('should return 200 and an LHCFill array for runs duration filter, < 6:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]=<&filter[runDuration][limit]=6:00:00') + .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(6); + + done(); + }); + }); + + it('should return 200 and an LHCFill array for runs duration filter, <= 5:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]=<=&filter[runDuration][limit]=5:00:00') + .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(6); + + done(); + }); + }); + /** + * TODO ISSUE WITH >= 0 since bigger than null is not a thing, modify UseCase for OR => 0 + */ + + it('should return 200 and an LHCFill array for runs duration filter, >= 12:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]=>=&filter[runDuration][limit]=12:00:00') + .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(6); + + done(); + }); + }); + + it('should return 200 and an LHCFill array for runs duration filter, > 12:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]=>&filter[runDuration][limit]=12:00:00') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(0); + + done(); + }); + }); }); describe('POST /api/lhcFills', () => { it('should return 201 if valid data is provided', async () => { From 133aa018bfdd02dee05c79bae5287177d4cfbf12 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 21 Jan 2026 15:07:23 +0100 Subject: [PATCH 61/76] [O2B-1505] Removed outdated 00:00:00 usecase test --- test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index f0f9c89cae..0d35c81e05 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -151,13 +151,9 @@ module.exports = () => { }) it('should only contain specified stable beam durations, = 00:00:00', async () => { - // Tests the usecase's ability to replace the request for 0 to a request for null. getAllLhcFillsDto.query = { filter: { hasStableBeams: true, beamDuration: {limit: '0', operator: '='} } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) - expect(lhcFills).to.be.an('array').and.lengthOf(1) - lhcFills.forEach((lhcFill) => { - expect(lhcFill.stableBeamsDuration).equals(null) - }); + expect(lhcFills).to.be.an('array').and.lengthOf(0) }) }; From 191fd52a10b69f4259bef7a7003e870eecdd1b25 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 21 Jan 2026 15:30:59 +0100 Subject: [PATCH 62/76] [O2B-1505] amountFilter -> durationFilter --- .../components/Filters/LhcFillsFilter/beamDurationFilter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js index 8417d3be21..2ef0bbc0af 100644 --- a/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js @@ -21,11 +21,11 @@ import { rawTextFilter } from '../common/filters/rawTextFilter.js'; * @returns {Component} the text field */ export const beamDurationFilter = (beamDurationFilterModel) => { - const amountFilter = rawTextFilter( + const durationFilter = rawTextFilter( beamDurationFilterModel.operandInputModel, { id: 'beam-duration-filter-operand', classes: ['w-100', 'beam-duration-filter'], placeholder: 'e.g 16:14:15 (HH:MM:SS)' }, ); - return comparisonOperatorFilter(amountFilter, beamDurationFilterModel.operatorSelectionModel.current, (value) => + return comparisonOperatorFilter(durationFilter, beamDurationFilterModel.operatorSelectionModel.current, (value) => beamDurationFilterModel.operatorSelectionModel.select(value), { id: 'beam-duration-filter-operator' }); }; From 15e6a39c698afdb7498ec8453e8ed89789079514 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 21 Jan 2026 17:30:36 +0100 Subject: [PATCH 63/76] [1506] Changed the handling of >= 00:00:00 for the runDuration on lhcfills DTO. QueryBuilder WhereAssociationQueryBuilder has been changed to handle associations with an OR operator. Added tests --- lib/database/utilities/QueryBuilder.js | 32 +++++++++++++++---- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 11 +++++-- test/api/lhcFills.test.js | 26 ++++----------- test/public/lhcFills/overview.test.js | 15 +++++++++ 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/lib/database/utilities/QueryBuilder.js b/lib/database/utilities/QueryBuilder.js index 2f522902e8..a8b01de2f1 100644 --- a/lib/database/utilities/QueryBuilder.js +++ b/lib/database/utilities/QueryBuilder.js @@ -360,18 +360,36 @@ 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. */ _op(operation) { - this.queryBuilder.include({ - association: this.association, - required: true, - where: { - [this.column]: operation, - }, - }); + // Check if this include association already exists + const existingInclude = this.queryBuilder.options.include?.find((include) => include.association === this.association); + + if (existingInclude && existingInclude.where) { + /* + * Replace existing where operation in the include with OR operation. + * This basically encapsulates the existing where operation in a OR operation together with the new operation. + */ + existingInclude.where = { + [Op.or]: [ + { [this.column]: existingInclude.where[this.column] }, + { [this.column]: operation }, + ], + }; + } else { + // Create new include + this.queryBuilder.include({ + association: this.association, + required: true, + where: { + [this.column]: operation, + }, + }); + } return this.queryBuilder; } diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 6873d5999d..81c92ea358 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -69,8 +69,15 @@ class GetAllLhcFillsUseCase { if (runDuration?.limit !== undefined && runDuration?.operator) { associatedStatisticsRequired = true; // 00:00:00 aka 0 value is saved in the DB as null (bookkeeping.fill_statistics.runs_coverage) - const runDurationLimit = Number(runDuration.limit) === 0 ? null : runDuration.limit; - queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator(runDuration.operator, runDurationLimit); + if ((runDuration.operator === '>=' || runDuration.operator === '<=') && Number(runDuration.limit) === 0) { + // Include 00:00:00 = 0 = null AND everything above 00:00:00 which is more or less than 0. + queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator(runDuration.operator, 0); + queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator('=', null); + } else if (Number(runDuration.limit) === 0) { + queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator(runDuration.operator, null); + } else { + queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator(runDuration.operator, runDuration.limit); + } } // Beam duration filter, limit and corresponding operator. diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index 8dbe802949..a9f4ace6b4 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -252,16 +252,6 @@ module.exports = () => { }); }); - - - - - - - - - // RUNS - it('should return 200 and an LHCFill array for runs duration filter, = 05:00:00', (done) => { request(server) .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]==&filter[runDuration][limit]=05:00:00') @@ -445,13 +435,10 @@ module.exports = () => { done(); }); }); - /** - * TODO ISSUE WITH >= 0 since bigger than null is not a thing, modify UseCase for OR => 0 - */ - it('should return 200 and an LHCFill array for runs duration filter, >= 12:00:00', (done) => { + it('should return 200 and an LHCFill array for runs duration filter, >= 00:00:00', (done) => { request(server) - .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]=>=&filter[runDuration][limit]=12:00:00') + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]=>=&filter[runDuration][limit]=00:00:00') .expect(200) .end((err, res) => { if (err) { @@ -459,16 +446,16 @@ module.exports = () => { return; } - expect(res.body.data).to.have.lengthOf(1); + expect(res.body.data).to.have.lengthOf(5); expect(res.body.data[0].fillNumber).to.equal(6); done(); }); }); - it('should return 200 and an LHCFill array for runs duration filter, > 12:00:00', (done) => { + it('should return 200 and an LHCFill array for runs duration filter, > 03:00:00', (done) => { request(server) - .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]=>&filter[runDuration][limit]=12:00:00') + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]=>&filter[runDuration][limit]=03:00:00') .expect(200) .end((err, res) => { if (err) { @@ -476,7 +463,8 @@ module.exports = () => { return; } - expect(res.body.data).to.have.lengthOf(0); + expect(res.body.data).to.have.lengthOf(1); + expect(res.body.data[0].fillNumber).to.equal(6); done(); }); diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 3ce7192d44..126d9fdfca 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -318,4 +318,19 @@ module.exports = () => { await fillInput(page, filterSBDurationOperand, '00:01:40', ['change']); await waitForTableLength(page, 4); }); + + it('should successfully apply run duration filter', async () => { + const filterRunDurationOperator= '#run-duration-filter-operator'; + const filterRunDurationOperand= '#run-duration-filter-operand'; + await goToPage(page, 'lhc-fill-overview'); + await waitForTableLength(page, 5); + // Open the filtering panel + await openFilteringPanel(page); + await page.select(filterRunDurationOperator, '<='); + await fillInput(page, filterRunDurationOperand, '00:00:00', ['change']); + await waitForTableLength(page, 4); + await page.select(filterRunDurationOperator, '>='); + await fillInput(page, filterRunDurationOperand, '00:00:00', ['change']); + await waitForTableLength(page, 5); + }); }; From 1bdf8d017c36c1a20849aa8b14815f71baa50aae Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 22 Jan 2026 10:23:50 +0100 Subject: [PATCH 64/76] [O2B-1506] fix outdated usecase test --- .../usecases/lhcFill/GetAllLhcFillsUseCase.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index 707e1e4ddd..96dc965d4e 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -157,7 +157,7 @@ module.exports = () => { }) it('should only contain specified total run duration, > 04:00:00', async () => { - getAllLhcFillsDto.query = { filter: { runDuration: '14400', runDurationOperator: '>' } }; + getAllLhcFillsDto.query = { filter: { runDuration: {limit: '14400', operator: '>'} } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(1) @@ -167,7 +167,7 @@ module.exports = () => { }) it('should only contain specified total run duration, >= 05:00:00', async () => { - getAllLhcFillsDto.query = { filter: { runDuration: '18000', runDurationOperator: '>=' } }; + getAllLhcFillsDto.query = { filter: { runDuration: {limit: '18000', operator: '>='} } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(1) @@ -177,7 +177,7 @@ module.exports = () => { }) it('should only contain specified total run duration, = 05:00:00', async () => { - getAllLhcFillsDto.query = { filter: { runDuration: '18000', runDurationOperator: '=' } }; + getAllLhcFillsDto.query = { filter: { runDuration: {limit: '18000', operator: '='} } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(1) @@ -188,7 +188,7 @@ 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: '=' } }; + getAllLhcFillsDto.query = { filter: { runDuration: {limit: '0', operator: '='} } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(4) @@ -198,7 +198,7 @@ module.exports = () => { }) it('should only contain specified total run duration, <= 05:00:00', async () => { - getAllLhcFillsDto.query = { filter: { runDuration: '18000', runDurationOperator: '<=' } }; + getAllLhcFillsDto.query = { filter: { runDuration: {limit: '18000', operator: '<='} } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(1) @@ -208,7 +208,7 @@ module.exports = () => { }) it('should only contain specified total run duration, < 06:30:59', async () => { - getAllLhcFillsDto.query = { filter: { runDuration: '23459', runDurationOperator: '<' } }; + getAllLhcFillsDto.query = { filter: { runDuration: {limit: '23459', operator: '<'} } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(1) From 2aec67f658de1a6daf0cecc6fa084e04acac6d72 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 22 Jan 2026 15:41:55 +0100 Subject: [PATCH 65/76] [O2B-1508] Tests --- test/api/lhcFills.test.js | 50 +++++++++++++++++++++++++++ test/public/lhcFills/overview.test.js | 12 +++++++ 2 files changed, 62 insertions(+) diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index a9f4ace6b4..1fcdc820ce 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -469,6 +469,56 @@ 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[beamsType]=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?age[offset]=0&page[limit]=15&filter[beamsType]=Pb-Pb,p-p,p-Pb') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(5); + expect(res.body.data[0].fillNumber).to.equal(6); + + done(); + }); + }); + + it('should return 400 for beam types filter, one wrong', (done) => { + request(server) + .get('/api/lhcFills?age[offset]=0&page[limit]=15&filter[beamsType]=Pb-Pb,Jasper-Jasper,p-p,p-Pb') + .expect(400) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.errors[0].title).to.equal('Invalid Attribute'); + + done(); + }); + }); }); describe('POST /api/lhcFills', () => { it('should return 201 if valid data is provided', async () => { diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 049e5d25eb..b2868cd66e 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -335,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 = '#beams-types-checkbox-p-Pb'; + const filterBeamTypePb_Pb = '#beams-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); + }); }; From 637ad11298617bbb1278c8044b13d1d8527006c4 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 22 Jan 2026 16:02:05 +0100 Subject: [PATCH 66/76] [O2B-1506] Consolidated beam and run duration filter elements --- .../LhcFillsFilter/beamDurationFilter.js | 31 ------------------- ...runDurationFilter.js => durationFilter.js} | 15 ++++----- .../ActiveColumns/lhcFillsActiveColumns.js | 7 ++--- test/public/lhcFills/overview.test.js | 4 +-- 4 files changed, 13 insertions(+), 44 deletions(-) delete mode 100644 lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js rename lib/public/components/Filters/LhcFillsFilter/{runDurationFilter.js => durationFilter.js} (54%) diff --git a/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js deleted file mode 100644 index 2ef0bbc0af..0000000000 --- a/lib/public/components/Filters/LhcFillsFilter/beamDurationFilter.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @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 beam duration - * - * @param {TextComparisonFilterModel} beamDurationFilterModel beamDurationFilterModel - * @returns {Component} the text field - */ -export const beamDurationFilter = (beamDurationFilterModel) => { - const durationFilter = rawTextFilter( - beamDurationFilterModel.operandInputModel, - { id: 'beam-duration-filter-operand', classes: ['w-100', 'beam-duration-filter'], placeholder: 'e.g 16:14:15 (HH:MM:SS)' }, - ); - - return comparisonOperatorFilter(durationFilter, beamDurationFilterModel.operatorSelectionModel.current, (value) => - beamDurationFilterModel.operatorSelectionModel.select(value), { id: 'beam-duration-filter-operator' }); -}; diff --git a/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js b/lib/public/components/Filters/LhcFillsFilter/durationFilter.js similarity index 54% rename from lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js rename to lib/public/components/Filters/LhcFillsFilter/durationFilter.js index 5fd185ae45..29a78ed81d 100644 --- a/lib/public/components/Filters/LhcFillsFilter/runDurationFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/durationFilter.js @@ -15,17 +15,18 @@ import { comparisonOperatorFilter } from '../common/filters/comparisonOperatorFi import { rawTextFilter } from '../common/filters/rawTextFilter.js'; /** - * Component to filter LHC-fills by run duration + * Component to filter LHC-fills by duration * - * @param {TextComparisonFilterModel} runDurationFilterModel runDurationFilterModel + * @param {TextComparisonFilterModel} durationFilterModel durationFilterModel + * @param {string} id id used for the operand and operator elements, becomes: `${id}-operator` OR `${id}-operand`. * @returns {Component} the text field */ -export const runDurationFilter = (runDurationFilterModel) => { +export const durationFilter = (durationFilterModel, id) => { const amountFilter = rawTextFilter( - runDurationFilterModel.operandInputModel, - { id: 'run-duration-filter-operand', classes: ['w-100', 'run-duration-filter'], placeholder: 'e.g 16:14:15 (HH:MM:SS)' }, + durationFilterModel.operandInputModel, + { id: `${id}-operand`, classes: ['w-100'], placeholder: 'e.g 16:14:15 (HH:MM:SS)' }, ); - return comparisonOperatorFilter(amountFilter, runDurationFilterModel.operatorSelectionModel.current, (value) => - runDurationFilterModel.operatorSelectionModel.select(value), { id: 'run-duration-filter-operator' }); + return comparisonOperatorFilter(amountFilter, durationFilterModel.operatorSelectionModel.current, (value) => + durationFilterModel.operatorSelectionModel.select(value), { id: `${id}-operator` }); }; diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 4b0edeb0b7..277bcb6752 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -25,8 +25,7 @@ import { formatBeamType } from '../../../utilities/formatting/formatBeamType.js' 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'; +import { durationFilter } from '../../../components/Filters/LhcFillsFilter/durationFilter.js'; /** * List of active columns for a lhc fills table @@ -110,7 +109,7 @@ export const lhcFillsActiveColumns = { return '-'; }, - filter: (lhcFillModel) => beamDurationFilter(lhcFillModel.filteringModel.get('beamDuration')), + filter: (lhcFillModel) => durationFilter(lhcFillModel.filteringModel.get('beamDuration'), 'beam-duration-filter'), profiles: { lhcFill: true, environment: true, @@ -142,7 +141,7 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-8', format: (duration) => formatDuration(duration), - filter: (lhcFillModel) => runDurationFilter(lhcFillModel.filteringModel.get('runDuration')), + filter: (lhcFillModel) => durationFilter(lhcFillModel.filteringModel.get('runDuration'), 'run-duration-filter'), }, efficiency: { name: 'Fill Efficiency', diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 126d9fdfca..47e320b01b 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -271,9 +271,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: '.beam-duration-filter', value: 'e.g 16:14:15 (HH:MM:SS)'} + const filterSBDurationPlaceholderExpect = {selector: '#beam-duration-filter-operand', 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)'}; + const filterRunDurationPlaceholderExpect = {selector: '#run-duration-filter-operand', value: 'e.g 16:14:15 (HH:MM:SS)'}; const filterSBDurationOperatorExpect = { value: true }; From 4e685239b5dc3e4187dac41c3f9d9bf0b46fe7c0 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 22 Jan 2026 16:30:00 +0100 Subject: [PATCH 67/76] [O2B-1506] Fixed > and < added tests --- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 2 + test/api/lhcFills.test.js | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 81c92ea358..8faf42824e 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -73,6 +73,8 @@ class GetAllLhcFillsUseCase { // Include 00:00:00 = 0 = null AND everything above 00:00:00 which is more or less than 0. queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator(runDuration.operator, 0); queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator('=', null); + } else if ((runDuration.operator === '>' || runDuration.operator === '<') && Number(runDuration.limit) === 0) { + queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator(runDuration.operator, 0); } else if (Number(runDuration.limit) === 0) { queryBuilder.whereAssociation('statistics', 'runsCoverage').applyOperator(runDuration.operator, null); } else { diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index a9f4ace6b4..8557945f9e 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -453,6 +453,56 @@ module.exports = () => { }); }); + it('should return 200 and an LHCFill array for runs duration filter, = 00:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]==&filter[runDuration][limit]=00:00:00') + .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(5); + + done(); + }); + }); + + it('should return 200 and an LHCFill array for runs duration filter, > 00:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]=>&filter[runDuration][limit]=00:00:00') + .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(6); + + done(); + }); + }); + + it('should return 200 and an LHCFill array for runs duration filter, < 00:00:00', (done) => { + request(server) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]=<&filter[runDuration][limit]=00:00:00') + .expect(200) + .end((err, res) => { + if (err) { + done(err); + return; + } + + expect(res.body.data).to.have.lengthOf(0); + + done(); + }); + }); + it('should return 200 and an LHCFill array for runs duration filter, > 03:00:00', (done) => { request(server) .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[runDuration][operator]=>&filter[runDuration][limit]=03:00:00') From 516bbbc3bb86d8ee43d18565c0293484a667d33d Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 22 Jan 2026 16:48:20 +0100 Subject: [PATCH 68/76] [O2B-1508] fixed test --- test/api/lhcFills.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index 2c43a607f0..20ae6ccb1b 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -539,7 +539,7 @@ module.exports = () => { it('should return 200 and an LHCFill array for beam types filter, multiple correct', (done) => { request(server) - .get('/api/lhcFills?age[offset]=0&page[limit]=15&filter[beamsType]=Pb-Pb,p-p,p-Pb') + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamsType]=Pb-Pb,p-p,p-Pb') .expect(200) .end((err, res) => { if (err) { @@ -547,8 +547,8 @@ module.exports = () => { return; } - expect(res.body.data).to.have.lengthOf(5); - expect(res.body.data[0].fillNumber).to.equal(6); + expect(res.body.data).to.have.lengthOf(4); + expect(res.body.data[0].fillNumber).to.equal(4); done(); }); From c6e68674af97f93c247615790077290f7d83176b Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 22 Jan 2026 16:50:45 +0100 Subject: [PATCH 69/76] [O2B-1508] Linting, missing semicolon --- lib/server/routers/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/server/routers/index.js b/lib/server/routers/index.js index a064f5a246..4c4774a4af 100644 --- a/lib/server/routers/index.js +++ b/lib/server/routers/index.js @@ -15,7 +15,7 @@ const { deepmerge, isPromise } = require('../../utilities'); const attachmentRoute = require('./attachments.router'); const { configurationRouter } = require('./configuration.router.js'); -const { beamsTypesRouter } = require('./beamsTypes.router.js') +const { beamsTypesRouter } = require('./beamsTypes.router.js'); const detectorsRoute = require('./detectors.router'); const { dplProcessRouter } = require('./dplProcess.router.js'); const environmentRoute = require('./environments.router'); From 475c273e3cf8baac5f4d7f00cf36494ca27387ee Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Thu, 22 Jan 2026 17:16:24 +0100 Subject: [PATCH 70/76] [O2B-1508] fixed api test --- test/api/lhcFills.test.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index 20ae6ccb1b..432aa47c7b 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -554,17 +554,19 @@ module.exports = () => { }); }); - it('should return 400 for beam types filter, one wrong', (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?age[offset]=0&page[limit]=15&filter[beamsType]=Pb-Pb,Jasper-Jasper,p-p,p-Pb') - .expect(400) + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamsType]=Pb-Pb,Jasper-Jasper,p-p,p-Pb') + .expect(200) .end((err, res) => { if (err) { done(err); return; } - expect(res.body.errors[0].title).to.equal('Invalid Attribute'); + expect(res.body.data).to.have.lengthOf(4); + expect(res.body.data[0].fillNumber).to.equal(4); done(); }); From e3014041c84825cc446a9a16bcd4a3a0e4374fc6 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:33:27 +0100 Subject: [PATCH 71/76] [O2B-1508] Refactor distinct beam types query in LhcFillRepository Replaces raw SQL query for fetching distinct beam types with a Sequelize-based implementation. --- lib/database/repositories/LhcFillRepository.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/database/repositories/LhcFillRepository.js b/lib/database/repositories/LhcFillRepository.js index c74f9ffdd4..48f61260d9 100644 --- a/lib/database/repositories/LhcFillRepository.js +++ b/lib/database/repositories/LhcFillRepository.js @@ -34,10 +34,6 @@ const getFillNumbersWithStableBeamsEndedInPeriodQuery = (period) => ` AND stable_beams_start IS NOT NULL `; -const getLhcFillDistinctBeamTypesQuery = () => ` - SELECT DISTINCT beam_type FROM bookkeeping.lhc_fills -`; - /** * Sequelize implementation of the RunRepository. */ @@ -54,7 +50,11 @@ class LhcFillRepository extends Repository { * @returns {Promise} */ async getLhcFillDistinctBeamTypes() { - return await sequelize.query(getLhcFillDistinctBeamTypesQuery(), { type: QueryTypes.SELECT, raw: true }); + return await LhcFill.findAll({ + attributes: [[sequelize.fn('DISTINCT', sequelize.col('beam_type')), 'beam_type']], + order: [['beam_type', 'ASC']], + raw: true, + }); } /** From 92c7f4fa1439e62f0337d88893b923858f2a0760 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:47:59 +0100 Subject: [PATCH 72/76] [O2B-1508] Remove support for 'null' beamType filtering Eliminated the ability to filter LHC fills by 'null' beamType values. Updates repository to exclude null beam types in distinct queries. Removed the oneOfOrNull method from QueryBuilder. Simplifies the beam type filtering in GetAllLhcFillsUseCase by removing the special handling for 'null' values. Adjusts related tests to match the new logic. --- .../repositories/LhcFillRepository.js | 7 +++- lib/database/utilities/QueryBuilder.js | 33 ------------------- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 10 ++---- .../lhcFill/GetAllLhcFillsUseCase.test.js | 19 ++--------- 4 files changed, 10 insertions(+), 59 deletions(-) diff --git a/lib/database/repositories/LhcFillRepository.js b/lib/database/repositories/LhcFillRepository.js index 48f61260d9..d4ab4ad314 100644 --- a/lib/database/repositories/LhcFillRepository.js +++ b/lib/database/repositories/LhcFillRepository.js @@ -15,7 +15,7 @@ const { models: { LhcFill }, sequelize } = require('../'); const Repository = require('./Repository'); const { timestampToMysql } = require('../../server/utilities/timestampToMysql.js'); -const { QueryTypes } = require('sequelize'); +const { QueryTypes, Op } = require('sequelize'); /** * Return the SQL query to use to fetch the list of LHC fills numbers for fills with stable beams that ended in the given period @@ -52,6 +52,11 @@ class LhcFillRepository extends Repository { async getLhcFillDistinctBeamTypes() { return await LhcFill.findAll({ attributes: [[sequelize.fn('DISTINCT', sequelize.col('beam_type')), 'beam_type']], + where: { + beamType: { + [Op.not]: null, + }, + }, order: [['beam_type', 'ASC']], raw: true, }); diff --git a/lib/database/utilities/QueryBuilder.js b/lib/database/utilities/QueryBuilder.js index 95d8841559..1f0a22fe0c 100644 --- a/lib/database/utilities/QueryBuilder.js +++ b/lib/database/utilities/QueryBuilder.js @@ -104,39 +104,6 @@ class WhereQueryBuilder { return this._op(operation); } - /** - * Sets an **OR** match filter using the provided values. - * Adds an **OR NULL** filter. - * If the spread returns empty array the filter becomes an **IS NULL** filter (**OR** is not valid anymore) - * - * @param {...any} values The required values. - * @returns {QueryBuilder} The current QueryBuilder instance. - */ - oneOfOrNull(...values) { - let operation; - if (this.notFlag) { - operation = values[0]?.length === 0 ? { - [Op.not]: null, - } : { - [Op.or]: { - [Op.notIn]: values, - [Op.not]: null, - }, - }; - } else { - operation = values[0]?.length === 0 ? { - [Op.is]: null, - } : { - [Op.or]: { - [Op.in]: values, - [Op.is]: null, - }, - }; - } - - return this._op(operation); - } - /** * Set a max range limit using the provided value * diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 2e610ac326..434a473033 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -88,14 +88,8 @@ class GetAllLhcFillsUseCase { // Beams type. if (beamsType) { - let beamTypes = beamsType.split(','); - // Check if 'null' is included in the request - if (beamTypes.find((type) => type.trim() === 'null') !== undefined) { - beamTypes = beamTypes.filter((type) => type.trim() !== 'null'); - queryBuilder.where('beamType').oneOfOrNull(beamTypes); - } else { - queryBuilder.where('beamType').oneOf(beamTypes); - } + const beamTypes = beamsType.split(','); + queryBuilder.where('beamType').oneOf(beamTypes); } } diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index a9b84b83ca..9b03fb887e 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -229,31 +229,16 @@ module.exports = () => { }); }) - it('should only contain specified beam types, OR NULL, {p-p, PROTON-PROTON, Pb-Pb, null}', async () => { + 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: { beamsType: beamTypes.join(',') } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) - expect(lhcFills).to.be.an('array').and.lengthOf(5) - - const nullIndex = beamTypes.findIndex((value) => value ==='null') - beamTypes[nullIndex] = null; + expect(lhcFills).to.be.an('array').and.lengthOf(4) lhcFills.forEach((lhcFill) => { expect(lhcFill.beamType).oneOf(beamTypes) }); }) - - it('should only contain specified beam type, IS NULL, {null}', async () => { - const beamTypes = ['null'] - - getAllLhcFillsDto.query = { filter: { beamsType: beamTypes.join(',') } }; - const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) - - expect(lhcFills).to.be.an('array').and.lengthOf(1) - lhcFills.forEach((lhcFill) => { - expect(lhcFill.beamType).oneOf([null]) - }); - }) }; From 8797eb68c041dab977bed857637ea467c8d9af9d Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:05:56 +0100 Subject: [PATCH 73/76] [O2B-1508] Refactor and relocate beam types API and service logic to be under LHCFills Removed the dedicated beamsTypes controller, router, DTO, and use case. Moved the beam types endpoint under the lhcFills controller and router, and implemented the service logic in lib/server/services/beam/getAllBeamTypes.js. Updated frontend provider and imports to use the new endpoint and naming (beamTypes instead of beamsTypes). --- .../repositories/LhcFillRepository.js | 19 +----- lib/domain/dtos/GetAllBeamsTypesDto.js | 28 --------- lib/domain/dtos/index.js | 2 - .../LhcFillsFilter/BeamsTypeFilterModel.js | 6 +- .../beamTypesProvider.js} | 6 +- .../controllers/beamsTypes.controller.js | 62 ------------------- lib/server/controllers/index.js | 2 - lib/server/controllers/lhcFill.controller.js | 26 ++++++++ lib/server/routers/beamsTypes.router.js | 20 ------ lib/server/routers/index.js | 2 - lib/server/routers/lhcFills.router.js | 5 ++ lib/server/services/beam/getAllBeamTypes.js | 34 ++++++++++ .../beamsType/GetAllBeamsTypesUseCase.js | 34 ---------- lib/usecases/beamsType/index.js | 17 ----- lib/usecases/index.js | 2 - 15 files changed, 72 insertions(+), 193 deletions(-) delete mode 100644 lib/domain/dtos/GetAllBeamsTypesDto.js rename lib/public/services/{beamsTypes/beamsTypesProvider.js => beamTypes/beamTypesProvider.js} (80%) delete mode 100644 lib/server/controllers/beamsTypes.controller.js delete mode 100644 lib/server/routers/beamsTypes.router.js create mode 100644 lib/server/services/beam/getAllBeamTypes.js delete mode 100644 lib/usecases/beamsType/GetAllBeamsTypesUseCase.js delete mode 100644 lib/usecases/beamsType/index.js diff --git a/lib/database/repositories/LhcFillRepository.js b/lib/database/repositories/LhcFillRepository.js index d4ab4ad314..8ed50d16cb 100644 --- a/lib/database/repositories/LhcFillRepository.js +++ b/lib/database/repositories/LhcFillRepository.js @@ -15,7 +15,7 @@ const { models: { LhcFill }, sequelize } = require('../'); const Repository = require('./Repository'); const { timestampToMysql } = require('../../server/utilities/timestampToMysql.js'); -const { QueryTypes, Op } = require('sequelize'); +const { QueryTypes } = require('sequelize'); /** * Return the SQL query to use to fetch the list of LHC fills numbers for fills with stable beams that ended in the given period @@ -45,23 +45,6 @@ class LhcFillRepository extends Repository { super(LhcFill); } - /** - * Return the list of LHC fills distict beam types. - * @returns {Promise} - */ - async getLhcFillDistinctBeamTypes() { - return await LhcFill.findAll({ - attributes: [[sequelize.fn('DISTINCT', sequelize.col('beam_type')), 'beam_type']], - where: { - beamType: { - [Op.not]: null, - }, - }, - order: [['beam_type', 'ASC']], - raw: true, - }); - } - /** * Return the list of LHC fills numbers for fills with stable beams that ended in the given period * diff --git a/lib/domain/dtos/GetAllBeamsTypesDto.js b/lib/domain/dtos/GetAllBeamsTypesDto.js deleted file mode 100644 index c4e78bfec5..0000000000 --- a/lib/domain/dtos/GetAllBeamsTypesDto.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @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 PaginationDto = require('./PaginationDto'); - -const QueryDto = Joi.object({ - page: PaginationDto, - token: Joi.string(), -}); - -const GetAllBeamsTypesDto = Joi.object({ - body: Joi.object({}), - params: Joi.object({}), - query: QueryDto, -}); - -module.exports = GetAllBeamsTypesDto; diff --git a/lib/domain/dtos/index.js b/lib/domain/dtos/index.js index d1bb88492f..01eb348934 100644 --- a/lib/domain/dtos/index.js +++ b/lib/domain/dtos/index.js @@ -18,7 +18,6 @@ const CreateLhcFillDto = require('./CreateLhcFillDto'); const CreateLogDto = require('./CreateLogDto'); const CreateTagDto = require('./CreateTagDto'); const EntityIdDto = require('./EntityIdDto'); -const GetAllBeamsTypesDto = require('./GetAllBeamsTypesDto.js'); const GetAllEnvironmentsDto = require('./GetAllEnvironmentsDto'); const GetAllLhcFillsDto = require('./GetAllLhcFillsDto'); const GetAllLogAttachmentsDto = require('./GetAllLogAttachmentsDto'); @@ -57,7 +56,6 @@ module.exports = { CreateTagDto, EndRunDto, EntityIdDto, - GetAllBeamsTypesDto, GetAllEnvironmentsDto, GetAllLhcFillsDto, GetAllLogAttachmentsDto, diff --git a/lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js index 389328b220..ff0b5a4227 100644 --- a/lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js +++ b/lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ -import { beamsTypesProvider } from '../../../services/beamsTypes/beamsTypesProvider.js'; +import { beamTypesProvider } from '../../../services/beamTypes/beamTypesProvider.js'; import { SelectionModel } from '../../common/selection/SelectionModel.js'; /** @@ -28,8 +28,8 @@ export class BeamsTypeFilterModel extends SelectionModel { multiple: true, allowEmpty: true }); - beamsTypesProvider.items$.observe(() => { - beamsTypesProvider.items$.getCurrent().apply({ + beamTypesProvider.items$.observe(() => { + beamTypesProvider.items$.getCurrent().apply({ Success: (types) => { beamTypes = types.map((type) => ({ value: String(type.beam_type) })); this.setAvailableOptions(beamTypes); diff --git a/lib/public/services/beamsTypes/beamsTypesProvider.js b/lib/public/services/beamTypes/beamTypesProvider.js similarity index 80% rename from lib/public/services/beamsTypes/beamsTypesProvider.js rename to lib/public/services/beamTypes/beamTypesProvider.js index 20a688d0a2..3b268c9597 100644 --- a/lib/public/services/beamsTypes/beamsTypesProvider.js +++ b/lib/public/services/beamTypes/beamTypesProvider.js @@ -17,14 +17,14 @@ import { RemoteDataProvider } from '../RemoteDataProvider.js'; /** * Service class to fetch beams types from the backend */ -export class BeamsTypesProvider extends RemoteDataProvider { +export class BeamTypesProvider extends RemoteDataProvider { /** * @inheritDoc */ async getRemoteData() { - const { data } = await getRemoteData('/api/beamsTypes'); + const { data } = await getRemoteData('/api/lhcFills/beamTypes'); return data; } } -export const beamsTypesProvider = new BeamsTypesProvider(); +export const beamTypesProvider = new BeamTypesProvider(); diff --git a/lib/server/controllers/beamsTypes.controller.js b/lib/server/controllers/beamsTypes.controller.js deleted file mode 100644 index e67b21718c..0000000000 --- a/lib/server/controllers/beamsTypes.controller.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * 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 { - beamsType: { - GetAllBeamsTypesUseCase, - }, -} = require('../../usecases/index.js'); -const { - dtos: { - GetAllBeamsTypesDto, - }, -} = require('../../domain/index.js'); -const { dtoValidator } = require('../utilities/index.js'); -const { ApiConfig } = require('../../config/index.js'); - -/** - * Get all beams 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. - * @returns {undefined} - */ -const listBeamsTypes = async (request, response) => { - const value = await dtoValidator(GetAllBeamsTypesDto, request, response); - if (!value) { - return; - } - - const { count, beamsTypes } = await new GetAllBeamsTypesUseCase() - .execute(value); - - const { query: { page: { limit = ApiConfig.pagination.limit } = {} } } = value; - const totalPages = Math.ceil(count / limit); - - response.status(200).json({ - data: beamsTypes, - meta: { - page: { - pageCount: totalPages, - totalCount: count, - }, - }, - }); -}; - -module.exports = { - listBeamsTypes, -}; diff --git a/lib/server/controllers/index.js b/lib/server/controllers/index.js index 904dadaeb1..48184378b2 100644 --- a/lib/server/controllers/index.js +++ b/lib/server/controllers/index.js @@ -12,7 +12,6 @@ */ const AttachmentsController = require('./attachments.controller'); -const BeamsTypeController = require('./beamsTypes.controller.js'); const ConfigurationController = require('./configuration.controller.js'); const DetectorsController = require('./detectors.controller'); const EnvironmentsController = require('./environments.controller'); @@ -27,7 +26,6 @@ const CtpTriggerCountersController = require('./ctpTriggerCounters.controller'); module.exports = { AttachmentsController, - BeamsTypeController, ConfigurationController, DetectorsController, EnvironmentsController, diff --git a/lib/server/controllers/lhcFill.controller.js b/lib/server/controllers/lhcFill.controller.js index 6624fed3a2..81b453a791 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 listBeamsTypes = async (_request, response, _next) => { + try { + const beamsTypes = await getAllBeamTypes(); + if (beamsTypes && beamsTypes.length > 0) { + response.status(200).json({ data: beamsTypes }); + } 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, + listBeamsTypes, }; diff --git a/lib/server/routers/beamsTypes.router.js b/lib/server/routers/beamsTypes.router.js deleted file mode 100644 index a64aa6b4f8..0000000000 --- a/lib/server/routers/beamsTypes.router.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @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 { BeamsTypeController } = require('../controllers'); - -exports.beamsTypesRouter = { - path: '/beamsTypes', - controller: BeamsTypeController.listBeamsTypes, - method: 'get', -}; diff --git a/lib/server/routers/index.js b/lib/server/routers/index.js index 4c4774a4af..cb83465446 100644 --- a/lib/server/routers/index.js +++ b/lib/server/routers/index.js @@ -15,7 +15,6 @@ const { deepmerge, isPromise } = require('../../utilities'); const attachmentRoute = require('./attachments.router'); const { configurationRouter } = require('./configuration.router.js'); -const { beamsTypesRouter } = require('./beamsTypes.router.js'); const detectorsRoute = require('./detectors.router'); const { dplProcessRouter } = require('./dplProcess.router.js'); const environmentRoute = require('./environments.router'); @@ -41,7 +40,6 @@ const { ctpTriggerCountersRouter } = require('./ctpTriggerCounters.router.js'); const routes = [ attachmentRoute, - beamsTypesRouter, configurationRouter, detectorsRoute, dataPassesRouter, diff --git a/lib/server/routers/lhcFills.router.js b/lib/server/routers/lhcFills.router.js index 7a99cfe85f..f923bd501a 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.listBeamsTypes, + }, { 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/beamsType/GetAllBeamsTypesUseCase.js b/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js deleted file mode 100644 index 405e9b5f14..0000000000 --- a/lib/usecases/beamsType/GetAllBeamsTypesUseCase.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @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 LhcFillRepository = require('../../database/repositories/LhcFillRepository.js'); - -/** - * GetAllBeamsTypesUseCase - */ -class GetAllBeamsTypesUseCase { - /** - * Executes this use case. - * - * @returns {Promise} Promise object represents the result of this use case. - */ - async execute() { - const result = await LhcFillRepository.getLhcFillDistinctBeamTypes(); - return { - count: result.length, - beamsTypes: result, - }; - } -} - -module.exports = GetAllBeamsTypesUseCase; diff --git a/lib/usecases/beamsType/index.js b/lib/usecases/beamsType/index.js deleted file mode 100644 index 5a60a1e65f..0000000000 --- a/lib/usecases/beamsType/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @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 GetAllBeamsTypesUseCase = require('./GetAllBeamsTypesUseCase.js'); - -module.exports = { - GetAllBeamsTypesUseCase, -}; diff --git a/lib/usecases/index.js b/lib/usecases/index.js index 413ffe4791..2dfa7807e5 100644 --- a/lib/usecases/index.js +++ b/lib/usecases/index.js @@ -12,7 +12,6 @@ */ const attachment = require('./attachment'); -const beamsType = require('./beamsType'); const environment = require('./environment'); const flp = require('./flp'); const lhcFill = require('./lhcFill'); @@ -25,7 +24,6 @@ const tag = require('./tag'); module.exports = { attachment, - beamsType, environment, flp, lhcFill, From cf3ebb05346754b3936580028154003511bdcc01 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:57:44 +0100 Subject: [PATCH 74/76] [O2B-1508] Remove redundant change to operator Change was needed for runDuration filter but then this was modified in a way that voids this. --- lib/database/utilities/QueryBuilder.js | 31 ++++++-------------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/lib/database/utilities/QueryBuilder.js b/lib/database/utilities/QueryBuilder.js index 42e7ffb7c7..80f5d5f25b 100644 --- a/lib/database/utilities/QueryBuilder.js +++ b/lib/database/utilities/QueryBuilder.js @@ -414,30 +414,13 @@ class WhereAssociationQueryBuilder extends WhereQueryBuilder { * @returns {QueryBuilder} The current QueryBuilder instance. */ _op(operation) { - // Check if this include association already exists - const existingInclude = this.queryBuilder.options.include?.find((include) => include.association === this.association); - - if (existingInclude && existingInclude.where) { - /* - * Replace existing where operation in the include with OR operation. - * This basically encapsulates the existing where operation in a OR operation together with the new operation. - */ - existingInclude.where = { - [Op.or]: [ - { [this.column]: existingInclude.where[this.column] }, - { [this.column]: operation }, - ], - }; - } else { - // Create new include - this.queryBuilder.include({ - association: this.association, - required: true, - where: { - [this.column]: operation, - }, - }); - } + this.queryBuilder.include({ + association: this.association, + required: true, + where: { + [this.column]: operation, + }, + }); return this.queryBuilder; } From 8ff1398c0a61498dacafe1ba9829872e76877f67 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:28:02 +0100 Subject: [PATCH 75/76] [O2B-1508] Rename beamsType to beamType across codebase --- lib/domain/dtos/filters/LhcFillsFilterDto.js | 2 +- ...{BeamsTypeFilterModel.js => BeamTypeFilterModel.js} | 2 +- .../{beamsTypeFilter.js => beamTypeFilter.js} | 8 ++++---- .../LhcFills/ActiveColumns/lhcFillsActiveColumns.js | 4 ++-- .../views/LhcFills/Overview/LhcFillsOverviewModel.js | 10 +++++----- lib/server/controllers/lhcFill.controller.js | 10 +++++----- lib/server/routers/lhcFills.router.js | 2 +- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 6 +++--- test/api/lhcFills.test.js | 6 +++--- .../lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js | 4 ++-- test/public/lhcFills/overview.test.js | 4 ++-- 11 files changed, 29 insertions(+), 29 deletions(-) rename lib/public/components/Filters/LhcFillsFilter/{BeamsTypeFilterModel.js => BeamTypeFilterModel.js} (96%) rename lib/public/components/Filters/LhcFillsFilter/{beamsTypeFilter.js => beamTypeFilter.js} (77%) diff --git a/lib/domain/dtos/filters/LhcFillsFilterDto.js b/lib/domain/dtos/filters/LhcFillsFilterDto.js index e67be936f3..aa4df8e4b5 100644 --- a/lib/domain/dtos/filters/LhcFillsFilterDto.js +++ b/lib/domain/dtos/filters/LhcFillsFilterDto.js @@ -21,5 +21,5 @@ exports.LhcFillsFilterDto = Joi.object({ }), runDuration: validateTimeDuration, beamDuration: validateTimeDuration, - beamsType: Joi.string(), + beamType: Joi.string(), }); diff --git a/lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js similarity index 96% rename from lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js rename to lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js index ff0b5a4227..baad0cc4f5 100644 --- a/lib/public/components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js +++ b/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js @@ -17,7 +17,7 @@ import { SelectionModel } from '../../common/selection/SelectionModel.js'; /** * Beam type filter model */ -export class BeamsTypeFilterModel extends SelectionModel { +export class BeamTypeFilterModel extends SelectionModel { /** * Constructor */ diff --git a/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js similarity index 77% rename from lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js rename to lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js index 94a5887d42..3e592129ed 100644 --- a/lib/public/components/Filters/LhcFillsFilter/beamsTypeFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js @@ -16,11 +16,11 @@ import { checkboxes } from '../common/filters/checkboxFilter.js'; /** * Renders a list of checkboxes that lets the user look for beam types * - * @param {BeamsTypeFilterModel} beamsTypeFilterModel beamsTypeFilterModel + * @param {BeamTypeFilterModel} beamTypeFilterModel beamTypeFilterModel * @return {Component} the filter */ -export const beamsTypeFilter = (beamsTypeFilterModel) => +export const beamTypeFilter = (beamTypeFilterModel) => checkboxes( - beamsTypeFilterModel, - { selector: 'beams-types' }, + beamTypeFilterModel, + { selector: 'beam-types' }, ); diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index a776928995..83cddcbe6e 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -26,7 +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 { beamsTypeFilter } from '../../../components/Filters/LhcFillsFilter/beamsTypeFilter.js'; +import { beamTypeFilter } from '../../../components/Filters/LhcFillsFilter/beamTypeFilter.js'; /** * List of active columns for a lhc fills table @@ -162,7 +162,7 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-8', format: (value) => formatBeamType(value), - filter: (lhcFillModel) => beamsTypeFilter(lhcFillModel.filteringModel.get('beamsType')), + 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 a5d1159288..389be9ef13 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -17,7 +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 { BeamsTypeFilterModel } from '../../../components/Filters/LhcFillsFilter/BeamsTypeFilterModel.js'; +import { BeamTypeFilterModel } from '../../../components/Filters/LhcFillsFilter/BeamTypeFilterModel.js'; import { TextComparisonFilterModel } from '../../../components/Filters/common/filters/TextComparisonFilterModel.js'; /** @@ -34,14 +34,14 @@ export class LhcFillsOverviewModel extends OverviewPageModel { constructor(stableBeamsOnly = false) { super(); - this._beamsTypes = []; + this._beamTypes = []; this._filteringModel = new FilteringModel({ fillNumbers: new RawTextFilterModel(), beamDuration: new TextComparisonFilterModel(), runDuration: new TextComparisonFilterModel(), hasStableBeams: new StableBeamFilterModel(), - beamsType: new BeamsTypeFilterModel(), + beamType: new BeamTypeFilterModel(), }); this._filteringModel.observe(() => this._applyFilters()); @@ -77,8 +77,8 @@ export class LhcFillsOverviewModel extends OverviewPageModel { /** * Getter */ - getBeamsTypes() { - return this._beamsTypes; + getBeamTypes() { + return this._beamTypes; } /** diff --git a/lib/server/controllers/lhcFill.controller.js b/lib/server/controllers/lhcFill.controller.js index 81b453a791..3637bf190b 100644 --- a/lib/server/controllers/lhcFill.controller.js +++ b/lib/server/controllers/lhcFill.controller.js @@ -268,11 +268,11 @@ const getAllLhcFillsWithStableBeamsEndedInPeriodHandler = async (request, respon * the next middleware function. * @returns {undefined} */ -const listBeamsTypes = async (_request, response, _next) => { +const listBeamTypes = async (_request, response, _next) => { try { - const beamsTypes = await getAllBeamTypes(); - if (beamsTypes && beamsTypes.length > 0) { - response.status(200).json({ data: beamsTypes }); + const beamTypes = await getAllBeamTypes(); + if (beamTypes && beamTypes.length > 0) { + response.status(200).json({ data: beamTypes }); } else { response.status(204).json({ data: [] }); } @@ -290,5 +290,5 @@ module.exports = { getLhcFillById, getAllLhcFillLogsHandler, getAllLhcFillsWithStableBeamsEndedInPeriodHandler, - listBeamsTypes, + listBeamTypes, }; diff --git a/lib/server/routers/lhcFills.router.js b/lib/server/routers/lhcFills.router.js index f923bd501a..2b33cedb8c 100644 --- a/lib/server/routers/lhcFills.router.js +++ b/lib/server/routers/lhcFills.router.js @@ -27,7 +27,7 @@ module.exports = { { method: 'get', path: 'beamTypes', - controller: LhcFillsController.listBeamsTypes, + controller: LhcFillsController.listBeamTypes, }, { path: 'stable-beams-ended-within', diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index 009f3e4111..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, beamsType } = 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); @@ -87,8 +87,8 @@ class GetAllLhcFillsUseCase { } // Beams type. - if (beamsType) { - const beamTypes = beamsType.split(','); + if (beamType) { + const beamTypes = beamType.split(','); queryBuilder.where('beamType').oneOf(beamTypes); } } diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index 432aa47c7b..34b5b4fb2d 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -522,7 +522,7 @@ module.exports = () => { 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[beamsType]=Pb-Pb') + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamType]=Pb-Pb') .expect(200) .end((err, res) => { if (err) { @@ -539,7 +539,7 @@ module.exports = () => { 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[beamsType]=Pb-Pb,p-p,p-Pb') + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamType]=Pb-Pb,p-p,p-Pb') .expect(200) .end((err, res) => { if (err) { @@ -557,7 +557,7 @@ module.exports = () => { // 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[beamsType]=Pb-Pb,Jasper-Jasper,p-p,p-Pb') + .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) { diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index e9e2271431..dd4c09f2d9 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -258,7 +258,7 @@ module.exports = () => { 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: { beamsType: beamTypes.join(',') } }; + getAllLhcFillsDto.query = { filter: { beamType: beamTypes.join(',') } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(4) @@ -270,7 +270,7 @@ module.exports = () => { 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: { beamsType: beamTypes.join(',') } }; + getAllLhcFillsDto.query = { filter: { beamType: beamTypes.join(',') } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(4) diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 3bea924a6d..14f541a956 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -337,8 +337,8 @@ module.exports = () => { }); it('should successfully apply beam types filter', async () => { - const filterBeamTypeP_Pb = '#beams-types-checkbox-p-Pb'; - const filterBeamTypePb_Pb = '#beams-types-checkbox-Pb-Pb'; + 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); From a0a8a985b72252876fa4f914d9e25cf6c753e39d Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:54:40 +0100 Subject: [PATCH 76/76] [O2B-1508] Refactor BeamTypeFilterModel to use SelectionFilterModel Replaces SelectionModel with SelectionFilterModel for improved filter handling. --- .../LhcFillsFilter/BeamTypeFilterModel.js | 33 +++---------------- .../Filters/LhcFillsFilter/beamTypeFilter.js | 2 +- 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js index baad0cc4f5..534e35ab1b 100644 --- a/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js +++ b/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js @@ -12,48 +12,25 @@ */ import { beamTypesProvider } from '../../../services/beamTypes/beamTypesProvider.js'; -import { SelectionModel } from '../../common/selection/SelectionModel.js'; +import { SelectionFilterModel } from '../common/filters/SelectionFilterModel.js'; /** * Beam type filter model */ -export class BeamTypeFilterModel extends SelectionModel { +export class BeamTypeFilterModel extends SelectionFilterModel { /** * Constructor */ constructor() { - let beamTypes = []; - super({ availableOptions: beamTypes, - defaultSelection: [], - multiple: true, - allowEmpty: true }); + super({ multiple: true, allowEmpty: true }); beamTypesProvider.items$.observe(() => { beamTypesProvider.items$.getCurrent().apply({ Success: (types) => { - beamTypes = types.map((type) => ({ value: String(type.beam_type) })); - this.setAvailableOptions(beamTypes); + const beamTypes = types.map((type) => ({ value: String(type.beam_type) })); + this._selectionModel.setAvailableOptions(beamTypes); }, }); }); } - - /** - * Get normalized selected option - */ - get normalized() { - return this.selected.join(','); - } - - /** - * Reset the filter to default values - * - * @return {void} - */ - resetDefaults() { - if (!this.isEmpty) { - this.reset(); - this.notify(); - } - } } diff --git a/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js index 3e592129ed..7872734704 100644 --- a/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js @@ -21,6 +21,6 @@ import { checkboxes } from '../common/filters/checkboxFilter.js'; */ export const beamTypeFilter = (beamTypeFilterModel) => checkboxes( - beamTypeFilterModel, + beamTypeFilterModel.selectionModel, { selector: 'beam-types' }, );