From 057c23588aee890905d701174cb0bed25bf9eb3e Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 26 Nov 2025 10:58:57 +0100 Subject: [PATCH 01/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] 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/41] [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/41] [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/41] [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/41] 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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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/41] [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 6a47048e79c32ccadda00af3556956c93cd80ca9 Mon Sep 17 00:00:00 2001 From: Jasper Houweling Date: Wed, 17 Dec 2025 15:45:36 +0100 Subject: [PATCH 39/41] [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 40/41] [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 41/41] [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');