diff --git a/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js new file mode 100644 index 0000000000..1bc3f8aed2 --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js @@ -0,0 +1,76 @@ +/** + * @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 model + * Holds true or false value + */ +export class StableBeamFilterModel extends SelectionModel { + /** + * Constructor + */ + constructor() { + super({ availableOptions: [{ value: true }, { value: false }], + defaultSelection: [{ value: false }], + multiple: false, + allowEmpty: false }); + } + + /** + * Returns true if the current filter is stable beams only + * + * @return {boolean} true if filter is stable beams only + */ + isStableBeamsOnly() { + return this.current; + } + + /** + * Sets the current filter to stable beams only + * + * @param {boolean} value value to set this stable beams only filter with + * @return {void} + */ + setStableBeamsOnly(value) { + this.select({ value }); + } + + /** + * Get normalized selected option + */ + get normalized() { + 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; + } + + /** + * 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 new file mode 100644 index 0000000000..b4429c002c --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js @@ -0,0 +1,49 @@ +/** + * @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'; +import { switchInput } from '../../common/form/switchInput.js'; +import { radioButton } from '../../common/form/inputs/radioButton.js'; + +/** + * Display a toggle switch or radio buttons to filter stable beams only + * + * @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 = (stableBeamFilterModel, radioButtonMode = false) => { + const name = 'stableBeamsOnlyRadio'; + const labelOff = 'OFF'; + const labelOn = 'ON'; + if (radioButtonMode) { + return h('.form-group-header.flex-row.w-100', [ + radioButton({ + label: labelOff, + isChecked: !stableBeamFilterModel.isStableBeamsOnly(), + action: () => stableBeamFilterModel.setStableBeamsOnly(false), + name: name, + }), + radioButton({ + label: labelOn, + isChecked: stableBeamFilterModel.isStableBeamsOnly(), + action: () => stableBeamFilterModel.setStableBeamsOnly(true), + name: name, + }), + ]); + } else { + return switchInput(stableBeamFilterModel.isStableBeamsOnly(), (newState) => { + stableBeamFilterModel.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 4486b98390..590eb81b78 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 labelAny = 'ANY'; + const labelOff = 'OFF'; + const labelOn = '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: labelAny, + isChecked: state === '', + action: () => runModel.removeDcs(), + name, + }), + radioButton({ + label: labelOff, + isChecked: state === false, + action: () => runModel.setDcsFilterOperation(false), + name, + }), + radioButton({ + label: labelOn, + isChecked: state === true, + action: () => runModel.setDcsFilterOperation(true), + 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..74bf28f4ba 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 labelAny = 'ANY'; + const labelOff = 'OFF'; + const labelOn = '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: labelAny, + isChecked: state === '', + action: () => runModel.removeDdflp(), + name, + }), + radioButton({ + label: labelOff, + isChecked: state === false, + action: () => runModel.setDdflpFilterOperation(false), + name, + }), + radioButton({ + label: labelOn, + isChecked: state === true, + action: () => runModel.setDdflpFilterOperation(true), + 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..5e639d8afb 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 labelAny = 'ANY'; + const labelOff = 'OFF'; + const labelOn = '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: labelAny, + isChecked: state === '', + action: () => runModel.removeEpn(), + name, + }), + radioButton({ + label: labelOff, + isChecked: state === false, + action: () => runModel.setEpnFilterOperation(false), + name, + }), + radioButton({ + label: labelOn, + isChecked: state === true, + action: () => runModel.setEpnFilterOperation(true), + 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..32b4a80cab --- /dev/null +++ b/lib/public/components/common/form/inputs/radioButton.js @@ -0,0 +1,61 @@ +/** + * @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 RadioButtonConfigStyle + * @property {string} labelStyle - value for the label's style property. + * @property {string} radioButtonStyle - value for the radio button's element styling. + */ + +/** + * @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 + * @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 + */ + +/** + * Build a radio button with its configuration and actions + * @param {RadioButtonConfig} configuration - configuration object for radioButton. + * @return {vnode} - radio button with associated label. + */ +export const radioButton = (configuration = {}) => { + const { + label = '', + isChecked = false, + action = () => { }, + name = '', + id = `${name}${label}`, + style = { labelStyle: 'cursor: pointer;', radioButtonStyle: '.w-33' }, + } = configuration; + return h(`${style.radioButtonStyle}.form-check`, [ + h('input.form-check-input', { + onchange: action, + type: 'radio', + id, + name, + value: label, + checked: isChecked, + }), + h('label.form-check-label', { + style: style.labelStyle, + for: id, + }, label), + ]); +}; diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 5efda8b1cb..f575652b34 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 Only', + visible: false, + format: (boolean) => boolean ? 'On' : 'Off', + filter: (lhcFillModel) => toggleStableBeamOnlyFilter(lhcFillModel.filteringModel.get('hasStableBeams'), true), + }, 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 99d95cb271..787d467fe5 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -11,6 +11,9 @@ * 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'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; @@ -28,7 +31,18 @@ export class LhcFillsOverviewModel extends OverviewPageModel { constructor(stableBeamsOnly = false) { super(); - this._stableBeamsOnly = stableBeamsOnly; + this._filteringModel = new FilteringModel({ + 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); + } } /** @@ -45,38 +59,47 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @inheritDoc */ getRootEndpoint() { - return '/api/lhcFills'; + return buildUrl('/api/lhcFills', { filter: this.filteringModel.normalized }); } /** - * @inheritDoc + * 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} */ - async getLoadParameters() { - return { - ...await super.getLoadParameters(), - 'filter[hasStableBeams]': this._stableBeamsOnly, - }; + reset(fetch = true) { + super.reset(); + this.resetFiltering(fetch); } /** - * Sets the stable beams filter - * - * @param {boolean} stableBeamsOnly the new stable beams filter value + * Reset all filtering models + * @param {boolean} fetch Whether to refetch all data after filters have been reset * @return {void} */ - setStableBeamsFilter(stableBeamsOnly) { - this._stableBeamsOnly = stableBeamsOnly; - this._applyFilters(); - this.notify(); + 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(); } /** - * Checks if the stable beams filter is set + * Return the filtering model * - * @return {boolean} true if the stable beams filter is active + * @return {FilteringModel} the filtering model */ - isStableBeamsOnly() { - return this._stableBeamsOnly; + get filteringModel() { + return this._filteringModel; } /** diff --git a/lib/public/views/LhcFills/Overview/index.js b/lib/public/views/LhcFills/Overview/index.js index 32c12b6b4f..cfef2aebad 100644 --- a/lib/public/views/LhcFills/Overview/index.js +++ b/lib/public/views/LhcFills/Overview/index.js @@ -18,25 +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. @@ -63,7 +51,8 @@ const showLhcFillsTable = (lhcFillsOverviewModel) => { return [ h('.flex-row.header-container.g2.pv2', [ frontLink(h('button.btn.btn-primary', 'Statistics'), 'statistics'), - toggleStableBeamOnlyFilter(lhcFillsOverviewModel), + filtersPanelPopover(lhcFillsOverviewModel, lhcFillsActiveColumns), + toggleStableBeamOnlyFilter(lhcFillsOverviewModel.filteringModel.get('hasStableBeams')), ]), h('.w-100.flex-column', [ table(lhcFillsOverviewModel.items, lhcFillsActiveColumns, null, { tableClasses: '.table-sm' }), diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 7ca7935b92..269239f2c2 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'); @@ -163,7 +164,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,7 +265,26 @@ module.exports = () => { await expectInnerText(page, efficiencyExpect.selector, efficiencyExpect.value); }); - it('should successfully toggle to stable beam only', async () => { + it('should successfully display filter elements', async () => { + 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 () => { + const filterButtonSBOnlySelector= '#stableBeamsOnlyRadioOFF'; + await goToPage(page, 'lhc-fill-overview'); + await waitForTableLength(page, 5); + // Open the filtering panel + await openFilteringPanel(page); + await pressElement(page, filterButtonSBOnlySelector); + await waitForTableLength(page, 6); + }); + + 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); 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');