Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
110f558
At server start fetch a list of detectors from Bookkeeping
hehoon Jan 10, 2026
91fc672
Move the list of detectors from the `BookkeepingService` to the `Filt…
hehoon Jan 10, 2026
7692359
Add detector filter option with grouped dropdown
hehoon Jan 10, 2026
aa9689c
Move object data filtering logic out of BookkeepingService.js and int…
hehoon Jan 14, 2026
348b3aa
Do not attempt to initialize detectors when bookkeeping is inactive
hehoon Jan 14, 2026
4bf8c3b
Pull OGUI-1866 and OGUI-1867
hehoon Jan 14, 2026
dd7009f
Filter for pass names to be filled based on Bookkeeping
hehoon Jan 14, 2026
258edb0
Add a test to check whether the clicked dropdown option is parsed as …
hehoon Jan 14, 2026
b687b7e
Merge branch 'dev' into task/QCG/OGUI-1866/fetch-detectors-from-bookk…
hehoon Jan 16, 2026
01e341f
Merge branch 'dev' into feature/QCG/OGUI-1856/filter-for-passnames-dr…
hehoon Jan 16, 2026
9ebbf47
Remove trailing spaces
hehoon Jan 16, 2026
8aa622b
Merge branch 'task/QCG/OGUI-1866/fetch-detectors-from-bookkeeping' of…
hehoon Jan 16, 2026
aa501c3
Merge branch 'dev' of github.com:AliceO2Group/WebUi into feature/QCG/…
graduta Jan 17, 2026
a8f59da
Put back export from merge conflicts
graduta Jan 17, 2026
209e3c0
Some more fixes from merge conflicts
graduta Jan 17, 2026
40af023
Add scrolling, label (frozen) and onchange to update URL as detectors…
graduta Jan 17, 2026
a2b5b80
Sort by most recent
graduta Jan 17, 2026
e4555b7
Update test following change of default value
graduta Jan 17, 2026
808d50a
Fix tests of sorting order of data passes
graduta Jan 17, 2026
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
18 changes: 18 additions & 0 deletions QualityControl/common/library/typedef/DataPass.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @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.
*/

/**
* @typedef {object} DataPass
* @property {string} name - Human-readable data pass name.
* @property {boolean} isFrozen - Whether the data pass is frozen.
*/
7 changes: 7 additions & 0 deletions QualityControl/lib/QCModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,11 @@ function initializeIntervals(intervalsService, qcObjectService, filterService, r
runModeService.refreshInterval,
);
}

if (filterService.dataPassesRefreshInterval > 0) {
intervalsService.register(
filterService.getDataPasses.bind(runModeService),
filterService.dataPassesRefreshInterval,
);
}
}
2 changes: 2 additions & 0 deletions QualityControl/lib/controllers/FilterController.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@ export class FilterController {
try {
const runTypes = this._filterService?.runTypes ?? [];
const detectors = this._filterService?.detectors ?? [];
const dataPasses = this._filterService?.dataPasses ?? [];
res.status(200).json({
runTypes,
detectors,
dataPasses,
});
} catch (error) {
res.status(503).json({ error: error.message || error });
Expand Down
18 changes: 18 additions & 0 deletions QualityControl/lib/services/BookkeepingService.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const GET_BKP_DATABASE_STATUS_PATH = '/api/status/database';
const GET_RUN_TYPES_PATH = '/api/runTypes';
const GET_RUN_PATH = '/api/runs';
export const GET_DETECTORS_PATH = '/api/detectors';
const GET_DATA_PASSES_PATH = '/api/dataPasses';

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

Expand Down Expand Up @@ -128,6 +129,23 @@ export class BookkeepingService {
return data;
}

/**
* Retrieve the list of data passes from the bookkeeping service.
* @returns {Promise<object[]>} Resolves with an array of data passes.
*/
async retrieveDataPasses() {
const { data } = await httpGetJson(
this._hostname,
this._port,
this._createPath(GET_DATA_PASSES_PATH),
{
protocol: this._protocol,
rejectUnauthorized: false,
},
);
return Array.isArray(data) ? data : [];
}

/**
* Retrieves the information of a specific run from the Bookkeeping service
* @param {number} runNumber - The run number to check the status for
Expand Down
41 changes: 40 additions & 1 deletion QualityControl/lib/services/FilterService.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ export class FilterService {
this._bookkeepingService = bookkeepingService;
this._runTypes = [];
this._detectors = Object.freeze([]);
this._dataPasses = Object.freeze([]);

this._runTypesRefreshInterval = config?.bookkeeping?.runTypesRefreshInterval ??
(config?.bookkeeping ? 24 * 60 * 60 * 1000 : -1);
(config?.bookkeeping ? 24 * 60 * 60 * 1000 : -1);// default interval is 1 day
this._dataPassesRefreshInterval = config?.bookkeeping?.dataPassesRefreshInterval ??
(config?.bookkeeping ? 6 * 60 * 60 * 1000 : -1);// default interval is 6 hour

this.initFilters().catch((error) => {
this._logger.errorMessage(`FilterService initialization failed: ${error.message || error}`);
Expand All @@ -49,6 +52,7 @@ export class FilterService {
await this._bookkeepingService.connect();
await this.getRunTypes();
await this._initializeDetectors();
await this.getDataPasses();
}

/**
Expand All @@ -72,6 +76,33 @@ export class FilterService {
}
}

/**
* This method is used to retrieve the list of data passes from the bookkeeping service.
* @returns {Promise<void>} Resolves when the list of data passes is available.
*/
async getDataPasses() {
try {
if (!this._bookkeepingService.active) {
return;
}

const rawDataPasses = await this._bookkeepingService.retrieveDataPasses();
this._dataPasses = Object.freeze(rawDataPasses
.filter(({ name, isFrozen }) => name && typeof isFrozen === 'boolean')
.map(({ name, isFrozen }) => Object.freeze({ name, isFrozen })));
} catch (error) {
this._logger.errorMessage(`Error while retrieving data passes: ${error.message || error}`);
}
}

/**
* Returns a list of data passes.
* @returns {Readonly<DataPass[]>} An immutable array of data passes.
*/
get dataPasses() {
return this._dataPasses;
}

/**
* This method is used to retrieve the list of detectors from the bookkeeping service
* @returns {Promise<undefined>} Resolves when the list of detectors is available
Expand Down Expand Up @@ -106,6 +137,14 @@ export class FilterService {
return this._runTypesRefreshInterval;
}

/**
* Returns the interval in milliseconds for how often the list of data passes should be refreshed.
* @returns {number} Interval in milliseconds for refreshing the list of data passes.
*/
get dataPassesRefreshInterval() {
return this._dataPassesRefreshInterval;
}

/**
* This method is used to initialize the filter service
* @returns {string[]} - resolves when the filter service is initialized
Expand Down
69 changes: 68 additions & 1 deletion QualityControl/public/common/filters/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/

import { FilterType } from './filterTypes.js';
import { h, RemoteData } from '/js/src/index.js';
import { h, RemoteData, DropdownComponent } from '/js/src/index.js';

/**
* Builds a filter element. If options to show, selector filter element; otherwise, input element.
Expand Down Expand Up @@ -136,6 +136,73 @@ export const groupedDropdownComponent = ({
]);
};

/**
* 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 {Record<string, object>} [config.options={}] - List of options for an input with 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.
*/
export const inputWithDropdownComponent = ({
queryLabel,
placeholder,
id,
filterMap,
options = {},
onChangeCallback,
onInputCallback,
onEnterCallback,
type = 'text',
width = '.w-20',
}) => {
const dropdownOptions = Object.keys(options);
if (!dropdownOptions.length) {
return filterInput({ queryLabel, placeholder, id, filterMap, onInputCallback, onEnterCallback, type, width });
}
const dropdownComponent = DropdownComponent(
filterInput({
queryLabel,
placeholder,
id,
filterMap,
type,
onInputCallback,
onEnterCallback,
width: '.w-100',
}),
h('', {
id: `${queryLabel?.toLowerCase()}-dropdown`,
style: 'max-height: 300px; overflow-y: auto;',
}, Object.entries(options)
.filter(([option]) => option.toLowerCase().includes(filterMap[queryLabel]?.toLowerCase() ?? ''))
.sort(([a], [b]) => b.localeCompare(a))
.map(([option, htmlOptions]) => h(
'button.btn.d-block.w-100',
{
onclick: () => {
onChangeCallback(queryLabel, option, true);
dropdownComponent.state.hidePopover();
},
...htmlOptions ?? {},
},
[option, Object.keys(htmlOptions).length > 0 ? ' (frozen)' : ''],
))),
);

return h(`${width}`, dropdownComponent);
};

/**
* 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 @@ -14,6 +14,7 @@

const FilterType = {
INPUT: 'input',
INPUT_WITH_DROPDOWN: 'inputWithDropdown',
DROPDOWN: 'dropdownSelector',
GROUPED_DROPDOWN: 'groupedDropdownSelector',
RUN_MODE: 'runModeSelector',
Expand Down
3 changes: 3 additions & 0 deletions QualityControl/public/common/filters/filterViews.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
dynamicSelector,
ongoingRunsSelector,
groupedDropdownComponent,
inputWithDropdownComponent,
} from './filter.js';
import { FilterType } from './filterTypes.js';
import { filtersConfig, runModeFilterConfig } from './filtersConfig.js';
Expand All @@ -33,7 +34,7 @@
* Creates an input element for a specific metadata field;
* @param {object} config - The configuration for this particular field
* @param {object} filterMap - An object that contains the keys and values of the filters
* @param {Function} onInputCallback - A callback function that triggers upon Input

Check warning on line 37 in QualityControl/public/common/filters/filterViews.js

View workflow job for this annotation

GitHub Actions / Check eslint rules on ubuntu-latest

Prefer a more specific type to `Function`
* @param {Function} onEnterCallback - A callback function that triggers upon Enter
* @param {Function} onChangeCallback - A callback function that triggers upon Change
* @param onFocusCallback
Expand All @@ -57,6 +58,8 @@
return dynamicSelector({ ...commonConfig, options, onChangeCallback, inputType });
case FilterType.GROUPED_DROPDOWN:
return groupedDropdownComponent({ ...commonConfig, options, onChangeCallback, inputType });
case FilterType.INPUT_WITH_DROPDOWN:
return inputWithDropdownComponent({ ...commonConfig, options, onChangeCallback, inputType });
case FilterType.RUN_MODE:
return ongoingRunsSelector(
{ ...commonConfig },
Expand Down
16 changes: 12 additions & 4 deletions QualityControl/public/common/filters/filtersConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ 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 {string[]} filterService.runTypes - run types to show in the filter
* @param {DetectorSummary[]} filterService.detectors - detectors to show in the filter
* @param {RemoteData<string[]>} filterService.runTypes - run types to show in the filter
* @param {RemoteData<DetectorSummary[]>} filterService.detectors - detectors to show in the filter
* @param {RemoteData<DataPass[]>} filterService.dataPasses - data passes to show in the filter
* @returns {object[]} Filter configuration array
*/
export const filtersConfig = ({ runTypes, detectors }) => [
export const filtersConfig = ({ runTypes, detectors, dataPasses }) => [
{
type: FilterType.INPUT,
queryLabel: 'RunNumber',
Expand Down Expand Up @@ -59,10 +60,17 @@ export const filtersConfig = ({ runTypes, detectors }) => [
id: 'periodNameFilter',
},
{
type: FilterType.INPUT,
type: FilterType.INPUT_WITH_DROPDOWN,
queryLabel: 'PassName',
placeholder: 'PassName (e.g. apass2)',
id: 'passNameFilter',
options: dataPasses.match({
Success: (payload) => payload.reduce((acc, dataPass) => {
acc[dataPass.name] = dataPass.isFrozen ? { style: 'color: var(--color-gray-dark);' } : {};
return acc;
}, {}),
Other: () => {},
}),
},
{
type: FilterType.INPUT,
Expand Down
12 changes: 12 additions & 0 deletions QualityControl/public/services/Filter.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default class FilterService {

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

this.ongoingRuns = RemoteData.notAsked();
}
Expand All @@ -40,14 +41,17 @@ export default class FilterService {
async getFilterConfigurations() {
this._runTypes = RemoteData.loading();
this._detectors = RemoteData.loading();
this._dataPasses = RemoteData.notAsked();
this.filterModel.notify();
const { result, ok } = await this.loader.get('/api/filter/configuration');
if (ok) {
this._runTypes = RemoteData.success(result?.runTypes || []);
this._detectors = RemoteData.success(result?.detectors || []);
this._dataPasses = RemoteData.success(result?.dataPasses || []);
} else {
this._runTypes = RemoteData.failure('Error retrieving runTypes');
this._detectors = RemoteData.failure('Error retrieving detectors');
this._dataPasses = RemoteData.failure('Error retrieving dataPasses');
}
this.filterModel.notify();
}
Expand Down Expand Up @@ -126,4 +130,12 @@ export default class FilterService {
get detectors() {
return this._detectors;
}

/**
* Returns a {@link RemoteData} object containing an array of data type {@link DataPass}.
* @returns {RemoteData<DataPass[]>} A {@link RemoteData} object containing an array of data type {@link DataPass}.
*/
get dataPasses() {
return this._dataPasses;
}
}
21 changes: 14 additions & 7 deletions QualityControl/test/lib/controllers/FiltersController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export const filtersControllerTestSuite = async () => {
});
});

suite('getFilterConfigurationHandler', () => {
test('should successfully retrieve run types and detectors from Bookkeeping service', async () => {
suite('getFilterConfigurationHandler', async () => {
test('should successfully retrieve run types, detectors and data passes from Bookkeeping service', async () => {
const filterService = sinon.createStubInstance(FilterService);
const mockedRunTypes = ['runType1', 'runType2'];
const mockedDetectors = [
Expand All @@ -47,8 +47,15 @@ export const filtersControllerTestSuite = async () => {
type: 'PHYSICAL',
},
];
const mockedDataPasses = [
{
name: 'LHC22a_apass1',
isFrozen: false,
},
];
sinon.stub(filterService, 'runTypes').get(() => mockedRunTypes);
sinon.stub(filterService, 'detectors').get(() => mockedDetectors);
sinon.stub(filterService, 'dataPasses').get(() => mockedDataPasses);

const res = {
status: sinon.stub().returnsThis(),
Expand All @@ -59,11 +66,11 @@ export const filtersControllerTestSuite = async () => {
filterController.getFilterConfigurationHandler(req, res);
ok(res.status.calledWith(200), 'Response status was not 200');
ok(
res.json.calledWith({ runTypes: mockedRunTypes, detectors: mockedDetectors }),
'Response should include runTypes and detectors',
res.json.calledWith({ runTypes: mockedRunTypes, detectors: mockedDetectors, dataPasses: mockedDataPasses }),
'Response should include runTypes, detectors and dataPasses',
);
});
test('should return an empty arrays if bookkeeping service is not defined', () => {
test('should return an empty arrays if bookkeeping service is not defined', async () => {
const bkpService = null;
const res = {
status: sinon.stub().returnsThis(),
Expand All @@ -74,8 +81,8 @@ export const filtersControllerTestSuite = async () => {
filterController.getFilterConfigurationHandler(req, res);
ok(res.status.calledWith(200), 'Response status was not 200');
ok(
res.json.calledWith({ runTypes: [], detectors: [] }),
'runTypes and detectors were not sent as an empty array',
res.json.calledWith({ runTypes: [], detectors: [], dataPasses: [] }),
'runTypes, detectors and dataPasses were not sent as an empty array',
);
});
});
Expand Down
Loading
Loading