Skip to content

Commit

Permalink
[O2B-536] Improve runs overview detectors filter (#1840)
Browse files Browse the repository at this point in the history
* [O2B-532] Improve runs overview detectors filter

* Uniformize remote-based selection dropdown

* Fix lint

* Try to fix qc flag types test randomly failing

* Fix typo
  • Loading branch information
martinboulais authored Jan 31, 2025
1 parent d333be2 commit f63cacb
Show file tree
Hide file tree
Showing 13 changed files with 228 additions and 165 deletions.
84 changes: 37 additions & 47 deletions lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,100 +10,90 @@
* 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 { ObservableBasedSelectionDropdownModel } from '../../detector/ObservableBasedSelectionDropdownModel.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
*
* @param {ObservableData<RemoteData<Detector[]>>} observableDetectors observable remote data of detectors list
*/
constructor(observableDetectors) {
super();
this._dropdownModel = new DetectorSelectionDropdownModel(observableDetectors);
this._dropdownModel.bubbleTo(this);
this._dropdownModel = new ObservableBasedSelectionDropdownModel(
observableDetectors,
({ name }) => ({ value: name }),
);
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
*/
isEmpty() {
return this.selected.length === 0 && !this.isNone();
}

/**
* Return true if the current combination operator is none
*
* @return {boolean} true if the current combination operator is none
*/
isNone() {
return this.combinationOperator === CombinationOperator.NONE.value;
}

/**
* Reset the model to its default state
*
* @return {void}
* @inheritDoc
*/
reset() {
this._dropdownModel.reset();
this._combinationOperatorModel.reset();
}

// eslint-disable-next-line valid-jsdoc
/**
* Return the model storing the list of selected detectors
*
* @return {DetectorSelectionDropdownModel} the detectors selection model
* @inheritDoc
*/
get dropdownModel() {
return this._dropdownModel;
get isEmpty() {
return this._dropdownModel.isEmpty && !this.isNone();
}

// eslint-disable-next-line valid-jsdoc
/**
* Shortcut to get the list of selected detectors
* @inheritDoc
*/
get selected() {
return this._dropdownModel.selected;
get normalized() {
const normalized = {
operator: this._combinationOperatorModel.current,
};
if (!this.isNone()) {
normalized.values = this._dropdownModel.selected.join();
}
return normalized;
}

/**
* Return the model storing the combination operator to apply on the list of detectors
* Return true if the current combination operator is none
*
* @return {CombinationOperatorChoiceModel} the model
* @return {boolean} true if the current combination operator is none
*/
get combinationOperatorModel() {
return this._combinationOperatorModel;
isNone() {
return this._combinationOperatorModel.current === CombinationOperator.NONE.value;
}

/**
* Shortcut to get the current combination operator
* Return the model storing the list of selected detectors
*
* @return {string} the current operator
* @return {ObservableBasedSelectionDropdownModel} the detectors selection model
*/
get combinationOperator() {
return this._combinationOperatorModel.current;
get dropdownModel() {
return this._dropdownModel;
}

/**
* Returns an observable notified any time a visual change occurs that has no impact on the actual selection
* Return the model storing the combination operator to apply on the list of detectors
*
* @return {Observable} the visual change observable
* @return {CombinationOperatorChoiceModel} the model
*/
get visualChange$() {
return this._dropdownModel.visualChange$;
get combinationOperatorModel() {
return this._combinationOperatorModel;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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' }),
];
11 changes: 8 additions & 3 deletions lib/public/components/Filters/common/TagFilterModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
* or submit itself to any jurisdiction.
*/
import { CombinationOperatorChoiceModel } from './CombinationOperatorChoiceModel.js';
import { TagSelectionDropdownModel } from '../../tag/TagSelectionDropdownModel.js';
import { FilterModel } from './FilterModel.js';
import { ObservableBasedSelectionDropdownModel } from '../../detector/ObservableBasedSelectionDropdownModel.js';
import { tagToOption } from '../../tag/tagToOption.js';

/**
* Model to handle the state of a tags filter
Expand All @@ -21,12 +22,16 @@ export class TagFilterModel extends FilterModel {
/**
* Constructor
*
* @param {ObservableData<RemoteData<Tag[]>>} observableTags observable remote data of tags list
* @param {SelectionOption} [operators] optionally the list of available operators for the filter
* @constructor
*/
constructor(operators) {
constructor(observableTags, operators) {
super();
this._selectionModel = new TagSelectionDropdownModel({ includeArchived: true });
this._selectionModel = new ObservableBasedSelectionDropdownModel(
observableTags,
tagToOption,
);
this._addSubmodel(this._selectionModel);

this._combinationOperatorModel = new CombinationOperatorChoiceModel(operators);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,27 @@ import { RemoteData } from '/js/src/index.js';

/**
* Model storing state of a selection of detectors picked from the list of all the existing detectors
*
* @template T
*/
export class DetectorSelectionDropdownModel extends SelectionDropdownModel {
export class ObservableBasedSelectionDropdownModel extends SelectionDropdownModel {
/**
* @callback ItemToOption
* @param {T} item the item to convert to option
* @return {SelectionOption} the corresponding selection option
*/

/**
* Constructor
*
* @param {ObservableData<RemoteData<Detector[]>>} observableDetectors observable remote data of detectors list
* @param {ObservableData<RemoteData<T[]>>} observableItems observable remote data of items to be used as selection options
* @param {ItemToOption} itemToOption function to map between remote data items to options
*/
constructor(observableDetectors) {
constructor(observableItems, itemToOption) {
super({ availableOptions: RemoteData.loading() });
observableDetectors.observe(() => {
this.setAvailableOptions(observableDetectors.getCurrent().apply({
Success: (detectors) => detectors.map(({ name }) => ({ value: name })),
observableItems.observe(() => {
this.setAvailableOptions(observableItems.getCurrent().apply({
Success: (items) => items.map(itemToOption),
}));
});
}
Expand Down
105 changes: 105 additions & 0 deletions lib/public/services/RemoteDataProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* @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 { ObservableData } from '../utilities/ObservableData.js';

/**
* Data provider based on remote data
*
* @template T
*/
export class RemoteDataProvider {
/**
* Constructor
*/
constructor() {
this._items = null;

this._items$ = ObservableData.builder()
.initialValue(RemoteData.notAsked())
.build();
}

/**
* Return the list of items
*
* @return {Promise<T[]>} the items
*/
async getItems() {
if (!this._items) {
this._items = await this.getRemoteData();
}

return this._items;
}

/**
* Return the observable list of items (lazy-load it if needed)
*
* @return {ObservableData<RemoteData<T[], ApiError>>} the observable list of items
*/
get items$() {
if (this._isStale()) {
this._load();
}

return this._items$;
}

/**
* Fetch the data
*
* @return {Promise<T[]>} the fetched items
* @abstract
*/
getRemoteData() {
throw new Error('Method not implemented');
}

/**
* Mark the state of the provider as being stale, data will be fetched again next time it will be asked
*
* @return {void}
*/
markAsStale() {
this._items = null;
this._items$.setCurrent(RemoteData.notAsked());
}

/**
* States if the items observable has never been asked
*
* @return {boolean} true if the items observable has never been asked
* @protected
*/
_isStale() {
return this._items$.getCurrent().match({
NotAsked: () => true,
Other: () => false,
});
}

/**
* Fill the items$ observable data
*
* @return {void}
* @protected
*/
_load() {
this._items$.setCurrent(RemoteData.loading());
this.getItems().then(
(items) => this._items$.setCurrent(RemoteData.success(items)),
(error) => this._items$.setCurrent(RemoteData.failure(error)),
);
}
}
Loading

0 comments on commit f63cacb

Please sign in to comment.