Skip to content

Commit c5cf947

Browse files
authored
[OGUI-1856] Filter for pass names to be filled based on Bookkeeping (#3283)
- Added backend changes to QCG to fetch pass names at load time and based on an interval - The default interval is 6 hours - The frontend fetches the pass names and builts a dropdown which is displayed upon clicking the Pass Name filter. - Typing updates the dropdown options list to match what is currently being typed.
1 parent 64ad126 commit c5cf947

16 files changed

Lines changed: 496 additions & 29 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @license
3+
* Copyright CERN and copyright holders of ALICE O2. This software is
4+
* distributed under the terms of the GNU General Public License v3 (GPL
5+
* Version 3), copied verbatim in the file "COPYING".
6+
*
7+
* See http://alice-o2.web.cern.ch/license for full licensing information.
8+
*
9+
* In applying this license CERN does not waive the privileges and immunities
10+
* granted to it by virtue of its status as an Intergovernmental Organization
11+
* or submit itself to any jurisdiction.
12+
*/
13+
14+
/**
15+
* @typedef {object} DataPass
16+
* @property {string} name - Human-readable data pass name.
17+
* @property {boolean} isFrozen - Whether the data pass is frozen.
18+
*/

‎QualityControl/lib/QCModel.js‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,11 @@ function initializeIntervals(intervalsService, qcObjectService, filterService, r
172172
runModeService.refreshInterval,
173173
);
174174
}
175+
176+
if (filterService.dataPassesRefreshInterval > 0) {
177+
intervalsService.register(
178+
filterService.getDataPasses.bind(runModeService),
179+
filterService.dataPassesRefreshInterval,
180+
);
181+
}
175182
}

‎QualityControl/lib/controllers/FilterController.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,11 @@ export class FilterController {
6565
try {
6666
const runTypes = this._filterService?.runTypes ?? [];
6767
const detectors = this._filterService?.detectors ?? [];
68+
const dataPasses = this._filterService?.dataPasses ?? [];
6869
res.status(200).json({
6970
runTypes,
7071
detectors,
72+
dataPasses,
7173
});
7274
} catch (error) {
7375
res.status(503).json({ error: error.message || error });

‎QualityControl/lib/services/BookkeepingService.js‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const GET_BKP_DATABASE_STATUS_PATH = '/api/status/database';
2121
const GET_RUN_TYPES_PATH = '/api/runTypes';
2222
const GET_RUN_PATH = '/api/runs';
2323
export const GET_DETECTORS_PATH = '/api/detectors';
24+
const GET_DATA_PASSES_PATH = '/api/dataPasses';
2425

2526
const LOG_FACILITY = `${process.env.npm_config_log_label ?? 'qcg'}/bkp-service`;
2627

@@ -128,6 +129,23 @@ export class BookkeepingService {
128129
return data;
129130
}
130131

132+
/**
133+
* Retrieve the list of data passes from the bookkeeping service.
134+
* @returns {Promise<object[]>} Resolves with an array of data passes.
135+
*/
136+
async retrieveDataPasses() {
137+
const { data } = await httpGetJson(
138+
this._hostname,
139+
this._port,
140+
this._createPath(GET_DATA_PASSES_PATH),
141+
{
142+
protocol: this._protocol,
143+
rejectUnauthorized: false,
144+
},
145+
);
146+
return Array.isArray(data) ? data : [];
147+
}
148+
131149
/**
132150
* Retrieves the information of a specific run from the Bookkeeping service
133151
* @param {number} runNumber - The run number to check the status for

‎QualityControl/lib/services/FilterService.js‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ export class FilterService {
3232
this._bookkeepingService = bookkeepingService;
3333
this._runTypes = [];
3434
this._detectors = Object.freeze([]);
35+
this._dataPasses = Object.freeze([]);
3536

3637
this._runTypesRefreshInterval = config?.bookkeeping?.runTypesRefreshInterval ??
37-
(config?.bookkeeping ? 24 * 60 * 60 * 1000 : -1);
38+
(config?.bookkeeping ? 24 * 60 * 60 * 1000 : -1);// default interval is 1 day
39+
this._dataPassesRefreshInterval = config?.bookkeeping?.dataPassesRefreshInterval ??
40+
(config?.bookkeeping ? 6 * 60 * 60 * 1000 : -1);// default interval is 6 hour
3841

3942
this.initFilters().catch((error) => {
4043
this._logger.errorMessage(`FilterService initialization failed: ${error.message || error}`);
@@ -49,6 +52,7 @@ export class FilterService {
4952
await this._bookkeepingService.connect();
5053
await this.getRunTypes();
5154
await this._initializeDetectors();
55+
await this.getDataPasses();
5256
}
5357

5458
/**
@@ -72,6 +76,33 @@ export class FilterService {
7276
}
7377
}
7478

79+
/**
80+
* This method is used to retrieve the list of data passes from the bookkeeping service.
81+
* @returns {Promise<void>} Resolves when the list of data passes is available.
82+
*/
83+
async getDataPasses() {
84+
try {
85+
if (!this._bookkeepingService.active) {
86+
return;
87+
}
88+
89+
const rawDataPasses = await this._bookkeepingService.retrieveDataPasses();
90+
this._dataPasses = Object.freeze(rawDataPasses
91+
.filter(({ name, isFrozen }) => name && typeof isFrozen === 'boolean')
92+
.map(({ name, isFrozen }) => Object.freeze({ name, isFrozen })));
93+
} catch (error) {
94+
this._logger.errorMessage(`Error while retrieving data passes: ${error.message || error}`);
95+
}
96+
}
97+
98+
/**
99+
* Returns a list of data passes.
100+
* @returns {Readonly<DataPass[]>} An immutable array of data passes.
101+
*/
102+
get dataPasses() {
103+
return this._dataPasses;
104+
}
105+
75106
/**
76107
* This method is used to retrieve the list of detectors from the bookkeeping service
77108
* @returns {Promise<undefined>} Resolves when the list of detectors is available
@@ -106,6 +137,14 @@ export class FilterService {
106137
return this._runTypesRefreshInterval;
107138
}
108139

140+
/**
141+
* Returns the interval in milliseconds for how often the list of data passes should be refreshed.
142+
* @returns {number} Interval in milliseconds for refreshing the list of data passes.
143+
*/
144+
get dataPassesRefreshInterval() {
145+
return this._dataPassesRefreshInterval;
146+
}
147+
109148
/**
110149
* This method is used to initialize the filter service
111150
* @returns {string[]} - resolves when the filter service is initialized

‎QualityControl/public/common/filters/filter.js‎

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*/
1414

1515
import { FilterType } from './filterTypes.js';
16-
import { h, RemoteData } from '/js/src/index.js';
16+
import { h, RemoteData, DropdownComponent } from '/js/src/index.js';
1717

1818
/**
1919
* Builds a filter element. If options to show, selector filter element; otherwise, input element.
@@ -136,6 +136,73 @@ export const groupedDropdownComponent = ({
136136
]);
137137
};
138138

139+
/**
140+
* Builds a filter element. If options to show, selector filter element; otherwise, input element.
141+
* @param {object} config - Configuration object for building the filter element.
142+
* @param {string} config.queryLabel - The key used to query the storage with this parameter.
143+
* @param {string} config.placeholder - The placeholder text to be displayed in the input field.
144+
* @param {string} config.id - The unique identifier for the input field.
145+
* @param {object} config.filterMap - Map of the current filter values.
146+
* @param {string} [config.type='text'] - The type of the filter element (e.g., 'text', 'number').
147+
* @param {Record<string, object>} [config.options={}] - List of options for an input with dropdown selector (optional).
148+
* @param {(filterId: string, value: string, setUrl: boolean) => void} config.onChangeCallback
149+
* - Callback to be triggered on the change event of the filter.
150+
* @param {(filterId: string, value: string, setUrl: boolean) => void} config.onInputCallback
151+
* - Callback to be triggered on the input event.
152+
* @param {(filterId: string, value: string, setUrl: boolean) => void} config.onEnterCallback
153+
* - Callback to be triggered when the Enter key is pressed.
154+
* @param {string} [config.width='.w-20'] - The CSS class that defines the width of the filter.
155+
* @returns {vnode} A virtual node element representing the filter element.
156+
*/
157+
export const inputWithDropdownComponent = ({
158+
queryLabel,
159+
placeholder,
160+
id,
161+
filterMap,
162+
options = {},
163+
onChangeCallback,
164+
onInputCallback,
165+
onEnterCallback,
166+
type = 'text',
167+
width = '.w-20',
168+
}) => {
169+
const dropdownOptions = Object.keys(options);
170+
if (!dropdownOptions.length) {
171+
return filterInput({ queryLabel, placeholder, id, filterMap, onInputCallback, onEnterCallback, type, width });
172+
}
173+
const dropdownComponent = DropdownComponent(
174+
filterInput({
175+
queryLabel,
176+
placeholder,
177+
id,
178+
filterMap,
179+
type,
180+
onInputCallback,
181+
onEnterCallback,
182+
width: '.w-100',
183+
}),
184+
h('', {
185+
id: `${queryLabel?.toLowerCase()}-dropdown`,
186+
style: 'max-height: 300px; overflow-y: auto;',
187+
}, Object.entries(options)
188+
.filter(([option]) => option.toLowerCase().includes(filterMap[queryLabel]?.toLowerCase() ?? ''))
189+
.sort(([a], [b]) => b.localeCompare(a))
190+
.map(([option, htmlOptions]) => h(
191+
'button.btn.d-block.w-100',
192+
{
193+
onclick: () => {
194+
onChangeCallback(queryLabel, option, true);
195+
dropdownComponent.state.hidePopover();
196+
},
197+
...htmlOptions ?? {},
198+
},
199+
[option, Object.keys(htmlOptions).length > 0 ? ' (frozen)' : ''],
200+
))),
201+
);
202+
203+
return h(`${width}`, dropdownComponent);
204+
};
205+
139206
/**
140207
* Builds a filter input element that allows the user to specify a parameter to be used when querying objects.
141208
* This function renders a text input element with event handling for input and Enter key press.

‎QualityControl/public/common/filters/filterTypes.js‎

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

1515
const FilterType = {
1616
INPUT: 'input',
17+
INPUT_WITH_DROPDOWN: 'inputWithDropdown',
1718
DROPDOWN: 'dropdownSelector',
1819
GROUPED_DROPDOWN: 'groupedDropdownSelector',
1920
RUN_MODE: 'runModeSelector',

‎QualityControl/public/common/filters/filterViews.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
dynamicSelector,
1818
ongoingRunsSelector,
1919
groupedDropdownComponent,
20+
inputWithDropdownComponent,
2021
} from './filter.js';
2122
import { FilterType } from './filterTypes.js';
2223
import { filtersConfig, runModeFilterConfig } from './filtersConfig.js';
@@ -57,6 +58,8 @@ const createFilterElement =
5758
return dynamicSelector({ ...commonConfig, options, onChangeCallback, inputType });
5859
case FilterType.GROUPED_DROPDOWN:
5960
return groupedDropdownComponent({ ...commonConfig, options, onChangeCallback, inputType });
61+
case FilterType.INPUT_WITH_DROPDOWN:
62+
return inputWithDropdownComponent({ ...commonConfig, options, onChangeCallback, inputType });
6063
case FilterType.RUN_MODE:
6164
return ongoingRunsSelector(
6265
{ ...commonConfig },

‎QualityControl/public/common/filters/filtersConfig.js‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ 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 {string[]} filterService.runTypes - run types to show in the filter
21-
* @param {DetectorSummary[]} filterService.detectors - detectors to show in the filter
20+
* @param {RemoteData<string[]>} filterService.runTypes - run types to show in the filter
21+
* @param {RemoteData<DetectorSummary[]>} filterService.detectors - detectors to show in the filter
22+
* @param {RemoteData<DataPass[]>} filterService.dataPasses - data passes to show in the filter
2223
* @returns {object[]} Filter configuration array
2324
*/
24-
export const filtersConfig = ({ runTypes, detectors }) => [
25+
export const filtersConfig = ({ runTypes, detectors, dataPasses }) => [
2526
{
2627
type: FilterType.INPUT,
2728
queryLabel: 'RunNumber',
@@ -59,10 +60,17 @@ export const filtersConfig = ({ runTypes, detectors }) => [
5960
id: 'periodNameFilter',
6061
},
6162
{
62-
type: FilterType.INPUT,
63+
type: FilterType.INPUT_WITH_DROPDOWN,
6364
queryLabel: 'PassName',
6465
placeholder: 'PassName (e.g. apass2)',
6566
id: 'passNameFilter',
67+
options: dataPasses.match({
68+
Success: (payload) => payload.reduce((acc, dataPass) => {
69+
acc[dataPass.name] = dataPass.isFrozen ? { style: 'color: var(--color-gray-dark);' } : {};
70+
return acc;
71+
}, {}),
72+
Other: () => {},
73+
}),
6674
},
6775
{
6876
type: FilterType.INPUT,

‎QualityControl/public/services/Filter.service.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export default class FilterService {
2929

3030
this._runTypes = RemoteData.notAsked();
3131
this._detectors = RemoteData.notAsked();
32+
this._dataPasses = RemoteData.notAsked();
3233

3334
this.ongoingRuns = RemoteData.notAsked();
3435
}
@@ -40,14 +41,17 @@ export default class FilterService {
4041
async getFilterConfigurations() {
4142
this._runTypes = RemoteData.loading();
4243
this._detectors = RemoteData.loading();
44+
this._dataPasses = RemoteData.notAsked();
4345
this.filterModel.notify();
4446
const { result, ok } = await this.loader.get('/api/filter/configuration');
4547
if (ok) {
4648
this._runTypes = RemoteData.success(result?.runTypes || []);
4749
this._detectors = RemoteData.success(result?.detectors || []);
50+
this._dataPasses = RemoteData.success(result?.dataPasses || []);
4851
} else {
4952
this._runTypes = RemoteData.failure('Error retrieving runTypes');
5053
this._detectors = RemoteData.failure('Error retrieving detectors');
54+
this._dataPasses = RemoteData.failure('Error retrieving dataPasses');
5155
}
5256
this.filterModel.notify();
5357
}
@@ -126,4 +130,12 @@ export default class FilterService {
126130
get detectors() {
127131
return this._detectors;
128132
}
133+
134+
/**
135+
* Returns a {@link RemoteData} object containing an array of data type {@link DataPass}.
136+
* @returns {RemoteData<DataPass[]>} A {@link RemoteData} object containing an array of data type {@link DataPass}.
137+
*/
138+
get dataPasses() {
139+
return this._dataPasses;
140+
}
129141
}

0 commit comments

Comments
 (0)