Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions QualityControl/lib/dtos/ObjectGetDto.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import Joi from 'joi';
import { RunNumberDto } from './filters/RunNumberDto.js';
import { QcDetectorNameDto } from './filters/QcDetectorNameDto.js';

const periodNamePattern = /^LHC\d{1,2}[a-z0-9]+$/i;

Expand All @@ -25,6 +26,7 @@ const periodNamePattern = /^LHC\d{1,2}[a-z0-9]+$/i;
function createFiltersSchema(runTypes) {
return Joi.object({
RunNumber: RunNumberDto.optional(),
QcDetectorName: QcDetectorNameDto.optional(),
RunType: runTypes.length > 0
? Joi.string().valid(...runTypes).optional()
: Joi.string().optional(),
Expand Down
22 changes: 22 additions & 0 deletions QualityControl/lib/dtos/filters/QcDetectorNameDto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @license
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
* All rights not expressly granted are reserved.
*
* This software is distributed under the terms of the GNU General Public
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
*
* 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 Joi from 'joi';

export const QcDetectorNameDto = Joi.string()
.min(1)
.messages({
'number.base': 'Detector name must be a string',
'number.min': 'Detector name must not be an empty string',
});
75 changes: 75 additions & 0 deletions QualityControl/public/common/filters/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,81 @@ export const dynamicSelector = (config) => {
});
};

/**
* Represents options grouped for HTML <optgroup>.
* Keys are group labels (for the <optgroup> label),
* values are arrays of option values (for <option> elements).
* @typedef {Record<string, string[]>} GroupedDropdownOptions
*/

/**
* Builds a filter element. If options to show, selector filter element; otherwise, input element.
* @param {object} config - Configuration object for building the filter element.
* @param {string} config.queryLabel - The key used to query the storage with this parameter.
* @param {string} config.placeholder - The placeholder text to be displayed in the input field.
* @param {string} config.id - The unique identifier for the input field.
* @param {object} config.filterMap - Map of the current filter values.
* @param {string} [config.type='text'] - The type of the filter element (e.g., 'text', 'number').
* @param {GroupedDropdownOptions} [config.options={}] - List of options for a grouped dropdown selector (optional).
* @param {(filterId: string, value: string, setUrl: boolean) => void} config.onChangeCallback
* - Callback to be triggered on the change event of the filter.
* @param {(filterId: string, value: string, setUrl: boolean) => void} config.onInputCallback
* - Callback to be triggered on the input event.
* @param {(filterId: string, value: string, setUrl: boolean) => void} config.onEnterCallback
* - Callback to be triggered when the Enter key is pressed.
* @param {string} [config.width='.w-20'] - The CSS class that defines the width of the filter.
* @returns {vnode} A virtual node element representing the filter element (input or grouped dropdown).
*/
export const groupedDropdownComponent = ({
queryLabel,
placeholder,
id,
filterMap,
options = {},
onChangeCallback,
onInputCallback,
onEnterCallback,
type = 'text',
width = '.w-20',
}) => {
const groups = Object.keys(options);
if (!groups.length) {
return filterInput({ queryLabel, placeholder, id, filterMap, onInputCallback, onEnterCallback, type, width });
}

const selectedOption = filterMap[queryLabel];
const validValue = Object.values(options).flat().some((option) => option === selectedOption);
if (selectedOption && !validValue) {
onChangeCallback(queryLabel, '', true);
}

const sortedGroupedOptions = groups
.sort((a, b) => a.localeCompare(b)) // sort group labels
.reduce((acc, key) => {
// sort option names and add to accumulator
acc[key] = [...options[key]].sort((a, b) => a.localeCompare(b));
return acc;
}, {});

return h(`${width}`, [
h('select.form-control', {
placeholder,
id,
name: id,
value: validValue ? selectedOption : '',
onchange: (event) => onChangeCallback(queryLabel, event.target.value, true),
}, [
h('option', { value: '' }, placeholder),
h('hr'),
...Object.entries(sortedGroupedOptions).map(([key, value]) => h(
'optgroup',
{ label: key },
value.map((option) => h('option', { value: option }, option)),
)),
]),
]);
};

/**
* Builds a filter input element that allows the user to specify a parameter to be used when querying objects.
* This function renders a text input element with event handling for input and Enter key press.
Expand Down
1 change: 1 addition & 0 deletions QualityControl/public/common/filters/filterTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
const FilterType = {
INPUT: 'input',
DROPDOWN: 'dropdownSelector',
GROUPED_DROPDOWN: 'groupedDropdownSelector',
RUN_MODE: 'runModeSelector',
};

Expand Down
9 changes: 8 additions & 1 deletion QualityControl/public/common/filters/filterViews.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
* or submit itself to any jurisdiction.
*/

import { filterInput, dynamicSelector, ongoingRunsSelector } from './filter.js';
import {
filterInput,
dynamicSelector,
ongoingRunsSelector,
groupedDropdownComponent,
} from './filter.js';
import { FilterType } from './filterTypes.js';
import { filtersConfig, runModeFilterConfig } from './filtersConfig.js';
import { runModeCheckbox } from './runMode/runModeCheckbox.js';
Expand Down Expand Up @@ -50,6 +55,8 @@ const createFilterElement =
case FilterType.INPUT: return filterInput({ ...commonConfig, type: inputType });
case FilterType.DROPDOWN:
return dynamicSelector({ ...commonConfig, options, onChangeCallback, inputType });
case FilterType.GROUPED_DROPDOWN:
return groupedDropdownComponent({ ...commonConfig, options, onChangeCallback, inputType });
case FilterType.RUN_MODE:
return ongoingRunsSelector(
{ ...commonConfig },
Expand Down
23 changes: 20 additions & 3 deletions QualityControl/public/common/filters/filtersConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ import { FilterType } from './filterTypes.js';
/**
* Returns an array of filter configuration objects used to render dynamic filter inputs.
* @param {FilterService} filterService - service to get the data to populate the filters
* @param {Array<string>} filterService.runTypes - run types to show in the filter
* @returns {Array<object>} Filter configuration array
* @param {string[]} filterService.runTypes - run types to show in the filter
* @param {DetectorSummary[]} filterService.detectors - detectors to show in the filter
* @returns {object[]} Filter configuration array
*/
export const filtersConfig = ({ runTypes }) => [
export const filtersConfig = ({ runTypes, detectors }) => [
{
type: FilterType.INPUT,
queryLabel: 'RunNumber',
Expand All @@ -35,6 +36,22 @@ export const filtersConfig = ({ runTypes }) => [
id: 'runTypeFilter',
options: runTypes,
},
{
type: FilterType.GROUPED_DROPDOWN,
queryLabel: 'QcDetectorName',
placeholder: 'Detector (any)',
id: 'detectorFilter',
options: detectors.match({
Success: (detectors) => detectors.reduce((acc, detector) => {
if (!acc[detector.type]) {
acc[detector.type] = [];
}
acc[detector.type].push(detector.name);
return acc;
}, {}),
Other: () => {},
}),
},
{
type: FilterType.INPUT,
queryLabel: 'PeriodName',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import FilterService from '../../../services/Filter.service.js';
import { RunStatus } from '../../../library/runStatus.enum.js';
import { prettyFormatDate } from '../../utils.js';

const CCDB_QUERY_PARAMS = ['PeriodName', 'PassName', 'RunNumber', 'RunType'];
const CCDB_QUERY_PARAMS = ['PeriodName', 'PassName', 'RunNumber', 'RunType', 'QcDetectorName'];

const RUN_INFORMATION_MAP = {
startTime: prettyFormatDate,
Expand Down
33 changes: 27 additions & 6 deletions QualityControl/public/services/Filter.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,27 @@ export default class FilterService {
this.filterModel = filterModel;
this.loader = filterModel.model.loader;

this.runTypes = RemoteData.notAsked();
this._runTypes = RemoteData.notAsked();
this._detectors = RemoteData.notAsked();

this.ongoingRuns = RemoteData.notAsked();
}

/**
* Method to get all run types to show in the filter
* @returns {RemoteData} - result within a RemoteData object
*/
async getRunTypes() {
this.runTypes = RemoteData.loading();
async getFilterConfigurations() {
this._runTypes = RemoteData.loading();
this._detectors = RemoteData.loading();
this.filterModel.notify();
const { result, ok } = await this.loader.get('/api/filter/configuration');
if (ok) {
this.runTypes = RemoteData.success(result?.runTypes || []);
this._runTypes = RemoteData.success(result?.runTypes || []);
this._detectors = RemoteData.success(result?.detectors || []);
} else {
this.runTypes = RemoteData.failure('Error retrieving runTypes');
this._runTypes = RemoteData.failure('Error retrieving runTypes');
this._detectors = RemoteData.failure('Error retrieving detectors');
}
this.filterModel.notify();
}
Expand Down Expand Up @@ -73,7 +78,7 @@ export default class FilterService {
* @returns {void}
*/
async initFilterService() {
await this.getRunTypes();
await this.getFilterConfigurations();
}

/**
Expand Down Expand Up @@ -105,4 +110,20 @@ export default class FilterService {
}
this.filterModel.notify();
}

/**
* Gets the list of run types.
* @returns {string[]} An array containing the run types.
*/
get runTypes() {
return this._runTypes;
}

/**
* Gets the list of detectors.
* @returns {DetectorSummary[]} An array containing detector objects.
*/
get detectors() {
return this._detectors;
}
}
31 changes: 31 additions & 0 deletions QualityControl/test/public/pages/object-tree.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,4 +337,35 @@ export const objectTreePageTests = async (url, page, timeout = 5000, testParent)
deepStrictEqual(options, ['', 'runType1', 'runType2']);
},
);

await testParent.test(
'should have a grouped selector with sorted options to filter by detector if there are detectors loaded',
{ timeout },
async () => {
const optionsObject = await page.evaluate(() => {
const optionElements = document.querySelectorAll('#detectorFilter > optgroup > option');

return Array.from(optionElements).reduce((acc, option) => {
const optgroup = option.parentElement.label;
if (!optgroup) {
return acc;
}

if (!acc[optgroup]) {
acc[optgroup] = [];
}
acc[optgroup].push(option.value);

return acc;
}, {});
});

deepStrictEqual(optionsObject, {
'AOT-EVENT': ['EVS'],
PHYSICAL: ['ACO', 'CPV'],
QC: ['GLO'],
VIRTUAL: ['TST'],
});
},
);
};
Loading