Skip to content

Commit 64ad126

Browse files
authored
[OGUI-1867] Add detector filter option with grouped dropdown (#3274)
* Add detector filter option with grouped dropdown * Keep only valid types*names of detectors
1 parent 54d76c2 commit 64ad126

12 files changed

Lines changed: 217 additions & 22 deletions

File tree

QualityControl/lib/dtos/ObjectGetDto.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import Joi from 'joi';
1616
import { RunNumberDto } from './filters/RunNumberDto.js';
17+
import { DetectorNameDto } from './filters/DetectorNameDto.js';
1718

1819
const periodNamePattern = /^LHC\d{1,2}[a-z0-9]+$/i;
1920
const qcVersionPattern = /^\d+\.\d+(\.\d+)?$/;
@@ -26,6 +27,7 @@ const qcVersionPattern = /^\d+\.\d+(\.\d+)?$/;
2627
function createFiltersSchema(runTypes) {
2728
return Joi.object({
2829
RunNumber: RunNumberDto.optional(),
30+
DetectorName: DetectorNameDto.optional(),
2931
RunType: runTypes.length > 0
3032
? Joi.string().valid(...runTypes).optional()
3133
: Joi.string().optional(),
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* @license
3+
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
4+
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
5+
* All rights not expressly granted are reserved.
6+
*
7+
* This software is distributed under the terms of the GNU General Public
8+
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
9+
*
10+
* In applying this license CERN does not waive the privileges and immunities
11+
* granted to it by virtue of its status as an Intergovernmental Organization
12+
* or submit itself to any jurisdiction.
13+
*/
14+
15+
import Joi from 'joi';
16+
17+
/**
18+
* Joi validation schema for QcDetectorName filter (also known as detector when fetched from BKP and used in UI)
19+
* @type {Joi.StringSchema}
20+
*/
21+
export const DetectorNameDto = Joi.string()
22+
.uppercase()
23+
.length(3)
24+
.pattern(/^[A-Z]{3}$/)
25+
.messages({
26+
'string.base': 'Detector name must be a string',
27+
'string.uppercase': 'Detector name must be uppercase',
28+
'string.length': 'Detector name must be exactly 3 characters',
29+
'string.pattern.base': 'Detector name must contain only uppercase letters (e.g., TPC, ITS)',
30+
});

QualityControl/lib/services/FilterService.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ export class FilterService {
8282
}
8383
try {
8484
const detectorSummaries = await this._bookkeepingService.retrieveDetectorSummaries();
85-
this._detectors = Object.freeze(detectorSummaries.map(({ name, type }) => Object.freeze({ name, type })));
85+
this._detectors = Object.freeze(detectorSummaries
86+
.filter(({ name, type }) => name && type)
87+
.map(({ name, type }) => Object.freeze({ name, type })));
8688
} catch (error) {
8789
this._logger.errorMessage(`Failed to retrieve detectors: ${error?.message || error}`);
8890
}

QualityControl/lib/services/ccdb/CcdbConstants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const CcdbMetadataFields = Object.freeze({
5151
PeriodName: 'PeriodName',
5252
PassName: 'PassName',
5353
QcVersion: 'qc_version',
54+
DetectorName: 'qc_detector_name',
5455
});
5556

5657
/**

QualityControl/public/common/filters/filter.js

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import { h, RemoteData } from '/js/src/index.js';
2424
* @param {object} config.filterMap - Map of the current filter values.
2525
* @param {string} [config.type='text'] - The type of the filter element (e.g., 'text', 'number').
2626
* @param {RemoteData} [config.options=RemoteData.notAsked()] - List of options for a dropdown selector (optional).
27-
* @param {Function} config.onChangeCallback - Callback to be triggered on the change event of the filter.
28-
* @param {Function} config.onInputCallback - Callback to be triggered on the input event.
29-
* @param {Function} config.onEnterCallback - Callback to be triggered when the Enter key is pressed.
27+
* @param {onchange} config.onChangeCallback - Callback to be triggered on the change event of the filter.
28+
* @param {oninput} config.onInputCallback - Callback to be triggered on the input event.
29+
* @param {onkeydown} config.onEnterCallback - Callback to be triggered when the Enter key is pressed.
3030
* @param {string} [config.filterType=FilterType.DROPDOWN] - The type of filter to be used.
3131
* @param {string} [config.width='.'] - The CSS class that defines the width of the filter.
3232
* @returns {vnode} - A virtual node element representing the filter element (input or dropdown).
@@ -61,6 +61,81 @@ export const dynamicSelector = (config) => {
6161
});
6262
};
6363

64+
/**
65+
* Represents options grouped for HTML <optgroup>.
66+
* Keys are group labels (for the <optgroup> label),
67+
* values are arrays of option values (for <option> elements).
68+
* @typedef {Record<string, string[]>} GroupedDropdownOptions
69+
*/
70+
71+
/**
72+
* Builds a filter element. If options to show, selector filter element; otherwise, input element.
73+
* @param {object} config - Configuration object for building the filter element.
74+
* @param {string} config.queryLabel - The key used to query the storage with this parameter.
75+
* @param {string} config.placeholder - The placeholder text to be displayed in the input field.
76+
* @param {string} config.id - The unique identifier for the input field.
77+
* @param {object} config.filterMap - Map of the current filter values.
78+
* @param {string} [config.type='text'] - The type of the filter element (e.g., 'text', 'number').
79+
* @param {GroupedDropdownOptions} [config.options={}] - List of options for a grouped dropdown selector (optional).
80+
* @param {(filterId: string, value: string, setUrl: boolean) => void} config.onChangeCallback
81+
* - Callback to be triggered on the change event of the filter.
82+
* @param {(filterId: string, value: string, setUrl: boolean) => void} config.onInputCallback
83+
* - Callback to be triggered on the input event.
84+
* @param {(filterId: string, value: string, setUrl: boolean) => void} config.onEnterCallback
85+
* - Callback to be triggered when the Enter key is pressed.
86+
* @param {string} [config.width='.w-20'] - The CSS class that defines the width of the filter.
87+
* @returns {vnode} A virtual node element representing the filter element (input or grouped dropdown).
88+
*/
89+
export const groupedDropdownComponent = ({
90+
queryLabel,
91+
placeholder,
92+
id,
93+
filterMap,
94+
options = {},
95+
onChangeCallback,
96+
onInputCallback,
97+
onEnterCallback,
98+
type = 'text',
99+
width = '.w-20',
100+
}) => {
101+
const groups = Object.keys(options);
102+
if (!groups.length) {
103+
return filterInput({ queryLabel, placeholder, id, filterMap, onInputCallback, onEnterCallback, type, width });
104+
}
105+
106+
const selectedOption = filterMap[queryLabel];
107+
const validValue = Object.values(options).flat().some((option) => option === selectedOption);
108+
if (selectedOption && !validValue) {
109+
onChangeCallback(queryLabel, '', true);
110+
}
111+
112+
const sortedGroupedOptions = groups
113+
.sort((a, b) => a.localeCompare(b)) // sort group labels
114+
.reduce((acc, key) => {
115+
// sort option names and add to accumulator
116+
acc[key] = [...options[key]].sort((a, b) => a.localeCompare(b));
117+
return acc;
118+
}, {});
119+
120+
return h(`${width}`, [
121+
h('select.form-control', {
122+
placeholder,
123+
id,
124+
name: id,
125+
value: validValue ? selectedOption : '',
126+
onchange: (event) => onChangeCallback(queryLabel, event.target.value, true),
127+
}, [
128+
h('option', { value: '' }, placeholder),
129+
h('hr'),
130+
...Object.entries(sortedGroupedOptions).map(([key, value]) => h(
131+
'optgroup',
132+
{ label: key },
133+
value.map((option) => h('option', { value: option }, option)),
134+
)),
135+
]),
136+
]);
137+
};
138+
64139
/**
65140
* Builds a filter input element that allows the user to specify a parameter to be used when querying objects.
66141
* This function renders a text input element with event handling for input and Enter key press.
@@ -69,8 +144,8 @@ export const dynamicSelector = (config) => {
69144
* @param {string} config.placeholder - The placeholder text to be displayed in the input field.
70145
* @param {string} config.id - The unique identifier for the input field.
71146
* @param {object} config.filterMap - Map of the current filter values.
72-
* @param {Function} config.onInputCallback - Callback to be triggered on the input event.
73-
* @param {Function} config.onEnterCallback - Callback to be triggered when the Enter key is pressed.
147+
* @param {oninput} config.onInputCallback - Callback to be triggered on the input event.
148+
* @param {onkeydown} config.onEnterCallback - Callback to be triggered when the Enter key is pressed.
74149
* @param {string} [config.type='text'] - The type of the filter element (e.g., 'text', 'number').
75150
* @param {string} [config.width='.w-20'] - The CSS class that defines the width of the filter.
76151
* @returns {vnode} - A virtual node element representing the filter input.
@@ -105,7 +180,7 @@ export const filterInput = (config) => {
105180
* @param {string} config.id - The unique identifier for the select field.
106181
* @param {object} config.filterMap - Map of the current filter values.
107182
* @param {Array<string>} config.options - List of available options to be shown in the dropdown.
108-
* @param {Function} config.onChangeCallback - Callback to be triggered on the change event of the selector.
183+
* @param {onchange} config.onChangeCallback - Callback to be triggered on the change event of the selector.
109184
* @param {string} [config.width='.w-20'] - The CSS class that defines the width of the dropdown.
110185
* @returns {vnode} - A virtual node element representing the dropdown selector.
111186
*/
@@ -139,9 +214,9 @@ const dropdownSelector = (config) => {
139214
* @param {object} config - Selector config ({ id, placeholder, width }).
140215
* @param {object} filterMap - Current filters (RunNumber or empty).
141216
* @param {RemoteData} options - Available ongoing runs.
142-
* @param {Function} onChangeCallback - To change the selection and update the filterMap
143-
* @param {Function} onEnterCallback - To trigger the filter
144-
* @param {Function} [onFocusCallback] - To retrieve ongoing runs
217+
* @param {onchange} onChangeCallback - To change the selection and update the filterMap
218+
* @param {onkeydown} onEnterCallback - To trigger the filter
219+
* @param {onfocus} [onFocusCallback] - To retrieve ongoing runs
145220
* @returns {object} Virtual DOM node (hyperscript element).
146221
*/
147222
export const ongoingRunsSelector = (config, filterMap, options, onChangeCallback, onEnterCallback, onFocusCallback) => {

QualityControl/public/common/filters/filterTypes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
const FilterType = {
1616
INPUT: 'input',
1717
DROPDOWN: 'dropdownSelector',
18+
GROUPED_DROPDOWN: 'groupedDropdownSelector',
1819
RUN_MODE: 'runModeSelector',
1920
};
2021

QualityControl/public/common/filters/filterViews.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
* or submit itself to any jurisdiction.
1313
*/
1414

15-
import { filterInput, dynamicSelector, ongoingRunsSelector } from './filter.js';
15+
import {
16+
filterInput,
17+
dynamicSelector,
18+
ongoingRunsSelector,
19+
groupedDropdownComponent,
20+
} from './filter.js';
1621
import { FilterType } from './filterTypes.js';
1722
import { filtersConfig, runModeFilterConfig } from './filtersConfig.js';
1823
import { runModeCheckbox } from './runMode/runModeCheckbox.js';
@@ -50,6 +55,8 @@ const createFilterElement =
5055
case FilterType.INPUT: return filterInput({ ...commonConfig, type: inputType });
5156
case FilterType.DROPDOWN:
5257
return dynamicSelector({ ...commonConfig, options, onChangeCallback, inputType });
58+
case FilterType.GROUPED_DROPDOWN:
59+
return groupedDropdownComponent({ ...commonConfig, options, onChangeCallback, inputType });
5360
case FilterType.RUN_MODE:
5461
return ongoingRunsSelector(
5562
{ ...commonConfig },

QualityControl/public/common/filters/filtersConfig.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ import { FilterType } from './filterTypes.js';
1717
/**
1818
* Returns an array of filter configuration objects used to render dynamic filter inputs.
1919
* @param {FilterService} filterService - service to get the data to populate the filters
20-
* @param {Array<string>} filterService.runTypes - run types to show in the filter
21-
* @returns {Array<object>} Filter configuration array
20+
* @param {string[]} filterService.runTypes - run types to show in the filter
21+
* @param {DetectorSummary[]} filterService.detectors - detectors to show in the filter
22+
* @returns {object[]} Filter configuration array
2223
*/
23-
export const filtersConfig = ({ runTypes }) => [
24+
export const filtersConfig = ({ runTypes, detectors }) => [
2425
{
2526
type: FilterType.INPUT,
2627
queryLabel: 'RunNumber',
@@ -35,6 +36,22 @@ export const filtersConfig = ({ runTypes }) => [
3536
id: 'runTypeFilter',
3637
options: runTypes,
3738
},
39+
{
40+
type: FilterType.GROUPED_DROPDOWN,
41+
queryLabel: 'DetectorName',
42+
placeholder: 'Detector (any)',
43+
id: 'detectorFilter',
44+
options: detectors.match({
45+
Success: (detectors) => detectors.reduce((acc, detector) => {
46+
if (!acc[detector.type]) {
47+
acc[detector.type] = [];
48+
}
49+
acc[detector.type].push(detector.name);
50+
return acc;
51+
}, {}),
52+
Other: () => {},
53+
}),
54+
},
3855
{
3956
type: FilterType.INPUT,
4057
queryLabel: 'PeriodName',

QualityControl/public/common/filters/model/FilterModel.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import FilterService from '../../../services/Filter.service.js';
1818
import { RunStatus } from '../../../library/runStatus.enum.js';
1919
import { prettyFormatDate } from '../../utils.js';
2020

21-
const CCDB_QUERY_PARAMS = ['PeriodName', 'PassName', 'RunNumber', 'RunType'];
21+
const CCDB_QUERY_PARAMS = ['PeriodName', 'PassName', 'RunNumber', 'RunType', 'DetectorName', 'QcVersion'];
2222

2323
const RUN_INFORMATION_MAP = {
2424
startTime: prettyFormatDate,

QualityControl/public/services/Filter.service.js

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,27 @@ export default class FilterService {
2727
this.filterModel = filterModel;
2828
this.loader = filterModel.model.loader;
2929

30-
this.runTypes = RemoteData.notAsked();
30+
this._runTypes = RemoteData.notAsked();
31+
this._detectors = RemoteData.notAsked();
32+
3133
this.ongoingRuns = RemoteData.notAsked();
3234
}
3335

3436
/**
3537
* Method to get all run types to show in the filter
3638
* @returns {RemoteData} - result within a RemoteData object
3739
*/
38-
async getRunTypes() {
39-
this.runTypes = RemoteData.loading();
40+
async getFilterConfigurations() {
41+
this._runTypes = RemoteData.loading();
42+
this._detectors = RemoteData.loading();
4043
this.filterModel.notify();
4144
const { result, ok } = await this.loader.get('/api/filter/configuration');
4245
if (ok) {
43-
this.runTypes = RemoteData.success(result?.runTypes || []);
46+
this._runTypes = RemoteData.success(result?.runTypes || []);
47+
this._detectors = RemoteData.success(result?.detectors || []);
4448
} else {
45-
this.runTypes = RemoteData.failure('Error retrieving runTypes');
49+
this._runTypes = RemoteData.failure('Error retrieving runTypes');
50+
this._detectors = RemoteData.failure('Error retrieving detectors');
4651
}
4752
this.filterModel.notify();
4853
}
@@ -73,7 +78,7 @@ export default class FilterService {
7378
* @returns {void}
7479
*/
7580
async initFilterService() {
76-
await this.getRunTypes();
81+
await this.getFilterConfigurations();
7782
}
7883

7984
/**
@@ -105,4 +110,20 @@ export default class FilterService {
105110
}
106111
this.filterModel.notify();
107112
}
113+
114+
/**
115+
* Gets the list of run types.
116+
* @returns {string[]} An array containing the run types.
117+
*/
118+
get runTypes() {
119+
return this._runTypes;
120+
}
121+
122+
/**
123+
* Gets the list of detectors.
124+
* @returns {DetectorSummary[]} An array containing detector objects.
125+
*/
126+
get detectors() {
127+
return this._detectors;
128+
}
108129
}

0 commit comments

Comments
 (0)