From e3a78a5a7b98d4f7f179e6dd00a6aaff0de1f18f Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:39:23 +0100 Subject: [PATCH 01/15] [O2B-532] Create a reusable filtering system and apply it to run tags filter --- .../components/Filters/common/FilterModel.js | 3 +- .../Filters/common/FilteringModel.js | 106 ++++++++++++++++++ .../Filters/common/TagFilterModel.js | 43 +++---- .../common/filters/remoteDataTagFilter.js | 34 ------ .../Logs/ActiveColumns/logsActiveColumns.js | 6 + .../views/Logs/Overview/LogsOverviewModel.js | 4 +- .../Runs/ActiveColumns/runsActiveColumns.js | 8 +- .../views/Runs/Overview/RunsOverviewModel.js | 53 ++++----- 8 files changed, 173 insertions(+), 84 deletions(-) create mode 100644 lib/public/components/Filters/common/FilteringModel.js delete mode 100644 lib/public/components/Filters/common/filters/remoteDataTagFilter.js diff --git a/lib/public/components/Filters/common/FilterModel.js b/lib/public/components/Filters/common/FilterModel.js index a0c3b17193..79bc54e501 100644 --- a/lib/public/components/Filters/common/FilterModel.js +++ b/lib/public/components/Filters/common/FilterModel.js @@ -14,6 +14,7 @@ import { Observable } from '/js/src/index.js'; /** * Model storing the state of a given filter + * * @abstract */ export class FilterModel extends Observable { @@ -49,7 +50,7 @@ export class FilterModel extends Observable { /** * Returns the normalized value of the filter, that can be used as URL parameter * - * @return {string|number|object|array|null} the normalized value + * @return {string|number|object|string[]|number[]|null} the normalized value * @abstract */ get normalized() { diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js new file mode 100644 index 0000000000..ab39d071a2 --- /dev/null +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -0,0 +1,106 @@ +/** + * @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 { Observable } from '/js/src/index.js'; + +/** + * @typedef FilteringItem + * @property {FilterModel} model the model of the filter + * @property {string} label the label of the filter + */ + +/** + * Model representing a filtering system, including filter inputs visibility, filters values and so on + */ +export class FilteringModel extends Observable { + /** + * Constructor + * + * @param {Object} filters the filters with their label and model + */ + constructor(filters) { + super(); + + this._visualChange$ = new Observable(); + + this._filters = filters; + this._filterModels = Object.values(filters).map(({ model }) => model); + for (const model of this._filterModels) { + model.bubbleTo(this); + model.visualChange$?.bubbleTo(this._visualChange$); + } + } + + /** + * Reset the filters + * + * @return {void} + */ + reset() { + for (const model of this._filterModels) { + model.reset(); + } + } + + /** + * Returns the normalized value of all the filters, without null values + * + * @return {object} the normalized values + */ + get normalized() { + const ret = {}; + for (const [filterKey, { model: filter }] of Object.entries(this._filters)) { + if (filter && !filter.isEmpty) { + ret[filterKey] = filter.normalized; + } + } + return ret; + } + + /** + * States if there is currently at least one filter active + * + * @return {boolean} true if at least one filter is active + */ + isAnyFilterActive() { + for (const model of this._filterModels) { + if (!model.isEmpty) { + return true; + } + } + return false; + } + + /** + * Returns the observable notified any time there is a visual change which has no impact on the actual filtering + * + * @return {Observable} the filters visibility observable + */ + get visualChange$() { + return this._visualChange$; + } + + /** + * Return the filter model for a given key + * + * @param {string} key the key of the filtering model + * @return {FilteringModel} the filtering model + */ + get(key) { + if (!(key in this._filters)) { + throw new Error(`No filter found with key ${key}`); + } + + return this._filters[key].model; + } +} diff --git a/lib/public/components/Filters/common/TagFilterModel.js b/lib/public/components/Filters/common/TagFilterModel.js index cd929076c3..c56b91da69 100644 --- a/lib/public/components/Filters/common/TagFilterModel.js +++ b/lib/public/components/Filters/common/TagFilterModel.js @@ -10,14 +10,14 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ -import { Observable } from '/js/src/index.js'; import { CombinationOperatorChoiceModel } from './CombinationOperatorChoiceModel.js'; import { TagSelectionDropdownModel } from '../../tag/TagSelectionDropdownModel.js'; +import { FilterModel } from './FilterModel.js'; /** * Model to handle the state of a tags filter */ -export class TagFilterModel extends Observable { +export class TagFilterModel extends FilterModel { /** * Constructor * @@ -28,28 +28,38 @@ export class TagFilterModel extends Observable { super(); this._selectionModel = new TagSelectionDropdownModel({ includeArchived: true }); this._selectionModel.bubbleTo(this); + this._selectionModel.visualChange$.bubbleTo(this.visualChange$); this._combinationOperatorModel = new CombinationOperatorChoiceModel(operators); this._combinationOperatorModel.bubbleTo(this); } + // eslint-disable-next-line valid-jsdoc /** - * States if the filter has no tags selected - * - * @return {boolean} true if no tags are selected + * @inheritDoc */ - isEmpty() { + reset() { + this._selectionModel.reset(); + this._combinationOperatorModel.reset(); + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + get isEmpty() { return this.selected.length === 0; } + // eslint-disable-next-line valid-jsdoc /** - * Reset the model to its default state - * - * @return {void} + * @inheritDoc */ - reset() { - this._selectionModel.reset(); - this._combinationOperatorModel.reset(); + get normalized() { + return { + values: this.selected.join(), + operation: this.combinationOperator, + }; } /** @@ -87,13 +97,4 @@ export class TagFilterModel extends Observable { get combinationOperator() { return this._combinationOperatorModel.current; } - - /** - * Returns an observable notified any time a visual change occurs that has no impact on the actual selection - * - * @return {Observable} the visual change observable - */ - get visualChange$() { - return this._selectionModel.visualChange$; - } } diff --git a/lib/public/components/Filters/common/filters/remoteDataTagFilter.js b/lib/public/components/Filters/common/filters/remoteDataTagFilter.js deleted file mode 100644 index 89f50cb4f4..0000000000 --- a/lib/public/components/Filters/common/filters/remoteDataTagFilter.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import spinner from '../../../common/spinner.js'; -import { tagFilter } from './tagFilter.js'; - -/** - * Return a filter component to apply filtering on a remote data list of tags, handling each possible remote data status - * - * @param {RemoteData} tagsRemoteData the remote data tags list - * @param {TagFilterModel} filter The model storing the filter's state - * - * @return {vnode|vnode[]|null} A collection of checkboxes to toggle table rows by tags - */ -export const remoteDataTagFilter = (tagsRemoteData, filter) => tagsRemoteData.match({ - NotAsked: () => null, - Loading: () => spinner({ - size: 2, - justify: 'left', - absolute: false, - }), - Success: (tags) => tagFilter(tags, filter), - Failure: () => null, -}); diff --git a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js index 0216b6ac33..c43b04b917 100644 --- a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js +++ b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js @@ -134,6 +134,12 @@ export const logsActiveColumns = { sortable: true, size: 'w-15', format: (tags) => formatTagsList(tags), + + /** + * Tag filter component + * @param {LogsOverviewModel} logsModel the log model + * @return {Component} the filter component + */ filter: (logsModel) => tagFilter(logsModel.listingTagsFilterModel), balloon: true, profiles: [profiles.none, 'embeded'], diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index 2972678393..65bbebe2f1 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -158,7 +158,7 @@ export class LogsOverviewModel extends Observable { || !this._authorFilter.isEmpty || this.createdFilterFrom !== '' || this.createdFilterTo !== '' - || !this.listingTagsFilterModel.isEmpty() + || !this.listingTagsFilterModel.isEmpty || this.runFilterValues.length !== 0 || this.environmentFilterValues.length !== 0 || this.lhcFillFilterValues.length !== 0 @@ -395,7 +395,7 @@ export class LogsOverviewModel extends Observable { 'filter[created][to]': new Date(`${this.createdFilterTo.replace(/\//g, '-')}T23:59:59.999`).getTime(), }, - ...!this.listingTagsFilterModel.isEmpty() && { + ...!this.listingTagsFilterModel.isEmpty && { 'filter[tags][values]': this.listingTagsFilterModel.selected.join(), 'filter[tags][operation]': this.listingTagsFilterModel.combinationOperator, }, diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 0ec18fb4b0..cfb1212eb8 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -134,7 +134,13 @@ export const runsActiveColumns = { classes: 'w-5 f6', format: (tags) => formatTagsList(tags), exportFormat: (tags) => tags?.length ? tags.map(({ text }) => text).join('-') : '-', - filter: (runModel) => tagFilter(runModel.listingTagsFilterModel), + + /** + * Tag filter component + * @param {RunsOverviewModel} runModel the runs overview model + * @return {Component} the filter component + */ + filter: (runModel) => tagFilter(runModel.filteringModel.get('tags')), balloon: (tags) => tags && tags.length > 0, }, fillNumber: { diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index 25084a524e..31fcf79143 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -26,6 +26,8 @@ import { TimeRangeInputModel } from '../../../components/Filters/common/filters/ import { FloatComparisonFilterModel } from '../../../components/Filters/common/filters/FloatComparisonFilterModel.js'; import { detectorsProvider } from '../../../services/detectors/detectorsProvider.js'; import { AliceL3AndDipoleFilteringModel } from '../../../components/Filters/RunsFilter/AliceL3AndDipoleFilteringModel.js'; +import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; +import { buildUrl } from '../../../utilities/fetch/buildUrl.js'; /** * Model representing handlers for runs page @@ -40,13 +42,16 @@ export class RunsOverviewModel extends OverviewPageModel { constructor(model) { super(); - this._listingTagsFilterModel = new TagFilterModel([ - CombinationOperator.AND, - CombinationOperator.OR, - CombinationOperator.NONE_OF, - ]); - this._listingTagsFilterModel.observe(() => this._applyFilters(true)); - this._listingTagsFilterModel.visualChange$.bubbleTo(this); + this._filteringModel = new FilteringModel({ + tags: { + model: new TagFilterModel([ + CombinationOperator.AND, + CombinationOperator.OR, + CombinationOperator.NONE_OF, + ]), + label: 'Tags', + }, + }); this._detectorsFilterModel = new DetectorsFilterModel(detectorsProvider.dataTaking$); this._detectorsFilterModel.observe(() => this._applyFilters(true)); @@ -92,6 +97,9 @@ export class RunsOverviewModel extends OverviewPageModel { this._inelasticInteractionRateAtEndFilterModel.observe(() => this._applyFilters()); this._inelasticInteractionRateAtEndFilterModel.visualChange$.bubbleTo(this); + this._filteringModel.observe(() => this._applyFilters(true)); + this._filteringModel.visualChange$.bubbleTo(this); + // Export items this._allRuns = RemoteData.NotAsked(); @@ -107,8 +115,7 @@ export class RunsOverviewModel extends OverviewPageModel { * @inheritdoc */ getRootEndpoint() { - const paramsString = new URLSearchParams(this._getFilterQueryParams()).toString(); - return `/api/runs?${paramsString}`; + return buildUrl('/api/runs', { ...this._getFilterQueryParams(), ...{ filter: this.filteringModel.normalized } }); } // eslint-disable-next-line valid-jsdoc @@ -176,6 +183,7 @@ export class RunsOverviewModel extends OverviewPageModel { */ reset(fetch = true) { super.reset(); + this._filteringModel.reset(); this.resetFiltering(fetch); } @@ -190,7 +198,6 @@ export class RunsOverviewModel extends OverviewPageModel { this._runDefinitionFilter = []; this._detectorsFilterModel.reset(); - this._listingTagsFilterModel.reset(); this._listingRunTypesFilterModel.reset(); this._eorReasonsFilterModel.reset(); this._o2StartFilterModel.reset(); @@ -240,13 +247,13 @@ export class RunsOverviewModel extends OverviewPageModel { * @return {Boolean} If any filter is active */ isAnyFilterActive() { - return this.runFilterValues !== '' + return this._filteringModel.isAnyFilterActive() + || this.runFilterValues !== '' || this._runDefinitionFilter.length > 0 || !this._eorReasonsFilterModel.isEmpty() || !this._o2StartFilterModel.isEmpty || !this._o2StopFilterModel.isEmpty || !this._detectorsFilterModel.isEmpty() - || !this._listingTagsFilterModel.isEmpty() || this._listingRunTypesFilterModel.selected.length !== 0 || this._aliceL3AndDipoleCurrentFilter.selected.length !== 0 || this._fillNumbersFilter !== '' @@ -269,6 +276,15 @@ export class RunsOverviewModel extends OverviewPageModel { || this._inelasticInteractionRateAtEndFilterModel.isEmpty; } + /** + * Return the filtering model + * + * @return {FilteringModel} the filtering model + */ + get filteringModel() { + return this._filteringModel; + } + /** * Set the export type parameter of the current export being created * @param {string} selectedExportType Received string from the view @@ -656,15 +672,6 @@ export class RunsOverviewModel extends OverviewPageModel { this._applyFilters(); } - /** - * Return the model handling the filtering on tags - * - * @return {TagFilterModel} the filtering model - */ - get listingTagsFilterModel() { - return this._listingTagsFilterModel; - } - /** * Returns the model handling the filtering on detectors * @@ -825,10 +832,6 @@ export class RunsOverviewModel extends OverviewPageModel { ...this._runDefinitionFilter.length > 0 && { 'filter[definitions]': this._runDefinitionFilter.join(','), }, - ...!this._listingTagsFilterModel.isEmpty() && { - 'filter[tags][values]': this._listingTagsFilterModel.selected.join(), - 'filter[tags][operation]': this._listingTagsFilterModel.combinationOperator, - }, ...this._fillNumbersFilter && { 'filter[fillNumbers]': this._fillNumbersFilter, }, From 07a4cdc8656cacd16fe5f7d2c7ca5ccd3ed3f596 Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:23:01 +0100 Subject: [PATCH 02/15] Minor improvements --- .../components/Filters/common/FilterModel.js | 15 +++++++++++++++ .../components/Filters/common/TagFilterModel.js | 5 ++--- .../views/Runs/ActiveColumns/runsActiveColumns.js | 3 ++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/public/components/Filters/common/FilterModel.js b/lib/public/components/Filters/common/FilterModel.js index 79bc54e501..cc7badb53c 100644 --- a/lib/public/components/Filters/common/FilterModel.js +++ b/lib/public/components/Filters/common/FilterModel.js @@ -65,4 +65,19 @@ export class FilterModel extends Observable { get visualChange$() { return this._visualChange$; } + + /** + * Utility function to register a filter model as sub-filter model + * + * TODO for now this function simply propagate observable notifications, in the future this should register submodels with a key and allow + * automated normalization (recursive filter model) + * + * @param {object} subModel the submodel to register + * @return {void} + * @protected + */ + _addSubmodel(subModel) { + subModel.bubbleTo(this); + subModel?.visualChange$.bubbleTo(this._visualChange$); + } } diff --git a/lib/public/components/Filters/common/TagFilterModel.js b/lib/public/components/Filters/common/TagFilterModel.js index c56b91da69..90213b329a 100644 --- a/lib/public/components/Filters/common/TagFilterModel.js +++ b/lib/public/components/Filters/common/TagFilterModel.js @@ -27,11 +27,10 @@ export class TagFilterModel extends FilterModel { constructor(operators) { super(); this._selectionModel = new TagSelectionDropdownModel({ includeArchived: true }); - this._selectionModel.bubbleTo(this); - this._selectionModel.visualChange$.bubbleTo(this.visualChange$); + this._addSubmodel(this._selectionModel); this._combinationOperatorModel = new CombinationOperatorChoiceModel(operators); - this._combinationOperatorModel.bubbleTo(this); + this._addSubmodel(this._combinationOperatorModel); } // eslint-disable-next-line valid-jsdoc diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index cfb1212eb8..17edd06250 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -136,7 +136,8 @@ export const runsActiveColumns = { exportFormat: (tags) => tags?.length ? tags.map(({ text }) => text).join('-') : '-', /** - * Tag filter component + * Tags filter component + * * @param {RunsOverviewModel} runModel the runs overview model * @return {Component} the filter component */ From 4129c8be7e660a695a8909166bd86bd4599890a9 Mon Sep 17 00:00:00 2001 From: martinboulais <31805063+martinboulais@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:27:46 +0100 Subject: [PATCH 03/15] Fix reset --- .../Filters/common/FilteringModel.js | 16 ++--- lib/public/views/Home/Overview/HomePage.js | 3 +- .../views/Runs/Overview/RunsOverviewModel.js | 68 +++++++++---------- 3 files changed, 40 insertions(+), 47 deletions(-) diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index ab39d071a2..9eb5f9cdb9 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -13,12 +13,6 @@ import { Observable } from '/js/src/index.js'; -/** - * @typedef FilteringItem - * @property {FilterModel} model the model of the filter - * @property {string} label the label of the filter - */ - /** * Model representing a filtering system, including filter inputs visibility, filters values and so on */ @@ -26,7 +20,7 @@ export class FilteringModel extends Observable { /** * Constructor * - * @param {Object} filters the filters with their label and model + * @param {Object} filters the filters with their label and model */ constructor(filters) { super(); @@ -34,7 +28,7 @@ export class FilteringModel extends Observable { this._visualChange$ = new Observable(); this._filters = filters; - this._filterModels = Object.values(filters).map(({ model }) => model); + this._filterModels = Object.values(filters); for (const model of this._filterModels) { model.bubbleTo(this); model.visualChange$?.bubbleTo(this._visualChange$); @@ -59,7 +53,7 @@ export class FilteringModel extends Observable { */ get normalized() { const ret = {}; - for (const [filterKey, { model: filter }] of Object.entries(this._filters)) { + for (const [filterKey, filter] of Object.entries(this._filters)) { if (filter && !filter.isEmpty) { ret[filterKey] = filter.normalized; } @@ -94,13 +88,13 @@ export class FilteringModel extends Observable { * Return the filter model for a given key * * @param {string} key the key of the filtering model - * @return {FilteringModel} the filtering model + * @return {FilterModel} the filtering model */ get(key) { if (!(key in this._filters)) { throw new Error(`No filter found with key ${key}`); } - return this._filters[key].model; + return this._filters[key]; } } diff --git a/lib/public/views/Home/Overview/HomePage.js b/lib/public/views/Home/Overview/HomePage.js index d57038c016..9a50ca2597 100644 --- a/lib/public/views/Home/Overview/HomePage.js +++ b/lib/public/views/Home/Overview/HomePage.js @@ -25,7 +25,8 @@ const MIN_ROWS = 5; /** * Home Page component - * @param {Object} model global model + * + * @param {Model} model global model * @return {Component} Return the component of the home page */ export const HomePage = ({ home: { logsOverviewModel, runsOverviewModel, lhcFillsOverviewModel } }) => { diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index 31fcf79143..1538f88ef1 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -43,14 +43,11 @@ export class RunsOverviewModel extends OverviewPageModel { super(); this._filteringModel = new FilteringModel({ - tags: { - model: new TagFilterModel([ - CombinationOperator.AND, - CombinationOperator.OR, - CombinationOperator.NONE_OF, - ]), - label: 'Tags', - }, + tags: new TagFilterModel([ + CombinationOperator.AND, + CombinationOperator.OR, + CombinationOperator.NONE_OF, + ]), }); this._detectorsFilterModel = new DetectorsFilterModel(detectorsProvider.dataTaking$); @@ -183,7 +180,6 @@ export class RunsOverviewModel extends OverviewPageModel { */ reset(fetch = true) { super.reset(); - this._filteringModel.reset(); this.resetFiltering(fetch); } @@ -193,6 +189,8 @@ export class RunsOverviewModel extends OverviewPageModel { * @return {void} */ resetFiltering(fetch = true) { + this._filteringModel.reset(); + this.runFilterOperation = 'AND'; this.runFilterValues = ''; this._runDefinitionFilter = []; @@ -248,32 +246,32 @@ export class RunsOverviewModel extends OverviewPageModel { */ isAnyFilterActive() { return this._filteringModel.isAnyFilterActive() - || this.runFilterValues !== '' - || this._runDefinitionFilter.length > 0 - || !this._eorReasonsFilterModel.isEmpty() - || !this._o2StartFilterModel.isEmpty - || !this._o2StopFilterModel.isEmpty - || !this._detectorsFilterModel.isEmpty() - || this._listingRunTypesFilterModel.selected.length !== 0 - || this._aliceL3AndDipoleCurrentFilter.selected.length !== 0 - || this._fillNumbersFilter !== '' - || this._runDurationFilter !== null - || this._lhcPeriodsFilter !== null - || this.environmentIdsFilter !== '' - || this.runQualitiesFilters.length !== 0 - || this._triggerValuesFilters.size !== 0 - || this.nDetectorsFilter !== null - || this._nEpnsFilter !== null - || this.nFlpsFilter !== null - || this.ddflpFilter !== '' - || this.dcsFilter !== '' - || this.epnFilter !== '' - || this._odcTopologyFullNameFilter !== '' - || this._muInelasticInteractionRateFilterModel.isEmpty - || this._inelasticInteractionRateAvgFilterModel.isEmpty - || this._inelasticInteractionRateAtStartFilterModel.isEmpty - || this._inelasticInteractionRateAtMidFilterModel.isEmpty - || this._inelasticInteractionRateAtEndFilterModel.isEmpty; + || this.runFilterValues !== '' + || this._runDefinitionFilter.length > 0 + || !this._eorReasonsFilterModel.isEmpty() + || !this._o2StartFilterModel.isEmpty + || !this._o2StopFilterModel.isEmpty + || !this._detectorsFilterModel.isEmpty() + || this._listingRunTypesFilterModel.selected.length !== 0 + || this._aliceL3AndDipoleCurrentFilter.selected.length !== 0 + || this._fillNumbersFilter !== '' + || this._runDurationFilter !== null + || this._lhcPeriodsFilter !== null + || this.environmentIdsFilter !== '' + || this.runQualitiesFilters.length !== 0 + || this._triggerValuesFilters.size !== 0 + || this.nDetectorsFilter !== null + || this._nEpnsFilter !== null + || this.nFlpsFilter !== null + || this.ddflpFilter !== '' + || this.dcsFilter !== '' + || this.epnFilter !== '' + || this._odcTopologyFullNameFilter !== '' + || this._muInelasticInteractionRateFilterModel.isEmpty + || this._inelasticInteractionRateAvgFilterModel.isEmpty + || this._inelasticInteractionRateAtStartFilterModel.isEmpty + || this._inelasticInteractionRateAtMidFilterModel.isEmpty + || this._inelasticInteractionRateAtEndFilterModel.isEmpty; } /** From f384b7b849e26fe1aa07c4f20c29fd4409d26540 Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:52:32 +0100 Subject: [PATCH 04/15] Encode URI when it makes sense --- lib/public/views/Runs/Overview/RunsOverviewModel.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index 1538f88ef1..a16723d996 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -824,7 +824,7 @@ export class RunsOverviewModel extends OverviewPageModel { }, ...!this._eorReasonsFilterModel.isEmpty() && this._eorReasonsFilterModel.getFilterQueryParams(), ...!this._detectorsFilterModel.isEmpty() && { - 'filter[detectors][operator]': this._detectorsFilterModel.combinationOperator, + 'filter[detectors][operator]': encodeURIComponent(this._detectorsFilterModel.combinationOperator), ...!this._detectorsFilterModel.isNone() && { 'filter[detectors][values]': this._detectorsFilterModel.selected.join() }, }, ...this._runDefinitionFilter.length > 0 && { @@ -846,7 +846,7 @@ export class RunsOverviewModel extends OverviewPageModel { 'filter[o2end][to]': this._o2StopFilterModel.normalized.to, }, ...this._runDurationFilter && this._runDurationFilter.limit !== null && { - 'filter[runDuration][operator]': this._runDurationFilter.operator, + 'filter[runDuration][operator]': encodeURIComponent(this._runDurationFilter.operator), // Convert filter to milliseconds 'filter[runDuration][limit]': this._runDurationFilter.limit * 60 * 1000, }, @@ -863,15 +863,15 @@ export class RunsOverviewModel extends OverviewPageModel { 'filter[triggerValues]': Array.from(this._triggerValuesFilters).join(), }, ...this.nDetectorsFilter && this.nDetectorsFilter.limit !== null && { - 'filter[nDetectors][operator]': this.nDetectorsFilter.operator, + 'filter[nDetectors][operator]': encodeURIComponent(this.nDetectorsFilter.operator), 'filter[nDetectors][limit]': this.nDetectorsFilter.limit, }, ...this.nFlpsFilter && this.nFlpsFilter.limit !== null && { - 'filter[nFlps][operator]': this.nFlpsFilter.operator, + 'filter[nFlps][operator]': encodeURIComponent(this.nFlpsFilter.operator), 'filter[nFlps][limit]': this.nFlpsFilter.limit, }, ...this.nEpnsFilter && this.nEpnsFilter.limit !== null && { - 'filter[nEpns][operator]': this.nEpnsFilter.operator, + 'filter[nEpns][operator]': encodeURIComponent(this.nEpnsFilter.operator), 'filter[nEpns][limit]': this.nEpnsFilter.limit, }, ...(this.ddflpFilter === true || this.ddflpFilter === false) && { @@ -897,7 +897,7 @@ export class RunsOverviewModel extends OverviewPageModel { ...Object.fromEntries(Object.entries(inelFilterModels) .filter(([_, filterModel]) => !filterModel.isEmpty) .map(([property, filterModel]) => [ - `filter[${property}][${filterModel.value.operator}]`, + `filter[${property}][${encodeURIComponent(filterModel.value.operator)}]`, filterModel.value.operand, ])), }; From ea8985855f67a6f32ce1d7fcd2289262b24109da Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:55:01 +0100 Subject: [PATCH 05/15] Minor test refactoring --- test/public/runs/overview.test.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 4f574cbbcf..3447de76dd 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -474,22 +474,13 @@ module.exports = () => { expect(await runDurationOperator.evaluate((element) => element.value)).to.equal('='); const runDurationLimitSelector = '#duration-limit'; - const runDurationLimit = await page.$(runDurationLimitSelector) || null; - - await page.waitForSelector(runDurationLimitSelector); - expect(runDurationLimit).to.not.be.null; - - await page.focus(runDurationLimitSelector); - await page.keyboard.type('1500'); + await fillInput(page, runDurationLimitSelector, '1500'); await waitForTableLength(page, 3); await page.select(runDurationOperatorSelector, '='); await waitForTableLength(page, 3); - let runDurationList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { - const rowId = row.id; - return document.querySelector(`#${rowId}-runDuration-text`)?.innerText; - })); + let runDurationList = await getColumnCellsInnerTexts(page, 'runDuration'); expect(runDurationList.every((runDuration) => { const time = runDuration.replace('*', ''); From 2c091ce567770bf33da1801caf678247b2831d29 Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:07:51 +0100 Subject: [PATCH 06/15] Fix linter and missing import --- test/public/runs/overview.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 3447de76dd..e8127d2a8d 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -36,7 +36,7 @@ const { waitForDownload, expectLink, expectUrlParams, - expectAttributeValue, + expectAttributeValue, getColumnCellsInnerTexts, } = require('../defaults.js'); const { RUN_QUALITIES, RunQualities } = require('../../../lib/domain/enums/RunQualities.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -1044,12 +1044,12 @@ module.exports = () => { await expectLink(page, `${popoverSelector} a:nth-of-type(1)`, { href: 'http://localhost:8081/?q={%22partition%22:{%22match%22:%22TDI59So3d%22},' - + '%22run%22:{%22match%22:%22104%22},%22severity%22:{%22in%22:%22W%20E%20F%22}}', + + '%22run%22:{%22match%22:%22104%22},%22severity%22:{%22in%22:%22W%20E%20F%22}}', innerText: 'Infologger FLP', }); await expectLink(page, `${popoverSelector} a:nth-of-type(2)`, { href: 'http://localhost:8082/' + - '?page=layoutShow&runNumber=104&definition=COMMISSIONING&detector=CPV&pdpBeamType=cosmic&runType=COSMICS', + '?page=layoutShow&runNumber=104&definition=COMMISSIONING&detector=CPV&pdpBeamType=cosmic&runType=COSMICS', innerText: 'QCG', }); From 54bd5fdb451e8d05f6f915af6551394ad2a781d4 Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:19:46 +0100 Subject: [PATCH 07/15] Reformat --- test/public/runs/overview.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index e8127d2a8d..a37e96a5c0 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -36,7 +36,8 @@ const { waitForDownload, expectLink, expectUrlParams, - expectAttributeValue, getColumnCellsInnerTexts, + expectAttributeValue, + getColumnCellsInnerTexts, } = require('../defaults.js'); const { RUN_QUALITIES, RunQualities } = require('../../../lib/domain/enums/RunQualities.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); From 6383dc04d3db15d10c1b6551b3aa80d48f45f477 Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:23:28 +0100 Subject: [PATCH 08/15] [O2B-532] Improve runs overview detectors filter --- .../RunsFilter/DetectorsFilterModel.js | 75 ++++++++----------- .../RunsFilter/detectorsFilterComponent.js | 9 ++- .../Runs/ActiveColumns/runsActiveColumns.js | 11 ++- .../views/Runs/Overview/RunsOverviewModel.js | 20 +---- 4 files changed, 46 insertions(+), 69 deletions(-) diff --git a/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js b/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js index bf8e7162d4..6098abe46b 100644 --- a/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js @@ -10,14 +10,14 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ -import { Observable } from '/js/src/index.js'; import { CombinationOperator, CombinationOperatorChoiceModel } from '../common/CombinationOperatorChoiceModel.js'; import { DetectorSelectionDropdownModel } from '../../detector/DetectorSelectionDropdownModel.js'; +import { FilterModel } from '../common/FilterModel.js'; /** * Model to store the state of the filtering on a detectors list */ -export class DetectorsFilterModel extends Observable { +export class DetectorsFilterModel extends FilterModel { /** * Constructor * @@ -26,42 +26,54 @@ export class DetectorsFilterModel extends Observable { constructor(observableDetectors) { super(); this._dropdownModel = new DetectorSelectionDropdownModel(observableDetectors); - this._dropdownModel.bubbleTo(this); + this._addSubmodel(this._dropdownModel); this._combinationOperatorModel = new CombinationOperatorChoiceModel([ CombinationOperator.AND, CombinationOperator.OR, CombinationOperator.NONE, ]); - this._combinationOperatorModel.bubbleTo(this); + this._addSubmodel(this._combinationOperatorModel); } + // eslint-disable-next-line valid-jsdoc /** - * States if the filter has no tags selected - * - * @return {boolean} true if no tags are selected + * @inheritDoc */ - isEmpty() { - return this.selected.length === 0 && !this.isNone(); + reset() { + this._dropdownModel.reset(); + this._combinationOperatorModel.reset(); } + // eslint-disable-next-line valid-jsdoc /** - * Return true if the current combination operator is none - * - * @return {boolean} true if the current combination operator is none + * @inheritDoc */ - isNone() { - return this.combinationOperator === CombinationOperator.NONE.value; + get isEmpty() { + return this._dropdownModel.selected.length === 0 && !this.isNone(); } + // eslint-disable-next-line valid-jsdoc /** - * Reset the model to its default state + * @inheritDoc + */ + get normalized() { + const normalized = { + operator: this._combinationOperatorModel.current, + }; + if (!this.isNone()) { + normalized.values = this._dropdownModel.selected.join(); + } + return normalized; + } + + /** + * Return true if the current combination operator is none * - * @return {void} + * @return {boolean} true if the current combination operator is none */ - reset() { - this._dropdownModel.reset(); - this._combinationOperatorModel.reset(); + isNone() { + return this._combinationOperatorModel.current === CombinationOperator.NONE.value; } /** @@ -73,13 +85,6 @@ export class DetectorsFilterModel extends Observable { return this._dropdownModel; } - /** - * Shortcut to get the list of selected detectors - */ - get selected() { - return this._dropdownModel.selected; - } - /** * Return the model storing the combination operator to apply on the list of detectors * @@ -88,22 +93,4 @@ export class DetectorsFilterModel extends Observable { get combinationOperatorModel() { return this._combinationOperatorModel; } - - /** - * Shortcut to get the current combination operator - * - * @return {string} the current operator - */ - get combinationOperator() { - return this._combinationOperatorModel.current; - } - - /** - * Returns an observable notified any time a visual change occurs that has no impact on the actual selection - * - * @return {Observable} the visual change observable - */ - get visualChange$() { - return this._dropdownModel.visualChange$; - } } diff --git a/lib/public/components/Filters/RunsFilter/detectorsFilterComponent.js b/lib/public/components/Filters/RunsFilter/detectorsFilterComponent.js index b9e834971e..66d37fd815 100644 --- a/lib/public/components/Filters/RunsFilter/detectorsFilterComponent.js +++ b/lib/public/components/Filters/RunsFilter/detectorsFilterComponent.js @@ -15,11 +15,12 @@ import { selectionDropdown } from '../../common/selection/dropdown/selectionDrop import { combinationOperatorChoiceComponent } from '../common/combinationOperatorChoiceComponent.js'; /** - * Returns the author filter component - * @param {RunsOverviewModel} runModel the run model object - * @return {Component} A text box that lets the user look for logs with a specific author + * Returns the detectors filter component + * + * @param {DetectorsFilterModel} detectorsFilterModel the detectors filter model + * @return {Component} the detectors filtering component */ -export const detectorsFilterComponent = ({ detectorsFilterModel }) => [ +export const detectorsFilterComponent = (detectorsFilterModel) => [ combinationOperatorChoiceComponent(detectorsFilterModel.combinationOperatorModel, { selectorPrefix: 'detector-filter' }), !detectorsFilterModel.isNone() && selectionDropdown(detectorsFilterModel.dropdownModel, { selectorPrefix: 'detector-filter' }), ]; diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 17edd06250..7c5505f3ca 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -18,7 +18,6 @@ import nDetectorsFilter from '../../../components/Filters/RunsFilter/nDetectors. import nFlpsFilter from '../../../components/Filters/RunsFilter/nFlps.js'; import odcTopologyFullName from '../../../components/Filters/RunsFilter/odcTopologyFullName.js'; import { displayRunEorReasonsOverview } from '../format/displayRunEorReasonOverview.js'; -import { detectorsFilterComponent } from '../../../components/Filters/RunsFilter/detectorsFilterComponent.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'; @@ -56,6 +55,7 @@ import { o2StartFilter } from '../../../components/Filters/RunsFilter/o2StartFil import { o2StopFilter } from '../../../components/Filters/RunsFilter/o2StopFilter.js'; import { isRunConsideredRunning } from '../../../services/run/isRunConsideredRunning.js'; import { aliEcsEnvironmentLinkComponent } from '../../../components/common/externalLinks/aliEcsEnvironmentLinkComponent.js'; +import { detectorsFilterComponent } from '../../../components/Filters/RunsFilter/detectorsFilterComponent.js'; /** * List of active columns for a generic runs table @@ -119,7 +119,14 @@ export const runsActiveColumns = { }, size: 'w-15 f6', format: (_, run) => formatRunDetectorsInline(run.detectorsQualities, run.nDetectors), - filter: detectorsFilterComponent, + + /** + * Detectors filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the filter component + */ + filter: (runsOverviewModel) => detectorsFilterComponent(runsOverviewModel.filteringModel.get('detectors')), balloon: true, }, tags: { diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index a16723d996..cb5b435820 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -43,6 +43,7 @@ export class RunsOverviewModel extends OverviewPageModel { super(); this._filteringModel = new FilteringModel({ + detectors: new DetectorsFilterModel(detectorsProvider.dataTaking$), tags: new TagFilterModel([ CombinationOperator.AND, CombinationOperator.OR, @@ -50,10 +51,6 @@ export class RunsOverviewModel extends OverviewPageModel { ]), }); - this._detectorsFilterModel = new DetectorsFilterModel(detectorsProvider.dataTaking$); - this._detectorsFilterModel.observe(() => this._applyFilters(true)); - this._detectorsFilterModel.visualChange$.bubbleTo(this); - this._listingRunTypesFilterModel = new RunTypesSelectionDropdownModel(); this._listingRunTypesFilterModel.observe(() => this._applyFilters(true)); this._listingRunTypesFilterModel.visualChange$.bubbleTo(this); @@ -195,7 +192,6 @@ export class RunsOverviewModel extends OverviewPageModel { this.runFilterValues = ''; this._runDefinitionFilter = []; - this._detectorsFilterModel.reset(); this._listingRunTypesFilterModel.reset(); this._eorReasonsFilterModel.reset(); this._o2StartFilterModel.reset(); @@ -251,7 +247,6 @@ export class RunsOverviewModel extends OverviewPageModel { || !this._eorReasonsFilterModel.isEmpty() || !this._o2StartFilterModel.isEmpty || !this._o2StopFilterModel.isEmpty - || !this._detectorsFilterModel.isEmpty() || this._listingRunTypesFilterModel.selected.length !== 0 || this._aliceL3AndDipoleCurrentFilter.selected.length !== 0 || this._fillNumbersFilter !== '' @@ -670,15 +665,6 @@ export class RunsOverviewModel extends OverviewPageModel { this._applyFilters(); } - /** - * Returns the model handling the filtering on detectors - * - * @return {DetectorsFilterModel} the detectors filtering model - */ - get detectorsFilterModel() { - return this._detectorsFilterModel; - } - /** * Return all the runs currently filtered, without paging * @@ -823,10 +809,6 @@ export class RunsOverviewModel extends OverviewPageModel { 'filter[runNumbers]': this.runFilterValues, }, ...!this._eorReasonsFilterModel.isEmpty() && this._eorReasonsFilterModel.getFilterQueryParams(), - ...!this._detectorsFilterModel.isEmpty() && { - 'filter[detectors][operator]': encodeURIComponent(this._detectorsFilterModel.combinationOperator), - ...!this._detectorsFilterModel.isNone() && { 'filter[detectors][values]': this._detectorsFilterModel.selected.join() }, - }, ...this._runDefinitionFilter.length > 0 && { 'filter[definitions]': this._runDefinitionFilter.join(','), }, From 05ca20bcda4430c9135f965dda2a65a20fceeb0f Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:46:55 +0100 Subject: [PATCH 09/15] [O2B-532] Improve runs overview runs type filter --- .../RunsFilter/DetectorsFilterModel.js | 2 +- .../Filters/common/TagFilterModel.js | 2 +- .../common/selection/SelectionModel.js | 9 +++ .../runTypes/RunTypesFilterModel.js | 70 +++++++++++++++++++ .../RunTypesSelectionDropdownModel.js | 40 ----------- .../Runs/ActiveColumns/runsActiveColumns.js | 12 +++- .../views/Runs/Overview/RunsOverviewModel.js | 22 +----- 7 files changed, 94 insertions(+), 63 deletions(-) create mode 100644 lib/public/components/runTypes/RunTypesFilterModel.js delete mode 100644 lib/public/components/runTypes/RunTypesSelectionDropdownModel.js diff --git a/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js b/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js index 6098abe46b..3847d8f041 100644 --- a/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js @@ -50,7 +50,7 @@ export class DetectorsFilterModel extends FilterModel { * @inheritDoc */ get isEmpty() { - return this._dropdownModel.selected.length === 0 && !this.isNone(); + return this._dropdownModel.isEmpty && !this.isNone(); } // eslint-disable-next-line valid-jsdoc diff --git a/lib/public/components/Filters/common/TagFilterModel.js b/lib/public/components/Filters/common/TagFilterModel.js index 90213b329a..08df672c9c 100644 --- a/lib/public/components/Filters/common/TagFilterModel.js +++ b/lib/public/components/Filters/common/TagFilterModel.js @@ -47,7 +47,7 @@ export class TagFilterModel extends FilterModel { * @inheritDoc */ get isEmpty() { - return this.selected.length === 0; + return this._selectionModel.isEmpty; } // eslint-disable-next-line valid-jsdoc diff --git a/lib/public/components/common/selection/SelectionModel.js b/lib/public/components/common/selection/SelectionModel.js index f02839b049..b703206e72 100644 --- a/lib/public/components/common/selection/SelectionModel.js +++ b/lib/public/components/common/selection/SelectionModel.js @@ -263,6 +263,15 @@ export class SelectionModel extends Observable { return [...new Set(this._selectedOptions.map(({ value }) => value))]; } + /** + * States if the current selection is empty + * + * @return {boolean} true if the selection is empty + */ + get isEmpty() { + return this.selected.length === 0; + } + /** * If the selection allows one and only one selection, current will return the currently selected option. In any other case it will throw an * error diff --git a/lib/public/components/runTypes/RunTypesFilterModel.js b/lib/public/components/runTypes/RunTypesFilterModel.js new file mode 100644 index 0000000000..8f5886d469 --- /dev/null +++ b/lib/public/components/runTypes/RunTypesFilterModel.js @@ -0,0 +1,70 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { SelectionDropdownModel } from '../common/selection/dropdown/SelectionDropdownModel.js'; +import { RemoteData } from '/js/src/index.js'; +import { runTypeToOption } from './runTypeToOption.js'; +import { runTypesProvider } from '../../services/runTypes/runTypesProvider.js'; +import { FilterModel } from '../Filters/common/FilterModel.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 { + /** + * Constructor + */ + constructor() { + super(); + this._selectionDropDownModel = new SelectionDropdownModel({ availableOptions: RemoteData.notAsked() }); + this._addSubmodel(this._selectionDropDownModel); + + runTypesProvider.getAll().then( + (runTypes) => this._selectionDropDownModel.setAvailableOptions(RemoteData.success(runTypes.map(runTypeToOption))), + (errors) => this._selectionDropDownModel.setAvailableOptions(RemoteData.failure(errors)), + ); + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + reset() { + this._selectionDropDownModel.reset(); + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + get isEmpty() { + return this._selectionDropDownModel.isEmpty; + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + get normalized() { + return this._selectionDropDownModel.selected; + } + + /** + * Return the underlying selection dropdown model + * + * @return {SelectionDropdownModel} the selection dropdown model + */ + get selectionDropdownModel() { + return this._selectionDropDownModel; + } +} diff --git a/lib/public/components/runTypes/RunTypesSelectionDropdownModel.js b/lib/public/components/runTypes/RunTypesSelectionDropdownModel.js deleted file mode 100644 index bfc0e8c484..0000000000 --- a/lib/public/components/runTypes/RunTypesSelectionDropdownModel.js +++ /dev/null @@ -1,40 +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 { SelectionDropdownModel } from '../common/selection/dropdown/SelectionDropdownModel.js'; -import { RemoteData } from '/js/src/index.js'; -import { runTypeToOption } from './runTypeToOption.js'; -import { runTypesProvider } from '../../services/runTypes/runTypesProvider.js'; - -/** - * Model storing state of a selection of run types picked from the list of all the existing run types - */ -export class RunTypesSelectionDropdownModel extends SelectionDropdownModel { - /** - * Constructor - */ - constructor() { - super({ availableOptions: RemoteData.notAsked() }); - } - - // eslint-disable-next-line valid-jsdoc - /** - * @inheritDoc - */ - _initialize() { - runTypesProvider.getAll().then( - (runTypes) => this.setAvailableOptions(RemoteData.success(runTypes.map(runTypeToOption))), - (errors) => this.setAvailableOptions(RemoteData.failure(errors)), - ); - } -} diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 7c5505f3ca..fe5ccef931 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -338,7 +338,17 @@ export const runsActiveColumns = { visible: false, classes: 'cell-l f6 w-wrapped', format: formatRunType, - filter: (runModel) => selectionDropdown(runModel.listingRunTypesFilterModel, { selectorPrefix: 'run-types' }), + + /** + * Run types filter component + * + * @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' }, + ), }, runQuality: { name: 'Quality', diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index cb5b435820..e51a2667ae 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -16,7 +16,7 @@ import { createCSVExport, createJSONExport } from '../../../utilities/export.js' import { TagFilterModel } from '../../../components/Filters/common/TagFilterModel.js'; import { debounce } from '../../../utilities/debounce.js'; import { DetectorsFilterModel } from '../../../components/Filters/RunsFilter/DetectorsFilterModel.js'; -import { RunTypesSelectionDropdownModel } from '../../../components/runTypes/RunTypesSelectionDropdownModel.js'; +import { RunTypesFilterModel } from '../../../components/runTypes/RunTypesFilterModel.js'; import { EorReasonFilterModel } from '../../../components/Filters/RunsFilter/EorReasonsFilterModel.js'; import pick from '../../../utilities/pick.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; @@ -49,12 +49,9 @@ export class RunsOverviewModel extends OverviewPageModel { CombinationOperator.OR, CombinationOperator.NONE_OF, ]), + runTypes: new RunTypesFilterModel(), }); - this._listingRunTypesFilterModel = new RunTypesSelectionDropdownModel(); - this._listingRunTypesFilterModel.observe(() => this._applyFilters(true)); - this._listingRunTypesFilterModel.visualChange$.bubbleTo(this); - this._eorReasonsFilterModel = new EorReasonFilterModel(); this._eorReasonsFilterModel.observe(() => this._applyFilters()); this._eorReasonsFilterModel.visualChange$.bubbleTo(this); @@ -192,7 +189,6 @@ export class RunsOverviewModel extends OverviewPageModel { this.runFilterValues = ''; this._runDefinitionFilter = []; - this._listingRunTypesFilterModel.reset(); this._eorReasonsFilterModel.reset(); this._o2StartFilterModel.reset(); this._o2StopFilterModel.reset(); @@ -247,7 +243,6 @@ export class RunsOverviewModel extends OverviewPageModel { || !this._eorReasonsFilterModel.isEmpty() || !this._o2StartFilterModel.isEmpty || !this._o2StopFilterModel.isEmpty - || this._listingRunTypesFilterModel.selected.length !== 0 || this._aliceL3AndDipoleCurrentFilter.selected.length !== 0 || this._fillNumbersFilter !== '' || this._runDurationFilter !== null @@ -691,15 +686,6 @@ export class RunsOverviewModel extends OverviewPageModel { return allRuns.payload.length < this._pagination.itemsCount; } - /** - * Return the model handling the filtering on run types - * - * @return {RunTypesSelectionDropdownModel} the run type filtering model - */ - get listingRunTypesFilterModel() { - return this._listingRunTypesFilterModel; - } - /** * Return the model handling the filtering on run types * @@ -868,10 +854,6 @@ export class RunsOverviewModel extends OverviewPageModel { ...this._odcTopologyFullNameFilter && { 'filter[odcTopologyFullName]': this._odcTopologyFullNameFilter, }, - ...this._listingRunTypesFilterModel.selected.length > 0 && { - 'filter[runTypes]': this._listingRunTypesFilterModel.selected, - }, - ...this._aliceL3AndDipoleCurrentFilter.selected.length > 0 && { 'filter[aliceL3Current]': this._aliceL3AndDipoleCurrentFilter.selectedOptions[0].filteringParams.aliceL3Current, 'filter[aliceDipoleCurrent]': this._aliceL3AndDipoleCurrentFilter.selectedOptions[0].filteringParams.aliceDipoleCurrent, From b33ee49489c9a44dad55f946381f541066a42e85 Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:28:33 +0100 Subject: [PATCH 10/15] [O2B-532] Improve runs overview EOR reasons filter --- .../RunsFilter/EorReasonFilterModel.js | 174 ++++++++++++++++++ .../RunsFilter/EorReasonsFilterModel.js | 149 --------------- ...ReasonFilter.js => eorReasonsSelection.js} | 12 +- .../runEorReasons/runEorReasonSelection.js | 64 ++++--- .../Runs/ActiveColumns/runsActiveColumns.js | 11 +- .../views/Runs/Overview/RunsOverviewModel.js | 28 +-- 6 files changed, 225 insertions(+), 213 deletions(-) create mode 100644 lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js delete mode 100644 lib/public/components/Filters/RunsFilter/EorReasonsFilterModel.js rename lib/public/components/Filters/RunsFilter/{eorReasonFilter.js => eorReasonsSelection.js} (71%) diff --git a/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js b/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js new file mode 100644 index 0000000000..b4bea6165b --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js @@ -0,0 +1,174 @@ +/** + * @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 { RemoteData } from '/js/src/index.js'; +import { getRemoteData } from '../../../utilities/fetch/getRemoteData.js'; +import { FilterModel } from '../common/FilterModel.js'; + +/** + * Model storing state of a selection of run types picked from the list of all the existing run types + */ +export class EorReasonFilterModel extends FilterModel { + /** + * Constructor + */ + constructor() { + super(); + this._eorReasonTypes = RemoteData.notAsked(); + // TODO this should go into a data provider + this._fetchReasonTypes(); + + this._category = ''; + this._title = ''; + this._rawDescription = ''; + this._description = ''; + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + reset() { + this._category = ''; + this._title = ''; + this._rawDescription = ''; + this._description = ''; + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + get isEmpty() { + return this._category === '' && this._title === '' && this._description === ''; + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + get normalized() { + const ret = {}; + + if (this._category !== '') { + ret.category = this._category; + } + if (this._title !== '') { + ret.title = this._title; + } + if (this._description !== '') { + ret.description = this._description; + } + + return ret; + } + + /** + * Returns the EOR reason filter category + * + * @return {string} the category + */ + get category() { + return this._category; + } + + /** + * Sets the category of the EOR reason filter category and reset the title + * + * @param {string} category the category of the new EOR reason filter + * @return {void} + */ + set category(category) { + this._category = category; + this._title = ''; + this.notify(); + } + + /** + * Returns the EOR reason filter title + * + * @return {string} the title + */ + get title() { + return this._title; + } + + /** + * Sets the EOR reason filter title + * + * @param {string} title the title of the new EOR reason filter + * @return {void} + */ + set title(title) { + this._title = title; + this.notify(); + } + + /** + * Returns the raw value of EOR reason filter description + * + * @return {string} the raw description + */ + get rawDescription() { + return this._rawDescription; + } + + /** + * Sets the raw value of EOR reason filter description + * + * @param {string} rawDescription the raw description + * @return {void} + */ + set rawDescription(rawDescription) { + this._rawDescription = rawDescription; + this.visualChange$.notify(); + } + + /** + * Sets the EOR reason filter description + * + * @param {string} description the description + * @return {void} + */ + set description(description) { + this._description = description; + this.notify(); + } + + /** + * Getter for the EOR reason types + * @return {Object} the EOR reason type + */ + get eorReasonTypes() { + return this._eorReasonTypes; + } + + /** + * Retrieve a list of reason types from the API + * + * @return {Promise} resolves once the data has been fetched + */ + async _fetchReasonTypes() { + this._eorReasonTypes = RemoteData.loading(); + this._visualChange$.notify(); + + try { + const { data: reasonTypes } = await getRemoteData('/api/runs/reasonTypes'); + this._eorReasonTypes = RemoteData.success(reasonTypes); + } catch (error) { + this._eorReasonTypes = RemoteData.failure(error); + } + + this._visualChange$.notify(); + } +} diff --git a/lib/public/components/Filters/RunsFilter/EorReasonsFilterModel.js b/lib/public/components/Filters/RunsFilter/EorReasonsFilterModel.js deleted file mode 100644 index 1b7d52edb7..0000000000 --- a/lib/public/components/Filters/RunsFilter/EorReasonsFilterModel.js +++ /dev/null @@ -1,149 +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 { RemoteData, Observable } from '/js/src/index.js'; -import { getRemoteData } from '../../../utilities/fetch/getRemoteData.js'; - -const emptyEorReason = { - category: '', - title: '', - description: '', -}; - -/** - * Model storing state of a selection of run types picked from the list of all the existing run types - */ -export class EorReasonFilterModel extends Observable { - /** - * Constructor - */ - constructor() { - super(); - this._visualChange$ = new Observable(); - this._eorReasonTypes = RemoteData.notAsked(); - this._fetchReasonTypes(); - this._filterEorReason = { ...emptyEorReason }; - } - - /** - * Retrieve a list of reason types from the API - * - * @return {Promise} resolves once the data has been fetched - */ - async _fetchReasonTypes() { - this._eorReasonTypes = RemoteData.loading(); - this._visualChange$.notify(); - - try { - const { data: reasonTypes } = await getRemoteData('/api/runs/reasonTypes'); - this._eorReasonTypes = RemoteData.success(reasonTypes); - } catch (error) { - this._eorReasonTypes = RemoteData.failure(error); - } - - this._visualChange$.notify(); - } - - /** - * Returns an observable notified any time a visual change occurs that has no impact on the actual selection - * @return {Observable} the visual change observable - */ - get visualChange$() { - return this._visualChange$; - } - - /** - * Returns a boolean indicating whether this filter is considered empty - * @return {boolean} indicating whether this filter is considered empty - */ - isEmpty() { - return this._filterEorReason.category === '' && this._filterEorReason.title === '' && this._filterEorReason.description === ''; - } - - /** - * Empties the filter. this.isEmpty() evaluates to true. - * @return {void} - */ - reset() { - this._filterEorReason = { ...emptyEorReason }; - } - - /** - * Gets an object of url params to value for the api query - * @return {Object} url query parameter to parameter value - */ - getFilterQueryParams() { - return { - ...this.filterEorReason.category !== '' && { 'filter[eorReason][category]': this.filterEorReason.category }, - ...this.filterEorReason.title !== '' && { 'filter[eorReason][title]': this.filterEorReason.title }, - ...this.filterEorReason.description !== '' && { 'filter[eorReason][description]': this.filterEorReason.description }, - }; - } - - /** - * Sets the category of the new EOR reason filter, and resets the title since the titles - * are different for each category. - * @param {string} category the category of the new EOR reason filter - * @return {void} - */ - setFilterEorReasonCategory(category) { - this._filterEorReason.category = category; - this._filterEorReason.title = emptyEorReason.title; - this.notify(); - } - - /** - * Sets the title of the new EOR reason filter - * @param {string} title the title of the EOR reason filter - * @return {void} - */ - setFilterEorReasonTitle(title) { - this._filterEorReason.title = title; - this.notify(); - } - - /** - * Sets the description of the new EOR reason filter - * @param {string} description the description of the new EOR reason - * @return {void} - */ - setFilterEorReasonDescription(description) { - this._filterEorReason.description = description; - this.notify(); - } - - /** - * Getter for the EOR reason types - * @return {Object} the EOR reason type - */ - get eorReasonTypes() { - return this._eorReasonTypes; - } - - /** - * Setter for the EOR reason types - * @param {Object} reasonTypes object representing the possible EOR reason types - * @return {void} - */ - set eorReasonTypes(reasonTypes) { - this._eorReasonTypes = reasonTypes; - } - - /** - * Getter for the current EOR reason - * @return {Object} the EOR reason - */ - get filterEorReason() { - return this._filterEorReason; - } -} diff --git a/lib/public/components/Filters/RunsFilter/eorReasonFilter.js b/lib/public/components/Filters/RunsFilter/eorReasonsSelection.js similarity index 71% rename from lib/public/components/Filters/RunsFilter/eorReasonFilter.js rename to lib/public/components/Filters/RunsFilter/eorReasonsSelection.js index 0d9d1b6afc..2d2fb85b28 100644 --- a/lib/public/components/Filters/RunsFilter/eorReasonFilter.js +++ b/lib/public/components/Filters/RunsFilter/eorReasonsSelection.js @@ -15,9 +15,8 @@ import { h } from '/js/src/index.js'; import { eorReasonSelectionComponent } from '../../runEorReasons/runEorReasonSelection.js'; /** - * Displays the eorReasonSelectionComponent if the eor reason types have been successfully fetched. - * Otherwise displays an appropriate message - * @param {Object} eorReasonsFilterModel the model responsible for the run details overview. + * Displays the eorReasonSelectionComponent if the eor reason types have been successfully fetched. Otherwise, displays an appropriate message + * @param {EorReasonFilterModel} eorReasonsFilterModel the model responsible for the run details overview. * @return {vnode} the appropriate vnode based on the status of remote data. */ export const eorReasonsSelection = (eorReasonsFilterModel) => { @@ -30,10 +29,3 @@ export const eorReasonsSelection = (eorReasonsFilterModel) => { Failure: () => h('.danger', 'Failure fetching EOR reason types'), }); }; - -/** - * Returns the EOR reason filter component - * @param {RunsOverviewModel} runModel the run model object - * @return {vnode} A series of dropdowns and a text field allowing the user to filter by EOR reason - */ -export const eorReasonFilter = (runModel) => eorReasonsSelection(runModel.eorReasonsFilterModel); diff --git a/lib/public/components/runEorReasons/runEorReasonSelection.js b/lib/public/components/runEorReasons/runEorReasonSelection.js index 8be023217d..28f3ee7df8 100644 --- a/lib/public/components/runEorReasons/runEorReasonSelection.js +++ b/lib/public/components/runEorReasons/runEorReasonSelection.js @@ -20,41 +20,53 @@ import { h } from '/js/src/index.js'; * @return {vnode} A pair of dropdowns and a text input to filter for eor reason category title, and description */ export const eorReasonSelectionComponent = (eorReasonFilterModel, eorReasonTypes) => { - const { filterEorReason } = eorReasonFilterModel; - const reasonTypeCategories = [...new Set(eorReasonTypes.map(({ category }) => category))]; + const eorReasonsCategories = [...new Set(eorReasonTypes.map(({ category }) => category))]; return [ h('.flex-row', [ - h('select#eorCategories.form-control', { - onchange: ({ target }) => { - eorReasonFilterModel.setFilterEorReasonCategory(target.value); + h( + 'select#eorCategories.form-control', + { + onchange: ({ target }) => { + eorReasonFilterModel.category = target.value; + }, }, - }, [ - h('option', { selected: filterEorReason.category === '', value: '' }, '-'), - reasonTypeCategories.map((category, index) => h(`option#eorCategory${index}`, { - value: category, - }, category)), - ]), - h('select#eorTitles.form-control', { - onchange: ({ target }) => { - eorReasonFilterModel.setFilterEorReasonTitle(target.value); - }, - }, [ - h('option', { selected: filterEorReason.title === '', value: '' }, '-'), - eorReasonTypes.filter((reason) => reason.category === filterEorReason.category) - .map((reason, index) => h( - `option#eorTitle${index}`, - { value: reason.title }, - reason.title || '(empty)', + [ + h('option', { selected: eorReasonFilterModel.category === '', value: '' }, '-'), + eorReasonsCategories.map((category, index) => h( + `option#eorCategory${index}`, + { key: category, value: category }, + category, )), - ]), + ], + ), + h( + 'select#eorTitles.form-control', + { + onchange: ({ target }) => { + eorReasonFilterModel.title = target.value; + }, + }, + [ + h('option', { selected: eorReasonFilterModel.title === '', value: '' }, '-'), + eorReasonTypes + .filter((reason) => reason.category === eorReasonFilterModel.category) + .map(({ title }, index) => h( + `option#eorTitle${index}`, + { key: title, value: title }, + title || '(empty)', + )), + ], + ), ]), h('input#eorDescription.form-control', { placeholder: 'Description', - type: 'text', - value: filterEorReason.description, + value: eorReasonFilterModel.rawDescription, oninput: ({ target }) => { - eorReasonFilterModel.setFilterEorReasonDescription(target.value); + eorReasonFilterModel.rawDescription = target.value; + }, + onchange: ({ target }) => { + eorReasonFilterModel.description = target.value; }, }), ]; diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index fe5ccef931..556421de70 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -39,7 +39,7 @@ import { formatRunEnd } from '../format/formatRunEnd.js'; import { tagFilter } from '../../../components/Filters/common/filters/tagFilter.js'; import { formatRunDuration } from '../../../utilities/formatting/formatRunDuration.mjs'; import { selectionDropdown } from '../../../components/common/selection/dropdown/selectionDropdown.js'; -import { eorReasonFilter } from '../../../components/Filters/RunsFilter/eorReasonFilter.js'; +import { eorReasonsSelection } from '../../../components/Filters/RunsFilter/eorReasonsSelection.js'; import { RunDefinition } from '../../../domain/enums/RunDefinition.js'; import { coloredCalibrationStatusComponent } from '../coloredCalibrationStatusComponent.js'; import { BeamModes } from '../../../domain/enums/BeamModes.js'; @@ -461,7 +461,14 @@ export const runsActiveColumns = { visible: true, profiles: [profiles.none, 'lhcFill', 'environment'], format: (eorReasons) => displayRunEorReasonsOverview(eorReasons), - filter: eorReasonFilter, + + /** + * EoR reason filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the filter component + */ + filter: (runsOverviewModel) => eorReasonsSelection(runsOverviewModel.filteringModel.get('eorReason')), balloon: true, }, pdpWorkflowParameters: { diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index e51a2667ae..29f9e24e66 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -17,7 +17,7 @@ import { TagFilterModel } from '../../../components/Filters/common/TagFilterMode import { debounce } from '../../../utilities/debounce.js'; import { DetectorsFilterModel } from '../../../components/Filters/RunsFilter/DetectorsFilterModel.js'; import { RunTypesFilterModel } from '../../../components/runTypes/RunTypesFilterModel.js'; -import { EorReasonFilterModel } from '../../../components/Filters/RunsFilter/EorReasonsFilterModel.js'; +import { EorReasonFilterModel } from '../../../components/Filters/RunsFilter/EorReasonFilterModel.js'; import pick from '../../../utilities/pick.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { getRemoteDataSlice } from '../../../utilities/fetch/getRemoteDataSlice.js'; @@ -50,12 +50,9 @@ export class RunsOverviewModel extends OverviewPageModel { CombinationOperator.NONE_OF, ]), runTypes: new RunTypesFilterModel(), + eorReason: new EorReasonFilterModel(), }); - this._eorReasonsFilterModel = new EorReasonFilterModel(); - this._eorReasonsFilterModel.observe(() => this._applyFilters()); - this._eorReasonsFilterModel.visualChange$.bubbleTo(this); - this._o2StartFilterModel = new TimeRangeInputModel(); this._o2StartFilterModel.observe(() => this._applyFilters(true)); this._o2StartFilterModel.visualChange$.bubbleTo(this); @@ -189,7 +186,6 @@ export class RunsOverviewModel extends OverviewPageModel { this.runFilterValues = ''; this._runDefinitionFilter = []; - this._eorReasonsFilterModel.reset(); this._o2StartFilterModel.reset(); this._o2StopFilterModel.reset(); @@ -240,7 +236,6 @@ export class RunsOverviewModel extends OverviewPageModel { return this._filteringModel.isAnyFilterActive() || this.runFilterValues !== '' || this._runDefinitionFilter.length > 0 - || !this._eorReasonsFilterModel.isEmpty() || !this._o2StartFilterModel.isEmpty || !this._o2StopFilterModel.isEmpty || this._aliceL3AndDipoleCurrentFilter.selected.length !== 0 @@ -686,15 +681,6 @@ export class RunsOverviewModel extends OverviewPageModel { return allRuns.payload.length < this._pagination.itemsCount; } - /** - * Return the model handling the filtering on run types - * - * @return {EorReasonFilterModel} the run type filtering model - */ - get eorReasonsFilterModel() { - return this._eorReasonsFilterModel; - } - /** * Returns the model for o2 start filter * @@ -713,15 +699,6 @@ export class RunsOverviewModel extends OverviewPageModel { return this._o2StopFilterModel; } - /** - * Set the model handling the filtering on run types - * @param {EorReasonFilterModel} model the model to set - * @return {void} - */ - set eorReasonsFilterModel(model) { - this._eorReasonsFilterModel = model; - } - /** * Get Alice L3 Current filter model * @return {SelectionDropdownModel} filter model @@ -794,7 +771,6 @@ export class RunsOverviewModel extends OverviewPageModel { ...this.runFilterValues && { 'filter[runNumbers]': this.runFilterValues, }, - ...!this._eorReasonsFilterModel.isEmpty() && this._eorReasonsFilterModel.getFilterQueryParams(), ...this._runDefinitionFilter.length > 0 && { 'filter[definitions]': this._runDefinitionFilter.join(','), }, From 572788222d586e3d31f5b30b936b8ae590f671ea Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:59:44 +0100 Subject: [PATCH 11/15] [O2B-532] Improve runs overview start/stop filters --- .../Filters/RunsFilter/TimeRangeFilter.js | 59 +++++++++++++++++++ .../Filters/RunsFilter/o2StartFilter.js | 22 ------- .../Filters/RunsFilter/o2StopFilter.js | 21 ------- .../Runs/ActiveColumns/runsActiveColumns.js | 21 +++++-- .../views/Runs/Overview/RunsOverviewModel.js | 46 +-------------- 5 files changed, 79 insertions(+), 90 deletions(-) create mode 100644 lib/public/components/Filters/RunsFilter/TimeRangeFilter.js delete mode 100644 lib/public/components/Filters/RunsFilter/o2StartFilter.js delete mode 100644 lib/public/components/Filters/RunsFilter/o2StopFilter.js diff --git a/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js b/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js new file mode 100644 index 0000000000..f5bc8b5a81 --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js @@ -0,0 +1,59 @@ +import { FilterModel } from '../common/FilterModel.js'; +import { TimeRangeInputModel } from '../common/filters/TimeRangeInputModel.js'; + +/** + * Time-range filter model + */ +export class TimeRangeFilterModel extends FilterModel { + /** + * Constructor + */ + constructor() { + super(); + + this._timeRangeInputModel = new TimeRangeInputModel(); + this._addSubmodel(this._timeRangeInputModel); + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + reset() { + this._timeRangeInputModel.reset(); + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + get isEmpty() { + return this._timeRangeInputModel.isEmpty; + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + get normalized() { + const normalized = {}; + + if (!this._timeRangeInputModel.fromTimeInputModel.isEmpty) { + normalized.from = this._timeRangeInputModel.normalized.from; + } + if (!this._timeRangeInputModel.toTimeInputModel.isEmpty) { + normalized.to = this._timeRangeInputModel.normalized.to; + } + + return normalized; + } + + /** + * Return the underlying time range input model + * + * @return {TimeRangeInputModel} the input model + */ + get timeRangeInputModel() { + return this._timeRangeInputModel; + } +} diff --git a/lib/public/components/Filters/RunsFilter/o2StartFilter.js b/lib/public/components/Filters/RunsFilter/o2StartFilter.js deleted file mode 100644 index ab10978cfa..0000000000 --- a/lib/public/components/Filters/RunsFilter/o2StartFilter.js +++ /dev/null @@ -1,22 +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 { timeRangeFilter } from '../common/filters/timeRangeFilter.js'; - -/** - * Returns a filter to be applied on run start - * - * @param {RunsOverviewModel} runsOverviewModel the run overview model object - * @return {Component} the filter component - */ -export const o2StartFilter = (runsOverviewModel) => timeRangeFilter(runsOverviewModel.o2StartFilterModel); diff --git a/lib/public/components/Filters/RunsFilter/o2StopFilter.js b/lib/public/components/Filters/RunsFilter/o2StopFilter.js deleted file mode 100644 index 74f00d4c39..0000000000 --- a/lib/public/components/Filters/RunsFilter/o2StopFilter.js +++ /dev/null @@ -1,21 +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 { timeRangeFilter } from '../common/filters/timeRangeFilter.js'; - -/** - * Returns a filter to be applied on run stop - * - * @param {RunsOverviewModel} runsOverviewModel the run overview model object - * @return {Component} the filter component - */ -export const o2StopFilter = (runsOverviewModel) => timeRangeFilter(runsOverviewModel.o2stopFilterModel); diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 556421de70..29aa80994b 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -51,11 +51,10 @@ import { CopyToClipboardComponent } from '../../../components/common/selection/i import { infologgerLinksComponents } from '../../../components/common/externalLinks/infologgerLinksComponents.js'; import { RunQualities } from '../../../domain/enums/RunQualities.js'; import { qcGuiLinkComponent } from '../../../components/common/externalLinks/qcGuiLinkComponent.js'; -import { o2StartFilter } from '../../../components/Filters/RunsFilter/o2StartFilter.js'; -import { o2StopFilter } from '../../../components/Filters/RunsFilter/o2StopFilter.js'; import { isRunConsideredRunning } from '../../../services/run/isRunConsideredRunning.js'; import { aliEcsEnvironmentLinkComponent } from '../../../components/common/externalLinks/aliEcsEnvironmentLinkComponent.js'; import { detectorsFilterComponent } from '../../../components/Filters/RunsFilter/detectorsFilterComponent.js'; +import { timeRangeFilter } from '../../../components/Filters/common/filters/timeRangeFilter.js'; /** * List of active columns for a generic runs table @@ -188,7 +187,14 @@ export const runsActiveColumns = { noEllipsis: true, format: (_, run) => formatRunStart(run, false), exportFormat: (timestamp) => formatTimestamp(timestamp), - filter: o2StartFilter, + + /** + * O2 Start filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the filter component + */ + filter: (runsOverviewModel) => timeRangeFilter(runsOverviewModel.filteringModel.get('o2start').timeRangeInputModel), profiles: { lhcFill: true, environment: true, @@ -214,7 +220,14 @@ export const runsActiveColumns = { noEllipsis: true, format: (_, run) => formatRunEnd(run, false), exportFormat: (timestamp) => formatTimestamp(timestamp), - filter: o2StopFilter, + + /** + * O2 end filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the filter component + */ + filter: (runsOverviewModel) => timeRangeFilter(runsOverviewModel.filteringModel.get('o2end').timeRangeInputModel), profiles: { lhcFill: true, environment: true, diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index 29f9e24e66..94058aac76 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -28,6 +28,7 @@ import { detectorsProvider } from '../../../services/detectors/detectorsProvider import { AliceL3AndDipoleFilteringModel } from '../../../components/Filters/RunsFilter/AliceL3AndDipoleFilteringModel.js'; import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; import { buildUrl } from '../../../utilities/fetch/buildUrl.js'; +import { TimeRangeFilterModel } from '../../../components/Filters/RunsFilter/TimeRangeFilter.js'; /** * Model representing handlers for runs page @@ -49,18 +50,12 @@ export class RunsOverviewModel extends OverviewPageModel { CombinationOperator.OR, CombinationOperator.NONE_OF, ]), + o2start: new TimeRangeFilterModel(), + o2end: new TimeRangeFilterModel(), runTypes: new RunTypesFilterModel(), eorReason: new EorReasonFilterModel(), }); - this._o2StartFilterModel = new TimeRangeInputModel(); - this._o2StartFilterModel.observe(() => this._applyFilters(true)); - this._o2StartFilterModel.visualChange$.bubbleTo(this); - - this._o2StopFilterModel = new TimeRangeInputModel(); - this._o2StopFilterModel.observe(() => this._applyFilters(true)); - this._o2StopFilterModel.visualChange$.bubbleTo(this); - this._aliceL3AndDipoleCurrentFilter = new AliceL3AndDipoleFilteringModel(); this._aliceL3AndDipoleCurrentFilter.observe(() => this._applyFilters()); this._aliceL3AndDipoleCurrentFilter.visualChange$.bubbleTo(this); @@ -186,9 +181,6 @@ export class RunsOverviewModel extends OverviewPageModel { this.runFilterValues = ''; this._runDefinitionFilter = []; - this._o2StartFilterModel.reset(); - this._o2StopFilterModel.reset(); - this._aliceL3AndDipoleCurrentFilter.reset(); this._fillNumbersFilter = ''; @@ -236,8 +228,6 @@ export class RunsOverviewModel extends OverviewPageModel { return this._filteringModel.isAnyFilterActive() || this.runFilterValues !== '' || this._runDefinitionFilter.length > 0 - || !this._o2StartFilterModel.isEmpty - || !this._o2StopFilterModel.isEmpty || this._aliceL3AndDipoleCurrentFilter.selected.length !== 0 || this._fillNumbersFilter !== '' || this._runDurationFilter !== null @@ -681,24 +671,6 @@ export class RunsOverviewModel extends OverviewPageModel { return allRuns.payload.length < this._pagination.itemsCount; } - /** - * Returns the model for o2 start filter - * - * @return {TimeRangeInputModel} the filter model - */ - get o2StartFilterModel() { - return this._o2StartFilterModel; - } - - /** - * Returns the model for o2 stop filter - * - * @return {TimeRangeInputModel} the filter model - */ - get o2stopFilterModel() { - return this._o2StopFilterModel; - } - /** * Get Alice L3 Current filter model * @return {SelectionDropdownModel} filter model @@ -777,18 +749,6 @@ export class RunsOverviewModel extends OverviewPageModel { ...this._fillNumbersFilter && { 'filter[fillNumbers]': this._fillNumbersFilter, }, - ...!this._o2StartFilterModel.fromTimeInputModel.isEmpty && { - 'filter[o2start][from]': this._o2StartFilterModel.normalized.from, - }, - ...!this._o2StartFilterModel.toTimeInputModel.isEmpty && { - 'filter[o2start][to]': this._o2StartFilterModel.normalized.to, - }, - ...!this._o2StopFilterModel.fromTimeInputModel.isEmpty && { - 'filter[o2end][from]': this._o2StopFilterModel.normalized.from, - }, - ...!this._o2StopFilterModel.toTimeInputModel.isEmpty && { - 'filter[o2end][to]': this._o2StopFilterModel.normalized.to, - }, ...this._runDurationFilter && this._runDurationFilter.limit !== null && { 'filter[runDuration][operator]': encodeURIComponent(this._runDurationFilter.operator), // Convert filter to milliseconds From 1c08a45a04d83529f820da808e854b86ef0c8561 Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:03:41 +0100 Subject: [PATCH 12/15] [O2B-532] Improve runs overview magnets filter --- lib/domain/dtos/filters/RunFilterDto.js | 6 +- .../AliceL3AndDipoleFilteringModel.js | 47 ---------- .../RunsFilter/MagnetsFilteringModel.js | 86 +++++++++++++++++++ .../Runs/ActiveColumns/runsActiveColumns.js | 12 ++- .../views/Runs/Overview/RunsOverviewModel.js | 23 +---- lib/usecases/run/GetAllRunsUseCase.js | 44 +++++----- test/api/runs.test.js | 4 +- .../usecases/run/GetAllRunsUseCase.test.js | 32 ++++--- 8 files changed, 151 insertions(+), 103 deletions(-) delete mode 100644 lib/public/components/Filters/RunsFilter/AliceL3AndDipoleFilteringModel.js create mode 100644 lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index 88d9e95b65..2f3130ad5b 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -59,8 +59,10 @@ exports.RunFilterDto = Joi.object({ dataPassIds: Joi.array().items(Joi.number()), simulationPassIds: Joi.array().items(Joi.number()), runTypes: CustomJoi.stringArray().items(Joi.string()).single().optional(), - aliceL3Current: Joi.number().integer(), - aliceDipoleCurrent: Joi.number().integer(), + magnets: Joi.object({ + l3: Joi.number().integer(), + dipole: Joi.number().integer(), + }), updatedAt: FromToFilterDto, muInelasticInteractionRate: FloatComparisonDto, diff --git a/lib/public/components/Filters/RunsFilter/AliceL3AndDipoleFilteringModel.js b/lib/public/components/Filters/RunsFilter/AliceL3AndDipoleFilteringModel.js deleted file mode 100644 index a5ba1b62b7..0000000000 --- a/lib/public/components/Filters/RunsFilter/AliceL3AndDipoleFilteringModel.js +++ /dev/null @@ -1,47 +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 { RemoteData } from '/js/src/index.js'; -import { getRemoteData } from '../../../utilities/fetch/getRemoteData.js'; -import { SelectionDropdownModel } from '../../common/selection/dropdown/SelectionDropdownModel.js'; - -/** - * AliceL3AndDipoleFilteringModel - */ -export class AliceL3AndDipoleFilteringModel extends SelectionDropdownModel { - /** - * Constructor - */ - constructor() { - super({ multiple: false }); - } - - // eslint-disable-next-line valid-jsdoc - /** - * @inheritDoc - */ - _initialize() { - getRemoteData('/api/runs/aliceMagnetsCurrentLevels').then(({ data }) => { - this.setAvailableOptions(data - .sort(({ l3Level: l3LevelA, dipoleLevel: dipoleLevelA }, { l3Level: l3LevelB, dipoleLevel: dipoleLevelB }) => - l3LevelA - l3LevelB || dipoleLevelA - dipoleLevelB) - .map(({ l3Level, dipoleLevel }) => ({ - value: `${l3Level}kA/${dipoleLevel}kA`, - filteringParams: { - aliceL3Current: l3Level, - aliceDipoleCurrent: dipoleLevel, - }, - }))); - }, (errors) => this.setAvailableOptions(RemoteData.failure(errors))); - } -} diff --git a/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js b/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js new file mode 100644 index 0000000000..84de64d3da --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js @@ -0,0 +1,86 @@ +/** + * @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 { RemoteData } from '/js/src/index.js'; +import { getRemoteData } from '../../../utilities/fetch/getRemoteData.js'; +import { SelectionDropdownModel } from '../../common/selection/dropdown/SelectionDropdownModel.js'; +import { FilterModel } from '../common/FilterModel.js'; + +/** + * AliceL3AndDipoleFilteringModel + */ +export class MagnetsFilteringModel extends FilterModel { + /** + * Constructor + */ + constructor() { + super(); + this._selectionDropdownModel = new SelectionDropdownModel({ multiple: false }); + this._addSubmodel(this._selectionDropdownModel); + + getRemoteData('/api/runs/aliceMagnetsCurrentLevels') + .then( + ({ data }) => { + this._selectionDropdownModel.setAvailableOptions(data + .sort(( + { l3Level: l3LevelA, dipoleLevel: dipoleLevelA }, + { l3Level: l3LevelB, dipoleLevel: dipoleLevelB }, + ) => l3LevelA - l3LevelB || dipoleLevelA - dipoleLevelB) + .map(({ l3Level, dipoleLevel }) => ({ + value: `${l3Level}kA/${dipoleLevel}kA`, + filteringParams: { + l3: l3Level, + dipole: dipoleLevel, + }, + }))); + }, + (errors) => this._selectionDropdownModel.setAvailableOptions(RemoteData.failure(errors)), + ); + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + reset() { + this._selectionDropdownModel.reset(); + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + get isEmpty() { + return this._selectionDropdownModel.isEmpty; + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + get normalized() { + return { + l3: this._selectionDropdownModel.selectedOptions[0].filteringParams.l3, + dipole: this._selectionDropdownModel.selectedOptions[0].filteringParams.dipole, + }; + } + + /** + * Return the underlying selection dropdown model + * + * @return {SelectionDropdownModel} the dropdown model + */ + get selectionDropdownModel() { + return this._selectionDropdownModel; + } +} diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 29aa80994b..5a3f75ba07 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -545,7 +545,17 @@ export const runsActiveColumns = { aliceL3AndDipoleCurrent: { name: 'L3 / Dipole', visible: false, - filter: ({ aliceL3AndDipoleCurrentFilter }) => selectionDropdown(aliceL3AndDipoleCurrentFilter, { selectorPrefix: 'l3-dipole-current' }), + + /** + * Run types filter component + * + * @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' }, + ), profiles: ['runsPerLhcPeriod', 'runsPerDataPass', 'runsPerSimulationPass', profiles.none], }, }; diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index 94058aac76..5cabed7a84 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -22,10 +22,9 @@ import pick from '../../../utilities/pick.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { getRemoteDataSlice } from '../../../utilities/fetch/getRemoteDataSlice.js'; import { CombinationOperator } from '../../../components/Filters/common/CombinationOperatorChoiceModel.js'; -import { TimeRangeInputModel } from '../../../components/Filters/common/filters/TimeRangeInputModel.js'; import { FloatComparisonFilterModel } from '../../../components/Filters/common/filters/FloatComparisonFilterModel.js'; import { detectorsProvider } from '../../../services/detectors/detectorsProvider.js'; -import { AliceL3AndDipoleFilteringModel } from '../../../components/Filters/RunsFilter/AliceL3AndDipoleFilteringModel.js'; +import { MagnetsFilteringModel } from '../../../components/Filters/RunsFilter/MagnetsFilteringModel.js'; import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; import { buildUrl } from '../../../utilities/fetch/buildUrl.js'; import { TimeRangeFilterModel } from '../../../components/Filters/RunsFilter/TimeRangeFilter.js'; @@ -54,12 +53,9 @@ export class RunsOverviewModel extends OverviewPageModel { o2end: new TimeRangeFilterModel(), runTypes: new RunTypesFilterModel(), eorReason: new EorReasonFilterModel(), + magnets: new MagnetsFilteringModel(), }); - this._aliceL3AndDipoleCurrentFilter = new AliceL3AndDipoleFilteringModel(); - this._aliceL3AndDipoleCurrentFilter.observe(() => this._applyFilters()); - this._aliceL3AndDipoleCurrentFilter.visualChange$.bubbleTo(this); - this._muInelasticInteractionRateFilterModel = new FloatComparisonFilterModel(); this._muInelasticInteractionRateFilterModel.observe(() => this._applyFilters()); this._muInelasticInteractionRateFilterModel.visualChange$.bubbleTo(this); @@ -181,8 +177,6 @@ export class RunsOverviewModel extends OverviewPageModel { this.runFilterValues = ''; this._runDefinitionFilter = []; - this._aliceL3AndDipoleCurrentFilter.reset(); - this._fillNumbersFilter = ''; this._runDurationFilter = null; @@ -228,7 +222,6 @@ export class RunsOverviewModel extends OverviewPageModel { return this._filteringModel.isAnyFilterActive() || this.runFilterValues !== '' || this._runDefinitionFilter.length > 0 - || this._aliceL3AndDipoleCurrentFilter.selected.length !== 0 || this._fillNumbersFilter !== '' || this._runDurationFilter !== null || this._lhcPeriodsFilter !== null @@ -671,14 +664,6 @@ export class RunsOverviewModel extends OverviewPageModel { return allRuns.payload.length < this._pagination.itemsCount; } - /** - * Get Alice L3 Current filter model - * @return {SelectionDropdownModel} filter model - */ - get aliceL3AndDipoleCurrentFilter() { - return this._aliceL3AndDipoleCurrentFilter; - } - /** * Get muInelasticInteractionRate filter model * @@ -790,10 +775,6 @@ export class RunsOverviewModel extends OverviewPageModel { ...this._odcTopologyFullNameFilter && { 'filter[odcTopologyFullName]': this._odcTopologyFullNameFilter, }, - ...this._aliceL3AndDipoleCurrentFilter.selected.length > 0 && { - 'filter[aliceL3Current]': this._aliceL3AndDipoleCurrentFilter.selectedOptions[0].filteringParams.aliceL3Current, - 'filter[aliceDipoleCurrent]': this._aliceL3AndDipoleCurrentFilter.selectedOptions[0].filteringParams.aliceDipoleCurrent, - }, ...Object.fromEntries(Object.entries(inelFilterModels) .filter(([_, filterModel]) => !filterModel.isEmpty) .map(([property, filterModel]) => [ diff --git a/lib/usecases/run/GetAllRunsUseCase.js b/lib/usecases/run/GetAllRunsUseCase.js index fcf67c329c..7041f83b3d 100644 --- a/lib/usecases/run/GetAllRunsUseCase.js +++ b/lib/usecases/run/GetAllRunsUseCase.js @@ -65,8 +65,7 @@ class GetAllRunsUseCase { runTypes, tags, triggerValues, - aliceL3Current, - aliceDipoleCurrent, + magnets, updatedAt, muInelasticInteractionRate, inelasticInteractionRateAvg, @@ -154,24 +153,29 @@ class GetAllRunsUseCase { if (triggerValues) { filteringQueryBuilder.where('triggerValue').oneOf(...triggerValues); } - if (aliceL3Current !== undefined) { - /** - * The alice l3 current computed from its absolute value and polarity - * @param {function} literal function to create an object representing a database literal expression - * @return {Object} the object representing the column - */ - const computedColumn = ({ literal }) => literal("ROUND(alice_l3_current * IF(`alice_l3_polarity` = 'NEGATIVE', -1, 1) / 1000)"); - filteringQueryBuilder.where(computedColumn).is(aliceL3Current); - } - if (aliceDipoleCurrent !== undefined) { - /** - * The alice dipole current computed from its absolute value and polarity - * @param {function} literal function to create an object representing a database literal expression - * @return {Object} the object representing the column - */ - const computedColumn = ({ literal }) => - literal("ROUND(alice_dipole_current * IF(`alice_dipole_polarity` = 'NEGATIVE', -1, 1) / 1000)"); - filteringQueryBuilder.where(computedColumn).is(aliceDipoleCurrent); + + if (magnets) { + const { l3: aliceL3Current, dipole: aliceDipoleCurrent } = magnets; + if (aliceL3Current !== undefined) { + /** + * The alice l3 current computed from its absolute value and polarity + * @param {function} literal function to create an object representing a database literal expression + * @return {Object} the object representing the column + */ + const computedColumn = ({ literal }) => + literal('ROUND(alice_l3_current * IF(`alice_l3_polarity` = \'NEGATIVE\', -1, 1) / 1000)'); + filteringQueryBuilder.where(computedColumn).is(aliceL3Current); + } + if (aliceDipoleCurrent !== undefined) { + /** + * The alice dipole current computed from its absolute value and polarity + * @param {function} literal function to create an object representing a database literal expression + * @return {Object} the object representing the column + */ + const computedColumn = ({ literal }) => + literal('ROUND(alice_dipole_current * IF(`alice_dipole_polarity` = \'NEGATIVE\', -1, 1) / 1000)'); + filteringQueryBuilder.where(computedColumn).is(aliceDipoleCurrent); + } } if (muInelasticInteractionRate) { diff --git a/test/api/runs.test.js b/test/api/runs.test.js index 53e8868d11..5d59d52e9b 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -526,7 +526,7 @@ module.exports = () => { it('should successfully filter by aliceL3Current', async () => { const response = - await request(server).get('/api/runs?filter[aliceL3Current]=30003'); + await request(server).get('/api/runs?filter[magnets][l3]=30003'); expect(response.status).to.equal(200); const { data: runs } = response.body; @@ -538,7 +538,7 @@ module.exports = () => { }); it('should successfully filter by aliceDipoleCurrent', async () => { - const response = await request(server).get('/api/runs?filter[aliceDipoleCurrent]=0'); + const response = await request(server).get('/api/runs?filter[magnets][dipole]=0'); expect(response.status).to.equal(200); const { data: runs } = response.body; diff --git a/test/lib/usecases/run/GetAllRunsUseCase.test.js b/test/lib/usecases/run/GetAllRunsUseCase.test.js index 652691499b..029b0aeb68 100644 --- a/test/lib/usecases/run/GetAllRunsUseCase.test.js +++ b/test/lib/usecases/run/GetAllRunsUseCase.test.js @@ -635,7 +635,9 @@ module.exports = () => { const { runs } = await new GetAllRunsUseCase().execute({ query: { filter: { - aliceL3Current: 30003, + magnets: { + l3: 30003, + }, }, }, }); @@ -649,7 +651,9 @@ module.exports = () => { const { runs } = await new GetAllRunsUseCase().execute({ query: { filter: { - aliceDipoleCurrent: 0, + magnets: { + dipole: 0, + }, }, }, }); @@ -679,18 +683,26 @@ module.exports = () => { it('should successfully filter by GAQ notBadFraction', async () => { const dataPassIds = [1]; { - const { runs } = await new GetAllRunsUseCase().execute({ query: { filter: { - dataPassIds, - gaq: { notBadFraction: { '<': 0.8 } }, - } } }); + const { runs } = await new GetAllRunsUseCase().execute({ + query: { + filter: { + dataPassIds, + gaq: { notBadFraction: { '<': 0.8 } }, + }, + }, + }); expect(runs).to.be.an('array'); expect(runs.map(({ runNumber }) => runNumber)).to.have.all.members([106]); } { - const { runs } = await new GetAllRunsUseCase().execute({ query: { filter: { - dataPassIds, - gaq: { notBadFraction: { '<': 0.8 }, mcReproducibleAsNotBad: true }, - } } }); + const { runs } = await new GetAllRunsUseCase().execute({ + query: { + filter: { + dataPassIds, + gaq: { notBadFraction: { '<': 0.8 }, mcReproducibleAsNotBad: true }, + }, + }, + }); expect(runs).to.have.lengthOf(0); } }); From e95b5bde5f782c079b4e3e3efec631b0f2021cf8 Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:01:51 +0100 Subject: [PATCH 13/15] [O2B-532] Improve runs overview inelastic interaction rates filtering --- .../filters/FloatComparisonFilterModel.js | 8 +- ...sticInteractionRateActiveColumnsForPbPb.js | 44 +++++++-- ...ractionRateActiveColumnsForProtonProton.js | 22 ++++- .../views/Runs/Overview/RunsOverviewModel.js | 94 +------------------ 4 files changed, 63 insertions(+), 105 deletions(-) diff --git a/lib/public/components/Filters/common/filters/FloatComparisonFilterModel.js b/lib/public/components/Filters/common/filters/FloatComparisonFilterModel.js index 70a911913f..378592c998 100644 --- a/lib/public/components/Filters/common/filters/FloatComparisonFilterModel.js +++ b/lib/public/components/Filters/common/filters/FloatComparisonFilterModel.js @@ -12,11 +12,12 @@ */ import { ComparisonSelectionModel } from './ComparisonSelectionModel.js'; import { FilterInputModel } from './FilterInputModel.js'; +import { FilterModel } from '../FilterModel.js'; /** * FloatComparisonFilterModel */ -export class FloatComparisonFilterModel extends FilterInputModel { +export class FloatComparisonFilterModel extends FilterModel { /** * Constructor */ @@ -67,10 +68,9 @@ export class FloatComparisonFilterModel extends FilterInputModel { * * @return {{ operator: string, operand: number }} current filtering parameters */ - get value() { + get normalized() { return { - operator: this._operatorSelectionModel.selectedOptions[0].label, - operand: this._operandInputModel.value, + [this._operatorSelectionModel.selectedOptions[0].label]: this._operandInputModel.value, }; } diff --git a/lib/public/views/Runs/ActiveColumns/inelasticInteractionRateActiveColumnsForPbPb.js b/lib/public/views/Runs/ActiveColumns/inelasticInteractionRateActiveColumnsForPbPb.js index 2c0c1f96cf..b8e479329e 100644 --- a/lib/public/views/Runs/ActiveColumns/inelasticInteractionRateActiveColumnsForPbPb.js +++ b/lib/public/views/Runs/ActiveColumns/inelasticInteractionRateActiveColumnsForPbPb.js @@ -22,8 +22,15 @@ export const inelasticInteractionRateActiveColumnsForPbPb = { inelasticInteractionRateAvg: { name: h('.flex-wrap', [h('', ['INEL', h('sub', 'avg')]), '(Hz)']), format: (a) => formatFloat(a), - filter: ({ inelasticInteractionRateAvgFilterModel }) => floatNumberFilter( - inelasticInteractionRateAvgFilterModel, + + /** + * Inelastic interaction rate avg filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the filtering component + */ + filter: (runsOverviewModel) => floatNumberFilter( + runsOverviewModel.filteringModel.get('inelasticInteractionRateAvg'), { selectorPrefix: 'inelasticInteractionRateAvg' }, ), visible: true, @@ -37,8 +44,15 @@ export const inelasticInteractionRateActiveColumnsForPbPb = { inelasticInteractionRateAtStart: { name: h('.flex-wrap', [h('', ['INEL', h('sub', 'start')]), '(Hz)']), format: formatFloat, - filter: ({ inelasticInteractionRateAtStartFilterModel }) => floatNumberFilter( - inelasticInteractionRateAtStartFilterModel, + + /** + * Inelastic interaction rate at start filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the filtering component + */ + filter: (runsOverviewModel) => floatNumberFilter( + runsOverviewModel.filteringModel.get('inelasticInteractionRateAtStart'), { selectorPrefix: 'inelasticInteractionRateAtStart' }, ), visible: true, @@ -52,8 +66,15 @@ export const inelasticInteractionRateActiveColumnsForPbPb = { inelasticInteractionRateAtMid: { name: h('.flex-wrap', [h('', ['INEL', h('sub', 'mid')]), '(Hz)']), format: formatFloat, - filter: ({ inelasticInteractionRateAtMidFilterModel }) => floatNumberFilter( - inelasticInteractionRateAtMidFilterModel, + + /** + * Inelastic interaction rate at start filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the filtering component + */ + filter: (runsOverviewModel) => floatNumberFilter( + runsOverviewModel.filteringModel.get('inelasticInteractionRateAtMid'), { selectorPrefix: 'inelasticInteractionRateAtMid' }, ), visible: true, @@ -67,8 +88,15 @@ export const inelasticInteractionRateActiveColumnsForPbPb = { inelasticInteractionRateAtEnd: { name: h('.flex-wrap', [h('', ['INEL', h('sub', 'end')]), '(Hz)']), format: formatFloat, - filter: ({ inelasticInteractionRateAtEndFilterModel }) => floatNumberFilter( - inelasticInteractionRateAtEndFilterModel, + + /** + * Inelastic interaction rate at start filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the filtering component + */ + filter: (runsOverviewModel) => floatNumberFilter( + runsOverviewModel.filteringModel.get('inelasticInteractionRateAtEnd'), { selectorPrefix: 'inelasticInteractionRateAtEnd' }, ), visible: true, diff --git a/lib/public/views/Runs/ActiveColumns/inelasticInteractionRateActiveColumnsForProtonProton.js b/lib/public/views/Runs/ActiveColumns/inelasticInteractionRateActiveColumnsForProtonProton.js index e51423f8e5..7316bb0f67 100644 --- a/lib/public/views/Runs/ActiveColumns/inelasticInteractionRateActiveColumnsForProtonProton.js +++ b/lib/public/views/Runs/ActiveColumns/inelasticInteractionRateActiveColumnsForProtonProton.js @@ -24,8 +24,15 @@ export const inelasticInteractionRateActiveColumnsForProtonProton = { muInelasticInteractionRate: { name: `${GREEK_LOWER_MU_CHAR}(INEL)`, format: formatFloat, - filter: ({ muInelasticInteractionRateFilterModel }) => floatNumberFilter( - muInelasticInteractionRateFilterModel, + + /** + * Mu inelastic interaction rate filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the filtering component + */ + filter: (runsOverviewModel) => floatNumberFilter( + runsOverviewModel.filteringModel.get('muInelasticInteractionRate'), { selectorPrefix: 'muInelasticInteractionRate' }, ), visible: true, @@ -39,8 +46,15 @@ export const inelasticInteractionRateActiveColumnsForProtonProton = { inelasticInteractionRateAvg: { name: h('.flex-wrap', [h('', ['INEL', h('sub', 'avg')]), '(Hz)']), format: (a) => formatFloat(a), - filter: ({ inelasticInteractionRateAvgFilterModel }) => floatNumberFilter( - inelasticInteractionRateAvgFilterModel, + + /** + * Inelastic interaction rate avg filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the filtering component + */ + filter: (runsOverviewModel) => floatNumberFilter( + runsOverviewModel.filteringModel.get('inelasticInteractionRateAvg'), { selectorPrefix: 'inelasticInteractionRateAvg' }, ), visible: true, diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index 5cabed7a84..6474acd0da 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -54,28 +54,13 @@ export class RunsOverviewModel extends OverviewPageModel { runTypes: new RunTypesFilterModel(), eorReason: new EorReasonFilterModel(), magnets: new MagnetsFilteringModel(), + muInelasticInteractionRate: new FloatComparisonFilterModel(), + inelasticInteractionRateAvg: new FloatComparisonFilterModel(), + inelasticInteractionRateAtStart: new FloatComparisonFilterModel(), + inelasticInteractionRateAtMid: new FloatComparisonFilterModel(), + inelasticInteractionRateAtEnd: new FloatComparisonFilterModel(), }); - this._muInelasticInteractionRateFilterModel = new FloatComparisonFilterModel(); - this._muInelasticInteractionRateFilterModel.observe(() => this._applyFilters()); - this._muInelasticInteractionRateFilterModel.visualChange$.bubbleTo(this); - - this._inelasticInteractionRateAvgFilterModel = new FloatComparisonFilterModel(); - this._inelasticInteractionRateAvgFilterModel.observe(() => this._applyFilters()); - this._inelasticInteractionRateAvgFilterModel.visualChange$.bubbleTo(this); - - this._inelasticInteractionRateAtStartFilterModel = new FloatComparisonFilterModel(); - this._inelasticInteractionRateAtStartFilterModel.observe(() => this._applyFilters()); - this._inelasticInteractionRateAtStartFilterModel.visualChange$.bubbleTo(this); - - this._inelasticInteractionRateAtMidFilterModel = new FloatComparisonFilterModel(); - this._inelasticInteractionRateAtMidFilterModel.observe(() => this._applyFilters()); - this._inelasticInteractionRateAtMidFilterModel.visualChange$.bubbleTo(this); - - this._inelasticInteractionRateAtEndFilterModel = new FloatComparisonFilterModel(); - this._inelasticInteractionRateAtEndFilterModel.observe(() => this._applyFilters()); - this._inelasticInteractionRateAtEndFilterModel.visualChange$.bubbleTo(this); - this._filteringModel.observe(() => this._applyFilters(true)); this._filteringModel.visualChange$.bubbleTo(this); @@ -203,12 +188,6 @@ export class RunsOverviewModel extends OverviewPageModel { this._odcTopologyFullNameFilter = ''; - this._muInelasticInteractionRateFilterModel.reset(); - this._inelasticInteractionRateAvgFilterModel.reset(); - this._inelasticInteractionRateAtStartFilterModel.reset(); - this._inelasticInteractionRateAtMidFilterModel.reset(); - this._inelasticInteractionRateAtEndFilterModel.reset(); - if (fetch) { this._applyFilters(true); } @@ -235,11 +214,6 @@ export class RunsOverviewModel extends OverviewPageModel { || this.dcsFilter !== '' || this.epnFilter !== '' || this._odcTopologyFullNameFilter !== '' - || this._muInelasticInteractionRateFilterModel.isEmpty - || this._inelasticInteractionRateAvgFilterModel.isEmpty - || this._inelasticInteractionRateAtStartFilterModel.isEmpty - || this._inelasticInteractionRateAtMidFilterModel.isEmpty - || this._inelasticInteractionRateAtEndFilterModel.isEmpty; } /** @@ -664,51 +638,6 @@ export class RunsOverviewModel extends OverviewPageModel { return allRuns.payload.length < this._pagination.itemsCount; } - /** - * Get muInelasticInteractionRate filter model - * - * @return {FloatComparisonFilterModel} filter model - */ - get muInelasticInteractionRateFilterModel() { - return this._muInelasticInteractionRateFilterModel; - } - - /** - * Get inelasticInteractionRateAvg filter model - * - * @return {FloatComparisonFilterModel} filter model - */ - get inelasticInteractionRateAvgFilterModel() { - return this._inelasticInteractionRateAvgFilterModel; - } - - /** - * Get inelasticInteractionRateAtStart filter model - * - * @return {FloatComparisonFilterModel} filter model - */ - get inelasticInteractionRateAtStartFilterModel() { - return this._inelasticInteractionRateAtStartFilterModel; - } - - /** - * Get inelasticInteractionRateAtMid filter model - * - * @return {FloatComparisonFilterModel} filter model - */ - get inelasticInteractionRateAtMidFilterModel() { - return this._inelasticInteractionRateAtMidFilterModel; - } - - /** - * Get inelasticInteractionRateAtEnd filter model - * - * @return {FloatComparisonFilterModel} filter model - */ - get inelasticInteractionRateAtEndFilterModel() { - return this._inelasticInteractionRateAtEndFilterModel; - } - /** * Returns the list of URL params corresponding to the currently applied filter * @@ -717,13 +646,6 @@ export class RunsOverviewModel extends OverviewPageModel { * @private */ _getFilterQueryParams() { - const inelFilterModels = { - muInelasticInteractionRate: this._muInelasticInteractionRateFilterModel, - inelasticInteractionRateAvg: this._inelasticInteractionRateAvgFilterModel, - inelasticInteractionRateAtStart: this._inelasticInteractionRateAtStartFilterModel, - inelasticInteractionRateAtMid: this._inelasticInteractionRateAtMidFilterModel, - inelasticInteractionRateAtEnd: this._inelasticInteractionRateAtEndFilterModel, - }; return { ...this.runFilterValues && { 'filter[runNumbers]': this.runFilterValues, @@ -775,12 +697,6 @@ export class RunsOverviewModel extends OverviewPageModel { ...this._odcTopologyFullNameFilter && { 'filter[odcTopologyFullName]': this._odcTopologyFullNameFilter, }, - ...Object.fromEntries(Object.entries(inelFilterModels) - .filter(([_, filterModel]) => !filterModel.isEmpty) - .map(([property, filterModel]) => [ - `filter[${property}][${encodeURIComponent(filterModel.value.operator)}]`, - filterModel.value.operand, - ])), }; } From b4166fe6cbcc1e48e3cdeeabd049b9e629897656 Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:28:30 +0100 Subject: [PATCH 14/15] [O2B-532] Improve runs overview run number filter --- .../{runNumber.js => runNumberFilter.js} | 27 +++++---- .../common/filters/RawTextFilterModel.js | 58 +++++++++++++++++++ .../Runs/ActiveColumns/runsActiveColumns.js | 11 +++- .../views/Runs/Overview/RunsOverviewModel.js | 25 +------- .../views/Runs/Overview/RunsOverviewPage.js | 4 +- .../RunsPerDataPassOverviewPage.js | 6 +- 6 files changed, 90 insertions(+), 41 deletions(-) rename lib/public/components/Filters/RunsFilter/{runNumber.js => runNumberFilter.js} (52%) create mode 100644 lib/public/components/Filters/common/filters/RawTextFilterModel.js diff --git a/lib/public/components/Filters/RunsFilter/runNumber.js b/lib/public/components/Filters/RunsFilter/runNumberFilter.js similarity index 52% rename from lib/public/components/Filters/RunsFilter/runNumber.js rename to lib/public/components/Filters/RunsFilter/runNumberFilter.js index 50164a002a..d711b966dc 100644 --- a/lib/public/components/Filters/RunsFilter/runNumber.js +++ b/lib/public/components/Filters/RunsFilter/runNumberFilter.js @@ -14,16 +14,19 @@ import { h } from '/js/src/index.js'; /** - * Returns the author filter component - * @param {RunsOverviewModel} runsOverviewModel the runs overview model - * @return {vnode} A text box that lets the user look for logs with a specific author + * Component to filter runs on run number + * @param {RawTextFilterModel} runNumberFilterModel the filter model + * @return {Component} the filter */ -const runNumberFilter = (runsOverviewModel) => h('input', { - type: 'text', - id: 'runNumber', - value: runsOverviewModel.getRunNumberFilter(), - placeholder: 'e.g. 534454, 534455...', - oninput: (e) => runsOverviewModel.setRunsFilter(e.target.value), -}, ''); - -export default runNumberFilter; +export const runNumberFilter = (runNumberFilterModel) => h( + 'input', + { + type: 'text', + id: 'runNumber', + value: runNumberFilterModel.value, + placeholder: 'e.g. 534454, 534455...', + oninput: (e) => { + runNumberFilterModel.value = e.target.value; + }, + }, +); diff --git a/lib/public/components/Filters/common/filters/RawTextFilterModel.js b/lib/public/components/Filters/common/filters/RawTextFilterModel.js new file mode 100644 index 0000000000..bda8fff4c0 --- /dev/null +++ b/lib/public/components/Filters/common/filters/RawTextFilterModel.js @@ -0,0 +1,58 @@ +import { FilterModel } from '../FilterModel.js'; + +const EMPTY_VALUE = ''; + +/** + * Filtering model to handle raw text value + */ +export class RawTextFilterModel extends FilterModel { + /** + * Constructor + */ + constructor() { + super(); + this._value = EMPTY_VALUE; + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + reset() { + this._value = EMPTY_VALUE; + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + get isEmpty() { + return this._value === EMPTY_VALUE; + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + get normalized() { + return this._value; + } + + /** + * Return the filter current value + * + * @return {string} the current value + */ + get value() { + return this._value; + } + + /** + * Sets the filter current value + * + * @param {string} value the current value + */ + set value(value) { + this._value = value; + } +} diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 5a3f75ba07..e63251955f 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -12,7 +12,7 @@ */ import { h } from '/js/src/index.js'; -import runNumberFilter from '../../../components/Filters/RunsFilter/runNumber.js'; +import { runNumberFilter } from '../../../components/Filters/RunsFilter/runNumberFilter.js'; import environmentIdFilter from '../../../components/Filters/RunsFilter/environmentId.js'; import nDetectorsFilter from '../../../components/Filters/RunsFilter/nDetectors.js'; import nFlpsFilter from '../../../components/Filters/RunsFilter/nFlps.js'; @@ -70,7 +70,14 @@ export const runsActiveColumns = { visible: true, classes: 'w-10 f6 w-wrapped', sortable: true, - filter: runNumberFilter, + + /** + * Run number filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the filter component + */ + filter: (runsOverviewModel) => runNumberFilter(runsOverviewModel.filteringModel.get('runNumber')), format: (runNumber, run) => buttonLinkWithDropdown( runNumber, 'run-detail', diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index 6474acd0da..0e3f8b6631 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -28,6 +28,7 @@ import { MagnetsFilteringModel } from '../../../components/Filters/RunsFilter/Ma import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; import { buildUrl } from '../../../utilities/fetch/buildUrl.js'; import { TimeRangeFilterModel } from '../../../components/Filters/RunsFilter/TimeRangeFilter.js'; +import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; /** * Model representing handlers for runs page @@ -43,6 +44,7 @@ export class RunsOverviewModel extends OverviewPageModel { super(); this._filteringModel = new FilteringModel({ + runNumber: new RawTextFilterModel(), detectors: new DetectorsFilterModel(detectorsProvider.dataTaking$), tags: new TagFilterModel([ CombinationOperator.AND, @@ -158,8 +160,6 @@ export class RunsOverviewModel extends OverviewPageModel { resetFiltering(fetch = true) { this._filteringModel.reset(); - this.runFilterOperation = 'AND'; - this.runFilterValues = ''; this._runDefinitionFilter = []; this._fillNumbersFilter = ''; @@ -199,7 +199,6 @@ export class RunsOverviewModel extends OverviewPageModel { */ isAnyFilterActive() { return this._filteringModel.isAnyFilterActive() - || this.runFilterValues !== '' || this._runDefinitionFilter.length > 0 || this._fillNumbersFilter !== '' || this._runDurationFilter !== null @@ -213,7 +212,7 @@ export class RunsOverviewModel extends OverviewPageModel { || this.ddflpFilter !== '' || this.dcsFilter !== '' || this.epnFilter !== '' - || this._odcTopologyFullNameFilter !== '' + || this._odcTopologyFullNameFilter !== ''; } /** @@ -248,24 +247,6 @@ export class RunsOverviewModel extends OverviewPageModel { this.notify(); } - /** - * Returns the current runNumber substring filter - * @return {String} The current runNumber substring filter - */ - getRunNumberFilter() { - return this.runFilterValues; - } - - /** - * Sets the run Number substring filter if no new inputs were detected for 200 milliseconds - * @param {string} runs The number of the run to apply to the filter - * @return {undefined} - */ - setRunsFilter(runs) { - this.runFilterValues = runs.trim(); - this._applyFilters(); - } - /** * States if the given definition is currently in the run definition filter, and it's the only one * diff --git a/lib/public/views/Runs/Overview/RunsOverviewPage.js b/lib/public/views/Runs/Overview/RunsOverviewPage.js index 1658bebfee..24ff126331 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewPage.js +++ b/lib/public/views/Runs/Overview/RunsOverviewPage.js @@ -18,7 +18,7 @@ import { filtersPanelPopover } from '../../../components/Filters/common/filtersP import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; import { runsActiveColumns } from '../ActiveColumns/runsActiveColumns.js'; import { table } from '../../../components/common/table/table.js'; -import runNumberFilter from '../../../components/Filters/RunsFilter/runNumber.js'; +import { runNumberFilter } from '../../../components/Filters/RunsFilter/runNumberFilter.js'; import { switchInput } from '../../../components/common/form/switchInput.js'; import { RunDefinition } from '../../../domain/enums/RunDefinition.js'; @@ -54,7 +54,7 @@ export const RunsOverviewPage = ({ runs: { overviewModel: runsOverviewModel }, m return h('', [ h('.flex-row.header-container.g2.pv2', [ filtersPanelPopover(runsOverviewModel, runsActiveColumns), - h('.pl2#runOverviewFilter', runNumberFilter(runsOverviewModel)), + h('.pl2#runOverviewFilter', runNumberFilter(runsOverviewModel.filteringModel.get('runNumber'))), togglePhysicsOnlyFilter(runsOverviewModel), exportRunsTriggerAndModal(runsOverviewModel, modalModel), ]), diff --git a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js index 17812d641e..dc73242c12 100644 --- a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js +++ b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js @@ -24,7 +24,7 @@ import { createRunDetectorsAsyncQcActiveColumns } from '../ActiveColumns/runDete import { inelasticInteractionRateActiveColumnsForPbPb } from '../ActiveColumns/inelasticInteractionRateActiveColumnsForPbPb.js'; import { inelasticInteractionRateActiveColumnsForProtonProton } from '../ActiveColumns/inelasticInteractionRateActiveColumnsForProtonProton.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; -import runNumberFilter from '../../../components/Filters/RunsFilter/runNumber.js'; +import { runNumberFilter } from '../../../components/Filters/RunsFilter/runNumberFilter.js'; import { qcSummaryLegendTooltip } from '../../../components/qcFlags/qcSummaryLegendTooltip.js'; import { isRunNotSubjectToQc } from '../../../components/qcFlags/isRunSubjectToQc.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; @@ -94,7 +94,7 @@ const skimmableControl = (dataPass, onclick, requestResult) => { /** * Render Runs Per LHC Period overview page * @param {Model} model The overall model object. - * @param {Model} [model.runs.perDataPassOverviewModel] model holding state for of the page + * @param {RunsPerDataPassOverviewModel} [model.runs.perDataPassOverviewModel] model holding state for of the page * @return {Component} The overview page */ export const RunsPerDataPassOverviewPage = ({ @@ -205,7 +205,7 @@ export const RunsPerDataPassOverviewPage = ({ return h('', { onremove: () => perDataPassOverviewModel.reset(false) }, [ h('.flex-row.justify-between.items-center.g2', [ filtersPanelPopover(perDataPassOverviewModel, activeColumns, { profile: 'runsPerDataPass' }), - h('.pl2#runOverviewFilter', runNumberFilter(perDataPassOverviewModel)), + h('.pl2#runOverviewFilter', runNumberFilter(perDataPassOverviewModel.filteringModel.get('runNumber'))), h( '.flex-row.g1.items-center', remoteDataPass.match({ From e8b872a98a65b906868bf2f1e2ead8f10f6f1124 Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Sun, 29 Dec 2024 21:13:00 +0100 Subject: [PATCH 15/15] [O2B-532] Improve runs overview run definition filter --- .../RunsFilter/RunDefinitionFilterModel.js | 109 ++++++++++++++++++ ...nitionFilter.js => runDefinitionFilter.js} | 17 ++- .../Runs/ActiveColumns/runsActiveColumns.js | 11 +- .../views/Runs/Overview/RunsOverviewModel.js | 63 +--------- .../views/Runs/Overview/RunsOverviewPage.js | 14 +-- 5 files changed, 135 insertions(+), 79 deletions(-) create mode 100644 lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js rename lib/public/components/Filters/RunsFilter/{definitionFilter.js => runDefinitionFilter.js} (59%) diff --git a/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js b/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js new file mode 100644 index 0000000000..dd5ee54cf9 --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js @@ -0,0 +1,109 @@ +import { RunDefinition } from '../../../domain/enums/RunDefinition.js'; +import { FilterModel } from '../common/FilterModel.js'; + +/** + * Run definition filter model + */ +export class RunDefinitionFilterModel extends FilterModel { + /** + * Constructor + */ + constructor() { + super(); + this._definitions = []; + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + reset() { + this._definitions = []; + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + get isEmpty() { + return this._definitions.length === 0; + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritDoc + */ + get normalized() { + return this._definitions.join(','); + } + + /** + * States if the given definition is currently in the run definition filter + * + * @param {string} definition the run definition to look for + * @return {boolean} true if the definition is included in the filter + */ + isDefinitionInFilter(definition) { + return this._definitions.includes(definition); + } + + /** + * Add a given definition in the current run definition filter if it is not already present + * + * @param {string} definition the run definition to add + * @return {void} + */ + addDefinition(definition) { + if (!this.isDefinitionInFilter(definition)) { + this._definitions.push(definition); + this.notify(); + } + } + + /** + * Remove a given definition from the current run definition filter if it is in it (else do nothing) + * + * @param {string} definition the definition to add + * @return {void} + */ + removeDefinition(definition) { + const originalLength = this._definitions._definitions; + this._definitions = this._definitions.filter((existingDefinition) => definition !== existingDefinition); + if (this._definitions.length !== originalLength) { + this.notify(); + } + } + + /** + * Sets the current filter to physics only + * + * @return {void} + */ + setPhysicsOnly() { + if (!this.isPhysicsOnly()) { + this._definitions = [RunDefinition.Physics]; + this.notify(); + } + } + + /** + * Returns true if the current filter is physics only + * + * @return {boolean} true if filter is physics only + */ + isPhysicsOnly() { + return this._definitions.length === 1 && this._definitions[0] === RunDefinition.Physics; + } + + /** + * Empty the current filter + * + * @return {void} + */ + setEmpty() { + if (!this.isEmpty) { + this.reset(); + this.notify(); + } + } +} diff --git a/lib/public/components/Filters/RunsFilter/definitionFilter.js b/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js similarity index 59% rename from lib/public/components/Filters/RunsFilter/definitionFilter.js rename to lib/public/components/Filters/RunsFilter/runDefinitionFilter.js index f0ec8f01f5..79e4f9e3f6 100644 --- a/lib/public/components/Filters/RunsFilter/definitionFilter.js +++ b/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js @@ -15,17 +15,16 @@ import { checkboxFilter } from '../common/filters/checkboxFilter.js'; import { RUN_DEFINITIONS } from '../../../domain/enums/RunDefinition.js'; /** - * Returns the definition filter component - * @param {RunsOverviewModel} runModel The run model - * @return {vnode} A list of checkboxes that lets the user look for runs with specific definition + * Renders a list of checkboxes that lets the user look for runs with specific definition + * + * @param {RunDefinitionFilterModel} runDefinitionFilterModel run definition filter model + * @return {Component} the filter */ -const definitionFilter = (runModel) => checkboxFilter( +export const runDefinitionFilter = (runDefinitionFilterModel) => checkboxFilter( 'runDefinition', RUN_DEFINITIONS, - (runQuality) => runModel.isDefinitionInFilter(runQuality), + (runDefinition) => runDefinitionFilterModel.isDefinitionInFilter(runDefinition), (e, definition) => e.target.checked - ? runModel.addDefinitionFilter(definition) - : runModel.removeDefinitionFilter(definition), + ? runDefinitionFilterModel.addDefinition(definition) + : runDefinitionFilterModel.removeDefinition(definition), ); - -export default definitionFilter; diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index e63251955f..c25c661122 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -31,7 +31,7 @@ import nEpnsFilter from '../../../components/Filters/RunsFilter/nEpns.js'; import { triggerValueFilter } from '../../../components/Filters/RunsFilter/triggerValueFilter.js'; import lhcPeriodsFilter from '../../../components/Filters/RunsFilter/lhcPeriod.js'; import { formatRunType } from '../../../utilities/formatting/formatRunType.js'; -import definitionFilter from '../../../components/Filters/RunsFilter/definitionFilter.js'; +import { runDefinitionFilter } from '../../../components/Filters/RunsFilter/runDefinitionFilter.js'; import { profiles } from '../../../components/common/table/profiles.js'; import { formatDuration } from '../../../utilities/formatting/formatDuration.mjs'; import { formatRunStart } from '../format/formatRunStart.js'; @@ -326,7 +326,14 @@ export const runsActiveColumns = { } return h('.flex-column.items-start', lines); }, - filter: definitionFilter, + + /** + * Run definition filter component + * + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the filter component + */ + filter: (runsOverviewModel) => runDefinitionFilter(runsOverviewModel.filteringModel.get('definitions')), }, runDuration: { name: 'Duration', diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index 0e3f8b6631..2ad4076d3a 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -29,6 +29,7 @@ import { FilteringModel } from '../../../components/Filters/common/FilteringMode import { buildUrl } from '../../../utilities/fetch/buildUrl.js'; import { TimeRangeFilterModel } from '../../../components/Filters/RunsFilter/TimeRangeFilter.js'; import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; +import { RunDefinitionFilterModel } from '../../../components/Filters/RunsFilter/RunDefinitionFilterModel.js'; /** * Model representing handlers for runs page @@ -53,6 +54,7 @@ export class RunsOverviewModel extends OverviewPageModel { ]), o2start: new TimeRangeFilterModel(), o2end: new TimeRangeFilterModel(), + definitions: new RunDefinitionFilterModel(), runTypes: new RunTypesFilterModel(), eorReason: new EorReasonFilterModel(), magnets: new MagnetsFilteringModel(), @@ -160,8 +162,6 @@ export class RunsOverviewModel extends OverviewPageModel { resetFiltering(fetch = true) { this._filteringModel.reset(); - this._runDefinitionFilter = []; - this._fillNumbersFilter = ''; this._runDurationFilter = null; @@ -199,7 +199,6 @@ export class RunsOverviewModel extends OverviewPageModel { */ isAnyFilterActive() { return this._filteringModel.isAnyFilterActive() - || this._runDefinitionFilter.length > 0 || this._fillNumbersFilter !== '' || this._runDurationFilter !== null || this._lhcPeriodsFilter !== null @@ -247,61 +246,6 @@ export class RunsOverviewModel extends OverviewPageModel { this.notify(); } - /** - * States if the given definition is currently in the run definition filter, and it's the only one - * - * @param {string} definition the run definition to look for - * @return {boolean} true if the definition is the only one currently applied - */ - isDefinitionOnlyOneInFilter(definition) { - return this._runDefinitionFilter.length === 1 && this._runDefinitionFilter[0] === definition; - } - - /** - * States if the given definition is currently in the run definition filter - * - * @param {string} definition the run definition to look for - * @return {boolean} true if the definition is included in the filter - */ - isDefinitionInFilter(definition) { - return this._runDefinitionFilter.includes(definition); - } - - /** - * Add a given definition in the current run definition filter if it is not already present, then refresh runs list - * - * @param {string} definition the run definition to add - * @return {void} - */ - addDefinitionFilter(definition) { - if (!this.isDefinitionInFilter(definition)) { - this._runDefinitionFilter.push(definition); - this._applyFilters(); - } - } - - /** - * Remove a given definition from the current run definition filter if it is in it (else do nothing) then refresh runs list - * - * @param {string} definition the definition to add - * @return {void} - */ - removeDefinitionFilter(definition) { - this._runDefinitionFilter = this._runDefinitionFilter.filter((existingDefinition) => definition !== existingDefinition); - this._applyFilters(); - } - - /** - * Set the list of definition to be used as the current run definition filter - * - * @param {string[]} definitions the new definition filter - * @return {void} - */ - setDefinitionFilter(definitions) { - this._runDefinitionFilter = [...definitions]; - this._applyFilters(); - } - /** * Return the currently applied fill number filter * @@ -631,9 +575,6 @@ export class RunsOverviewModel extends OverviewPageModel { ...this.runFilterValues && { 'filter[runNumbers]': this.runFilterValues, }, - ...this._runDefinitionFilter.length > 0 && { - 'filter[definitions]': this._runDefinitionFilter.join(','), - }, ...this._fillNumbersFilter && { 'filter[fillNumbers]': this._fillNumbersFilter, }, diff --git a/lib/public/views/Runs/Overview/RunsOverviewPage.js b/lib/public/views/Runs/Overview/RunsOverviewPage.js index 24ff126331..495853fbda 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewPage.js +++ b/lib/public/views/Runs/Overview/RunsOverviewPage.js @@ -20,7 +20,6 @@ import { runsActiveColumns } from '../ActiveColumns/runsActiveColumns.js'; import { table } from '../../../components/common/table/table.js'; import { runNumberFilter } from '../../../components/Filters/RunsFilter/runNumberFilter.js'; import { switchInput } from '../../../components/common/form/switchInput.js'; -import { RunDefinition } from '../../../domain/enums/RunDefinition.js'; const TABLEROW_HEIGHT = 59; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -29,14 +28,15 @@ const PAGE_USED_HEIGHT = 215; /** * Display a toggle switch to display physics runs only * - * @param {RunsOverviewModel} runsOverviewModel the model of the runs overview + * @param {RunDefinitionFilterModel} runDefinitionFilterModel the run definition filter model * @returns {Component} the toggle switch */ -export const togglePhysicsOnlyFilter = (runsOverviewModel) => { - const isPhysicsOnly = runsOverviewModel.isDefinitionOnlyOneInFilter(RunDefinition.Physics); +export const togglePhysicsOnlyFilter = (runDefinitionFilterModel) => { + const isPhysicsOnly = runDefinitionFilterModel.isPhysicsOnly(); const onChange = isPhysicsOnly - ? () => runsOverviewModel.setDefinitionFilter([]) - : () => runsOverviewModel.setDefinitionFilter([RunDefinition.Physics]); + ? () => runDefinitionFilterModel.setEmpty() + : () => runDefinitionFilterModel.setPhysicsOnly(); + return switchInput(isPhysicsOnly, onChange, { labelAfter: 'PHYSICS ONLY' }); }; @@ -55,7 +55,7 @@ export const RunsOverviewPage = ({ runs: { overviewModel: runsOverviewModel }, m h('.flex-row.header-container.g2.pv2', [ filtersPanelPopover(runsOverviewModel, runsActiveColumns), h('.pl2#runOverviewFilter', runNumberFilter(runsOverviewModel.filteringModel.get('runNumber'))), - togglePhysicsOnlyFilter(runsOverviewModel), + togglePhysicsOnlyFilter(runsOverviewModel.filteringModel.get('definitions')), exportRunsTriggerAndModal(runsOverviewModel, modalModel), ]), h('.flex-column.w-100', [