diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index c66a194778..e27c523ab3 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -99,10 +99,10 @@ exports.RunFilterDto = Joi.object({ mcReproducibleAsNotBad: Joi.boolean().optional(), }), - detectorsQc: Joi.object() + detectorsQcNotBadFraction: Joi.object() .pattern( Joi.string().regex(/^_\d+$/), // Detector id with '_' prefix - Joi.object({ notBadFraction: FloatComparisonDto }), + FloatComparisonDto, ) .keys({ mcReproducibleAsNotBad: Joi.boolean().optional(), diff --git a/lib/public/Model.js b/lib/public/Model.js index 6818118c81..0d0ae222f3 100644 --- a/lib/public/Model.js +++ b/lib/public/Model.js @@ -95,21 +95,27 @@ export default class Model extends Observable { this._appConfiguration$ = new Observable(); this._inputDebounceTime = INPUT_DEBOUNCE_TIME; + // Setup router + this.router = new QueryRouter(); + this.router.observe(this.handleLocationChange.bind(this)); + this.router.bubbleTo(this); + registerFrontLinkListener((e) => this.router.handleLinkEvent(e)); + // Models this.home = new HomePageModel(this); this.home.bubbleTo(this); - this.lhcPeriods = new LhcPeriodsModel(this); + this.lhcPeriods = new LhcPeriodsModel(this.router); this.lhcPeriods.bubbleTo(this); - this.dataPasses = new DataPassesModel(this); + this.dataPasses = new DataPassesModel(this.router); this.dataPasses.bubbleTo(this); this.qcFlags = new QcFlagsModel(this); this.qcFlags.bubbleTo(this); - this.simulationPasses = new SimulationPassesModel(this); + this.simulationPasses = new SimulationPassesModel(this.router); this.simulationPasses.bubbleTo(this); this.qcFlagTypes = new QcFlagTypesModel(this); @@ -178,12 +184,6 @@ export default class Model extends Observable { this.errorModel = new ErrorModel(); this.errorModel.bubbleTo(this); - // Setup router - this.router = new QueryRouter(); - this.router.observe(this.handleLocationChange.bind(this)); - this.router.bubbleTo(this); - registerFrontLinkListener((e) => this.router.handleLinkEvent(e)); - // Init pages this.handleLocationChange(); this.window.addEventListener('resize', debounce(() => this.notify(), 100)); diff --git a/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js index 18be7af40d..fc0964da04 100644 --- a/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js +++ b/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js @@ -12,12 +12,12 @@ */ import { beamTypesProvider } from '../../../services/beamTypes/beamTypesProvider.js'; -import { SelectionFilterModel } from '../common/filters/SelectionFilterModel.js'; +import { SelectionModel } from '../../common/selection/SelectionModel.js'; /** * Beam type filter model */ -export class BeamTypeFilterModel extends SelectionFilterModel { +export class BeamTypeFilterModel extends SelectionModel { /** * Constructor */ @@ -28,7 +28,7 @@ export class BeamTypeFilterModel extends SelectionFilterModel { beamTypesProvider.items$.getCurrent().apply({ Success: (types) => { const beamTypes = types.map((type) => ({ value: type.beam_type })); - this._selectionModel.setAvailableOptions(beamTypes); + this.setAvailableOptions(beamTypes); }, }); }); diff --git a/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js deleted file mode 100644 index 1bc3f8aed2..0000000000 --- a/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE Trg. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-Trg.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { 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/beamTypeFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js index 7872734704..83f1487922 100644 --- a/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js @@ -19,8 +19,4 @@ import { checkboxes } from '../common/filters/checkboxFilter.js'; * @param {BeamTypeFilterModel} beamTypeFilterModel beamTypeFilterModel * @return {Component} the filter */ -export const beamTypeFilter = (beamTypeFilterModel) => - checkboxes( - beamTypeFilterModel.selectionModel, - { selector: 'beam-types' }, - ); +export const beamTypeFilter = (beamTypeFilterModel) => checkboxes(beamTypeFilterModel, { selector: 'beam-types' }); diff --git a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js deleted file mode 100644 index b4429c002c..0000000000 --- a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE Trg. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-Trg.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { 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/BeamModeFilterModel.js b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js index 0704fc684d..626644ae88 100644 --- a/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js @@ -12,50 +12,17 @@ */ import { ObservableBasedSelectionDropdownModel } from '../../detector/ObservableBasedSelectionDropdownModel.js'; -import { FilterModel } from '../common/FilterModel.js'; /** * Beam mode filter model */ -export class BeamModeFilterModel extends FilterModel { +export class BeamModeFilterModel extends ObservableBasedSelectionDropdownModel { /** * Constructor * * @param {ObservableData>} beamModes$ observable remote data of objects representing beam modes */ constructor(beamModes$) { - super(); - this._selectionDropdownModel = new ObservableBasedSelectionDropdownModel(beamModes$, ({ name }) => ({ value: name })); - this._addSubmodel(this._selectionDropdownModel); - } - - /** - * @inheritDoc - */ - reset() { - this._selectionDropdownModel.reset(); - } - - /** - * @inheritDoc - */ - get isEmpty() { - return this._selectionDropdownModel.isEmpty; - } - - /** - * Return the underlying dropdown model - * - * @return {ObservableDropDownModel} the underlying dropdown model - */ - get selectionDropdownModel() { - return this._selectionDropdownModel; - } - - /** - * @inheritDoc - */ - get normalized() { - return this._selectionDropdownModel.selected; + super(beamModes$, ({ name }) => ({ value: name })); } } diff --git a/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js b/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js index 432ecc58df..4ae402b9d9 100644 --- a/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js @@ -67,6 +67,14 @@ export class DetectorsFilterModel extends FilterModel { return normalized; } + /** + * @inheritDoc + */ + set normalized({ operator, values }) { + this._combinationOperatorModel.normalized = operator; + this._dropdownModel.normalized = values; + } + /** * Return true if the current combination operator is none * diff --git a/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js b/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js index f57c810cce..b3b1e649bf 100644 --- a/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js @@ -66,6 +66,15 @@ export class EorReasonFilterModel extends FilterModel { return ret; } + /** + * @inheritDoc + */ + set normalized({ category, title, description }) { + this._category = category; + this._title = title; + this._description = description; + } + /** * Returns the EOR reason filter category * diff --git a/lib/public/components/Filters/RunsFilter/GaqFilterModel.js b/lib/public/components/Filters/RunsFilter/GaqFilterModel.js new file mode 100644 index 0000000000..6669a22ee1 --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/GaqFilterModel.js @@ -0,0 +1,97 @@ +/** + * @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 { FilterModel } from '../common/FilterModel.js'; +import { NumericalComparisonFilterModel } from '../common/filters/NumericalComparisonFilterModel.js'; + +/** + * FilterModel that filters by the fraction of gaq that was not bad + */ +export class GaqFilterModel extends FilterModel { + /** + * Constructor + * @param {ToggleFilterModel} mcReproducibleAsNotBad model that determines if a 'not bad' status was reproduceable for a Monte Carlo. + * This param is required as multiple other filters models need to make use of the same ToggleFilterModel instance + */ + constructor(mcReproducibleAsNotBad) { + super(); + + this._notBadFraction = new NumericalComparisonFilterModel({ scale: 0.01, integer: false }); + this._addSubmodel(this._notBadFraction); + this._mcReproducibleAsNotBad = mcReproducibleAsNotBad; + + /** + * _mcReproducableAsNotBad will only be added to the normalize call notBadFraction is not empty + * So, notifying when it is empty will just send an unneeded request. + */ + this._mcReproducibleAsNotBad.visualChange$.bubbleTo(this._visualChange$); + this._mcReproducibleAsNotBad.observe(() => { + if (!this.notBadFraction.isEmpty) { + this.notify(); + } + }); + } + + /** + * @inheritDoc + */ + reset() { + this._notBadFraction.reset(); + } + + /** + * @inheritDoc + */ + get isEmpty() { + return this._notBadFraction.isEmpty; + } + + /** + * @inheritDoc + */ + get normalized() { + const normalized = { notBadFraction: this._notBadFraction.normalized }; + + if (!this.isEmpty) { + normalized.mcReproducibleAsNotBad = this._mcReproducibleAsNotBad.isToggled(); + } + + return normalized; + } + + /** + * @inheritDoc + */ + set normalized({ notBadFraction, mcReproducibleAsNotBad }) { + this._notBadFraction.normalized = notBadFraction; + this._mcReproducibleAsNotBad.normalized = mcReproducibleAsNotBad; + } + + /** + * Return the underlying notBadFraction model + * + * @return {NumericalComparisonFilterModel} the filter model + */ + get notBadFraction() { + return this._notBadFraction; + } + + /** + * Return the underlying mcReproducibleAsNotBad model + * + * @return {ToggleFilterModel} the filter model + */ + get mcReproducibleAsNotBad() { + return this._mcReproducibleAsNotBad; + } +} diff --git a/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js b/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js index 015f991286..5e2c16b206 100644 --- a/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js +++ b/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js @@ -11,21 +11,32 @@ * or submit itself to any jurisdiction. */ -import { FilterModel } from '../common/FilterModel.js'; import { ObservableBasedSelectionDropdownModel } from '../../detector/ObservableBasedSelectionDropdownModel.js'; /** * Return the option value corresponding to a given magnets current level * * @param {MagnetsCurrentLevels} currentLevels the current levels - * @return {string} the option's value + * @return {object} the option's value */ -const magnetsCurrentLevelsToOptionValue = ({ l3, dipole }) => `${l3}kA/${dipole}kA`; +const magnetsCurrentLevelsToKey = ({ l3, dipole }) => ({ value: `${l3}kA/${dipole}kA` }); + +/** + * Return the magnets current lever based on a key string + * + * @param {object} option string containing the current levels + * @param {string} option.value string containing the current levels + * @return {MagnetsCurrentLevels} + */ +const keyToMagnetsCurrentLevels = (value) => { + const [l3, dipole] = value.split('/').map((str) => parseFloat(str.slice(0, -2))); + return { l3, dipole }; +}; /** * AliceL3AndDipoleFilteringModel */ -export class MagnetsFilteringModel extends FilterModel { +export class MagnetsFilteringModel extends ObservableBasedSelectionDropdownModel { /** * Constructor * @@ -33,64 +44,24 @@ export class MagnetsFilteringModel extends FilterModel { * levels */ constructor(magnetsCurrentLevels$) { - super(); - this._selectionDropdownModel = new ObservableBasedSelectionDropdownModel( - magnetsCurrentLevels$, - (magnetsCurrentLevels) => ({ value: magnetsCurrentLevelsToOptionValue(magnetsCurrentLevels) }), - { multiple: false }, - ); - this._addSubmodel(this._selectionDropdownModel); - - this._valueToFilteringParamsMap = new Map(); - magnetsCurrentLevels$.observe(() => { - magnetsCurrentLevels$.getCurrent().match({ - - /** - * Fill map indexing current level by their corresponding value - * - * @param {MagnetsCurrentLevels[]} currentLevels the current levels to map - * @return {void} - */ - Success: (currentLevels) => { - this._valueToFilteringParamsMap = new Map(currentLevels.map(({ l3, dipole }) => [ - magnetsCurrentLevelsToOptionValue({ l3, dipole }), - { l3, dipole }, - ])); - }, - Other: () => { - this._valueToFilteringParamsMap = new Map(); - }, - }); - }); - } - - /** - * @inheritDoc - */ - reset() { - this._selectionDropdownModel.reset(); - } - - /** - * @inheritDoc - */ - get isEmpty() { - return this._selectionDropdownModel.isEmpty; + super(magnetsCurrentLevels$, magnetsCurrentLevelsToKey, { multiple: false }); } /** * @inheritDoc */ get normalized() { - return this._valueToFilteringParamsMap.get(this._selectionDropdownModel.selected[0]) ?? null; + const [selectedOption] = this.selected; + return keyToMagnetsCurrentLevels(selectedOption); } /** - * Return the underlying selection dropdown model + * Sets selected options based on a comma-seperated string. + * Accounts for the options being either RemoteData or an array. * - * @return {SelectionDropdownModel} the dropdown model + * @return {string} */ - get selectionDropdownModel() { - return this._selectionDropdownModel; + set normalized(value) { + super.normalized = magnetsCurrentLevelsToKey(value).value; } } diff --git a/lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js b/lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js new file mode 100644 index 0000000000..6dadd9f363 --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js @@ -0,0 +1,104 @@ +/** + * @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 { FilterModel } from '../common/FilterModel.js'; + +/** + * FilterModel that allows devs to create custom filters from multiple other filters during instantiation, or using putFilter + */ +export class MultiCompositionFilterModel extends FilterModel { + /** + * Constructor + * @param {Object} filters the filters that will make up the composite filter + */ + constructor(filters = {}) { + super(); + + /** + * @type {Object} + */ + this._filters = {}; + this._normalizeBacklog = {}; + + Object.entries(filters).forEach(([key, filter]) => this.putFilter(key, filter)); + } + + /** + * Return a subfilter by key + * + * @param {string} key the key of the subfilter + * @return {FilterModel} the subfilter + */ + putFilter(key, filterModel) { + if (key in this._filters) { + return; + } + + this._filters[key] = filterModel; + this._addSubmodel(filterModel); + } + + /** + * Add new subfilter + * + * @param {string} key key of the subfilter + * @param {FilterModel} filter the the subfilter + */ + getFilter(key) { + if (!(key in this._filters)) { + throw new Error(`No filter found with key ${key}`); + } + + return this._filters[key]; + } + + /** + * @inheritDoc + */ + reset() { + Object.values(this._filters).forEach((filter) => filter.reset()); + } + + /** + * @inheritDoc + */ + get isEmpty() { + return Object.values(this._filters).every((filter) => filter.isEmpty); + } + + /** + * @inheritDoc + */ + get normalized() { + const normalized = {}; + + for (const [id, detector] of Object.entries(this._filters)) { + if (!detector.isEmpty) { + normalized[id] = detector.normalized; + } + } + + return normalized; + } + + /** + * @inheritDoc + */ + set normalized(filters) { + for (const [key, value] of Object.entries(filters)) { + if (key in this._filters) { + this._filters[key].normalized = value; + } + } + } +} diff --git a/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js b/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js index 8fb9347735..b0011177bf 100644 --- a/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js @@ -1,10 +1,10 @@ import { RUN_DEFINITIONS, RunDefinition } from '../../../domain/enums/RunDefinition.js'; -import { SelectionFilterModel } from '../common/filters/SelectionFilterModel.js'; +import { SelectionModel } from '../../common/selection/SelectionModel.js'; /** * Run definition filter model */ -export class RunDefinitionFilterModel extends SelectionFilterModel { +export class RunDefinitionFilterModel extends SelectionModel { /** * Constructor */ @@ -18,7 +18,7 @@ export class RunDefinitionFilterModel extends SelectionFilterModel { * @return {boolean} true if filter is physics only */ isPhysicsOnly() { - const selectedOptions = this._selectionModel.selected; + const selectedOptions = this.selected; return selectedOptions.length === 1 && selectedOptions[0] === RunDefinition.Physics; } @@ -29,8 +29,8 @@ export class RunDefinitionFilterModel extends SelectionFilterModel { */ setPhysicsOnly() { if (!this.isPhysicsOnly()) { - this._selectionModel.selectedOptions = []; - this._selectionModel.select(RunDefinition.Physics); + this.selectedOptions = []; + this.select(RunDefinition.Physics); this.notify(); } } diff --git a/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js b/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js index e765137afa..296e4f4753 100644 --- a/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js +++ b/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js @@ -45,6 +45,13 @@ export class TimeRangeFilterModel extends FilterModel { return normalized; } + /** + * @inheritDoc + */ + set normalized({ from, to }) { + this._timeRangeInputModel.normalized = { from, to }; + } + /** * Return the underlying time range input model * diff --git a/lib/public/components/Filters/RunsFilter/dcs.js b/lib/public/components/Filters/RunsFilter/dcs.js deleted file mode 100644 index 590eb81b78..0000000000 --- a/lib/public/components/Filters/RunsFilter/dcs.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE Trg. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-Trg.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { radioButton } from '../../common/form/inputs/radioButton.js'; -import { h } from '/js/src/index.js'; - -/** - * Filter panel for DCS toggle; ON/OFF/ANY - * @param {RunsOverviewModel} runModel the run model object - * @return {vnode} Three radio buttons inline - */ -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({ - 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, - }), - ]); -}; - -export default dcsOperationRadioButtons; diff --git a/lib/public/components/Filters/RunsFilter/ddflp.js b/lib/public/components/Filters/RunsFilter/ddflp.js deleted file mode 100644 index 74bf28f4ba..0000000000 --- a/lib/public/components/Filters/RunsFilter/ddflp.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE Trg. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-Trg.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { radioButton } from '../../common/form/inputs/radioButton.js'; -import { h } from '/js/src/index.js'; - -/** - * Filter panel for Data Distribution toggle; ON/OFF/ANY - * @param {RunsOverviewModel} runModel the run model object - * @return {vnode} Three radio buttons inline - */ -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({ - 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, - }), - ]); -}; - -export default ddflpOperationRadioButtons; diff --git a/lib/public/components/Filters/RunsFilter/epn.js b/lib/public/components/Filters/RunsFilter/epn.js deleted file mode 100644 index 5e639d8afb..0000000000 --- a/lib/public/components/Filters/RunsFilter/epn.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE Trg. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-Trg.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { radioButton } from '../../common/form/inputs/radioButton.js'; -import { h } from '/js/src/index.js'; - -/** - * Filter panel for EPN toggle; ON/OFF/ANY - * @param {RunsOverviewModel} runModel the run model object - * @return {vnode} Three radio buttons inline - */ -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({ - 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, - }), - ]); -}; - -export default epnOperationRadioButtons; diff --git a/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js b/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js index d53ba62428..2a799ff675 100644 --- a/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js +++ b/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js @@ -19,7 +19,4 @@ import { checkboxes } from '../common/filters/checkboxFilter.js'; * @param {RunDefinitionFilterModel} runDefinitionFilterModel run definition filter model * @return {Component} the filter */ -export const runDefinitionFilter = (runDefinitionFilterModel) => checkboxes( - runDefinitionFilterModel.selectionModel, - { selector: 'run-definition' }, -); +export const runDefinitionFilter = (runDefinitionFilterModel) => checkboxes(runDefinitionFilterModel, { selector: 'run-definition' }); diff --git a/lib/public/components/Filters/RunsFilter/triggerValueFilter.js b/lib/public/components/Filters/RunsFilter/triggerValueFilter.js deleted file mode 100644 index 5addab02fe..0000000000 --- a/lib/public/components/Filters/RunsFilter/triggerValueFilter.js +++ /dev/null @@ -1,21 +0,0 @@ -import { checkboxFilter } from '../common/filters/checkboxFilter.js'; -import { TRIGGER_VALUES } from '../../../domain/enums/TriggerValue.js'; - -/** - * Returns a panel to be used by user to filter runs by trigger value - * @param {RunsOverviewModel} runModel The global model object - * @return {vnode} Multiple checkboxes for a user to select the values to be filtered. - */ -export const triggerValueFilter = (runModel) => checkboxFilter( - 'triggerValue', - TRIGGER_VALUES, - (value) => runModel.triggerValuesFilters.has(value), - (e, value) => { - if (e.target.checked) { - runModel.triggerValuesFilters.add(value); - } else { - runModel.triggerValuesFilters.delete(value); - } - runModel.triggerValuesFilters = Array.from(runModel.triggerValuesFilters); - }, -); diff --git a/lib/public/components/Filters/common/FilterModel.js b/lib/public/components/Filters/common/FilterModel.js index cc7badb53c..2aae7b1a10 100644 --- a/lib/public/components/Filters/common/FilterModel.js +++ b/lib/public/components/Filters/common/FilterModel.js @@ -57,6 +57,17 @@ export class FilterModel extends Observable { throw new Error('Abstract function call'); } + /** + * Sets filters from normalised values to submodels in needed. + * + * @param {string|number|object|string[]|number[]|null} _value The value used to set filters + * @return {void} the normalized value + * @abstract + */ + set normalized(_value) { + throw new Error('Abstract function call'); + } + /** * Returns the observable notified any time there is a visual change which has no impact on the actual filter value * diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index e937786456..19e59f7b65 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -12,7 +12,9 @@ */ import { expandQueryLikeNestedKey } from '../../../utilities/expandNestedKey.js'; -import { Observable } from '/js/src/index.js'; +import { SelectionModel } from '../../common/selection/SelectionModel.js'; +import { FilterModel } from './FilterModel.js'; +import { buildUrl, Observable } from '/js/src/index.js'; /** * Model representing a filtering system, including filter inputs visibility, filters values and so on @@ -21,28 +23,39 @@ export class FilteringModel extends Observable { /** * Constructor * + * @param {QueryRouter} router router that controls the application's page navigation * @param {Object} filters the filters with their label and model */ - constructor(filters) { + constructor(router, filters) { super(); - this._visualChange$ = new Observable(); + this._pageIdentifier = null; - this._filters = filters; - this._filterModels = Object.values(filters); - for (const model of this._filterModels) { - model.bubbleTo(this); - model.visualChange$?.bubbleTo(this._visualChange$); - } + this._router = router; + this._filters = {}; + this._filterModels = []; + Object.entries(filters).forEach(([key, model]) => this.put(key, model)); + } + + /** + * Sets the page identifiers + * + * @param {string} identifier a string identifies a page from the router params. + * Used to prevent unneeded reads/writes from/to the url + * @returns {void} + */ + set pageIdentifier(identifier) { + this._pageIdentifier = identifier; } /** * Reset the filters * * @param {boolean} [notify=false] if true the model notifies its observers + * @param {boolean} [clearUrl=false] if true filters will be removed from the url * @return {void} */ - reset(notify = false) { + reset(notify = false, clearUrl = false) { for (const model of this._filterModels) { model.reset(); } @@ -50,6 +63,12 @@ export class FilteringModel extends Observable { if (notify) { this.notify(); } + + if (clearUrl) { + const { params } = this._router; + delete params.filter; + this._router.go(buildUrl('?', params), false, true); + } } /** @@ -105,6 +124,46 @@ export class FilteringModel extends Observable { return this._filters[key]; } + /** + * When the user updates the displayed Objects, the filters should be placed in the URL as well + * @returns {undefined} + */ + setFilterToURL() { + const { params } = this._router; + const newParams = { ...params }; + newParams.filter = this.normalized; + + if (this._pageIdentifier === params.page) { + this._router.go(buildUrl('?', newParams), false, true); + } + + this.notify(); + } + + /** + * Look for parameters used for filtering in URL and apply them in the layout if it exists + * @returns {undefined} + */ + async setFilterFromURL() { + const { params: { page = '', filter = {} } } = this._router; + + if (!(this._pageIdentifier === page)) { + return; + } + + for (const [key, value] of Object.entries(filter)) { + const filterModel = this._filters[key]; + + if (!filterModel) { + continue; + } + + filterModel.normalized = value; + } + + this.notify(); + } + /** * Add new filter * @@ -118,9 +177,13 @@ export class FilteringModel extends Observable { return; } + if (!(filter instanceof FilterModel || filter instanceof SelectionModel)) { + throw new Error('Filter must extend FilterModel or SelectionModel'); + } + this._filters[key] = filter; this._filterModels.push(filter); - filter.bubbleTo(this); + filter.observe(() => this.setFilterToURL()); filter.visualChange$?.bubbleTo(this._visualChange$); } } diff --git a/lib/public/components/Filters/common/RadioButtonFilterModel.js b/lib/public/components/Filters/common/RadioButtonFilterModel.js new file mode 100644 index 0000000000..5e93205bfc --- /dev/null +++ b/lib/public/components/Filters/common/RadioButtonFilterModel.js @@ -0,0 +1,34 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { SelectionModel } from '../../common/selection/SelectionModel.js'; + +/** + * Model for managing a radiobutton view and state + */ +export class RadioButtonFilterModel extends SelectionModel { + /** + * Constructor + * + * @param {SelectionOption[]} [availableOptions] the list of possible operators + * @param {function} [setDefault] function that selects the default from the list of available options. Selects first entry by default + */ + constructor(availableOptions, setDefault = (options) => [options[0]]) { + super({ + availableOptions, + defaultSelection: setDefault(availableOptions), + multiple: false, + allowEmpty: false, + }); + } +} diff --git a/lib/public/components/Filters/common/TagFilterModel.js b/lib/public/components/Filters/common/TagFilterModel.js index c3ce81e09f..489dc05709 100644 --- a/lib/public/components/Filters/common/TagFilterModel.js +++ b/lib/public/components/Filters/common/TagFilterModel.js @@ -58,11 +58,19 @@ export class TagFilterModel extends FilterModel { */ get normalized() { return { - values: this.selected.join(), + values: this._selectionModel.normalized, operation: this.combinationOperator, }; } + /** + * @inheritDoc + */ + set normalized({ values, operation }) { + this._selectionModel.normalized = values; + this._combinationOperatorModel.normalized = operation; + } + /** * Return the model handling tag selection state * diff --git a/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js b/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js index ee00126389..79452f2a29 100644 --- a/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js +++ b/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js @@ -27,6 +27,7 @@ export class NumericalComparisonFilterModel extends FilterModel { constructor(options) { super(); const { scale = 1, integer = false } = options || {}; + this._scale = scale; this._operatorSelectionModel = new ComparisonSelectionModel(); this._operatorSelectionModel.visualChange$.bubbleTo(this._visualChange$); @@ -82,11 +83,19 @@ export class NumericalComparisonFilterModel extends FilterModel { */ get normalized() { return { - operator: this._operatorSelectionModel.current, - limit: this._operandInputModel.value, + operator: this._operatorSelectionModel.normalized, + limit: this._operandInputModel.normalized, }; } + /** + * @inheritDoc + */ + set normalized({ operator, limit }) { + this._operatorSelectionModel.normalized = operator; + this._operandInputModel.normalized = parseFloat(limit) / this._scale; + } + /** * @inheritDoc */ diff --git a/lib/public/components/Filters/common/filters/ProcessedTextInputModel.js b/lib/public/components/Filters/common/filters/ProcessedTextInputModel.js index 9e46fe95b5..effa03a166 100644 --- a/lib/public/components/Filters/common/filters/ProcessedTextInputModel.js +++ b/lib/public/components/Filters/common/filters/ProcessedTextInputModel.js @@ -98,6 +98,26 @@ export class ProcessedTextInputModel extends Observable { this._value = null; } + /** + * Returns the normalized value of the filter, that can be used as URL parameter + * @returns {string} + */ + get normalized() { + return this._value; + } + + /** + * Sets filters from normalised values. + * + * @param {string} value The value used to set filters + * @return {void} + * @abstract + */ + set normalized(value) { + this._value = value; + this._raw = value; + } + /** * Return the visual change observable * diff --git a/lib/public/components/Filters/common/filters/RawTextFilterModel.js b/lib/public/components/Filters/common/filters/RawTextFilterModel.js index f996b7b976..d156c86e10 100644 --- a/lib/public/components/Filters/common/filters/RawTextFilterModel.js +++ b/lib/public/components/Filters/common/filters/RawTextFilterModel.js @@ -35,6 +35,13 @@ export class RawTextFilterModel extends FilterModel { return this._value; } + /** + * @inheritDoc + */ + set normalized(value) { + this._value = value; + } + /** * Return the filter current value * diff --git a/lib/public/components/Filters/common/filters/SelectionFilterModel.js b/lib/public/components/Filters/common/filters/SelectionFilterModel.js deleted file mode 100644 index 4bb602d7aa..0000000000 --- a/lib/public/components/Filters/common/filters/SelectionFilterModel.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { FilterModel } from '../FilterModel.js'; -import { SelectionModel } from '../../../common/selection/SelectionModel.js'; - -/** - * Filter model based on a selection model - */ -export class SelectionFilterModel extends FilterModel { - /** - * Constructor - * - * @param {object} [configuration] the selection filter configuration - * @param {SelectionOption[]} [configuration.availableOptions=[]] the list of available options - */ - constructor(configuration) { - super(); - - this._selectionModel = new SelectionModel({ availableOptions: configuration.availableOptions }); - this._selectionModel.bubbleTo(this); - } - - /** - * @inheritDoc - */ - reset() { - this._selectionModel.reset(); - } - - /** - * @inheritDoc - */ - get isEmpty() { - return this._selectionModel.isEmpty; - } - - /** - * @inheritDoc - */ - get normalized() { - return this._selectionModel.selected.join(','); - } - - /** - * Return the underlying selection model - * - * @return {SelectionModel} the underlying selection model - */ - get selectionModel() { - return this._selectionModel; - } -} diff --git a/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js b/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js index b6510f8fae..7f843d6295 100644 --- a/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js +++ b/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js @@ -64,11 +64,19 @@ export class TextComparisonFilterModel extends FilterModel { */ get normalized() { return { - operator: this._operatorSelectionModel.current, - limit: this._operandInputModel.value, + operator: this._operatorSelectionModel.normalized, + limit: this._operandInputModel.normalized, }; } + /** + * @inheritDoc + */ + set normalized({ operator, limit }) { + this._operatorSelectionModel.normalized = operator; + this._operandInputModel.normalized = limit; + } + /** * @inheritDoc */ diff --git a/lib/public/components/Filters/common/filters/TextTokensFilterModel.js b/lib/public/components/Filters/common/filters/TextTokensFilterModel.js index 60e192febe..8c838e5abf 100644 --- a/lib/public/components/Filters/common/filters/TextTokensFilterModel.js +++ b/lib/public/components/Filters/common/filters/TextTokensFilterModel.js @@ -78,6 +78,13 @@ export class TextTokensFilterModel extends FilterModel { .filter((token) => token.length > 0); } + /** + * @inheritDoc + */ + set normalized(value) { + this._raw = value.join(TOKENS_DELIMITER); + } + /** * Returns the observable notified any time there is a visual change which has no impact on the actual filter value * @return {Observable} the observable diff --git a/lib/public/components/Filters/common/filters/TimeRangeInputModel.js b/lib/public/components/Filters/common/filters/TimeRangeInputModel.js index 54ee3fe7b0..66a4481847 100644 --- a/lib/public/components/Filters/common/filters/TimeRangeInputModel.js +++ b/lib/public/components/Filters/common/filters/TimeRangeInputModel.js @@ -142,6 +142,14 @@ export class TimeRangeInputModel extends FilterModel { }; } + /** + * @inheritDoc + */ + set normalized({ from, to }) { + this._fromTimeInputModel.setValue(parseInt(from, 10), true); + this._toTimeInputModel.setValue(parseInt(to, 10), true); + } + /** * States if the filter value is valid * diff --git a/lib/public/components/Filters/common/filters/ToggleFilterModel.js b/lib/public/components/Filters/common/filters/ToggleFilterModel.js new file mode 100644 index 0000000000..b5a98fd4a4 --- /dev/null +++ b/lib/public/components/Filters/common/filters/ToggleFilterModel.js @@ -0,0 +1,63 @@ +/** + * @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 { SelectionModel } from '../../../common/selection/SelectionModel.js'; + +/** + * SelectionModel that restricts the selection to a boolean toggle (true/false). + */ +export class ToggleFilterModel extends SelectionModel { + /** + * Constructor + * @param {boolean} toggledByDefault If the filter should be toggled by default + * @param {boolean} falseIsEmpty if true, will treat false as empty. + */ + constructor(toggledByDefault = false, falseIsEmpty = false) { + super({ + availableOptions: [{ value: true }, { value: false }], + defaultSelection: [{ value: toggledByDefault }], + multiple: false, + allowEmpty: false, + }); + + this._falseIsEmpty = falseIsEmpty; + } + + /** + * Returns true if the current value is set to true + * + * @return {boolean} true if filter is stable beams only + */ + isToggled() { + return this.current; + } + + /** + * Toggles the filter state + * + * @return {void} + */ + toggle() { + this.select({ value: !this.current }); + } + + /** + * @inheritdoc + */ + get isEmpty() { + if (!this._falseIsEmpty) { + return this.current === false; + } + + return false; + } +} diff --git a/lib/public/components/Filters/common/filters/checkboxFilter.js b/lib/public/components/Filters/common/filters/checkboxFilter.js index dcfcb4a95b..2cf550c091 100644 --- a/lib/public/components/Filters/common/filters/checkboxFilter.js +++ b/lib/public/components/Filters/common/filters/checkboxFilter.js @@ -14,32 +14,6 @@ import { h } from '/js/src/index.js'; -/** - * A general component for generating checkboxes. - * - * @param {string} name The general name of the element. - * @param {Array} values the list of options to display - * @param {function} isChecked true if the checkbox is checked, else false - * @param {function} onChange the handler called once the checkbox state changes (change event is passed as first parameter, value as second) - * @param {Object} [additionalProperties] Additional options that can be given to the class. - * @returns {vnode} An object that has one or multiple checkboxes. - * @deprecated use checkboxes - */ -export const checkboxFilter = (name, values, isChecked, onChange, additionalProperties) => - h('.flex-row.flex-wrap', values.map((value) => h('.form-check.flex-grow', [ - h('input.form-check-input', { - id: `${name}Checkbox${value}`, - class: name, - type: 'checkbox', - checked: isChecked(value), - onchange: (e) => onChange(e, value), - ...additionalProperties || {}, - }), - h('label.form-check-label', { - for: `${name}Checkbox${value}`, - }, value.toUpperCase()), - ]))); - /** * Display a filter composed of checkbox listing pre-defined options * @param {SelectionModel} selectionModel filter model diff --git a/lib/public/components/Filters/common/filters/radioButtonFilter.js b/lib/public/components/Filters/common/filters/radioButtonFilter.js new file mode 100644 index 0000000000..1b1d91d7ed --- /dev/null +++ b/lib/public/components/Filters/common/filters/radioButtonFilter.js @@ -0,0 +1,38 @@ +/** + * @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 { radioButton } from '../../../common/form/inputs/radioButton.js'; +import { h } from '/js/src/index.js'; + +/** + * Radiobutton filter component + * + * @param {RadioSelectionModel} selectionModel the a selectionmodel + * @param {string} filterName the name of the filter + * @return {vnode} A number of radiobuttons corresponding with the selection options + */ +const radioButtonFilter = (selectionModel, filterName) => { + const name = `${filterName}FilterRadio`; + return h( + '.form-group-header.flex-row.w-100', + selectionModel.options.map((option) => { + const { label } = option; + const action = () => selectionModel.select(option); + const isChecked = selectionModel.isSelected(option); + + return radioButton({ label, isChecked, action, name }); + }), + ); +}; + +export default radioButtonFilter; diff --git a/lib/public/components/Filters/common/filters/toggleFilter.js b/lib/public/components/Filters/common/filters/toggleFilter.js new file mode 100644 index 0000000000..064ce2f0f9 --- /dev/null +++ b/lib/public/components/Filters/common/filters/toggleFilter.js @@ -0,0 +1,45 @@ +/** + * @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 for toggle filters + * + * @param {ToggleFilterModel} toggleFilterModel a ToggleFilterModel + * @param {name} toggleFilterModel the name used to identify and label the filter + * @param {boolean} radioButtonMode define whether or not to return radio buttons or a switch. + * @returns {Component} the toggle switch + */ +export const toggleFilter = (toggleFilterModel, name, id, radioButtonMode = false) => { + if (radioButtonMode) { + return h('.form-group-header.flex-row.w-100', [ + radioButton({ + label: 'OFF', + isChecked: !toggleFilterModel.isToggled(), + action: () => toggleFilterModel.toggle(), + name, + }), + radioButton({ + label: 'ON', + isChecked: toggleFilterModel.isToggled(), + action: () => toggleFilterModel.toggle(), + name, + }), + ]); + } + + return switchInput(toggleFilterModel.isToggled(), () => toggleFilterModel.toggle(), { labelAfter: name, id }); +}; diff --git a/lib/public/components/Filters/common/filtersPanelPopover.js b/lib/public/components/Filters/common/filtersPanelPopover.js index e0c0a7490c..1d729eb93e 100644 --- a/lib/public/components/Filters/common/filtersPanelPopover.js +++ b/lib/public/components/Filters/common/filtersPanelPopover.js @@ -48,8 +48,8 @@ const filtersToggleContentHeader = (filteringModel) => h('.flex-row.justify-betw 'button#reset-filters.btn.btn-danger', { onclick: () => filteringModel.resetFiltering - ? filteringModel.resetFiltering() - : filteringModel.reset(true), + ? filteringModel.resetFiltering(true, true) + : filteringModel.reset(true, true), disabled: !filteringModel.isAnyFilterActive(), }, 'Reset all filters', diff --git a/lib/public/components/common/form/switchInput.js b/lib/public/components/common/form/switchInput.js index ad7f7f8135..f06cb5154a 100644 --- a/lib/public/components/common/form/switchInput.js +++ b/lib/public/components/common/form/switchInput.js @@ -32,7 +32,7 @@ import { h } from '/js/src/index.js'; * @return {Component} the switch component */ export const switchInput = (value, onChange, options) => { - const { key, labelAfter, labelBefore, color } = options || {}; + const { key, labelAfter, labelBefore, color, id } = options || {}; const attributes = { ...key ? { key } : {} }; return h( @@ -40,7 +40,7 @@ export const switchInput = (value, onChange, options) => { attributes, [ labelBefore, - h('.switch', [ + h('.switch', { id }, [ h('input', { onchange: (e) => onChange(e.target.checked), type: 'checkbox', diff --git a/lib/public/components/common/selection/SelectionModel.js b/lib/public/components/common/selection/SelectionModel.js index 419f7c5a12..9b812eabf5 100644 --- a/lib/public/components/common/selection/SelectionModel.js +++ b/lib/public/components/common/selection/SelectionModel.js @@ -42,6 +42,12 @@ export class SelectionModel extends Observable { super(); const { availableOptions = [], defaultSelection = [], multiple = true, allowEmpty = true } = configuration || {}; + /** + * @type {SelectionOption[]} + * @protected + */ + this._selectionBacklog = []; + /** * @type {RemoteData|SelectionOption[]} * @protected @@ -152,11 +158,12 @@ export class SelectionModel extends Observable { if (typeof option === 'string' || typeof option === 'number') { if (this._availableOptions instanceof RemoteData) { selectOption = this._availableOptions.match({ - Success: (options) => options.find(({ value }) => value === option), + // String comparison in needed since select may use url parameters which are always strings, causing false negatives + Success: (options) => options.find(({ value }) => String(value) === String(option)), Other: () => null, }); } else { - selectOption = this._availableOptions.find(({ value }) => value === option); + selectOption = this._availableOptions.find(({ value }) => String(value) === String(option)); } } else { selectOption = option; @@ -243,7 +250,7 @@ export class SelectionModel extends Observable { } /** - * Defines the list of available options + * Defines the list of available options and if there is a selection backlog, these will be applied * * @param {RemoteData|SelectionOption[]} availableOptions the new available options * @return {void} @@ -251,6 +258,11 @@ export class SelectionModel extends Observable { setAvailableOptions(availableOptions) { this._availableOptions = availableOptions; this.visualChange$.notify(); + + if (this._selectionBacklog.length) { + this.selectedOptions = this._selectionBacklog; + this._selectionBacklog = []; + } } /** @@ -315,12 +327,19 @@ export class SelectionModel extends Observable { } /** - * Define (overrides) the list of currently selected options + * Define (overrides) the list of currently selected options. + * Invalid selection options are excluded * * @param {SelectionOption[]} selected the list of selected options */ set selectedOptions(selected) { - this._selectedOptions = selected; + let { options } = this; + + if (this.options instanceof RemoteData) { + options = options.isSuccess() ? options.payload : []; + } + + this._selectedOptions = options.filter((option) => selected.some(({ value }) => String(value) === String(option.value)));; } /** @@ -332,6 +351,23 @@ export class SelectionModel extends Observable { return this._defaultSelection; } + /** + * Sets selected options based on a comma-seperated string. + * Accounts for the options being either RemoteData or an array. + * + * @return {string} + */ + set normalized(value) { + const options = value.split(',').map((option) => ({ value: option })); + const postponeSelection = this.options instanceof RemoteData || ! this.options?.length; + + if (postponeSelection) { + this._selectionBacklog = options; + } else { + this.selectedOptions = options; + } + } + /** * Returns the normalized value of the selection * diff --git a/lib/public/components/runEorReasons/runEorReasonSelection.js b/lib/public/components/runEorReasons/runEorReasonSelection.js index dbe86cde87..c7a3ad14a3 100644 --- a/lib/public/components/runEorReasons/runEorReasonSelection.js +++ b/lib/public/components/runEorReasons/runEorReasonSelection.js @@ -22,6 +22,7 @@ import { h } from '/js/src/index.js'; */ export const eorReasonFilterComponent = (eorReasonFilterModel, eorReasonTypes) => { const eorReasonsCategories = [...new Set(eorReasonTypes.map(({ category }) => category))]; + const { category: currentCategory, title: currentTitle } = eorReasonFilterModel; return [ h('.flex-row', [ @@ -36,7 +37,7 @@ export const eorReasonFilterComponent = (eorReasonFilterModel, eorReasonTypes) = h('option', { selected: eorReasonFilterModel.category === '', value: '' }, '-'), eorReasonsCategories.map((category, index) => h( `option#eorCategory${index}`, - { key: category, value: category }, + { key: category, value: category, selected: category === currentCategory }, category, )), ], @@ -54,7 +55,7 @@ export const eorReasonFilterComponent = (eorReasonFilterModel, eorReasonTypes) = .filter((reason) => reason.category === eorReasonFilterModel.category) .map(({ title }, index) => h( `option#eorTitle${index}`, - { key: title, value: title }, + { key: title, value: title, selected: title === currentTitle }, title || '(empty)', )), ], diff --git a/lib/public/components/runTypes/RunTypesFilterModel.js b/lib/public/components/runTypes/RunTypesFilterModel.js index 60a923cbc6..31e3a6d5f8 100644 --- a/lib/public/components/runTypes/RunTypesFilterModel.js +++ b/lib/public/components/runTypes/RunTypesFilterModel.js @@ -12,43 +12,19 @@ */ import { runTypeToOption } from './runTypeToOption.js'; -import { FilterModel } from '../Filters/common/FilterModel.js'; import { ObservableBasedSelectionDropdownModel } from '../detector/ObservableBasedSelectionDropdownModel.js'; /** * Model storing state of a selection of run types picked from the list of all the existing run types */ -export class RunTypesFilterModel extends FilterModel { +export class RunTypesFilterModel extends ObservableBasedSelectionDropdownModel { /** * Constructor * * @param {ObservableData>} runTypes$ observable remote data of run types list */ constructor(runTypes$) { - super(); - this._selectionDropdownModel = new ObservableBasedSelectionDropdownModel(runTypes$, runTypeToOption); - this._addSubmodel(this._selectionDropdownModel); - } - - /** - * @inheritDoc - */ - reset() { - this._selectionDropdownModel.reset(); - } - - /** - * @inheritDoc - */ - get isEmpty() { - return this._selectionDropdownModel.isEmpty; - } - - /** - * @inheritDoc - */ - get normalized() { - return this._selectionDropdownModel.selected; + super(runTypes$, runTypeToOption); } /** diff --git a/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js b/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js index ca3ed1b38d..e0b6d87316 100644 --- a/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js +++ b/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js @@ -20,7 +20,7 @@ import { h } from '/js/src/index.js'; import { formatDataPassName } from '../format/formatDataPassName.js'; import { formatDataPassStatusHistory } from '../format/formatStatusHistory.js'; import { checkboxes } from '../../../components/Filters/common/filters/checkboxFilter.js'; -import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; +import { textFilter } from '../../../components/Filters/common/filters/textFilter.js'; /** * List of active columns for a generic data passes table @@ -35,7 +35,7 @@ export const dataPassesActiveColumns = { visible: true, sortable: true, format: (_, dataPass) => formatDataPassName(dataPass), - filter: (filteringModel) => textInputFilter(filteringModel, 'names', 'e.g. LHC22a_apass1, ...', 'w-75'), + filter: (filteringModel) => textFilter(filteringModel.get('names'), { class: 'w-75 mt1', placeholder: 'e.g. LHC22a, lhc23b, ...' }), balloon: true, classes: 'w-20', }, @@ -102,7 +102,7 @@ export const dataPassesActiveColumns = { nonPhysicsProductions: { name: 'Include nonphysics productions', - filter: (filteringModel) => checkboxes(filteringModel.get('include[byName]').selectionModel), + filter: (filteringModel) => checkboxes(filteringModel.get('permittedNonPhysicsNames')), visible: false, }, }; diff --git a/lib/public/views/DataPasses/DataPassesModel.js b/lib/public/views/DataPasses/DataPassesModel.js index 5d987b31d7..78589a56cd 100644 --- a/lib/public/views/DataPasses/DataPassesModel.js +++ b/lib/public/views/DataPasses/DataPassesModel.js @@ -21,14 +21,15 @@ import { DataPassesPerSimulationPassOverviewModel } from './PerSimulationPassOve export class DataPassesModel extends Observable { /** * The constructor of the model + * @param {QueryRouter} router router that controls the application's page navigation */ - constructor() { + constructor(router) { super(); - this._perLhcPeriodOverviewModel = new DataPassesPerLhcPeriodOverviewModel(); + this._perLhcPeriodOverviewModel = new DataPassesPerLhcPeriodOverviewModel(router, 'data-passes-per-lhc-period-overview'); this._perLhcPeriodOverviewModel.bubbleTo(this); - this._perSimulationPassOverviewModel = new DataPassesPerSimulationPassOverviewModel(); + this._perSimulationPassOverviewModel = new DataPassesPerSimulationPassOverviewModel(router, 'data-passes-per-simulation-pass-overview'); this._perSimulationPassOverviewModel.bubbleTo(this); } diff --git a/lib/public/views/DataPasses/DataPassesOverviewModel.js b/lib/public/views/DataPasses/DataPassesOverviewModel.js index b85cc052d7..3be195e0b7 100644 --- a/lib/public/views/DataPasses/DataPassesOverviewModel.js +++ b/lib/public/views/DataPasses/DataPassesOverviewModel.js @@ -10,8 +10,8 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ +import { SelectionModel } from '../../components/common/selection/SelectionModel.js'; import { FilteringModel } from '../../components/Filters/common/FilteringModel.js'; -import { SelectionFilterModel } from '../../components/Filters/common/filters/SelectionFilterModel.js'; import { TextTokensFilterModel } from '../../components/Filters/common/filters/TextTokensFilterModel.js'; import { NON_PHYSICS_PRODUCTIONS_NAMES_WORDS } from '../../domain/enums/NonPhysicsProductionsNamesWords.js'; import { OverviewPageModel } from '../../models/OverviewModel.js'; @@ -22,16 +22,23 @@ import { OverviewPageModel } from '../../models/OverviewModel.js'; export class DataPassesOverviewModel extends OverviewPageModel { /** * Constructor + * @param {QueryRouter} router router that controls the application's page navigation + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor() { + constructor(router, pageIdentifier) { super(); - this._filteringModel = new FilteringModel({ - names: new TextTokensFilterModel(), - 'include[byName]': new SelectionFilterModel({ - availableOptions: NON_PHYSICS_PRODUCTIONS_NAMES_WORDS.map((word) => ({ label: word.toUpperCase(), value: word })), - }), - }); + this._filteringModel = new FilteringModel( + router, + { + names: new TextTokensFilterModel(), + permittedNonPhysicsNames: new SelectionModel({ + availableOptions: NON_PHYSICS_PRODUCTIONS_NAMES_WORDS.map((word) => ({ label: word.toUpperCase(), value: word })), + }), + }, + ); + this._filteringModel.pageIdentifier = pageIdentifier; + this._filteringModel.setFilterFromURL(); this._filteringModel.visualChange$.bubbleTo(this); this._filteringModel.observe(() => { this._pagination.currentPage = 1; @@ -51,10 +58,12 @@ export class DataPassesOverviewModel extends OverviewPageModel { /** * Reset this model to its default * - * @returns {void} + * @param {boolean} _fetch Whether to refetch all data after filters have been reset + * @param {boolean} [clearUrl=false] if true filters will be removed from the url + * @return {void} */ - reset() { - this._filteringModel.reset(); + reset(_fetch = true, clearUrl = false) { + this._filteringModel.reset(false, clearUrl); super.reset(); } diff --git a/lib/public/views/DataPasses/PerLhcPeriodOverview/DataPassesPerLhcPeriodOverviewModel.js b/lib/public/views/DataPasses/PerLhcPeriodOverview/DataPassesPerLhcPeriodOverviewModel.js index dc125e1a94..6da2205751 100644 --- a/lib/public/views/DataPasses/PerLhcPeriodOverview/DataPassesPerLhcPeriodOverviewModel.js +++ b/lib/public/views/DataPasses/PerLhcPeriodOverview/DataPassesPerLhcPeriodOverviewModel.js @@ -19,9 +19,11 @@ import { buildUrl } from '/js/src/index.js'; export class DataPassesPerLhcPeriodOverviewModel extends DataPassesOverviewModel { /** * Constructor + * @param {QueryRouter} router router that controls the application's page navigation + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor() { - super(); + constructor(router, pageIdentifier) { + super(router, pageIdentifier); this._lhcPeriodId = null; } diff --git a/lib/public/views/DataPasses/PerSimulationPassOverview/DataPassesPerSimulationPassOverviewModel.js b/lib/public/views/DataPasses/PerSimulationPassOverview/DataPassesPerSimulationPassOverviewModel.js index 30fd3c616c..d9b1008552 100644 --- a/lib/public/views/DataPasses/PerSimulationPassOverview/DataPassesPerSimulationPassOverviewModel.js +++ b/lib/public/views/DataPasses/PerSimulationPassOverview/DataPassesPerSimulationPassOverviewModel.js @@ -21,9 +21,11 @@ import { DataPassesOverviewModel } from '../DataPassesOverviewModel.js'; export class DataPassesPerSimulationPassOverviewModel extends DataPassesOverviewModel { /** * Constructor + * @param {QueryRouter} router router that controls the application's page navigation + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor() { - super(); + constructor(router, pageIdentifier) { + super(router, pageIdentifier); this._simulationPass = new ObservableData(RemoteData.notAsked()); this._simulationPass.bubbleTo(this); } diff --git a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js index 37121a34a3..1226d7f7cb 100644 --- a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js +++ b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js @@ -117,7 +117,7 @@ export const environmentsActiveColumns = { * @param {EnvironmentOverviewModel} environmentOverviewModel the environment overview model * @return {Component} the filter component */ - filter: (environmentOverviewModel) => checkboxes(environmentOverviewModel.filteringModel.get('currentStatus').selectionModel), + filter: (environmentOverviewModel) => checkboxes(environmentOverviewModel.filteringModel.get('currentStatus')), }, historyItems: { name: h('.flex-row.g2.items-center', ['Status History', infoTooltip(environmentStatusHistoryLegendComponent())]), diff --git a/lib/public/views/Environments/EnvironmentModel.js b/lib/public/views/Environments/EnvironmentModel.js index ba4b1e86bf..53be81b0a0 100644 --- a/lib/public/views/Environments/EnvironmentModel.js +++ b/lib/public/views/Environments/EnvironmentModel.js @@ -29,7 +29,7 @@ export class EnvironmentModel extends Observable { super(); // Sub-models - this._overviewModel = new EnvironmentOverviewModel(model); + this._overviewModel = new EnvironmentOverviewModel(model, 'env-overview'); this._overviewModel.bubbleTo(this); this._detailsModel = new EnvironmentDetailsModel(); diff --git a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js index 8498a02d79..5e220e758a 100644 --- a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js +++ b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js @@ -15,11 +15,11 @@ import { buildUrl } from '/js/src/index.js'; import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { TimeRangeInputModel } from '../../../components/Filters/common/filters/TimeRangeInputModel.js'; -import { SelectionFilterModel } from '../../../components/Filters/common/filters/SelectionFilterModel.js'; import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; import { debounce } from '../../../utilities/debounce.js'; import { coloredEnvironmentStatusComponent } from '../ColoredEnvironmentStatusComponent.js'; import { StatusAcronym } from '../../../domain/enums/statusAcronym.mjs'; +import { SelectionModel } from '../../../components/common/selection/SelectionModel.js'; /** * Environment overview page model @@ -28,28 +28,33 @@ export class EnvironmentOverviewModel extends OverviewPageModel { /** * Constructor * @param {Model} model global model + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(model) { + constructor(model, pageIdentifier) { super(); - this._filteringModel = new FilteringModel({ - created: new TimeRangeInputModel(), - runNumbers: new RawTextFilterModel(), - statusHistory: new RawTextFilterModel(), - currentStatus: new SelectionFilterModel({ - availableOptions: Object.keys(StatusAcronym).map((status) => ({ - value: status, - label: coloredEnvironmentStatusComponent(status), - rawLabel: status, - })), - }), - ids: new RawTextFilterModel(), - }); + this._filteringModel = new FilteringModel( + model.router, + { + created: new TimeRangeInputModel(), + runNumbers: new RawTextFilterModel(), + statusHistory: new RawTextFilterModel(), + currentStatus: new SelectionModel({ + availableOptions: Object.keys(StatusAcronym).map((status) => ({ + value: status, + label: coloredEnvironmentStatusComponent(status), + rawLabel: status, + })), + }), + ids: new RawTextFilterModel(), + }, + ); + this._filteringModel.pageIdentifier = pageIdentifier; + this._filteringModel.setFilterFromURL(); this._filteringModel.observe(() => this._applyFilters(true)); this._filteringModel.visualChange$?.bubbleTo(this); - this.reset(false); const updateDebounceTime = () => { this._debouncedLoad = debounce(this.load.bind(this), model.inputDebounceTime); }; @@ -87,10 +92,11 @@ export class EnvironmentOverviewModel extends OverviewPageModel { /** * Reset all filtering models * @param {boolean} fetch Whether to refetch all data after filters have been reset + * @param {boolean} resetUrl Whether to remove all the active filters from the urls * @return {void} */ - resetFiltering(fetch = true) { - this._filteringModel.reset(); + resetFiltering(fetch = true, clearUrl = false) { + this._filteringModel.reset(false, clearUrl); if (fetch) { this._applyFilters(true); diff --git a/lib/public/views/Home/Overview/HomePageModel.js b/lib/public/views/Home/Overview/HomePageModel.js index 40b6cfac85..d291fc9b47 100644 --- a/lib/public/views/Home/Overview/HomePageModel.js +++ b/lib/public/views/Home/Overview/HomePageModel.js @@ -26,13 +26,13 @@ export class HomePageModel extends Observable { */ constructor(model) { super(); - this._runsOverviewModel = new RunsOverviewModel(model); + this._runsOverviewModel = new RunsOverviewModel(model, 'home'); this._runsOverviewModel.bubbleTo(this); - this._logsOverviewModel = new LogsOverviewModel(model, true); + this._logsOverviewModel = new LogsOverviewModel(model, true, 'home'); this._logsOverviewModel.bubbleTo(this); - this._lhcFillsOverviewModel = new LhcFillsOverviewModel(true); + this._lhcFillsOverviewModel = new LhcFillsOverviewModel(model.router, true, 'home'); this._lhcFillsOverviewModel.bubbleTo(this); } diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index f9850c1d2b..be4311d7e4 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 { toggleFilter } from '../../../components/Filters/common/filters/toggleFilter.js'; import { durationFilter } from '../../../components/Filters/LhcFillsFilter/durationFilter.js'; import { beamTypeFilter } from '../../../components/Filters/LhcFillsFilter/beamTypeFilter.js'; import { timeRangeFilter } from '../../../components/Filters/common/filters/timeRangeFilter.js'; @@ -117,7 +117,8 @@ export const lhcFillsActiveColumns = { name: 'Stable Beams Only', visible: false, format: (boolean) => boolean ? 'On' : 'Off', - filter: (lhcFillModel) => toggleStableBeamOnlyFilter(lhcFillModel.filteringModel.get('hasStableBeams'), true), + filter: (lhcFillModel) => + toggleFilter(lhcFillModel.filteringModel.get('hasStableBeams'), 'stableBeamsOnlyRadio', 'stableBeamsOnlyRadio', true), }, stableBeamsDuration: { name: 'SB Duration', diff --git a/lib/public/views/LhcFills/LhcFills.js b/lib/public/views/LhcFills/LhcFills.js index 70b6c5eb3d..1fda55ab58 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.router, true, 'lhc-fill-overview'); 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 c57ae69c25..04d746aea3 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -13,13 +13,13 @@ 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'; import { BeamTypeFilterModel } from '../../../components/Filters/LhcFillsFilter/BeamTypeFilterModel.js'; import { TextComparisonFilterModel } from '../../../components/Filters/common/filters/TextComparisonFilterModel.js'; import { TimeRangeFilterModel } from '../../../components/Filters/RunsFilter/TimeRangeFilter.js'; +import { ToggleFilterModel } from '../../../components/Filters/common/filters/ToggleFilterModel.js'; /** * Model for the LHC fills overview page @@ -30,30 +30,31 @@ export class LhcFillsOverviewModel extends OverviewPageModel { /** * Constructor * + * @param {QueryRouter} router router that controls the application's page navigation * @param {boolean} [stableBeamsOnly=false] if true, overview will load stable beam only + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(stableBeamsOnly = false) { + constructor(router, stableBeamsOnly = false, pageIdentifier) { super(); - this._filteringModel = new FilteringModel({ - fillNumbers: new RawTextFilterModel(), - beamDuration: new TextComparisonFilterModel(), - runDuration: new TextComparisonFilterModel(), - hasStableBeams: new StableBeamFilterModel(), - stableBeamsStart: new TimeRangeFilterModel(), - stableBeamsEnd: new TimeRangeFilterModel(), - beamTypes: new BeamTypeFilterModel(), - schemeName: new RawTextFilterModel(), - }); + this._filteringModel = new FilteringModel( + router, + { + fillNumbers: new RawTextFilterModel(), + beamDuration: new TextComparisonFilterModel(), + runDuration: new TextComparisonFilterModel(), + hasStableBeams: new ToggleFilterModel(stableBeamsOnly), + stableBeamsStart: new TimeRangeFilterModel(), + stableBeamsEnd: new TimeRangeFilterModel(), + beamTypes: new BeamTypeFilterModel(), + schemeName: new RawTextFilterModel(), + }, + ); + this._filteringModel.pageIdentifier = pageIdentifier; + this._filteringModel.setFilterFromURL(); this._filteringModel.observe(() => this._applyFilters()); this._filteringModel.visualChange$.bubbleTo(this); - - this.reset(false); - - if (stableBeamsOnly) { - this._filteringModel.get('hasStableBeams').setStableBeamsOnly(true); - } } /** @@ -70,10 +71,7 @@ export class LhcFillsOverviewModel extends OverviewPageModel { * @inheritDoc */ getRootEndpoint() { - const params = { - filter: this.filteringModel.normalized, - }; - return buildUrl('/api/lhcFills', params); + return buildUrl('/api/lhcFills', { filter: this.filteringModel.normalized }); } /** @@ -89,10 +87,11 @@ export class LhcFillsOverviewModel extends OverviewPageModel { /** * Reset all filtering models * @param {boolean} fetch Whether to refetch all data after filters have been reset + * @param {boolean} [clearUrl=false] if true filters will be removed from the url * @return {void} */ - resetFiltering(fetch = true) { - this._filteringModel.reset(); + resetFiltering(fetch = true, clearUrl = false) { + this._filteringModel.reset(false, clearUrl); if (fetch) { this._applyFilters(); diff --git a/lib/public/views/LhcFills/Overview/index.js b/lib/public/views/LhcFills/Overview/index.js index e81409f06c..a29abf5145 100644 --- a/lib/public/views/LhcFills/Overview/index.js +++ b/lib/public/views/LhcFills/Overview/index.js @@ -18,7 +18,7 @@ import { lhcFillsActiveColumns } from '../ActiveColumns/lhcFillsActiveColumns.js import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; -import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; +import { toggleFilter } from '../../../components/Filters/common/filters/toggleFilter.js'; const TABLEROW_HEIGHT = 53.3; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -50,7 +50,7 @@ const showLhcFillsTable = (lhcFillsOverviewModel) => { return [ h('.flex-row.header-container.g2.pv2', [ filtersPanelPopover(lhcFillsOverviewModel, lhcFillsActiveColumns), - toggleStableBeamOnlyFilter(lhcFillsOverviewModel.filteringModel.get('hasStableBeams')), + toggleFilter(lhcFillsOverviewModel.filteringModel.get('hasStableBeams'), 'STABLE BEAM ONLY'), ]), h('.w-100.flex-column', [ table(lhcFillsOverviewModel.items, lhcFillsActiveColumns, null, { tableClasses: '.table-sm' }), diff --git a/lib/public/views/Logs/LogsModel.js b/lib/public/views/Logs/LogsModel.js index b4f9342d42..6c5c463649 100644 --- a/lib/public/views/Logs/LogsModel.js +++ b/lib/public/views/Logs/LogsModel.js @@ -30,7 +30,7 @@ export class LogsModel extends Observable { super(); this.model = model; - this._overviewModel = new LogsOverviewModel(model); + this._overviewModel = new LogsOverviewModel(model, false, 'log-overview'); this._overviewModel.bubbleTo(this); this._treeViewModel = new LogTreeViewModel(); diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index 4cb565e9b9..b3591d4e0b 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -34,44 +34,49 @@ export class LogsOverviewModel extends Observable { * * @param {Model} model global model * @param {boolean} excludeAnonymous Whether to exclude anonymous logs + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(model, excludeAnonymous = false) { + constructor(model, excludeAnonymous = false, pageIdentifier) { super(); - this._filteringModel = new FilteringModel({ - author: new AuthorFilterModel(), - title: new RawTextFilterModel(), - content: new RawTextFilterModel(), - tags: new TagFilterModel(tagsProvider.items$), - runNumbers: new RawTextFilterModel(), - environmentIds: new RawTextFilterModel(), - fillNumbers: new RawTextFilterModel(), - created: new TimeRangeInputModel(), - }); + this._filteringModel = new FilteringModel( + model.router, + { + author: new AuthorFilterModel(), + title: new RawTextFilterModel(), + content: new RawTextFilterModel(), + tags: new TagFilterModel(tagsProvider.items$), + runNumbers: new RawTextFilterModel(), + environmentIds: new RawTextFilterModel(), + fillNumbers: new RawTextFilterModel(), + created: new TimeRangeInputModel(), + }, + ); + + this._overviewSortModel = new SortModel(); + this._pagination = new PaginationModel(); + const updateDebounceTime = () => { + this._debouncedFetchAllLogs = debounce(this.fetchLogs.bind(this), model.inputDebounceTime); + }; + + updateDebounceTime(); + model.appConfiguration$.observe(() => updateDebounceTime()); + // Filters + this.filteringModel.pageIdentifier = pageIdentifier; + excludeAnonymous && this._filteringModel.get('author').update('!Anonymous'); + this._filteringModel.setFilterFromURL(); this._filteringModel.observe(() => this._applyFilters()); this._filteringModel.visualChange$.bubbleTo(this); // Sub-models - this._overviewSortModel = new SortModel(); this._overviewSortModel.observe(() => this._applyFilters(true)); this._overviewSortModel.visualChange$.bubbleTo(this); - this._pagination = new PaginationModel(); this._pagination.observe(() => this.fetchLogs()); this._pagination.itemsPerPageSelector$.observe(() => this.notify()); this._logs = RemoteData.NotAsked(); - - const updateDebounceTime = () => { - this._debouncedFetchAllLogs = debounce(this.fetchLogs.bind(this), model.inputDebounceTime); - }; - model.appConfiguration$.observe(() => updateDebounceTime()); - updateDebounceTime(); - - excludeAnonymous && this._filteringModel.get('author').update('!Anonymous'); - - this.reset(false); } /** @@ -122,11 +127,12 @@ export class LogsOverviewModel extends Observable { /** * Reset all filtering, sorting and pagination settings to their default values * - * @param {boolean} fetch Whether to refetch all logs after filters have been reset - * @return {undefined} + * @param {boolean} _fetch Whether to refetch all data after filters have been reset + * @param {boolean} [clearUrl=false] if true filters will be removed from the url + * @return {void} */ - reset(fetch = true) { - this._filteringModel.reset(); + reset(_fetch = true, clearUrl = false) { + this._filteringModel.reset(false, clearUrl); this._pagination.reset(); if (fetch) { diff --git a/lib/public/views/QcFlagTypes/ActiveColumns/qcFlagTypesActiveColumns.js b/lib/public/views/QcFlagTypes/ActiveColumns/qcFlagTypesActiveColumns.js index 95923cd0d7..6f1ae72b84 100644 --- a/lib/public/views/QcFlagTypes/ActiveColumns/qcFlagTypesActiveColumns.js +++ b/lib/public/views/QcFlagTypes/ActiveColumns/qcFlagTypesActiveColumns.js @@ -15,7 +15,7 @@ import { h } from '/js/src/index.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; import { textFilter } from '../../../components/Filters/common/filters/textFilter.js'; import { qcFlagTypeColoredBadge } from '../../../components/qcFlags/qcFlagTypeColoredBadge.js'; -import badFilterRadioButtons from '../../../components/Filters/QcFlagTypesFilter/bad.js'; +import radioButtonFilter from '../../../components/Filters/common/filters/radioButtonFilter.js'; /** * List of active columns for a QC Flag Types table @@ -48,7 +48,7 @@ export const qcFlagTypesActiveColumns = { name: 'Bad', visible: true, sortable: true, - filter: ({ filteringModel }) => badFilterRadioButtons(filteringModel.get('bad')), + filter: ({ filteringModel }) => radioButtonFilter(filteringModel.get('bad'), 'bad'), classes: 'f6 w-5', format: (bad) => bad ? h('.danger', 'Yes') : h('.success', 'No'), }, diff --git a/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js b/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js index cc4ced6716..060ce94c9b 100644 --- a/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js +++ b/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js @@ -13,9 +13,9 @@ import { TextTokensFilterModel } from '../../../components/Filters/common/filters/TextTokensFilterModel.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; -import { SelectionModel } from '../../../components/common/selection/SelectionModel.js'; import { buildUrl } from '/js/src/index.js'; import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; +import { RadioButtonFilterModel } from '../../../components/Filters/common/RadioButtonFilterModel.js'; /** * QcFlagTypesOverviewModel @@ -23,21 +23,23 @@ import { FilteringModel } from '../../../components/Filters/common/FilteringMode export class QcFlagTypesOverviewModel extends OverviewPageModel { /** * Constructor + * @param {QueryRouter} router router that controls the application's page navigation + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor() { + constructor(router, pageIdentifier) { super(); - this._filteringModel = new FilteringModel({ - names: new TextTokensFilterModel(), - methods: new TextTokensFilterModel(), - bad: new SelectionModel({ - availableOptions: [{ label: 'Any' }, { label: 'Bad', value: true }, { label: 'Not Bad', value: false }], - defaultSelection: [{ label: 'Any' }], - allowEmpty: false, - multiple: false, - }), - }); + this._filteringModel = new FilteringModel( + router, + { + names: new TextTokensFilterModel(), + methods: new TextTokensFilterModel(), + bad: new RadioButtonFilterModel([{ label: 'Any' }, { label: 'Bad', value: true }, { label: 'Not Bad', value: false }]), + }, + ); + this._filteringModel.pageIdentifier = pageIdentifier; + this._filteringModel.setFilterFromURL(); this._filteringModel.observe(() => { this._pagination.silentlySetCurrentPage(1); this.load(); @@ -74,10 +76,12 @@ export class QcFlagTypesOverviewModel extends OverviewPageModel { /** * Reset this model to its default * - * @returns {void} + * @param {boolean} _fetch Whether to refetch all data after filters have been reset + * @param {boolean} [clearUrl=false] if true filters will be removed from the url + * @return {void} */ - reset() { - this._filteringModel.reset(); + reset(_fetch = true, clearUrl = false) { + this._filteringModel.reset(false, clearUrl); super.reset(); } } diff --git a/lib/public/views/QcFlagTypes/QcFlagTypesModel.js b/lib/public/views/QcFlagTypes/QcFlagTypesModel.js index 43468d3e34..fd5c391ada 100644 --- a/lib/public/views/QcFlagTypes/QcFlagTypesModel.js +++ b/lib/public/views/QcFlagTypes/QcFlagTypesModel.js @@ -29,7 +29,7 @@ export class QcFlagTypesModel extends Observable { this.model = model; // Overview - this._overviewModel = new QcFlagTypesOverviewModel(); + this._overviewModel = new QcFlagTypesOverviewModel(model.router, 'qc-flag-types-overview'); this._overviewModel.bubbleTo(this); } diff --git a/lib/public/views/Runs/ActiveColumns/runDetectorsAsyncQcActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runDetectorsAsyncQcActiveColumns.js index f4497010c4..2647f8589a 100644 --- a/lib/public/views/Runs/ActiveColumns/runDetectorsAsyncQcActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runDetectorsAsyncQcActiveColumns.js @@ -161,7 +161,7 @@ export const createRunDetectorsAsyncQcActiveColumns = ( visible: false, profiles: profile, filter: (filteringModel) => { - const filterModel = filteringModel.get(`detectorsQc[_${dplDetectorId}][notBadFraction]`); + const filterModel = filteringModel.get('detectorsQcNotBadFraction').getFilter(`_${dplDetectorId}`); return filterModel ? numericalComparisonFilter(filterModel, { step: 0.1, selectorPrefix: `detectorsQc-for-${dplDetectorId}-notBadFraction` }) : null; diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 66b1306ecb..9992a93407 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -13,9 +13,6 @@ import { CopyToClipboardComponent, h } from '/js/src/index.js'; import { displayRunEorReasonsOverview } from '../format/displayRunEorReasonOverview.js'; -import ddflpFilter from '../../../components/Filters/RunsFilter/ddflp.js'; -import dcsFilter from '../../../components/Filters/RunsFilter/dcs.js'; -import epnFilter from '../../../components/Filters/RunsFilter/epn.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; import { displayRunDuration } from '../format/displayRunDuration.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; @@ -45,7 +42,7 @@ import { detectorsFilterComponent } from '../../../components/Filters/RunsFilter import { timeRangeFilter } from '../../../components/Filters/common/filters/timeRangeFilter.js'; import { numericalComparisonFilter } from '../../../components/Filters/common/filters/numericalComparisonFilter.js'; import { checkboxes } from '../../../components/Filters/common/filters/checkboxFilter.js'; -import { triggerValueFilter } from '../../../components/Filters/RunsFilter/triggerValueFilter.js'; +import radioButtonFilter from '../../../components/Filters/common/filters/radioButtonFilter.js'; import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; /** @@ -161,8 +158,7 @@ export const runsActiveColumns = { * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the beam modes filter component */ - filter: (runsOverviewModel) => - selectionDropdown(runsOverviewModel.filteringModel.get('beamModes').selectionDropdownModel, { selectorPrefix: 'beam-mode' }), + filter: (runsOverviewModel) => selectionDropdown(runsOverviewModel.filteringModel.get('beamModes'), { selectorPrefix: 'beam-mode' }), }, fillNumber: { name: 'Fill No.', @@ -410,10 +406,7 @@ export const runsActiveColumns = { * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the run types filter component */ - filter: (runsOverviewModel) => selectionDropdown( - runsOverviewModel.filteringModel.get('runTypes').selectionDropdownModel, - { selectorPrefix: 'run-types' }, - ), + filter: (runsOverviewModel) => selectionDropdown(runsOverviewModel.filteringModel.get('runTypes'), { selectorPrefix: 'run-types' }), }, runQuality: { name: 'Quality', @@ -444,7 +437,7 @@ export const runsActiveColumns = { * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the run quality filter component */ - filter: (runsOverviewModel) => checkboxes(runsOverviewModel.filteringModel.get('runQualities').selectionModel), + filter: (runsOverviewModel) => checkboxes(runsOverviewModel.filteringModel.get('runQualities')), }, nDetectors: { name: 'DETs #', @@ -515,7 +508,7 @@ export const runsActiveColumns = { classes: 'w-2 f6 w-wrapped', format: (boolean) => boolean ? 'On' : 'Off', exportFormat: (boolean) => boolean ? 'On' : 'Off', - filter: ddflpFilter, + filter: ({ filteringModel }) => radioButtonFilter(filteringModel.get('ddflp'), 'ddFlp'), }, dcs: { name: 'DCS', @@ -524,14 +517,21 @@ export const runsActiveColumns = { classes: 'w-2 f6 w-wrapped', format: (boolean) => boolean ? 'On' : 'Off', exportFormat: (boolean) => boolean ? 'On' : 'Off', - filter: dcsFilter, + filter: ({ filteringModel }) => radioButtonFilter(filteringModel.get('dcs'), 'dcs'), }, triggerValue: { name: 'TRG', visible: true, profiles: [profiles.none, 'lhcFill', 'environment'], classes: 'w-5 f6 w-wrapped', - filter: triggerValueFilter, + + /** + * TriggerValue filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the trigger value filter component + */ + filter: ({ filteringModel }) => checkboxes(filteringModel.get('triggerValues'), { selector: 'triggerValue' }), format: (trgValue) => trgValue ? trgValue : '-', }, epn: { @@ -541,7 +541,7 @@ export const runsActiveColumns = { classes: 'w-2 f6 w-wrapped', format: (boolean) => boolean ? 'On' : 'Off', exportFormat: (boolean) => boolean ? 'On' : 'Off', - filter: epnFilter, + filter: ({ filteringModel }) => radioButtonFilter(filteringModel.get('epn'), 'epn'), }, epnTopology: { name: 'EPN Topology', @@ -663,10 +663,8 @@ export const runsActiveColumns = { * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the run types filter component */ - filter: (runsOverviewModel) => selectionDropdown( - runsOverviewModel.filteringModel.get('magnets').selectionDropdownModel, - { selectorPrefix: 'l3-dipole-current' }, - ), + filter: (runsOverviewModel) => + selectionDropdown(runsOverviewModel.filteringModel.get('magnets'), { selectorPrefix: 'l3-dipole-current' }), profiles: ['runsPerLhcPeriod', 'runsPerDataPass', 'runsPerSimulationPass', profiles.none], }, diff --git a/lib/public/views/Runs/Overview/FixedPdpBeamTypeRunsOverviewModel.js b/lib/public/views/Runs/Overview/FixedPdpBeamTypeRunsOverviewModel.js index 82eaf9e819..5edce4c14e 100644 --- a/lib/public/views/Runs/Overview/FixedPdpBeamTypeRunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/FixedPdpBeamTypeRunsOverviewModel.js @@ -23,9 +23,10 @@ export class FixedPdpBeamTypeRunsOverviewModel extends RunsWithQcModel { /** * Constructor * @param {Model} model global model + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(model) { - super(model); + constructor(model, pageIdentifier) { + super(model, pageIdentifier); this._pdpBeamTypes = []; } diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index 0249c66085..1b276299f7 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -31,11 +31,13 @@ import { magnetsCurrentLevelsProvider } from '../../../services/magnets/magnetsC import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; import { RunDefinitionFilterModel } from '../../../components/Filters/RunsFilter/RunDefinitionFilterModel.js'; import { RUN_QUALITIES } from '../../../domain/enums/RunQualities.js'; -import { SelectionFilterModel } from '../../../components/Filters/common/filters/SelectionFilterModel.js'; import { DataExportModel } from '../../../models/DataExportModel.js'; import { runsActiveColumns as dataExportConfiguration } from '../ActiveColumns/runsActiveColumns.js'; import { BeamModeFilterModel } from '../../../components/Filters/RunsFilter/BeamModeFilterModel.js'; import { beamModesProvider } from '../../../services/beamModes/beamModesProvider.js'; +import { RadioButtonFilterModel } from '../../../components/Filters/common/RadioButtonFilterModel.js'; +import { SelectionModel } from '../../../components/common/selection/SelectionModel.js'; +import { TRIGGER_VALUES } from '../../../domain/enums/TriggerValue.js'; /** * Model representing handlers for runs page @@ -46,56 +48,64 @@ export class RunsOverviewModel extends OverviewPageModel { /** * The constructor of the Overview model object * @param {Model} model global model + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(model) { + constructor(model, pageIdentifier) { super(); - this._filteringModel = new FilteringModel({ - runNumbers: new RawTextFilterModel(), - detectors: new DetectorsFilterModel(detectorsProvider.dataTaking$), - tags: new TagFilterModel( - tagsProvider.items$, - [ - CombinationOperator.AND, - CombinationOperator.OR, - CombinationOperator.NONE_OF, - ], - ), - fillNumbers: new RawTextFilterModel(), - lhcPeriods: new RawTextFilterModel(), - o2start: new TimeRangeFilterModel(), - o2end: new TimeRangeFilterModel(), - definitions: new RunDefinitionFilterModel(), - runDuration: new NumericalComparisonFilterModel({ scale: 60 * 1000 }), - environmentIds: new RawTextFilterModel(), - runTypes: new RunTypesFilterModel(runTypesProvider.items$), - beamModes: new BeamModeFilterModel(beamModesProvider.items$), - runQualities: new SelectionFilterModel({ - availableOptions: RUN_QUALITIES.map((quality) => ({ - label: quality.toUpperCase(), - value: quality, - })), - }), - nDetectors: new NumericalComparisonFilterModel({ integer: true }), - nEpns: new NumericalComparisonFilterModel({ integer: true }), - nFlps: new NumericalComparisonFilterModel({ integer: true }), - ctfFileCount: new NumericalComparisonFilterModel({ integer: true }), - tfFileCount: new NumericalComparisonFilterModel({ integer: true }), - otherFileCount: new NumericalComparisonFilterModel({ integer: true }), - odcTopologyFullName: new RawTextFilterModel(), - eorReason: new EorReasonFilterModel(eorReasonTypeProvider.items$), - magnets: new MagnetsFilteringModel(magnetsCurrentLevelsProvider.items$), - muInelasticInteractionRate: new NumericalComparisonFilterModel(), - inelasticInteractionRateAvg: new NumericalComparisonFilterModel(), - inelasticInteractionRateAtStart: new NumericalComparisonFilterModel(), - inelasticInteractionRateAtMid: new NumericalComparisonFilterModel(), - inelasticInteractionRateAtEnd: new NumericalComparisonFilterModel(), - }); + this._filteringModel = new FilteringModel( + model.router, + { + runNumbers: new RawTextFilterModel(), + detectors: new DetectorsFilterModel(detectorsProvider.dataTaking$), + tags: new TagFilterModel( + tagsProvider.items$, + [ + CombinationOperator.AND, + CombinationOperator.OR, + CombinationOperator.NONE_OF, + ], + ), + fillNumbers: new RawTextFilterModel(), + lhcPeriods: new RawTextFilterModel(), + o2start: new TimeRangeFilterModel(), + o2end: new TimeRangeFilterModel(), + definitions: new RunDefinitionFilterModel(), + runDuration: new NumericalComparisonFilterModel({ scale: 60 * 1000 }), + environmentIds: new RawTextFilterModel(), + runTypes: new RunTypesFilterModel(runTypesProvider.items$), + beamModes: new BeamModeFilterModel(beamModesProvider.items$), + runQualities: new SelectionModel({ + availableOptions: RUN_QUALITIES.map((quality) => ({ + label: quality.toUpperCase(), + value: quality, + })), + }), + nDetectors: new NumericalComparisonFilterModel({ integer: true }), + nEpns: new NumericalComparisonFilterModel({ integer: true }), + nFlps: new NumericalComparisonFilterModel({ integer: true }), + ctfFileCount: new NumericalComparisonFilterModel({ integer: true }), + tfFileCount: new NumericalComparisonFilterModel({ integer: true }), + otherFileCount: new NumericalComparisonFilterModel({ integer: true }), + odcTopologyFullName: new RawTextFilterModel(), + eorReason: new EorReasonFilterModel(eorReasonTypeProvider.items$), + magnets: new MagnetsFilteringModel(magnetsCurrentLevelsProvider.items$), + muInelasticInteractionRate: new NumericalComparisonFilterModel(), + inelasticInteractionRateAvg: new NumericalComparisonFilterModel(), + inelasticInteractionRateAtStart: new NumericalComparisonFilterModel(), + inelasticInteractionRateAtMid: new NumericalComparisonFilterModel(), + inelasticInteractionRateAtEnd: new NumericalComparisonFilterModel(), + ddflp: new RadioButtonFilterModel([{ label: 'ANY' }, { label: 'ON', value: true }, { label: 'OFF', value: false }]), + dcs: new RadioButtonFilterModel([{ label: 'ANY' }, { label: 'ON', value: true }, { label: 'OFF', value: false }]), + epn: new RadioButtonFilterModel([{ label: 'ANY' }, { label: 'ON', value: true }, { label: 'OFF', value: false }]), + triggerValues: new SelectionModel({ availableOptions: TRIGGER_VALUES.map((value) => ({ label: value, value })) }), + }, + ); + this._filteringModel.pageIdentifier = pageIdentifier; this._filteringModel.observe(() => this._applyFilters(true)); this._filteringModel.visualChange$.bubbleTo(this); - this.reset(false); const updateDebounceTime = () => { this._debouncedLoad = debounce(this.load.bind(this), model.inputDebounceTime); }; @@ -123,7 +133,7 @@ export class RunsOverviewModel extends OverviewPageModel { * @inheritdoc */ getRootEndpoint() { - return buildUrl('/api/runs', { ...this._getFilterQueryParams(), ...{ filter: this.filteringModel.normalized } }); + return buildUrl('/api/runs', { filter: this.filteringModel.normalized }); } /** @@ -140,18 +150,11 @@ export class RunsOverviewModel extends OverviewPageModel { /** * Reset all filtering models * @param {boolean} fetch Whether to refetch all data after filters have been reset + * @param {boolean} [clearUrl=false] if true filters will be removed from the url * @return {void} */ - resetFiltering(fetch = true) { - this._filteringModel.reset(); - - this._triggerValuesFilters = new Set(); - - this.ddflpFilter = ''; - - this.dcsFilter = ''; - - this.epnFilter = ''; + resetFiltering(fetch = true, clearUrl = false) { + this._filteringModel.reset(false, clearUrl); if (fetch) { this._applyFilters(true); @@ -163,11 +166,7 @@ export class RunsOverviewModel extends OverviewPageModel { * @return {Boolean} If any filter is active */ isAnyFilterActive() { - return this._filteringModel.isAnyFilterActive() - || this._triggerValuesFilters.size !== 0 - || this.ddflpFilter !== '' - || this.dcsFilter !== '' - || this.epnFilter !== ''; + return this._filteringModel.isAnyFilterActive(); } /** @@ -179,130 +178,6 @@ export class RunsOverviewModel extends OverviewPageModel { return this._filteringModel; } - /** - * Getter for the trigger values filter Set - * @return {Set} set of trigger filter values - */ - get triggerValuesFilters() { - return this._triggerValuesFilters; - } - - /** - * Setter for trigger values filter, this replaces the current set - * @param {Array} newTriggerValues new Set of values - * @return {undefined} - */ - set triggerValuesFilters(newTriggerValues) { - this._triggerValuesFilters = new Set(newTriggerValues); - this._applyFilters(); - } - - /** - * Returns the boolean of ddflp - * @return {Boolean} if ddflp is on - */ - getDdflpFilterOperation() { - return this.ddflpFilter; - } - - /** - * Sets the boolean of the filter if no new inputs were detected for 200 milliseconds - * @param {boolean} operation if the ddflp is on - * @return {undefined} - */ - setDdflpFilterOperation(operation) { - this.ddflpFilter = operation; - this._applyFilters(); - } - - /** - * Unchecks the ddflp checkbox and fetches all the runs. - * @return {undefined} - * - */ - removeDdflp() { - this.ddflpFilter = ''; - this._applyFilters(); - } - - /** - * Returns the boolean of dcs - * @return {Boolean} if dcs is on - */ - getDcsFilterOperation() { - return this.dcsFilter; - } - - /** - * Sets the boolean of the filter if no new inputs were detected for 200 milliseconds - * @param {boolean} operation if the dcs is on - * @return {undefined} - */ - setDcsFilterOperation(operation) { - this.dcsFilter = operation; - this._applyFilters(); - } - - /** - * Unchecks the dcs checkbox and fetches all the runs. - * @return {undefined} - */ - removeDcs() { - this.dcsFilter = ''; - this._applyFilters(); - } - - /** - * Returns the boolean of epn - * @return {Boolean} if epn is on - */ - getEpnFilterOperation() { - return this.epnFilter; - } - - /** - * Sets the boolean of the filter if no new inputs were detected for 200 milliseconds - * @param {boolean} operation if the epn is on - * @return {undefined} - */ - setEpnFilterOperation(operation) { - this.epnFilter = operation; - this._applyFilters(); - } - - /** - * Unchecks the epn checkbox and fetches all the runs. - * @return {undefined} - */ - removeEpn() { - this.epnFilter = ''; - this._applyFilters(); - } - - /** - * Returns the list of URL params corresponding to the currently applied filter - * - * @return {Object} the URL params - * - * @private - */ - _getFilterQueryParams() { - return { - ...this._triggerValuesFilters.size !== 0 && { - 'filter[triggerValues]': Array.from(this._triggerValuesFilters).join(), - }, - ...(this.ddflpFilter === true || this.ddflpFilter === false) && { - 'filter[ddflp]': this.ddflpFilter, - }, - ...(this.dcsFilter === true || this.dcsFilter === false) && { - 'filter[dcs]': this.dcsFilter, - }, - ...(this.epnFilter === true || this.epnFilter === false) && { - 'filter[epn]': this.epnFilter, - }, - }; - } - /** * Apply the current filtering and update the remote data list * diff --git a/lib/public/views/Runs/Overview/RunsWithQcModel.js b/lib/public/views/Runs/Overview/RunsWithQcModel.js index dcc239c156..534eb3e372 100644 --- a/lib/public/views/Runs/Overview/RunsWithQcModel.js +++ b/lib/public/views/Runs/Overview/RunsWithQcModel.js @@ -43,6 +43,8 @@ const qcFlagsExportConfigurationFactory = (detectors) => Object.fromEntries(dete import { ObservableData } from '../../../utilities/ObservableData.js'; import { DetectorType } from '../../../domain/enums/DetectorTypes.js'; import { mergeRemoteData } from '../../../utilities/mergeRemoteData.js'; +import { ToggleFilterModel } from '../../../components/Filters/common/filters/ToggleFilterModel.js'; +import { MultiCompositionFilterModel } from '../../../components/Filters/RunsFilter/MultiCompositionFilterModel.js'; /** * Merge QC summaries @@ -66,12 +68,18 @@ export class RunsWithQcModel extends RunsOverviewModel { /** * Constructor * @param {Model} model global model + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(model) { - super(model); + constructor(model, pageIdentifier) { + super(model, pageIdentifier); + + this._detectorsNotBadFractionRegistered = false; + this._detectorsForQcFlagRegistered = false; + this._qcSummaryObservableRegistered = false; this._observablesQcFlagsSummaryDependsOn$ = null; - this._mcReproducibleAsNotBad = false; + // This filter instance will be added as a sub-filter for a MultiCompositionFilter and a GaqFilter later. + this._mcReproducibleAsNotBad = new ToggleFilterModel(); this._runDetectorsSelectionModel = new RunDetectorsSelectionModel(); this._runDetectorsSelectionModel.bubbleTo(this); @@ -84,35 +92,22 @@ export class RunsWithQcModel extends RunsOverviewModel { verticalScrollEnabled: true, freezeFirstColumn: true, }); + + this._filteringModel + .put('detectorsQcNotBadFraction', new MultiCompositionFilterModel({ mcReproducibleAsNotBad: this._mcReproducibleAsNotBad })); } /** * @inheritdoc */ getRootEndpoint() { - const filter = {}; - filter.detectorsQc = { - mcReproducibleAsNotBad: this._mcReproducibleAsNotBad, - }; - - return buildUrl(super.getRootEndpoint(), { filter, include: { effectiveQcFlags: true } }); - } - - /** - * Set mcReproducibleAsNotBad - * - * @param {boolean} mcReproducibleAsNotBad new value - * @return {void} - */ - setMcReproducibleAsNotBad(mcReproducibleAsNotBad) { - this._mcReproducibleAsNotBad = mcReproducibleAsNotBad; - this.load(); + return buildUrl(super.getRootEndpoint(), { include: { effectiveQcFlags: true } }); } /** * Get mcReproducibleAsNotBad * - * @return {boolean} mcReproducibleAsNotBad + * @return {ToggleFilterModel} mcReproducibleAsNotBad */ get mcReproducibleAsNotBad() { return this._mcReproducibleAsNotBad; @@ -146,19 +141,25 @@ export class RunsWithQcModel extends RunsOverviewModel { * @param {ObservableData>} detectors$ detectors remote data observable */ registerDetectorsNotBadFractionFilterModels(detectors$) { + const detectorsQcNotBadFraction = this._filteringModel.get('detectorsQcNotBadFraction'); + const callback = (observableData) => { const current = observableData.getCurrent(); current?.apply({ - Success: (detectors) => detectors.forEach(({ id }) => { - this._filteringModel.put(`detectorsQc[_${id}][notBadFraction]`, new NumericalComparisonFilterModel({ - scale: 0.01, - integer: false, - })); - }), + Success: (detectors) => detectors.forEach(({ id }) => + detectorsQcNotBadFraction.putFilter(`_${id}`, new NumericalComparisonFilterModel({ scale: 0.01, integer: false }))), }); + + if (current?.isSuccess() && !this._detectorsNotBadFractionRegistered) { + this.filteringModel.setFilterFromURL(); + this._detectorsNotBadFractionRegistered = true; + } }; - detectors$.observe(callback); - callback(detectors$); + + if (!this._detectorsNotBadFractionRegistered) { + detectors$.observe(callback.bind(this)); + callback(detectors$); + } } /** @@ -172,6 +173,7 @@ export class RunsWithQcModel extends RunsOverviewModel { const current = observableData.getCurrent(); current?.apply({ Success: (detectors) => { + this._detectorsForQcFlagRegistered = true; this._exportModel.setDataExportConfiguration({ ...baseDataExportConfiguration, ...qcFlagsExportConfigurationFactory(detectors), @@ -180,9 +182,11 @@ export class RunsWithQcModel extends RunsOverviewModel { Other: () => null, }); }; - detectors$.observe(callback); - // Also trigger immediately if detectors are already loaded - callback(detectors$); + + if (!this._detectorsForQcFlagRegistered) { + detectors$.observe(callback.bind(this)); + callback(detectors$); + } } /** @@ -191,6 +195,10 @@ export class RunsWithQcModel extends RunsOverviewModel { * @param {ObservableData>} detectors$ observable data which QC flags fetching operation success depends on */ registerObservablesQcSummaryDependsOn(detectors$) { + if (detectors$ === this._observablesQcFlagsSummaryDependsOn$) { + return; + } + this._observablesQcFlagsSummaryDependsOn$ = detectors$; const callback = (observableData) => { const current = observableData.getCurrent(); @@ -235,7 +243,7 @@ export class RunsWithQcModel extends RunsOverviewModel { detectorIds: detectors .filter(({ type }) => type === DetectorType.PHYSICAL) .map(({ id }) => id).join(','), - mcReproducibleAsNotBad: this._mcReproducibleAsNotBad, + mcReproducibleAsNotBad: this._mcReproducibleAsNotBad.isToggled(), })); const { data: qcSummary2 } = await getRemoteData(buildUrl('/api/qcFlags/summary', { @@ -249,7 +257,7 @@ export class RunsWithQcModel extends RunsOverviewModel { operator: 'none', }, }, - mcReproducibleAsNotBad: this._mcReproducibleAsNotBad, + mcReproducibleAsNotBad: this._mcReproducibleAsNotBad.isToggled(), })); this._qcSummary$.setCurrent(RemoteData.success(mergeQcSummaries([qcSummary1, qcSummary2]))); } catch (error) { diff --git a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewModel.js b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewModel.js index baa4745a4d..2819b28c1e 100644 --- a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewModel.js +++ b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewModel.js @@ -19,11 +19,11 @@ import { FixedPdpBeamTypeRunsOverviewModel } from '../Overview/FixedPdpBeamTypeR import { jsonPatch } from '../../../utilities/fetch/jsonPatch.js'; import { jsonPut } from '../../../utilities/fetch/jsonPut.js'; import { SkimmingStage } from '../../../domain/enums/SkimmingStage.js'; -import { NumericalComparisonFilterModel } from '../../../components/Filters/common/filters/NumericalComparisonFilterModel.js'; import { jsonFetch } from '../../../utilities/fetch/jsonFetch.js'; import { mergeRemoteData } from '../../../utilities/mergeRemoteData.js'; import { RemoteDataSource } from '../../../utilities/fetch/RemoteDataSource.js'; import { DetectorType } from '../../../domain/enums/DetectorTypes.js'; +import { GaqFilterModel } from '../../../components/Filters/RunsFilter/GaqFilterModel.js'; const ALL_CPASS_PRODUCTIONS_REGEX = /cpass\d+/; const DETECTOR_NAMES_NOT_IN_CPASSES = ['EVS']; @@ -35,9 +35,10 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo /** * Constructor * @param {Model} model global model + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(model) { - super(model); + constructor(model, pageIdentifier) { + super(model, pageIdentifier); this._dataPass$ = new ObservableData(RemoteData.notAsked()); this._dataPass$.bubbleTo(this); @@ -51,6 +52,9 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo })) .build(); + this._filteringModel.put('gaq', new GaqFilterModel(this._mcReproducibleAsNotBad)); + this._filteringModel.setFilterFromURL(); + this._detectors$.bubbleTo(this); this._markAsSkimmableRequestResult$ = new ObservableData(RemoteData.notAsked()); @@ -65,11 +69,6 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo this._skimmableRuns$ = new ObservableData(RemoteData.notAsked()); this._skimmableRuns$.bubbleTo(this); - this._filteringModel.put('gaq[notBadFraction]', new NumericalComparisonFilterModel({ - scale: 0.01, - integer: false, - })); - this._freezeOrUnfreezeActionState$ = new ObservableData(RemoteData.notAsked()); this._freezeOrUnfreezeActionState$.bubbleTo(this); @@ -144,22 +143,15 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo * @inheritdoc */ getRootEndpoint() { - const gaqNotBadFilter = this._filteringModel.get('gaq[notBadFraction]'); - const filter = { dataPassIds: [this._dataPassId] }; - if (!gaqNotBadFilter.isEmpty) { - filter.gaq = { - mcReproducibleAsNotBad: this._mcReproducibleAsNotBad, - }; - } - + const filter = { ...this._filteringModel.normalized, dataPassIds: [this._dataPassId] }; return buildUrl(super.getRootEndpoint(), { filter }); } /** * @inheritdoc */ - resetFiltering(fetch = true) { - super.resetFiltering(fetch); + resetFiltering(fetch = true, resetUrl = false) { + super.resetFiltering(fetch, resetUrl); } /** @@ -281,7 +273,7 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo * @param {number} dataPassId id of Data Pass */ set dataPassId(dataPassId) { - if (dataPassId !== this._dataPassId) { + if (this._dataPassId && dataPassId !== this._dataPassId) { this.reset(false); } this._dataPassId = dataPassId; @@ -365,7 +357,7 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo }); const url = buildUrl('/api/qcFlags/summary/gaq', { dataPassId: this._dataPassId, - mcReproducibleAsNotBad: this._mcReproducibleAsNotBad, + mcReproducibleAsNotBad: this._mcReproducibleAsNotBad.isToggled(), runNumber: runNumber, }); await this._gaqSummarySources[runNumber].fetch(url); diff --git a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js index b8fc6c1164..59f396515c 100644 --- a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js +++ b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js @@ -37,8 +37,8 @@ import { iconCaretBottom } from '/js/src/icons.js'; import { BkpRoles } from '../../../domain/enums/BkpRoles.js'; import { getInelasticInteractionRateColumns } from '../ActiveColumns/getInelasticInteractionRateActiveColumns.js'; import { exportTriggerAndModal } from '../../../components/common/dataExport/exportTriggerAndModal.js'; -import { mcReproducibleAsNotBadToggle } from '../mcReproducibleAsNotBadToggle.js'; import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; +import { toggleFilter } from '../../../components/Filters/common/filters/toggleFilter.js'; const TABLEROW_HEIGHT = 59; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -174,13 +174,8 @@ export const RunsPerDataPassOverviewPage = ({ ), }); }, - filter: ({ filteringModel }) => - numericalComparisonFilter( - filteringModel.get('gaq[notBadFraction]'), - { step: 0.1, selectorPrefix: 'gaqNotBadFraction' }, - ), - + numericalComparisonFilter(filteringModel.get('gaq').notBadFraction, { step: 0.1, selectorPrefix: 'gaqNotBadFraction' }), filterTooltip: 'not-bad fraction expressed as a percentage', profiles: ['runsPerDataPass'], }, @@ -246,10 +241,7 @@ export const RunsPerDataPassOverviewPage = ({ )), ]), ), - mcReproducibleAsNotBadToggle( - mcReproducibleAsNotBad, - () => perDataPassOverviewModel.setMcReproducibleAsNotBad(!mcReproducibleAsNotBad), - ), + toggleFilter(mcReproducibleAsNotBad, h('em', 'MC.R as not-bad'), 'mcReproducibleAsNotBadToggle'), h('.mlauto', qcSummaryLegendTooltip()), h('#actions-dropdown-button', DropdownComponent( h('button.btn.btn-primary', h('.flex-row.g2', ['Actions', iconCaretBottom()])), diff --git a/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewModel.js b/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewModel.js index 8c2332fb22..5b15f173a7 100644 --- a/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewModel.js +++ b/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewModel.js @@ -31,9 +31,10 @@ export class RunsPerLhcPeriodOverviewModel extends FixedPdpBeamTypeRunsOverviewM * Constructor * * @param {Model} model global model + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(model) { - super(model); + constructor(model, pageIdentifier) { + super(model, pageIdentifier); this._lhcPeriodId = null; this._lhcPeriodStatistics$ = new ObservableData(RemoteData.notAsked()); @@ -55,6 +56,7 @@ export class RunsPerLhcPeriodOverviewModel extends FixedPdpBeamTypeRunsOverviewM this._tabbedPanelModel = new RunsPerLhcPeriodTabbedPanelModel(this._qcSummary$); this._tabbedPanelModel.bubbleTo(this); + this._filteringModel.setFilterFromURL(); } /** @@ -95,13 +97,8 @@ export class RunsPerLhcPeriodOverviewModel extends FixedPdpBeamTypeRunsOverviewM * @inheritdoc */ getRootEndpoint() { - return buildUrl(super.getRootEndpoint(), { - filter: { - lhcPeriodIds: [this._lhcPeriodId], - runQualities: 'good', - definitions: 'PHYSICS', - }, - }); + const filter = { lhcPeriodIds: [this._lhcPeriodId], runQualities: 'good', definitions: 'PHYSICS' }; + return buildUrl(super.getRootEndpoint(), { filter }); } /** @@ -151,7 +148,7 @@ export class RunsPerLhcPeriodOverviewModel extends FixedPdpBeamTypeRunsOverviewM * @param {string} lhcPeriodId id of a LHC period */ set lhcPeriodId(lhcPeriodId) { - if (lhcPeriodId !== this._lhcPeriodId) { + if (this._lhcPeriodId && lhcPeriodId !== this._lhcPeriodId) { this.reset(false); } this._lhcPeriodId = lhcPeriodId; diff --git a/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js b/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js index f832c0f151..1006c5f44e 100644 --- a/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js +++ b/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js @@ -26,9 +26,9 @@ import errorAlert from '../../../components/common/errorAlert.js'; import spinner from '../../../components/common/spinner.js'; import { getInelasticInteractionRateColumns } from '../ActiveColumns/getInelasticInteractionRateActiveColumns.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; -import { mcReproducibleAsNotBadToggle } from '../mcReproducibleAsNotBadToggle.js'; import { exportTriggerAndModal } from '../../../components/common/dataExport/exportTriggerAndModal.js'; import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; +import { toggleFilter } from '../../../components/Filters/common/filters/toggleFilter.js'; const TABLEROW_HEIGHT = 62; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -110,10 +110,7 @@ export const RunsPerLhcPeriodOverviewPage = ({ runs: { perLhcPeriodOverviewModel filtersPanelPopover(perLhcPeriodOverviewModel, activeColumns, { profile: 'runsPerLhcPeriod' }), h('.pl2#runOverviewFilter', textInputFilter(perLhcPeriodOverviewModel.filteringModel, 'runNumbers', 'e.g. 534454, 534455...')), h('h2.flex-row', ['Good, physics runs of ', lhcPeriodName]), - mcReproducibleAsNotBadToggle( - mcReproducibleAsNotBad, - () => perLhcPeriodOverviewModel.setMcReproducibleAsNotBad(!mcReproducibleAsNotBad), - ), + toggleFilter(mcReproducibleAsNotBad, h('em', 'MC.R as not-bad'), 'mcReproducibleAsNotBadToggle'), exportTriggerAndModal(perLhcPeriodOverviewModel.exportModel, modalModel), ]), h( diff --git a/lib/public/views/Runs/RunsModel.js b/lib/public/views/Runs/RunsModel.js index 934c278d90..e55192446c 100644 --- a/lib/public/views/Runs/RunsModel.js +++ b/lib/public/views/Runs/RunsModel.js @@ -32,13 +32,13 @@ export class RunsModel extends Observable { super(); this._detailsModel = new RunDetailsModel(); this._detailsModel.bubbleTo(this); - this._overviewModel = new RunsOverviewModel(model); + this._overviewModel = new RunsOverviewModel(model, 'run-overview'); this._overviewModel.bubbleTo(this); - this._perLhcPeriodOverviewModel = new RunsPerLhcPeriodOverviewModel(model); + this._perLhcPeriodOverviewModel = new RunsPerLhcPeriodOverviewModel(model, 'runs-per-lhc-period'); this._perLhcPeriodOverviewModel.bubbleTo(this); - this._perDataPassOverviewModel = new RunsPerDataPassOverviewModel(model); + this._perDataPassOverviewModel = new RunsPerDataPassOverviewModel(model, 'runs-per-data-pass'); this._perDataPassOverviewModel.bubbleTo(this); - this._perSimulationPassOverviewModel = new RunsPerSimulationPassOverviewModel(model); + this._perSimulationPassOverviewModel = new RunsPerSimulationPassOverviewModel(model, 'runs-per-simulation-pass'); this._perSimulationPassOverviewModel.bubbleTo(this); } @@ -49,6 +49,12 @@ export class RunsModel extends Observable { loadOverview() { if (! this._overviewModel.pagination.isInfiniteScrollEnabled) { this._overviewModel.load(); + + /** + * This is done outside the constructor because the child classes need to set their filters after their filters are ready setting up. + * Putting it in the constructor would cause duplicate api calls + */ + this._overviewModel.filteringModel.setFilterFromURL(); } } diff --git a/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewModel.js b/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewModel.js index d5fbeb400f..cd93b6ea13 100644 --- a/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewModel.js +++ b/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewModel.js @@ -23,9 +23,10 @@ export class RunsPerSimulationPassOverviewModel extends FixedPdpBeamTypeRunsOver /** * Constructor * @param {Model} model global model + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(model) { - super(model); + constructor(model, pageIdentifier) { + super(model, pageIdentifier); this._simulationPass$ = new ObservableData(RemoteData.notAsked()); @@ -33,6 +34,7 @@ export class RunsPerSimulationPassOverviewModel extends FixedPdpBeamTypeRunsOver this._detectors$.bubbleTo(this); this._simulationPass$.bubbleTo(this); + this._filteringModel.setFilterFromURL(); } /** @@ -74,13 +76,8 @@ export class RunsPerSimulationPassOverviewModel extends FixedPdpBeamTypeRunsOver * @inheritdoc */ getRootEndpoint() { - const params = { - filter: { - simulationPassIds: [this._simulationPassId], - }, - }; - - return buildUrl(super.getRootEndpoint(), params); + const filter = { simulationPassIds: [this._simulationPassId] }; + return buildUrl(super.getRootEndpoint(), { filter }); } /** @@ -88,7 +85,7 @@ export class RunsPerSimulationPassOverviewModel extends FixedPdpBeamTypeRunsOver * @param {number} simulationPassId simulation pass id */ set simulationPassId(simulationPassId) { - if (simulationPassId !== this._simulationPassId) { + if (this._simulationPassId && simulationPassId !== this._simulationPassId) { this.reset(false); } this._simulationPassId = simulationPassId; diff --git a/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewPage.js b/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewPage.js index c026c6912e..3b4ad6157f 100644 --- a/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewPage.js +++ b/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewPage.js @@ -27,7 +27,7 @@ import errorAlert from '../../../components/common/errorAlert.js'; import { getInelasticInteractionRateColumns } from '../ActiveColumns/getInelasticInteractionRateActiveColumns.js'; import { exportTriggerAndModal } from '../../../components/common/dataExport/exportTriggerAndModal.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; -import { mcReproducibleAsNotBadToggle } from '../mcReproducibleAsNotBadToggle.js'; +import { toggleFilter } from '../../../components/Filters/common/filters/toggleFilter.js'; import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; const TABLEROW_HEIGHT = 59; @@ -102,10 +102,7 @@ export const RunsPerSimulationPassOverviewPage = ({ h('h2#breadcrumb-simulation-pass-name', simulationPass?.name ?? spinner({ size: 1, absolute: false })), ]), ), - mcReproducibleAsNotBadToggle( - mcReproducibleAsNotBad, - () => perSimulationPassOverviewModel.setMcReproducibleAsNotBad(!mcReproducibleAsNotBad), - ), + toggleFilter(mcReproducibleAsNotBad, h('em', 'MC.R as not-bad'), 'mcReproducibleAsNotBadToggle'), h('.mlauto', qcSummaryLegendTooltip()), exportTriggerAndModal(perSimulationPassOverviewModel.exportModel, modalModel, { autoMarginLeft: false }), frontLink( diff --git a/lib/public/views/Runs/mcReproducibleAsNotBadToggle.js b/lib/public/views/Runs/mcReproducibleAsNotBadToggle.js deleted file mode 100644 index 636ed0f245..0000000000 --- a/lib/public/views/Runs/mcReproducibleAsNotBadToggle.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { switchInput } from '../../components/common/form/switchInput.js'; -import { h } from '/js/src/index.js'; - -/** - * Display a toggle switch to change interpretation of MC.Reproducible flag type from bad to not-bad - * - * @param {boolean} value current value - * @param {function} onChange to be called when switching - * @returns {Component} the toggle switch - */ -export const mcReproducibleAsNotBadToggle = (value, onChange) => h('#mcReproducibleAsNotBadToggle', switchInput( - value, - onChange, - { labelAfter: h('em', 'MC.R as not-bad') }, -)); diff --git a/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js b/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js index d986ece53b..42d14c2a3e 100644 --- a/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js +++ b/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js @@ -23,12 +23,16 @@ import { FilteringModel } from '../../../components/Filters/common/FilteringMode export class AnchoredSimulationPassesOverviewModel extends OverviewPageModel { /** * Constructor + * @param {QueryRouter} router router that controls the application's page navigation + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor() { + constructor(router, pageIdentifier) { super(); - this._filteringModel = new FilteringModel({ names: new TextTokensFilterModel() }); + this._filteringModel = new FilteringModel(router, { names: new TextTokensFilterModel() }); + this._filteringModel.pageIdentifier = pageIdentifier; + this._filteringModel.setFilterFromURL(); this._filteringModel.observe(() => { this._pagination.silentlySetCurrentPage(1); this.load(); @@ -72,10 +76,12 @@ export class AnchoredSimulationPassesOverviewModel extends OverviewPageModel { /** * Reset this model to its default * - * @returns {void} + * @param {boolean} _fetch Whether to refetch all data after filters have been reset + * @param {boolean} [clearUrl=false] if true filters will be removed from the url + * @return {void} */ - reset() { - this._filteringModel.reset(); + reset(_fetch = true, clearUrl = false) { + this._filteringModel.reset(false, clearUrl); super.reset(); } diff --git a/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js index 0d8af9b309..869bf418f9 100644 --- a/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js +++ b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js @@ -23,11 +23,16 @@ import { FilteringModel } from '../../../components/Filters/common/FilteringMode export class SimulationPassesPerLhcPeriodOverviewModel extends OverviewPageModel { /** * Constructor + * @param {QueryRouter} router router that controls the application's page navigation + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor() { + constructor(router, pageIdentifier) { super(); - this._filteringModel = new FilteringModel({ names: new TextTokensFilterModel() }); + this._filteringModel = new FilteringModel(router, { names: new TextTokensFilterModel() }); + + this._filteringModel.pageIdentifier = pageIdentifier; + this._filteringModel.setFilterFromURL(); this._filteringModel.visualChange$.bubbleTo(this); this._filteringModel.observe(() => { this._pagination.silentlySetCurrentPage(1); @@ -82,10 +87,12 @@ export class SimulationPassesPerLhcPeriodOverviewModel extends OverviewPageModel /** * Reset this model to its default * - * @returns {void} + * @param {boolean} _fetch Whether to refetch all data after filters have been reset + * @param {boolean} [clearUrl=false] if true filters will be removed from the url + * @return {void} */ - reset() { - this._filteringModel.reset(); + reset(_fetch = true, clearUrl = false) { + this._filteringModel.reset(false, clearUrl); super.reset(); } diff --git a/lib/public/views/SimulationPasses/SimulationPassesModel.js b/lib/public/views/SimulationPasses/SimulationPassesModel.js index 8e8d6e7969..03ec1818fd 100644 --- a/lib/public/views/SimulationPasses/SimulationPassesModel.js +++ b/lib/public/views/SimulationPasses/SimulationPassesModel.js @@ -21,14 +21,15 @@ import { AnchoredSimulationPassesOverviewModel } from './AnchoredOverview/Anchor export class SimulationPassesModel extends Observable { /** * The constructor of the model + * @param {QueryRouter} router router that controls the application's page navigation */ - constructor() { + constructor(router) { super(); - this._perLhcPeriodOverviewModel = new SimulationPassesPerLhcPeriodOverviewModel(); + this._perLhcPeriodOverviewModel = new SimulationPassesPerLhcPeriodOverviewModel(router, 'simulation-passes-per-lhc-period-overview'); this._perLhcPeriodOverviewModel.bubbleTo(this); - this._anchoredOverviewModel = new AnchoredSimulationPassesOverviewModel(); + this._anchoredOverviewModel = new AnchoredSimulationPassesOverviewModel(router, 'anchored-simulation-passes-overview'); this._anchoredOverviewModel.bubbleTo(this); } diff --git a/lib/public/views/lhcPeriods/LhcPeriodsModel.js b/lib/public/views/lhcPeriods/LhcPeriodsModel.js index 4f9d0ed185..7400961fec 100644 --- a/lib/public/views/lhcPeriods/LhcPeriodsModel.js +++ b/lib/public/views/lhcPeriods/LhcPeriodsModel.js @@ -20,11 +20,12 @@ import { LhcPeriodsOverviewModel } from './Overview/LhcPeriodsOverviewModel.js'; export class LhcPeriodsModel extends Observable { /** * The constructor of the model + * @param {QueryRouter} router router that controls the application's page navigation */ - constructor() { + constructor(router) { super(); - this._overviewModel = new LhcPeriodsOverviewModel(); + this._overviewModel = new LhcPeriodsOverviewModel(router, 'lhc-period-overview'); this._overviewModel.bubbleTo(this); } diff --git a/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js b/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js index e6073eb68f..2d886ac3e2 100644 --- a/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js +++ b/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js @@ -24,16 +24,23 @@ import { buildUrl } from '/js/src/index.js'; export class LhcPeriodsOverviewModel extends OverviewPageModel { /** * The constructor of the Overview model object + * @param {QueryRouter} router router that controls the application's page navigation + * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor() { + constructor(router, pageIdentifier) { super(); - this._filteringModel = new FilteringModel({ - names: new TextTokensFilterModel(), - years: new TextTokensFilterModel(), - pdpBeamTypes: new TextTokensFilterModel(), - }); + this._filteringModel = new FilteringModel( + router, + { + names: new TextTokensFilterModel(), + years: new TextTokensFilterModel(), + pdpBeamTypes: new TextTokensFilterModel(), + }, + ); + this._filteringModel.pageIdentifier = pageIdentifier; + this._filteringModel.setFilterFromURL(); this._filteringModel.visualChange$.bubbleTo(this); this._filteringModel.observe(() => { this._pagination.silentlySetCurrentPage(1); @@ -76,11 +83,13 @@ export class LhcPeriodsOverviewModel extends OverviewPageModel { /** * Reset this model to its default * - * @returns {void} + * @param {boolean} _fetch Whether to refetch all data after filters have been reset + * @param {boolean} [clearUrl=false] if true filters will be removed from the url + * @return {void} */ - reset() { + reset(_fetch = true, clearUrl = false) { super.reset(); - this._filteringModel.reset(); + this._filteringModel.reset(false, clearUrl); } /** diff --git a/lib/server/controllers/dataPasses.controller.js b/lib/server/controllers/dataPasses.controller.js index 81e2de5d6b..b31f702cac 100644 --- a/lib/server/controllers/dataPasses.controller.js +++ b/lib/server/controllers/dataPasses.controller.js @@ -34,17 +34,15 @@ const listDataPassesHandler = async (req, res) => { lhcPeriodIds: Joi.array().items(Joi.number()), ids: Joi.array().items(Joi.number()), names: Joi.array().items(Joi.string()), - include: Joi.object({ byName: Joi.string().custom((value, helper) => { - if (value.length > 10) { - return helper.error('byName cannot have more than 10 characters'); - } - const nameTokens = value?.split(','); + // 'debug,test' or the reverse have a length of 10 + permittedNonPhysicsNames: Joi.string().max(10).custom((value, helper) => { + const nameTokens = value.split(','); const allTokensCorrect = nameTokens.every((token) => NON_PHYSICS_PRODUCTIONS_NAMES_WORDS.includes(token)); if (!allTokensCorrect) { - return helper.error(`All byName must comma delimited list of ${NON_PHYSICS_PRODUCTIONS_NAMES_WORDS}`); + return helper.error(`All permittedNonPhysicsNames must comma delimited list of ${NON_PHYSICS_PRODUCTIONS_NAMES_WORDS}`); } return nameTokens; - }) }), + }), }, page: PaginationDto, sort: DtoFactory.order(['id', 'name']), diff --git a/lib/server/services/dataPasses/DataPassService.js b/lib/server/services/dataPasses/DataPassService.js index 617aa9c7e4..8ad241f7b0 100644 --- a/lib/server/services/dataPasses/DataPassService.js +++ b/lib/server/services/dataPasses/DataPassService.js @@ -88,13 +88,25 @@ class DataPassService { * @returns {Promise>} result */ async getAll({ - filter, + filter = {}, limit, offset, sort, } = {}) { const queryBuilder = this.prepareQueryBuilder(); + /** + * @typedef + * @property {object} filter + * @property {number[]} [filter.lhcPeriodIds] lhcPeriod identifier to filter with + * @property {number[]} [filter.simulationPassIds] simulationPass identifier to filter with + * @property {number[]} [filter.ids] data passes identifier to filter with + * @property {string[]} [filter.names] data passes names to filter with + * @property {boolean} [filter.includeByName] list of tokens in data passes names which indicate + * a given data pass should not be excluded, possible tokens are 'test', 'debug'. + */ + const { ids, names, permittedNonPhysicsNames = [], lhcPeriodIds, simulationPassIds } = filter; + if (sort) { for (const property in sort) { queryBuilder.orderBy(property, sort[property]); @@ -108,37 +120,24 @@ class DataPassService { queryBuilder.offset(offset); } - if (filter) { - /** - * @typedef - * @property {object} filter - * @property {number[]} [filter.lhcPeriodIds] lhcPeriod identifier to filter with - * @property {number[]} [filter.simulationPassIds] simulationPass identifier to filter with - * @property {number[]} [filter.ids] data passes identifier to filter with - * @property {string[]} [filter.names] data passes names to filter with - * @property {boolean} [filter.include.byName] list of tokens in data passes names which indicate - * a given data pass should not be excluded, possible tokens are 'test', 'debug'. - */ - const { ids, names, lhcPeriodIds, simulationPassIds } = filter; - if (lhcPeriodIds) { - queryBuilder.where('lhcPeriodId').oneOf(...lhcPeriodIds); - } - if (simulationPassIds) { - queryBuilder.whereAssociation('anchoredSimulationPasses', 'id').oneOf(...simulationPassIds); - } - if (ids) { - queryBuilder.where('id').oneOf(...ids); - } - if (names) { - queryBuilder.where('name').oneOf(...names); - } + if (lhcPeriodIds) { + queryBuilder.where('lhcPeriodId').oneOf(...lhcPeriodIds); + } + if (simulationPassIds) { + queryBuilder.whereAssociation('anchoredSimulationPasses', 'id').oneOf(...simulationPassIds); + } + if (ids) { + queryBuilder.where('id').oneOf(...ids); + } + if (names) { + queryBuilder.where('name').oneOf(...names); } - const byName = filter?.include?.byName ?? []; - if (!byName.includes(NonPhysicsProductionsNamesWords.TEST)) { + if (!permittedNonPhysicsNames.includes(NonPhysicsProductionsNamesWords.TEST)) { queryBuilder.where('name').not().substring(`\\_${NonPhysicsProductionsNamesWords.TEST}`); } - if (!byName.includes(NonPhysicsProductionsNamesWords.DEBUG)) { + + if (!permittedNonPhysicsNames.includes(NonPhysicsProductionsNamesWords.DEBUG)) { queryBuilder.where('name').not().substring(`\\_${NonPhysicsProductionsNamesWords.DEBUG}`); } diff --git a/lib/usecases/run/GetAllRunsUseCase.js b/lib/usecases/run/GetAllRunsUseCase.js index fe9fdd5c95..ae0b14d071 100644 --- a/lib/usecases/run/GetAllRunsUseCase.js +++ b/lib/usecases/run/GetAllRunsUseCase.js @@ -82,7 +82,7 @@ class GetAllRunsUseCase { inelasticInteractionRateAtMid, inelasticInteractionRateAtEnd, gaq, - detectorsQc, + detectorsQcNotBadFraction, beamModes, } = filter; @@ -389,28 +389,21 @@ class GetAllRunsUseCase { } } - if (detectorsQc) { + if (detectorsQcNotBadFraction) { const [dataPassId] = dataPassIds ?? []; const [simulationPassId] = simulationPassIds ?? []; const [lhcPeriodId] = lhcPeriodIds ?? []; - const { mcReproducibleAsNotBad } = detectorsQc; - delete detectorsQc.mcReproducibleAsNotBad; + const { mcReproducibleAsNotBad } = detectorsQcNotBadFraction; + delete detectorsQcNotBadFraction.mcReproducibleAsNotBad; - const dplDetectorIds = Object.keys(detectorsQc).map((id) => parseInt(id.slice(1), 10)); + const dplDetectorIds = Object.keys(detectorsQcNotBadFraction).map((id) => parseInt(id.slice(1), 10)); if (dplDetectorIds.length > 0) { - const qcSummary = await qcFlagSummaryService.getSummary( - { - dataPassId, - simulationPassId, - lhcPeriodId, - dplDetectorIds, - }, - { mcReproducibleAsNotBad }, - ); + const scope = { dataPassId, simulationPassId, lhcPeriodId, dplDetectorIds }; + const qcSummary = await qcFlagSummaryService.getSummary(scope, { mcReproducibleAsNotBad }); const runNumbers = Object.entries(qcSummary) .filter(([_, runSummary]) => { - const mask = Object.entries(detectorsQc).map(([prefixedDetectorId, { notBadFraction: { operator, limit } }]) => { + const mask = Object.entries(detectorsQcNotBadFraction).map(([prefixedDetectorId, { operator, limit }]) => { const dplDetectorId = parseInt(prefixedDetectorId.slice(1), 10); if (!(dplDetectorId in runSummary)) { return false; diff --git a/test/api/dataPasses.test.js b/test/api/dataPasses.test.js index 3ce5947435..a092118267 100644 --- a/test/api/dataPasses.test.js +++ b/test/api/dataPasses.test.js @@ -296,13 +296,13 @@ module.exports = () => { }); }); it('should successfully include TEST productions', async () => { - const response = await request(server).get('/api/dataPasses?filter[lhcPeriodIds][]=2&filter[include][byName]=test'); + const response = await request(server).get('/api/dataPasses?filter[lhcPeriodIds][]=2&filter[permittedNonPhysicsNames]=test'); expect(response.status).to.be.equal(200); const { data } = await response.body; expect(data.map(({ name }) => name)).to.have.all.members(['LHC22b_apass1', 'LHC22b_skimming','LHC22b_apass2_skimmed', 'LHC22b_test']); }); it('should successfully include DEBUG productions', async () => { - const response = await request(server).get('/api/dataPasses?filter[lhcPeriodIds][]=2&filter[include][byName]=debug'); + const response = await request(server).get('/api/dataPasses?filter[lhcPeriodIds][]=2&filter[permittedNonPhysicsNames]=debug'); expect(response.status).to.be.equal(200); const { data } = await response.body; expect(data.map(({ name }) => name)).to.have.all.members(['LHC22b_apass1', 'LHC22b_skimming','LHC22b_apass2_skimmed', 'LHC22b_debug']); diff --git a/test/api/runs.test.js b/test/api/runs.test.js index 083771bf02..36bc52673b 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -454,11 +454,11 @@ module.exports = () => { } }); - it('should successfully filter by detectors notBadFraction', async () => { + it('should successfully filter by detectorsQcNotBadFraction', async () => { const dataPassId = 1; { const response = await request(server).get(`/api/runs?filter[dataPassIds][]=${dataPassId}` - + '&filter[detectorsQc][_1][notBadFraction][operator]=>&filter[detectorsQc][_1][notBadFraction][limit]=0.7'); + + '&filter[detectorsQcNotBadFraction][_1][operator]=>&filter[detectorsQcNotBadFraction][_1][limit]=0.7'); expect(response.status).to.equal(200); const { data: runs } = response.body; @@ -468,7 +468,7 @@ module.exports = () => { } { const response = await request(server).get(`/api/runs?filter[dataPassIds][]=${dataPassId}` - + '&filter[detectorsQc][_1][notBadFraction][operator]=<&filter[detectorsQc][_1][notBadFraction][limit]=0.9&filter[detectorsQc][mcReproducibleAsNotBad]=true'); + + '&filter[detectorsQcNotBadFraction][_1][operator]=<&filter[detectorsQcNotBadFraction][_1][limit]=0.9&filter[detectorsQcNotBadFraction][mcReproducibleAsNotBad]=true'); expect(response.status).to.equal(200); const { data: runs } = response.body; @@ -478,8 +478,8 @@ module.exports = () => { } { const response = await request(server).get(`/api/runs?filter[dataPassIds][]=${dataPassId}` - + '&filter[detectorsQc][_1][notBadFraction][operator]=<&filter[detectorsQc][_1][notBadFraction][limit]=0.7' - + '&filter[detectorsQc][_16][notBadFraction][operator]=>&filter[detectorsQc][_16][notBadFraction][limit]=0.9' + + '&filter[detectorsQcNotBadFraction][_1][operator]=<&filter[detectorsQcNotBadFraction][_1][limit]=0.7' + + '&filter[detectorsQcNotBadFraction][_16][operator]=>&filter[detectorsQcNotBadFraction][_16][limit]=0.9' ); expect(response.status).to.equal(200); diff --git a/test/lib/usecases/run/GetAllRunsUseCase.test.js b/test/lib/usecases/run/GetAllRunsUseCase.test.js index 5b080d056c..febeae02aa 100644 --- a/test/lib/usecases/run/GetAllRunsUseCase.test.js +++ b/test/lib/usecases/run/GetAllRunsUseCase.test.js @@ -831,7 +831,7 @@ module.exports = () => { query: { filter: { dataPassIds, - detectorsQc: { '_1': { notBadFraction: { operator: '<', limit: 0.7 } } }, + detectorsQcNotBadFraction: { '_1': { operator: '<', limit: 0.7 } }, }, }, }); @@ -843,7 +843,7 @@ module.exports = () => { query: { filter: { dataPassIds, - detectorsQc: { '_1': { notBadFraction: { operator: '<', limit: 0.8 } } }, + detectorsQcNotBadFraction: { '_1': { operator: '<', limit: 0.8 } }, }, }, }); @@ -855,7 +855,7 @@ module.exports = () => { query: { filter: { dataPassIds, - detectorsQc: { '_1': { notBadFraction: { operator: '<', limit: 0.9 } }, mcReproducibleAsNotBad: true }, + detectorsQcNotBadFraction: { '_1': { operator: '<', limit: 0.9 }, mcReproducibleAsNotBad: true }, }, }, }); @@ -867,9 +867,10 @@ module.exports = () => { query: { filter: { dataPassIds, - detectorsQc: { - '_2': { notBadFraction: { operator: '>', limit: 0.8 } }, - '_1': { notBadFraction: {operator: '<', limit: 0.8 } }, + detectorsQcNotBadFraction: + { + '_2': { operator: '>', limit: 0.8 }, + '_1': { operator: '<', limit: 0.8 }, }, }, }, diff --git a/test/public/Filters/filtersToUrl.test.js b/test/public/Filters/filtersToUrl.test.js new file mode 100644 index 0000000000..755979fb4b --- /dev/null +++ b/test/public/Filters/filtersToUrl.test.js @@ -0,0 +1,528 @@ +/** + * @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 { expect } = require('chai'); +const { + defaultBefore, + defaultAfter, + goToPage, + fillInput, + getPopoverSelector, + getPeriodInputsSelectors, + pressElement, + openFilteringPanel, + waitForTableLength, +} = require('../defaults.js'); + +module.exports = () => { + let page; + let browser; + + before(async () => { + [page, browser] = await defaultBefore(); + }); + + const getQueryParameters = (page) => Object.fromEntries(new URL(page.url()).searchParams.entries()); + + it('should set filters from LogsOverview to the URL', async () => { + await goToPage(page, 'log-overview'); + const firstCheckboxId = 'tag-dropdown-option-DPG'; + const popoverTrigger = '.createdAt-filter .popover-trigger'; + + await page.waitForSelector(popoverTrigger); + await openFilteringPanel(page); + + const popOverSelector = await getPopoverSelector(await page.$(popoverTrigger)); + const { fromDateSelector, toDateSelector, fromTimeSelector, toTimeSelector } = getPeriodInputsSelectors(popOverSelector); + + await fillInput(page, '.title-textFilter', 'bogusbogusbogus', ['change']); + await fillInput(page, '#authorFilterText', 'Jane', ['change']); + await fillInput(page, '.content-textFilter', 'particle', ['change']); + await pressElement(page, '.tags-filter .dropdown-trigger'); + await pressElement(page, `#${firstCheckboxId}`, true); + await fillInput(page, '.environments-filter input', '8E4aZTjY', ['change']); + await fillInput(page, '.runNumbers-textFilter', '1,2', ['change']); + await fillInput(page, '.fillNumbers-textFilter', '1, 6', ['change']); + await fillInput(page, fromDateSelector, '2020-02-02', ['change']); + await fillInput(page, toDateSelector, '2020-02-02', ['change']); + await fillInput(page, fromTimeSelector, '11:00', ['change']); + await fillInput(page, toTimeSelector, '12:00', ['change']); + + const queryParameters = getQueryParameters(page); + expect(queryParameters).to.deep.equal({ + "page": "log-overview", + "filter[author]": "Jane", + "filter[title]": "bogusbogusbogus", + "filter[content]": "particle", + "filter[tags][values]": "DPG", + "filter[tags][operation]": "and", + "filter[runNumbers]": "1,2", + "filter[environmentIds]": "8E4aZTjY", + "filter[fillNumbers]": "1, 6", + "filter[created][from]": "1580641200000", + "filter[created][to]": "1580644800000" + }); + }); + + it('should set filters from EnvironmentsOverview to the URL', async () => { + await goToPage(page, 'env-overview'); + const popoverTrigger = '.createdAt-filter .popover-trigger'; + + await page.waitForSelector(popoverTrigger); + await openFilteringPanel(page); + + const createdAtPopoverSelector = await getPopoverSelector(await page.$(popoverTrigger)); + const periodInputsSelectors = getPeriodInputsSelectors(createdAtPopoverSelector); + + await fillInput(page, '.runs-filter input', '10', ['change']); + await fillInput(page, '.id-filter input', 'Dxi029djX, TDI59So3d', ['change']); + await pressElement(page, '#checkboxes-checkbox-DESTROYED'); + await fillInput(page, '.historyItems-filter input', 'C-R-D-X', ['change']); + await fillInput(page, periodInputsSelectors.fromDateSelector, '2019-08-09', ['change']); + await fillInput(page, periodInputsSelectors.toDateSelector, '2019-08-10', ['change']); + await fillInput(page, periodInputsSelectors.fromTimeSelector, '00:00', ['change']); + await fillInput(page, periodInputsSelectors.toTimeSelector, '23:59', ['change']); + + const queryParameters = getQueryParameters(page); + expect(queryParameters).to.deep.equal({ + "page": "env-overview", + "filter[created][from]": "1565308800000", + "filter[created][to]": "1565481540000", + "filter[runNumbers]": "10", + "filter[statusHistory]": "C-R-D-X", + "filter[currentStatus]": "DESTROYED", + "filter[ids]": "Dxi029djX, TDI59So3d" + }); + }); + + it('should set filters from LhcFillsOverview to the URL', async () => { + await goToPage(page, 'lhc-fill-overview'); + await waitForTableLength(page, 5); + const sbEndPopoverTrigger = '.stableBeamsEnd-filter .popover-trigger'; + const sbStartPopoverTrigger = '.stableBeamsStart-filter .popover-trigger'; + const sbStartPopOverSelector = await getPopoverSelector(await page.$(sbStartPopoverTrigger)); + const sbEndPopOverSelector = await getPopoverSelector(await page.$(sbEndPopoverTrigger)); + const filterSchemeNameInputField= '.fillingSchemeName-filter input'; + const { + fromDateSelector: sbStartFromDateSelector, + toDateSelector: sbStartToDateSelector, + fromTimeSelector: sbStartFromTimeSelector, + toTimeSelector: sbStartToTimeSelector + } = getPeriodInputsSelectors(sbStartPopOverSelector); + + const { + fromDateSelector: sbEndFromDateSelector, + toDateSelector: sbEndToDateSelector, + fromTimeSelector: sbEndFromTimeSelector, + toTimeSelector: sbEndToTimeSelector + } = getPeriodInputsSelectors(sbEndPopOverSelector); + + await openFilteringPanel(page); + await fillInput(page, '#beam-duration-filter-operand', '00:01:40', ['change']); + await fillInput(page, '#run-duration-filter-operand', '00:00:00', ['change']); + await pressElement(page, '#beam-types-checkbox-p-Pb'); + await fillInput(page, sbStartFromDateSelector, '2019-08-08', ['change']); + await fillInput(page, sbStartToDateSelector, '2019-08-08', ['change']); + await fillInput(page, sbStartFromTimeSelector, '10:00', ['change']); + await fillInput(page, sbStartToTimeSelector, '12:00', ['change']); + await fillInput(page, sbEndFromDateSelector, '2022-03-22', ['change']); + await fillInput(page, sbEndToDateSelector, '2022-03-22', ['change']); + await fillInput(page, sbEndFromTimeSelector, '01:00', ['change']); + await fillInput(page, sbEndToTimeSelector, '23:59', ['change']); + await fillInput(page, filterSchemeNameInputField, 'Single_12b_8_1024_8_2018', ['change']); + + const queryParameters = getQueryParameters(page); + expect(queryParameters).to.deep.equal({ + "page": "lhc-fill-overview", + "filter[beamDuration][operator]": "=", + "filter[beamDuration][limit]": "00:01:40", + "filter[runDuration][operator]": "=", + "filter[runDuration][limit]": "00:00:00", + "filter[hasStableBeams]": "true", + "filter[stableBeamsEnd][from]": "1647910800000", + "filter[stableBeamsEnd][to]": "1647993540000", + "filter[stableBeamsStart][from]": "1565258400000", + "filter[stableBeamsStart][to]": "1565265600000", + "filter[beamTypes]": "p-Pb", + "filter[schemeName]": "Single_12b_8_1024_8_2018" + }); + }); + + it('should set filters from runsOverview to the URL', async () => { + await goToPage(page, 'run-overview'); + const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); + const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); + const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); + + const { + fromDateSelector: startFromDateSelector, + toDateSelector: startToDateSelector, + fromTimeSelector: startFromTimeSelector, + toTimeSelector: startToTimeSelector + } = getPeriodInputsSelectors(startPopoverSelector); + + const { + fromDateSelector: endFromDateSelector, + toDateSelector: endToDateSelector, + fromTimeSelector: endFromTimeSelector, + toTimeSelector: endToTimeSelector + } = getPeriodInputsSelectors(endPopoverSelector); + + await openFilteringPanel(page); + await pressElement(page, '#detector-filter-dropdown-option-ITS', true); + await pressElement(page, '#tag-dropdown-option-FOOD', true); + await pressElement(page, '#run-definition-checkbox-PHYSICS', true); + await pressElement(page, '.timeO2Start-filter .popover-trigger'); + await fillInput(page, startFromTimeSelector, '11:11', ['change']); + await fillInput(page, startToTimeSelector, '14:00', ['change']); + await fillInput(page, startFromDateSelector, '2021-02-03', ['change']); + await fillInput(page, startToDateSelector, '2021-02-03', ['change']); + await fillInput(page, endFromTimeSelector, '11:11', ['change']); + await fillInput(page, endToTimeSelector, '14:00', ['change']); + await fillInput(page, endFromDateSelector, '2021-02-03', ['change']); + await fillInput(page, endToDateSelector, '2021-02-03', ['change']); + await fillInput(page, '#duration-operand', '1500', ['change']); + await pressElement(page, `${dipolePopoverSelector} .dropdown-option:last-child`, true); + await pressElement(page, '#checkboxes-checkbox-bad'); + await pressElement(page, '#triggerValue-checkbox-OFF'); + await fillInput(page, '#runOverviewFilter .runNumbers-textFilter', '101'); + await fillInput(page, '.fillNumbers-textFilter', '1, 3', ['change']); + await fillInput(page, '.environmentIds-textFilter', 'Dxi029djX, TDI59So3d', ['change']); + await pressElement(page, '#run-types-dropdown-option-2', true); + await pressElement(page, '#beam-mode-dropdown-option-NO\\ BEAM', true); + await fillInput(page, '#nDetectors-operand', '1', ['change']); + await fillInput(page, '#nFlps-operand', '10', ['change']); + await fillInput(page, '#nEpns-operand', '10', ['change']); + await fillInput(page, '#ctfFileCount-operand', '1', ['change']); + await fillInput(page, '#tfFileCount-operand', '1', ['change']); + await fillInput(page, '#otherFileCount-operand', '1', ['change']); + await pressElement(page, '#epnFilterRadioOFF', true); + await page.select('#eorCategories', 'DETECTORS'); + await page.select('#eorTitles', 'CPV'); + await fillInput(page, '#eorDescription', 'some', ['change']); + + const queryParameters = getQueryParameters(page); + expect(queryParameters).to.deep.equal({ + "page": "run-overview", + "filter[runNumbers]": "101", + "filter[detectors][operator]": "and", + "filter[detectors][values]": "ITS", + "filter[tags][values]": "FOOD", + "filter[tags][operation]": "and", + "filter[fillNumbers]": "1, 3", + "filter[o2start][from]": "1612350660000", + "filter[o2start][to]": "1612360800000", + "filter[o2end][from]": "1612350660000", + "filter[o2end][to]": "1612360800000", + "filter[definitions]": "PHYSICS", + "filter[runDuration][operator]": "=", + "filter[runDuration][limit]": "90000000", + "filter[environmentIds]": "Dxi029djX, TDI59So3d", + "filter[runTypes]": "2", + "filter[beamModes]": "NO BEAM", + "filter[runQualities]": "bad", + "filter[nDetectors][operator]": "=", + "filter[nDetectors][limit]": "1", + "filter[nEpns][operator]": "=", + "filter[nEpns][limit]": "10", + "filter[nFlps][operator]": "=", + "filter[nFlps][limit]": "10", + "filter[ctfFileCount][operator]": "=", + "filter[ctfFileCount][limit]": "1", + "filter[tfFileCount][operator]": "=", + "filter[tfFileCount][limit]": "1", + "filter[otherFileCount][operator]": "=", + "filter[otherFileCount][limit]": "1", + "filter[eorReason][category]": "DETECTORS", + "filter[eorReason][title]": "CPV", + "filter[eorReason][description]": "some", + "filter[magnets][l3]": "30003", + "filter[magnets][dipole]": "0", + "filter[epn]": "false", + "filter[triggerValues]": "OFF" + }); + }); + + it('should set filters from lhcPriodOverview to the URL', async () => { + await goToPage(page, 'lhc-period-overview'); + + await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22a'); + await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(2) input[type=text]', '2022'); + await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(3) input[type=text]', 'PbPb'); + const queryParameters = getQueryParameters(page); + expect(queryParameters).to.deep.equal({ + "page": "lhc-period-overview", + "filter[names][]": "LHC22a", + "filter[years][]": "2022", + "filter[pdpBeamTypes][]": "PbPb" + }); + }); + + it('should set filters from qcFlagTypesOverview to the URL', async () => { + await goToPage(page, 'qc-flag-types-overview'); + + await fillInput(page, '.name-filter input[type=text]', 'bad'); + await fillInput(page, '.method-filter input[type=text]', 'bad'); + await pressElement(page, '#badFilterRadioBad', true); + + const queryParameters = getQueryParameters(page); + expect(queryParameters).to.deep.equal({ + "page": "qc-flag-types-overview", + "filter[names][]": "bad", + "filter[methods][]": "bad", + "filter[bad]": "true" + }); + }); + + it('should set filters from runsPerLhcPeriodOverview to the URL', async () => { + await goToPage(page, 'runs-per-lhc-period', { queryParameters: { lhcPeriodId: 2 }}); + const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); + const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); + const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); + + const { + fromDateSelector: startFromDateSelector, + toDateSelector: startToDateSelector, + fromTimeSelector: startFromTimeSelector, + toTimeSelector: startToTimeSelector + } = getPeriodInputsSelectors(startPopoverSelector); + + const { + fromDateSelector: endFromDateSelector, + toDateSelector: endToDateSelector, + fromTimeSelector: endFromTimeSelector, + toTimeSelector: endToTimeSelector + } = getPeriodInputsSelectors(endPopoverSelector); + + await fillInput(page, '#inelasticInteractionRateAvg-operand', '100000', ['change']); + await fillInput(page, '#muInelasticInteractionRate-operand', '100000', ['change']); + await fillInput(page, '#runOverviewFilter .runNumbers-textFilter', '101'); + await fillInput(page, '.fillNumbers-textFilter', '1, 3', ['change']); + + await pressElement(page, `${dipolePopoverSelector} .dropdown-option:last-child`, true); + await fillInput(page, startFromTimeSelector, '11:11', ['change']); + await fillInput(page, startToTimeSelector, '14:00', ['change']); + await fillInput(page, startFromDateSelector, '2021-02-03', ['change']); + await fillInput(page, startToDateSelector, '2021-02-03', ['change']); + await fillInput(page, endFromTimeSelector, '11:11', ['change']); + await fillInput(page, endToTimeSelector, '14:00', ['change']); + await fillInput(page, endFromDateSelector, '2021-02-03', ['change']); + await fillInput(page, endToDateSelector, '2021-02-03', ['change']); + + + const queryParameters = getQueryParameters(page); + expect(queryParameters).to.deep.equal({ + "page": "runs-per-lhc-period", + "lhcPeriodId": "2", + "filter[runNumbers]": "101", + "filter[fillNumbers]": "1, 3", + "filter[o2end][from]": "1612350660000", + "filter[o2end][to]": "1612360800000", + "filter[o2start][from]": "1612350660000", + "filter[o2start][to]": "1612360800000", + "filter[magnets][l3]": "30003", + "filter[magnets][dipole]": "0", + "filter[muInelasticInteractionRate][operator]": "=", + "filter[muInelasticInteractionRate][limit]": "100000", + "filter[inelasticInteractionRateAvg][operator]": "=", + "filter[inelasticInteractionRateAvg][limit]": "100000" + }); + }); + + it('should set filters from DataPassesPerLhcPeriodOverview to the URL', async () => { + await goToPage(page, 'data-passes-per-lhc-period-overview', { queryParameters: { lhcPeriodId: 2 }}); + + await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1', ['change']); + await pressElement(page, '#checkboxes-checkbox-test', true); + + + const queryParameters = getQueryParameters(page); + expect(queryParameters).to.deep.equal({ + "page": "data-passes-per-lhc-period-overview", + "lhcPeriodId": "2", + "filter[names][]": "LHC22b_apass1", + "filter[permittedNonPhysicsNames]": "test" + }); + }); + + it('should set filters from DataPassesPerSimulationPassOverview to the URL', async () => { + await goToPage(page, 'data-passes-per-simulation-pass-overview', { queryParameters: { simulationPassId: 1 }}); + + await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1', ['change']); + await pressElement(page, '#checkboxes-checkbox-test', true); + + const queryParameters = getQueryParameters(page); + expect(queryParameters).to.deep.equal({ + "page": "data-passes-per-simulation-pass-overview", + "simulationPassId": "1", + "filter[names][]": "LHC22b_apass1", + "filter[permittedNonPhysicsNames]": "test" + }); + }); + + it('should set filters from AnchoredSimulationPassesOverview to the URL', async () => { + await goToPage(page, 'anchored-simulation-passes-overview', { queryParameters: { dataPassId: 1 }}); + + await fillInput(page, '.name-filter input', 'LHC23k6c', ['input']); + + const queryParameters = getQueryParameters(page); + expect(queryParameters).to.deep.equal({ + "page": "anchored-simulation-passes-overview", + "dataPassId": "1", + "filter[names][]": "LHC23k6c" + }); + }); + + it('should set filters from RunsPerSimulationPass to the URL', async () => { + await goToPage(page, 'runs-per-simulation-pass', { queryParameters: { simulationPassId: 2 }}); + + const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); + const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); + const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); + + const { + fromDateSelector: startFromDateSelector, + toDateSelector: startToDateSelector, + fromTimeSelector: startFromTimeSelector, + toTimeSelector: startToTimeSelector + } = getPeriodInputsSelectors(startPopoverSelector); + + const { + fromDateSelector: endFromDateSelector, + toDateSelector: endToDateSelector, + fromTimeSelector: endFromTimeSelector, + toTimeSelector: endToTimeSelector + } = getPeriodInputsSelectors(endPopoverSelector); + + await openFilteringPanel(page); + await pressElement(page, '.timeO2Start-filter .popover-trigger'); + await fillInput(page, startFromTimeSelector, '11:11', ['change']); + await fillInput(page, startToTimeSelector, '14:00', ['change']); + await fillInput(page, startFromDateSelector, '2021-02-03', ['change']); + await fillInput(page, startToDateSelector, '2021-02-03', ['change']); + await fillInput(page, endFromTimeSelector, '11:11', ['change']); + await fillInput(page, endToTimeSelector, '14:00', ['change']); + await fillInput(page, endFromDateSelector, '2021-02-03', ['change']); + await fillInput(page, endToDateSelector, '2021-02-03', ['change']); + await fillInput(page, '.inelasticInteractionRateAtMid-filter input', '1', ['change']); + await fillInput(page, '.inelasticInteractionRateAtEnd-filter input', '1', ['change']); + await fillInput(page, '.inelasticInteractionRateAtStart-filter input', '1', ['change']); + await pressElement(page, `${dipolePopoverSelector} .dropdown-option:last-child`, true); + await pressElement(page, '#mcReproducibleAsNotBadToggle', true); + + // These two are detectorQCNotBadFraction[_id] filters. There are a dozen more, but they are all identical hence why only these were tested + await fillInput(page, '.QC-SPECIFIC-filter input', '1', ['change']); + await fillInput(page, '.ACO-filter input', '1', ['change']); + + const queryParameters = getQueryParameters(page); + expect(queryParameters).to.deep.equal({ + "page": "runs-per-simulation-pass", + "simulationPassId": "2", + "filter[o2end][from]": "1612350660000", + "filter[o2start][from]": "1612350660000", + "filter[o2end][to]": "1612360800000", + "filter[o2start][to]": "1612360800000", + "filter[magnets][l3]": "30003", + "filter[magnets][dipole]": "0", + "filter[inelasticInteractionRateAtStart][operator]": "=", + "filter[inelasticInteractionRateAtStart][limit]": "1", + "filter[inelasticInteractionRateAtMid][operator]": "=", + "filter[inelasticInteractionRateAtMid][limit]": "1", + "filter[inelasticInteractionRateAtEnd][operator]": "=", + "filter[inelasticInteractionRateAtEnd][limit]": "1", + "filter[detectorsQcNotBadFraction][mcReproducibleAsNotBad]": "true", + "filter[detectorsQcNotBadFraction][_20][operator]": "=", + "filter[detectorsQcNotBadFraction][_20][limit]": "0.01", + "filter[detectorsQcNotBadFraction][_17][operator]": "=", + "filter[detectorsQcNotBadFraction][_17][limit]": "0.01" + }); + }); + + it('should set filters from RunsPerSimulationPass to the URL', async () => { + await goToPage(page, 'runs-per-data-pass', { queryParameters: { dataPassId: 1 }}); + + const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); + const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); + const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); + + const { + fromDateSelector: startFromDateSelector, + toDateSelector: startToDateSelector, + fromTimeSelector: startFromTimeSelector, + toTimeSelector: startToTimeSelector + } = getPeriodInputsSelectors(startPopoverSelector); + + const { + fromDateSelector: endFromDateSelector, + toDateSelector: endToDateSelector, + fromTimeSelector: endFromTimeSelector, + toTimeSelector: endToTimeSelector + } = getPeriodInputsSelectors(endPopoverSelector); + + await openFilteringPanel(page); + await pressElement(page, '#detector-filter-dropdown-option-ITS', true); + await pressElement(page, '#tag-dropdown-option-FOOD', true); + await pressElement(page, '.timeO2Start-filter .popover-trigger'); + await fillInput(page, startFromTimeSelector, '11:11', ['change']); + await fillInput(page, startToTimeSelector, '14:00', ['change']); + await fillInput(page, startFromDateSelector, '2021-02-03', ['change']); + await fillInput(page, startToDateSelector, '2021-02-03', ['change']); + await fillInput(page, endFromTimeSelector, '11:11', ['change']); + await fillInput(page, endToTimeSelector, '14:00', ['change']); + await fillInput(page, endFromDateSelector, '2021-02-03', ['change']); + await fillInput(page, endToDateSelector, '2021-02-03', ['change']); + await fillInput(page, '#duration-operand', '1500', ['change']); + await fillInput(page, '.muInelasticInteractionRate-filter input', '1', ['change']); + await fillInput(page, '.inelasticInteractionRateAvg-filter input', '1', ['change']); + await fillInput(page, '.globalAggregatedQuality-filter input', '1', ['change']); + + await pressElement(page, `${dipolePopoverSelector} .dropdown-option:last-child`, true); + await pressElement(page, '#mcReproducibleAsNotBadToggle', true); + + // These two are detectorQCNotBadFraction[_id] filters. There are a dozen more, but they are all identical hence why only these were tested + await fillInput(page, '.QC-SPECIFIC-filter input', '1', ['change']); + await fillInput(page, '.ACO-filter input', '1', ['change']); + + const queryParameters = getQueryParameters(page); + expect(queryParameters).to.deep.equal({ + "page": "runs-per-data-pass", + "dataPassId": "1", + "filter[detectors][operator]": "and", + "filter[detectors][values]": "ITS", + "filter[tags][values]": "FOOD", + "filter[tags][operation]": "and", + "filter[o2end][from]": "1612350660000", + "filter[o2end][to]": "1612360800000", + "filter[o2start][from]": "1612350660000", + "filter[o2start][to]": "1612360800000", + "filter[runDuration][limit]": "90000000", + "filter[runDuration][operator]": "=", + "filter[magnets][l3]": "30003", + "filter[magnets][dipole]": "0", + "filter[muInelasticInteractionRate][operator]": "=", + "filter[muInelasticInteractionRate][limit]": "1", + "filter[inelasticInteractionRateAvg][operator]": "=", + "filter[inelasticInteractionRateAvg][limit]": "1", + "filter[detectorsQcNotBadFraction][mcReproducibleAsNotBad]": "true", + "filter[detectorsQcNotBadFraction][_20][operator]": "=", + "filter[detectorsQcNotBadFraction][_20][limit]": "0.01", + "filter[detectorsQcNotBadFraction][_17][operator]": "=", + "filter[detectorsQcNotBadFraction][_17][limit]": "0.01", + "filter[gaq][notBadFraction][operator]": "=", + "filter[gaq][notBadFraction][limit]": "0.01", + "filter[gaq][mcReproducibleAsNotBad]": "true" + }); + }); + + after(async () => await defaultAfter(page, browser)); +} diff --git a/test/public/Filters/index.js b/test/public/Filters/index.js new file mode 100644 index 0000000000..cdb17bbbb3 --- /dev/null +++ b/test/public/Filters/index.js @@ -0,0 +1,20 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const ToUrlSuite = require('./filtersToUrl.test.js'); +const ToFilterSuite = require('./urlToFilter.test.js'); + +module.exports = () => { + // describe('Filters to URL', ToUrlSuite); + describe('URL to Filters', ToFilterSuite); +}; diff --git a/test/public/Filters/urlToFilter.test.js b/test/public/Filters/urlToFilter.test.js new file mode 100644 index 0000000000..06eb280039 --- /dev/null +++ b/test/public/Filters/urlToFilter.test.js @@ -0,0 +1,372 @@ +/** + * @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 { + defaultBefore, + defaultAfter, + fillInput, + getPopoverSelector, + getPeriodInputsSelectors, + pressElement, + openFilteringPanel, + expectInputValue, +} = require('../defaults.js'); + +module.exports = () => { + let page; + let browser; + + before(async () => { + [page, browser] = await defaultBefore(); + }); + + it('should apply filters from url in logsOverviewPage', async () => { + const url = 'http://localhost:4000/?page=log-overview&filter[author]=Jane&filter[title]=bogusbogusbogus&filter[content]=particle'+ + '&filter[tags][values]=DPG&filter[tags][operation]=and&filter[runNumbers]=1%2C2&filter[environmentIds]=8E4aZTjY'+ + '&filter[fillNumbers]=1%2C%206&filter[created][from]=1580637600000&filter[created][to]=1580641200000'; + + + await page.goto(url, { waitUntil: 'load' }); + + const firstCheckboxId = 'tag-dropdown-option-DPG'; + const popoverTrigger = '.createdAt-filter .popover-trigger'; + + await page.waitForSelector(popoverTrigger); + await openFilteringPanel(page); + + const popOverSelector = await getPopoverSelector(await page.$(popoverTrigger)); + const { fromDateSelector, toDateSelector, fromTimeSelector, toTimeSelector } = getPeriodInputsSelectors(popOverSelector); + + await expectInputValue(page, '.title-textFilter', 'bogusbogusbogus'); + await expectInputValue(page, '#authorFilterText', 'Jane'); + await expectInputValue(page, '.content-textFilter', 'particle'); + await pressElement(page, '.tags-filter .dropdown-trigger'); + await page.waitForSelector(`#${firstCheckboxId}:checked`); + await expectInputValue(page, '.environments-filter input', '8E4aZTjY'); + await expectInputValue(page, '.runNumbers-textFilter', '1,2'); + await expectInputValue(page, '.fillNumbers-textFilter', '1, 6'); + await expectInputValue(page, fromDateSelector, '2020-02-02'); + await expectInputValue(page, toDateSelector, '2020-02-02'); + + await expectInputValue(page, fromTimeSelector, '10:00'); + await expectInputValue(page, toTimeSelector, '11:00'); + }); + + it('should set filters from EnvironmentsOverview to the URL', async () => { + const url = 'http://localhost:4000/?page=env-overview&filter[created][from]=1565301600000&filter[created][to]=1565474340000' + + '&filter[runNumbers]=10&filter[statusHistory]=C-R-D-X&filter[currentStatus]=DESTROYED&filter[ids]=Dxi029djX%2C%20TDI59So3d'; + await page.goto(url, { waitUntil: 'load' }); + await openFilteringPanel(page); + + const popoverTrigger = '.createdAt-filter .popover-trigger'; + const createdAtPopoverSelector = await getPopoverSelector(await page.$(popoverTrigger)); + const periodInputsSelectors = getPeriodInputsSelectors(createdAtPopoverSelector); + + await expectInputValue(page, '.runs-filter input', '10'); + await expectInputValue(page, '.id-filter input', 'Dxi029djX, TDI59So3d'); + await page.waitForSelector('#checkboxes-checkbox-DESTROYED:checked'); + await expectInputValue(page, '.historyItems-filter input', 'C-R-D-X'); + await expectInputValue(page, periodInputsSelectors.fromDateSelector, '2019-08-08'); + await expectInputValue(page, periodInputsSelectors.toDateSelector, '2019-08-10'); + await expectInputValue(page, periodInputsSelectors.fromTimeSelector, '22:00'); + await expectInputValue(page, periodInputsSelectors.toTimeSelector, '21:59'); + }); + + it('should set filters from LhcFillsOverview to the URL', async () => { + const url = 'http://localhost:4000/?page=lhc-fill-overview&filter[beamDuration][operator]=%3D&filter[beamDuration][limit]=00%3A01%3A40&' + + 'filter[runDuration][operator]=%3D&filter[runDuration][limit]=00%3A00%3A00&filter[hasStableBeams]=true&filter[stableBeamsStart][from]=1565251200000&' + + 'filter[stableBeamsStart][to]=1565258400000&filter[stableBeamsEnd][from]=1647907200000&filter[stableBeamsEnd][to]=1647989940000&filter[beamTypes]=p-Pb&filter[schemeName]=Single_12b_8_1024_8_2018'; + + await page.goto(url, { waitUntil: 'load' }); + + const sbEndPopoverTrigger = '.stableBeamsEnd-filter .popover-trigger'; + const sbStartPopoverTrigger = '.stableBeamsStart-filter .popover-trigger'; + const sbStartPopOverSelector = await getPopoverSelector(await page.$(sbStartPopoverTrigger)); + const sbEndPopOverSelector = await getPopoverSelector(await page.$(sbEndPopoverTrigger)); + const filterSchemeNameInputField= '.fillingSchemeName-filter input'; + const { + fromDateSelector: sbStartFromDateSelector, + toDateSelector: sbStartToDateSelector, + fromTimeSelector: sbStartFromTimeSelector, + toTimeSelector: sbStartToTimeSelector + } = getPeriodInputsSelectors(sbStartPopOverSelector); + + const { + fromDateSelector: sbEndFromDateSelector, + toDateSelector: sbEndToDateSelector, + fromTimeSelector: sbEndFromTimeSelector, + toTimeSelector: sbEndToTimeSelector + } = getPeriodInputsSelectors(sbEndPopOverSelector); + + await openFilteringPanel(page); + await expectInputValue(page, '#beam-duration-filter-operand', '00:01:40'); + await expectInputValue(page, '#run-duration-filter-operand', '00:00:00'); + await expectInputValue(page, sbStartFromDateSelector, '2019-08-08'); + await expectInputValue(page, sbStartToDateSelector, '2019-08-08'); + await expectInputValue(page, sbStartFromTimeSelector, '08:00'); + await expectInputValue(page, sbStartToTimeSelector, '10:00'); + await expectInputValue(page, sbEndFromDateSelector, '2022-03-22'); + await expectInputValue(page, sbEndToDateSelector, '2022-03-22'); + await expectInputValue(page, sbEndFromTimeSelector, '00:00'); + await expectInputValue(page, sbEndToTimeSelector, '22:59'); + await expectInputValue(page, filterSchemeNameInputField, 'Single_12b_8_1024_8_2018'); + await page.waitForSelector('#beam-types-checkbox-p-Pb:checked'); + }); + + it('should set filters from runsOverview to the URL', async () => { + const url = 'http://localhost:4000/?page=run-overview&filter[runNumbers]=101&filter[detectors][operator]=and&filter[detectors][values]=ITS&filter[tags][values]=FOOD&' + + 'filter[tags][operation]=and&filter[fillNumbers]=1%2C%203&filter[o2start][from]=1612347060000&filter[o2start][to]=1612357200000&filter[o2end][from]=1612347060000&' + + 'filter[o2end][to]=1612357200000&filter[definitions]=PHYSICS&filter[runDuration][operator]=%3D&filter[runDuration][limit]=90000000' + + '&filter[environmentIds]=Dxi029djX%2C%20TDI59So3d&filter[runTypes]=2&filter[beamModes]=NO%20BEAM&filter[runQualities]=bad&filter[nDetectors][operator]=%3D&' + + 'filter[nDetectors][limit]=1&filter[nEpns][operator]=%3D&filter[nEpns][limit]=10&filter[nFlps][operator]=%3D&filter[nFlps][limit]=10&filter[ctfFileCount][operator]=%3D&' + + 'filter[ctfFileCount][limit]=1&filter[tfFileCount][operator]=%3D&filter[tfFileCount][limit]=1&filter[otherFileCount][operator]=%3D&filter[otherFileCount][limit]=1&' + + 'filter[eorReason][category]=DETECTORS&filter[eorReason][title]=CPV&filter[eorReason][description]=some&filter[magnets][l3]=30003&filter[magnets][dipole]=0&filter[epn]=false&filter[triggerValues]=OFF'; + + await page.goto(url, { waitUntil: 'load' }); + + const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); + const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); + const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); + + const { + fromDateSelector: startFromDateSelector, + toDateSelector: startToDateSelector, + fromTimeSelector: startFromTimeSelector, + toTimeSelector: startToTimeSelector + } = getPeriodInputsSelectors(startPopoverSelector); + + const { + fromDateSelector: endFromDateSelector, + toDateSelector: endToDateSelector, + fromTimeSelector: endFromTimeSelector, + toTimeSelector: endToTimeSelector + } = getPeriodInputsSelectors(endPopoverSelector); + + await openFilteringPanel(page); + await page.waitForSelector('#detector-filter-dropdown-option-ITS:checked'); + await page.waitForSelector('#run-types-dropdown-option-2:checked'); + await page.waitForSelector('#beam-mode-dropdown-option-NO\\ BEAM:checked'); + await page.waitForSelector('#tag-dropdown-option-FOOD:checked'); + await page.waitForSelector('#run-definition-checkbox-PHYSICS:checked'); + await page.waitForSelector('#epnFilterRadioOFF:checked'); + await pressElement(page, '.timeO2Start-filter .popover-trigger'); + await page.waitForSelector('#checkboxes-checkbox-bad:checked'); + await page.waitForSelector('#triggerValue-checkbox-OFF:checked'); + await page.waitForSelector(`${dipolePopoverSelector} .dropdown-option:last-child input:checked`); + await expectInputValue(page, '#duration-operand', '1500'); + await expectInputValue(page, '#runOverviewFilter .runNumbers-textFilter', '101'); + await expectInputValue(page, '.fillNumbers-textFilter', '1, 3'); + await expectInputValue(page, '.environmentIds-textFilter', 'Dxi029djX, TDI59So3d'); + await expectInputValue(page, '#nDetectors-operand', '1'); + await expectInputValue(page, '#nFlps-operand', '10'); + await expectInputValue(page, '#nEpns-operand', '10'); + await expectInputValue(page, '#ctfFileCount-operand', '1'); + await expectInputValue(page, '#tfFileCount-operand', '1'); + await expectInputValue(page, '#otherFileCount-operand', '1'); + await expectInputValue(page, '#eorDescription', 'some'); + await expectInputValue(page, '#eorTitles', 'CPV'); + await expectInputValue(page, '#eorCategories', 'DETECTORS'); + await expectInputValue(page, startFromTimeSelector, '10:11'); + await expectInputValue(page, startToTimeSelector, '13:00'); + await expectInputValue(page, startFromDateSelector, '2021-02-03'); + await expectInputValue(page, startToDateSelector, '2021-02-03'); + await expectInputValue(page, endFromTimeSelector, '10:11'); + await expectInputValue(page, endToTimeSelector, '13:00'); + await expectInputValue(page, endFromDateSelector, '2021-02-03'); + await expectInputValue(page, endToDateSelector, '2021-02-03'); + }); + + it('should set filters from lhcPriodOverview to the URL', async () => { + const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=LHC22a&filter[years][]=2022&filter[pdpBeamTypes][]=PbPb'; + await page.goto(url, { waitUntil: 'load' }); + + await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22a'); + await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(2) input[type=text]', '2022'); + await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(3) input[type=text]', 'PbPb'); + }); + + it('should set filters from qcFlagTypesOverview to the URL', async () => { + const url = 'http://localhost:4000/?page=qc-flag-types-overview&filter[names][]=bad&filter[methods][]=bad&filter[bad]=true'; + await page.goto(url, { waitUntil: 'load' }); + + await expectInputValue(page, '.name-filter input[type=text]', 'bad'); + await expectInputValue(page, '.method-filter input[type=text]', 'bad'); + await page.waitForSelector('#badFilterRadioBad:checked'); + }); + + it('should set filters from runsPerLhcPeriodOverview to the URL', async () => { + const url = 'http://localhost:4000/?page=runs-per-lhc-period&lhcPeriodId=2&filter[runNumbers]=101&filter[fillNumbers]=1%2C%203&filter[o2start][from]=1612347060000&' + + 'filter[o2start][to]=1612357200000&filter[o2end][from]=1612347060000&filter[o2end][to]=1612357200000&filter[magnets][l3]=30003&filter[magnets][dipole]=0&' + + 'filter[muInelasticInteractionRate][operator]=%3D&filter[muInelasticInteractionRate][limit]=100000&filter[inelasticInteractionRateAvg][operator]=%3D&filter[inelasticInteractionRateAvg][limit]=100000'; + await page.goto(url, { waitUntil: 'load' }); + + const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); + const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); + const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); + + const { + fromDateSelector: startFromDateSelector, + toDateSelector: startToDateSelector, + fromTimeSelector: startFromTimeSelector, + toTimeSelector: startToTimeSelector + } = getPeriodInputsSelectors(startPopoverSelector); + + const { + fromDateSelector: endFromDateSelector, + toDateSelector: endToDateSelector, + fromTimeSelector: endFromTimeSelector, + toTimeSelector: endToTimeSelector + } = getPeriodInputsSelectors(endPopoverSelector); + + await expectInputValue(page, '#inelasticInteractionRateAvg-operand', '100000'); + await expectInputValue(page, '#muInelasticInteractionRate-operand', '100000'); + await expectInputValue(page, '#runOverviewFilter .runNumbers-textFilter', '101'); + await expectInputValue(page, '.fillNumbers-textFilter', '1, 3'); + await expectInputValue(page, startFromTimeSelector, '10:11'); + await expectInputValue(page, startToTimeSelector, '13:00'); + await expectInputValue(page, startFromDateSelector, '2021-02-03'); + await expectInputValue(page, startToDateSelector, '2021-02-03'); + await expectInputValue(page, endFromTimeSelector, '10:11'); + await expectInputValue(page, endToTimeSelector, '13:00'); + await expectInputValue(page, endFromDateSelector, '2021-02-03'); + await expectInputValue(page, endToDateSelector, '2021-02-03'); + await page.waitForSelector(`${dipolePopoverSelector} .dropdown-option:last-child input:checked`); + }); + + it('should set filters from DataPassesPerLhcPeriodOverview to the URL', async () => { + const url = 'http://localhost:4000/?page=data-passes-per-lhc-period-overview&lhcPeriodId=2&filter[names][]=LHC22b_apass1&filter[permittedNonPhysicsNames]=test'; + await page.goto(url, { waitUntil: 'load' }); + + await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1'); + await page.waitForSelector('#checkboxes-checkbox-test:checked'); + }); + + it('should set filters from DataPassesPerSimulationPassOverview to the URL', async () => { + const url = 'http://localhost:4000/?page=data-passes-per-simulation-pass-overview&simulationPassId=1&filter[names][]=LHC22b_apass1&filter[permittedNonPhysicsNames]=test'; + await page.goto(url, { waitUntil: 'load' }); + + await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1'); + await page.waitForSelector('#checkboxes-checkbox-test:checked'); + }); + + it('should set filters from AnchoredSimulationPassesOverview to the URL', async () => { + const url = 'http://localhost:4000/?page=anchored-simulation-passes-overview&dataPassId=1&filter[names][]=LHC23k6c'; + await page.goto(url, { waitUntil: 'load' }); + + await expectInputValue(page, '.name-filter input', 'LHC23k6c'); + }); + + it('should set filters from RunsPerSimulationPass to the URL', async () => { + const url = 'http://localhost:4000/?page=runs-per-simulation-pass&simulationPassId=2&filter[o2start][from]=1612347060000&' + + 'filter[o2start][to]=1612357200000&filter[o2end][from]=1612347060000&filter[o2end][to]=1612357200000&' + + 'filter[magnets][l3]=30003&filter[magnets][dipole]=0&filter[inelasticInteractionRateAtStart][operator]=%3D&' + + 'filter[inelasticInteractionRateAtStart][limit]=1&filter[inelasticInteractionRateAtMid][operator]=%3D&' + + 'filter[inelasticInteractionRateAtMid][limit]=1&filter[inelasticInteractionRateAtEnd][operator]=%3D&' + + 'filter[inelasticInteractionRateAtEnd][limit]=1&filter[detectorsQcNotBadFraction][mcReproducibleAsNotBad]=true&' + + 'filter[detectorsQcNotBadFraction][_20][operator]=%3D&filter[detectorsQcNotBadFraction][_20][limit]=0.01&' + + 'filter[detectorsQcNotBadFraction][_17][operator]=%3D&filter[detectorsQcNotBadFraction][_17][limit]=0.01'; + + await page.goto(url, { waitUntil: 'load' }); + + const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); + const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); + const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); + + const { + fromDateSelector: startFromDateSelector, + toDateSelector: startToDateSelector, + fromTimeSelector: startFromTimeSelector, + toTimeSelector: startToTimeSelector + } = getPeriodInputsSelectors(startPopoverSelector); + + const { + fromDateSelector: endFromDateSelector, + toDateSelector: endToDateSelector, + fromTimeSelector: endFromTimeSelector, + toTimeSelector: endToTimeSelector + } = getPeriodInputsSelectors(endPopoverSelector); + + await openFilteringPanel(page); + await expectInputValue(page, '.inelasticInteractionRateAtMid-filter input', '1'); + await expectInputValue(page, '.inelasticInteractionRateAtEnd-filter input', '1'); + await expectInputValue(page, '.inelasticInteractionRateAtStart-filter input', '1'); + await expectInputValue(page, startFromTimeSelector, '10:11'); + await expectInputValue(page, startToTimeSelector, '13:00'); + await expectInputValue(page, startFromDateSelector, '2021-02-03'); + await expectInputValue(page, startToDateSelector, '2021-02-03'); + await expectInputValue(page, endFromTimeSelector, '10:11'); + await expectInputValue(page, endToTimeSelector, '13:00'); + await expectInputValue(page, endFromDateSelector, '2021-02-03'); + await expectInputValue(page, endToDateSelector, '2021-02-03'); + await page.waitForSelector(`${dipolePopoverSelector} .dropdown-option:last-child input:checked`); + await page.waitForSelector('#mcReproducibleAsNotBadToggle input:checked'); + + + // These two are detectorQCNotBadFraction[_id] filters. There are a dozen more, but they are all identical hence why only these were tested + await expectInputValue(page, '.QC-SPECIFIC-filter input', '1'); + await expectInputValue(page, '.ACO-filter input', '1'); + }); + + it('should set filters from RunsPerSimulationPass to the URL', async () => { + const url = 'http://localhost:4000/?page=runs-per-data-pass&dataPassId=1&filter[detectors][operator]=and&filter[detectors][values]=ITS&' + + 'filter[tags][values]=FOOD&filter[tags][operation]=and&filter[o2start][from]=1612347060000&filter[o2start][to]=1612357200000&' + + 'filter[o2end][from]=1612347060000&filter[o2end][to]=1612357200000&filter[runDuration][operator]=%3D&filter[runDuration][limit]=90000000&' + + 'filter[magnets][l3]=30003&filter[magnets][dipole]=0&filter[muInelasticInteractionRate][operator]=%3D&filter[muInelasticInteractionRate][limit]=1&' + + 'filter[inelasticInteractionRateAvg][operator]=%3D&filter[inelasticInteractionRateAvg][limit]=1&filter[detectorsQcNotBadFraction][mcReproducibleAsNotBad]=true&' + + 'filter[detectorsQcNotBadFraction][_20][operator]=%3D&filter[detectorsQcNotBadFraction][_20][limit]=0.01&filter[detectorsQcNotBadFraction][_17][operator]=%3D&' + + 'filter[detectorsQcNotBadFraction][_17][limit]=0.01&filter[gaq][notBadFraction][operator]=%3D&filter[gaq][notBadFraction][limit]=0.01&filter[gaq][mcReproducibleAsNotBad]=true'; + + await page.goto(url, { waitUntil: 'load' }); + + const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); + const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); + const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); + + const { + fromDateSelector: startFromDateSelector, + toDateSelector: startToDateSelector, + fromTimeSelector: startFromTimeSelector, + toTimeSelector: startToTimeSelector + } = getPeriodInputsSelectors(startPopoverSelector); + + const { + fromDateSelector: endFromDateSelector, + toDateSelector: endToDateSelector, + fromTimeSelector: endFromTimeSelector, + toTimeSelector: endToTimeSelector + } = getPeriodInputsSelectors(endPopoverSelector); + + await openFilteringPanel(page); + await expectInputValue(page, startFromTimeSelector, '10:11'); + await expectInputValue(page, startToTimeSelector, '13:00'); + await expectInputValue(page, startFromDateSelector, '2021-02-03'); + await expectInputValue(page, startToDateSelector, '2021-02-03'); + await expectInputValue(page, endFromTimeSelector, '10:11'); + await expectInputValue(page, endToTimeSelector, '13:00'); + await expectInputValue(page, endFromDateSelector, '2021-02-03'); + await expectInputValue(page, endToDateSelector, '2021-02-03'); + await expectInputValue(page, '#duration-operand', '1500'); + await expectInputValue(page, '.muInelasticInteractionRate-filter input', '1'); + await expectInputValue(page, '.inelasticInteractionRateAvg-filter input', '1'); + await expectInputValue(page, '.globalAggregatedQuality-filter input', '1'); + await fillInput(page, '.ACO-filter input', '1', ['change']); + await fillInput(page, '.QC-SPECIFIC-filter input', '1', ['change']); + + await page.waitForSelector('#detector-filter-dropdown-option-ITS'); + await page.waitForSelector('#tag-dropdown-option-FOOD'); + await page.waitForSelector(`${dipolePopoverSelector} .dropdown-option:last-child input:checked`); + await page.waitForSelector('#mcReproducibleAsNotBadToggle input:checked'); + }); + + after(async () => await defaultAfter(page, browser)); +} diff --git a/test/public/dataPasses/overviewPerLhcPeriod.test.js b/test/public/dataPasses/overviewPerLhcPeriod.test.js index 4ab08e8de0..a6215dd989 100644 --- a/test/public/dataPasses/overviewPerLhcPeriod.test.js +++ b/test/public/dataPasses/overviewPerLhcPeriod.test.js @@ -164,7 +164,7 @@ module.exports = () => { it('should successfully apply data pass name filter', async () => { await pressElement(page, '#openFilterToggle'); - await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1', ['change']); + await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1'); await expectColumnValues(page, 'name', ['deleted\nLHC22b_apass1\nSkimmable']); diff --git a/test/public/dataPasses/overviewPerSimulationPass.test.js b/test/public/dataPasses/overviewPerSimulationPass.test.js index 27b6c2d2c9..188ec17dc2 100644 --- a/test/public/dataPasses/overviewPerSimulationPass.test.js +++ b/test/public/dataPasses/overviewPerSimulationPass.test.js @@ -113,7 +113,7 @@ module.exports = () => { it('should successfully apply data pass name filter', async () => { await pressElement(page, '#openFilterToggle'); - await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1', ['change']); + await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1'); await expectColumnValues(page, 'name', ['deleted\nLHC22b_apass1\nSkimmable']); await pressElement(page, '#reset-filters', true); diff --git a/test/public/defaults.js b/test/public/defaults.js index 0523d8462b..ed6726bece 100644 --- a/test/public/defaults.js +++ b/test/public/defaults.js @@ -667,14 +667,24 @@ module.exports.checkColumnBalloon = async (page, rowIndex, columnIndex) => { * @return {Promise} resolves once the value has been typed */ module.exports.fillInput = async (page, inputSelector, value, events = ['input']) => { - await page.waitForSelector(inputSelector); - await page.evaluate((inputSelector, value, events) => { + await page.waitForFunction((inputSelector, value, events) => { const element = document.querySelector(inputSelector); + + if (!element) { + return false; + } + element.value = value; + for (const eventKey of events) { element.dispatchEvent(new Event(eventKey, { bubbles: true })); } - }, inputSelector, value, events); + + return true; + }, + {}, + inputSelector, value, events + ); }; /** diff --git a/test/public/index.js b/test/public/index.js index 8ebdc23e68..293d9a9e94 100644 --- a/test/public/index.js +++ b/test/public/index.js @@ -27,9 +27,11 @@ const ComponentsSuite = require('./components'); const SimulationPassesSuite = require('./simulationPasses'); const QcFlagTypesSuite = require('./qcFlagTypes'); const QcFlagsSuite = require('./qcFlags'); +const FilterSuite = require('./Filters'); module.exports = () => { describe('Components', ComponentsSuite); + describe('Filters', FilterSuite); describe('LhcPeriods', LhcPeriodsSuite); describe('LhcFills', LhcFillsSuite); describe('Logs', LogsSuite); diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index c16cc0d251..66333186d8 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -601,7 +601,7 @@ module.exports = () => { it('Should successfully filter runs by their trigger value', async () => { await navigateToRunsOverview(page); - const filterInputSelectorPrefix = '#triggerValueCheckbox'; + const filterInputSelectorPrefix = '#triggerValue-checkbox-'; const offFilterSelector = `${filterInputSelectorPrefix}OFF`; const ltuFilterSelector = `${filterInputSelectorPrefix}LTU`; diff --git a/test/public/runs/runsPerDataPass.overview.test.js b/test/public/runs/runsPerDataPass.overview.test.js index 70f8a7ac0a..4d1edbb4d6 100644 --- a/test/public/runs/runsPerDataPass.overview.test.js +++ b/test/public/runs/runsPerDataPass.overview.test.js @@ -516,8 +516,6 @@ module.exports = () => { it('should successfully apply gaqNotBadFraction filters', async () => { await navigateToRunsPerDataPass(page, 2, 1, 3); - await pressElement(page, '#openFilterToggle', true); - await page.waitForSelector('#gaqNotBadFraction-operator'); await page.select('#gaqNotBadFraction-operator', '<='); await fillInput(page, '#gaqNotBadFraction-operand', '80', ['change']); @@ -526,7 +524,6 @@ module.exports = () => { await pressElement(page, '#mcReproducibleAsNotBadToggle input', true); await expectColumnValues(page, 'runNumber', []); - await pressElement(page, '#openFilterToggle', true); await pressElement(page, '#reset-filters', true); await expectColumnValues(page, 'runNumber', ['108', '107', '106']); }); @@ -535,12 +532,8 @@ module.exports = () => { await page.waitForSelector('#detectorsQc-for-1-notBadFraction-operator'); await page.select('#detectorsQc-for-1-notBadFraction-operator', '<='); await fillInput(page, '#detectorsQc-for-1-notBadFraction-operand', '90', ['change']); - await expectColumnValues(page, 'runNumber', ['106']); - - await pressElement(page, '#mcReproducibleAsNotBadToggle input', true); await expectColumnValues(page, 'runNumber', ['107', '106']); - await pressElement(page, '#openFilterToggle', true); await pressElement(page, '#reset-filters', true); await expectColumnValues(page, 'runNumber', ['108', '107', '106']); });