From 5f45fb4f3dedce9fd4bede18f6a9146a05e91583 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 15 Jan 2026 11:46:32 +0100 Subject: [PATCH 01/17] feat: add fetching of already ongoing runs at server start --- QualityControl/lib/QCModel.js | 1 + .../lib/services/BookkeepingService.js | 39 +++++++++++++++ QualityControl/lib/services/FilterService.js | 1 - QualityControl/lib/services/RunModeService.js | 49 +++++++++++++++---- 4 files changed, 80 insertions(+), 10 deletions(-) diff --git a/QualityControl/lib/QCModel.js b/QualityControl/lib/QCModel.js index aa5364af6..a1faae493 100644 --- a/QualityControl/lib/QCModel.js +++ b/QualityControl/lib/QCModel.js @@ -116,6 +116,7 @@ export const setupQcModel = async (eventEmitter) => { const intervalsService = new IntervalsService(); const bookkeepingService = new BookkeepingService(config.bookkeeping); + await bookkeepingService.connect(); const filterService = new FilterService(bookkeepingService, config); const runModeService = new RunModeService(config.bookkeeping, bookkeepingService, ccdbService, eventEmitter); const objectController = new ObjectController(qcObjectService, runModeService, qcdbDownloadService); diff --git a/QualityControl/lib/services/BookkeepingService.js b/QualityControl/lib/services/BookkeepingService.js index f5d9f6353..f8c412665 100644 --- a/QualityControl/lib/services/BookkeepingService.js +++ b/QualityControl/lib/services/BookkeepingService.js @@ -23,6 +23,8 @@ const GET_RUN_PATH = '/api/runs'; const LOG_FACILITY = `${process.env.npm_config_log_label ?? 'qcg'}/bkp-service`; +const RECENT_RUN_THRESHOLD_MS = 2 * 24 * 60 * 60 * 1000; // -2 days in milliseconds + /** * BookkeepingService class to be used to retrieve data from Bookkeeping */ @@ -181,6 +183,43 @@ export class BookkeepingService { } } + /** + * Retrieves runs that are currently ongoing (started within the last 48 hours but have not yet ended). + * @returns {Promise|undefined>} A promise that resolves to an array of run objects, + * or undefined if the service is inactive, no data is found, or an error occurs + */ + async retrieveOnGoingRuns() { + if (!this.active) { + return; + } + + const timestamp = Date.now() - RECENT_RUN_THRESHOLD_MS; + + const queryParams = `page[offset]=0&page[limit]=100&filter[o2start][from]=${timestamp}&token=${this._token}`; + + try { + const { data } = await httpGetJson( + this._hostname, + this._port, + `${GET_RUN_PATH}?${queryParams}`, + { + protocol: this._protocol, + rejectUnauthorized: false, + }, + ); + + if (data.length === 0) { + return []; + } + + return data.filter((run) => !run.timeO2End); + } catch (error) { + const msg = error?.message ?? String(error); + this._logger.errorMessage(msg); + return; + } + } + /** * Helper method to construct a URL path with the required authentication token. * Appends the service's token as a query parameter to the provided path. diff --git a/QualityControl/lib/services/FilterService.js b/QualityControl/lib/services/FilterService.js index d6da52c57..9f31b88dc 100644 --- a/QualityControl/lib/services/FilterService.js +++ b/QualityControl/lib/services/FilterService.js @@ -46,7 +46,6 @@ export class FilterService { * @returns {Promise} - resolves when the filter service is initialized */ async initFilters() { - await this._bookkeepingService.connect(); await this.getRunTypes(); } diff --git a/QualityControl/lib/services/RunModeService.js b/QualityControl/lib/services/RunModeService.js index 4c773410a..67dcf203a 100644 --- a/QualityControl/lib/services/RunModeService.js +++ b/QualityControl/lib/services/RunModeService.js @@ -50,6 +50,7 @@ export class RunModeService { this._logger = LogManager.getLogger(`${process.env.npm_config_log_label ?? 'qcg'}/run-mode-service`); this._listenToEvents(); + this._fetchOnGoingRunsAtStart(); } /** @@ -112,6 +113,27 @@ export class RunModeService { this._eventEmitter.on(EmitterKeys.RUN_TRACK, (runEvent) => this._onRunTrackEvent(runEvent)); } + /** + * Fetches the already ongoing runs from Bookkeeping service, becaue Kafka only sends an event at START of run. + * @returns {Promise} + */ + async _fetchOnGoingRunsAtStart() { + if (!this._bookkeepingService.active) { + return; + } + + const alreadyOngoingRuns = await this._bookkeepingService.retrieveOnGoingRuns(); + if (!alreadyOngoingRuns || alreadyOngoingRuns.length === 0) { + this._logger.infoMessage('No already ongoing runs detected at server start'); + return; + } + + const runNumbers = alreadyOngoingRuns.map((run) => run.runNumber); + const tasks = runNumbers.map(async (runNumber) => await this._initializeRunData(runNumber)); + await Promise.all(tasks); + await this.refreshRunsCache(); + } + /** * Handles run track events emitted by the event emitter. * Updates the ongoing runs cache based on the transition type. @@ -122,20 +144,29 @@ export class RunModeService { */ async _onRunTrackEvent({ runNumber, transition }) { if (transition === Transition.START_ACTIVITY) { - let rawPaths = []; - try { - rawPaths = await this._dataService.getObjectsLatestVersionList({ - filters: { RunNumber: runNumber }, - }); - } catch (error) { - this._logger.errorMessage(`Error fetching initial paths for run ${runNumber}: ${error.message || error}`); - } - this._ongoingRuns.set(runNumber, rawPaths); + await this._initializeRunData(runNumber); } else if (transition === Transition.STOP_ACTIVITY) { this._ongoingRuns.delete(runNumber); } } + /** + * Fetches the latest object versions for each run and populates the local `ongoingRuns` map. + * @param {number} runNumber - The run number associated with the event. + * @returns {Promise} + */ + async _initializeRunData(runNumber) { + let rawPaths = []; + try { + rawPaths = await this._dataService.getObjectsLatestVersionList({ + filters: { RunNumber: runNumber }, + }); + } catch (error) { + this._logger.errorMessage(`Error fetching initial paths for run ${runNumber}: ${error.message || error}`); + } + this._ongoingRuns.set(runNumber, rawPaths); + } + /** * Returns the last time the ongoing runs cache was refreshed. * @returns {number} - Timestamp of the last refresh. (ms) From 882fa2bb925f93fcc37b63e5f4746663600d4f5f Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 15 Jan 2026 12:51:21 +0100 Subject: [PATCH 02/17] test: add tests for fetching ongoing runs at server start --- .../lib/services/BookkeepingService.js | 2 +- QualityControl/lib/services/RunModeService.js | 6 +- .../lib/services/BookkeepingService.test.js | 64 +++++++++++++++++++ .../test/lib/services/RunModeService.test.js | 16 +++++ 4 files changed, 82 insertions(+), 6 deletions(-) diff --git a/QualityControl/lib/services/BookkeepingService.js b/QualityControl/lib/services/BookkeepingService.js index f8c412665..987d0f133 100644 --- a/QualityControl/lib/services/BookkeepingService.js +++ b/QualityControl/lib/services/BookkeepingService.js @@ -188,7 +188,7 @@ export class BookkeepingService { * @returns {Promise|undefined>} A promise that resolves to an array of run objects, * or undefined if the service is inactive, no data is found, or an error occurs */ - async retrieveOnGoingRuns() { + async retrieveOngoingRuns() { if (!this.active) { return; } diff --git a/QualityControl/lib/services/RunModeService.js b/QualityControl/lib/services/RunModeService.js index 67dcf203a..8d65bfae9 100644 --- a/QualityControl/lib/services/RunModeService.js +++ b/QualityControl/lib/services/RunModeService.js @@ -118,11 +118,7 @@ export class RunModeService { * @returns {Promise} */ async _fetchOnGoingRunsAtStart() { - if (!this._bookkeepingService.active) { - return; - } - - const alreadyOngoingRuns = await this._bookkeepingService.retrieveOnGoingRuns(); + const alreadyOngoingRuns = await this._bookkeepingService.retrieveOngoingRuns(); if (!alreadyOngoingRuns || alreadyOngoingRuns.length === 0) { this._logger.infoMessage('No already ongoing runs detected at server start'); return; diff --git a/QualityControl/test/lib/services/BookkeepingService.test.js b/QualityControl/test/lib/services/BookkeepingService.test.js index 9b1c0de5e..08d07a321 100644 --- a/QualityControl/test/lib/services/BookkeepingService.test.js +++ b/QualityControl/test/lib/services/BookkeepingService.test.js @@ -332,5 +332,69 @@ export const bookkeepingServiceTestSuite = async () => { strictEqual(runStatus, RunStatus.BOOKKEEPING_UNAVAILABLE); }); }); + + suite('`retrieveOngoingRuns()` tests', () => { + let bkpService = null; + const runsPathPattern = new RegExp(`/api/runs\\?.*token=${VALID_CONFIG.bookkeeping.token}`); + + beforeEach(() => { + bkpService = new BookkeepingService(VALID_CONFIG.bookkeeping); + bkpService.validateConfig(); + bkpService.active = true; + }); + + afterEach(() => { + nock.cleanAll(); + }); + + test('should return all already ongoing runs', async () => { + const mockResponse = { + data: [ + { + runNumber: 1, + timeO2End: undefined, + }, + { + runNumber: 2, + timeO2End: 1, + }, + { + runNumber: 3, + timeO2End: undefined, + }, + ], + }; + + nock(VALID_CONFIG.bookkeeping.url).get(runsPathPattern).reply(200, mockResponse); + const ongoingRuns = await bkpService.retrieveOngoingRuns(); + strictEqual(ongoingRuns.length, 2); + deepStrictEqual(ongoingRuns.map((run) => run.runNumber), [1, 3]); + }); + + test('should return an empty array when data when no runs are retrieved', async () => { + const mockResponse = { + data: [], + }; + + nock(VALID_CONFIG.bookkeeping.url).get(runsPathPattern).reply(200, mockResponse); + const ongoingRuns = await bkpService.retrieveOngoingRuns(); + strictEqual(ongoingRuns.length, 0); + }); + + test('should return an empty array when all runs have an end time specified', async () => { + const mockResponse = { + data: [ + { + runNumber: 99, + timeO2End: 1, + }, + ], + }; + + nock(VALID_CONFIG.bookkeeping.url).get(runsPathPattern).reply(200, mockResponse); + const ongoingRuns = await bkpService.retrieveOngoingRuns(); + strictEqual(ongoingRuns.length, 0); + }); + }); }); }; diff --git a/QualityControl/test/lib/services/RunModeService.test.js b/QualityControl/test/lib/services/RunModeService.test.js index 5802005a4..f53594543 100644 --- a/QualityControl/test/lib/services/RunModeService.test.js +++ b/QualityControl/test/lib/services/RunModeService.test.js @@ -32,6 +32,7 @@ export const runModeServiceTestSuite = async () => { beforeEach(() => { bookkeepingService = { retrieveRunInformation: sinon.stub(), + retrieveOngoingRuns: sinon.stub(), }; dataService = { @@ -128,6 +129,21 @@ export const runModeServiceTestSuite = async () => { }); }); + suite('`_fetchOnGoingRunsAtStart` tests', () => { + test('should populate ongoing runs on startup', async () => { + const runNumber = 1; + const mockRun = { runNumber }; + const mockPaths = [{ path: '/run/path1' }]; + + bookkeepingService.retrieveOngoingRuns.resolves([mockRun]); + + dataService.getObjectsLatestVersionList.resolves(mockPaths); + + await runModeService._fetchOnGoingRunsAtStart(); + strictEqual(runModeService._ongoingRuns.has(runNumber), true); + }); + }); + suite('_onRunTrackEvent - test suite', () => { test('should correctly parse event to RUN_TRACK and update ongoing runs map', async () => { const runEvent = { runNumber: 1234, transition: 'START_ACTIVITY' }; From 6e9ee664fe8dce743e6fdd1f9a511f1ae3c687af Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Mon, 5 Jan 2026 14:11:10 +0100 Subject: [PATCH 03/17] feat: add combobox option to the filter input types --- .../public/common/filters/filter.js | 39 +++++++++++ .../public/common/filters/filterTypes.js | 1 + .../public/common/filters/filterViews.js | 4 +- .../public/common/filters/filtersConfig.js | 65 +++++++++++-------- 4 files changed, 80 insertions(+), 29 deletions(-) diff --git a/QualityControl/public/common/filters/filter.js b/QualityControl/public/common/filters/filter.js index 6d658d9e2..61ad5fc56 100644 --- a/QualityControl/public/common/filters/filter.js +++ b/QualityControl/public/common/filters/filter.js @@ -196,3 +196,42 @@ export const ongoingRunsSelector = (config, filterMap, options, onChangeCallback ), ]); }; + +export const combobox = ( + { id, queryLabel, placeholder, width = '.w-20' }, + filterMap, + options, + onEnterCallback, + onInputCallback, +) => { + const filtered = filterMap[queryLabel] + ? options.payload.filter((option) => + option.toLowerCase().includes(filterMap[queryLabel].toLowerCase())) + : options.payload; + + return h(`${width}.combobox-container`, [ + h('input.form-control', { + id, + placeholder, + min: 0, + value: filterMap[queryLabel] || '', + oninput: (event) => onInputCallback(queryLabel, event.target.value), + onkeydown: ({ keyCode }) => { + if (keyCode === 13) { + onEnterCallback(); + } + }, + }), + + filterMap[queryLabel]?.length > 0 && filtered.length > 0 && h( + 'ul.combobox-list', + filtered.map((option) => + h('li.combobox-item', { + onclick: () => { + onInputCallback(queryLabel, option); + onEnterCallback(); + }, + }, option)), + ), + ]); +}; diff --git a/QualityControl/public/common/filters/filterTypes.js b/QualityControl/public/common/filters/filterTypes.js index 1ef8f14b2..267b8e330 100644 --- a/QualityControl/public/common/filters/filterTypes.js +++ b/QualityControl/public/common/filters/filterTypes.js @@ -16,6 +16,7 @@ const FilterType = { INPUT: 'input', DROPDOWN: 'dropdownSelector', RUN_MODE: 'runModeSelector', + COMBOBOX: 'combobox', }; export { FilterType }; diff --git a/QualityControl/public/common/filters/filterViews.js b/QualityControl/public/common/filters/filterViews.js index e5eb8edd7..87d1582af 100644 --- a/QualityControl/public/common/filters/filterViews.js +++ b/QualityControl/public/common/filters/filterViews.js @@ -12,7 +12,7 @@ * or submit itself to any jurisdiction. */ -import { filterInput, dynamicSelector, ongoingRunsSelector } from './filter.js'; +import { filterInput, dynamicSelector, ongoingRunsSelector, combobox } from './filter.js'; import { FilterType } from './filterTypes.js'; import { filtersConfig, runModeFilterConfig } from './filtersConfig.js'; import { runModeCheckbox } from './runMode/runModeCheckbox.js'; @@ -59,6 +59,8 @@ const createFilterElement = onEnterCallback, onFocusCallback, ); + case FilterType.COMBOBOX: + return combobox({ ...config }, filterMap, options, onEnterCallback, onInputCallback); default: return null; } }; diff --git a/QualityControl/public/common/filters/filtersConfig.js b/QualityControl/public/common/filters/filtersConfig.js index 74f4faf7a..c85070170 100644 --- a/QualityControl/public/common/filters/filtersConfig.js +++ b/QualityControl/public/common/filters/filtersConfig.js @@ -20,34 +20,43 @@ import { FilterType } from './filterTypes.js'; * @param {Array} filterService.runTypes - run types to show in the filter * @returns {Array} Filter configuration array */ -export const filtersConfig = ({ runTypes }) => [ - { - type: FilterType.INPUT, - queryLabel: 'RunNumber', - placeholder: 'RunNumber (e.g. 546783)', - id: 'runNumberFilter', - inputType: 'number', - }, - { - type: FilterType.DROPDOWN, - queryLabel: 'RunType', - placeholder: 'RunType (any)', - id: 'runTypeFilter', - options: runTypes, - }, - { - type: FilterType.INPUT, - queryLabel: 'PeriodName', - placeholder: 'PeriodName (e.g. LHC23c)', - id: 'periodNameFilter', - }, - { - type: FilterType.INPUT, - queryLabel: 'PassName', - placeholder: 'PassName (e.g. apass2)', - id: 'passNameFilter', - }, -]; +export const filtersConfig = (service) => { + const { runTypes, ongoingRuns } = service; + + if (ongoingRuns.isNotAsked()) { + service.fetchOngoingRuns(); + } + + return [ + { + type: ongoingRuns.isSuccess() ? FilterType.COMBOBOX : FilterType.INPUT, + queryLabel: 'RunNumber', + placeholder: 'RunNumber (e.g. 546783)', + id: 'runNumberFilter', + inputType: 'number', + options: ongoingRuns, + }, + { + type: FilterType.DROPDOWN, + queryLabel: 'RunType', + placeholder: 'RunType (any)', + id: 'runTypeFilter', + options: runTypes, + }, + { + type: FilterType.INPUT, + queryLabel: 'PeriodName', + placeholder: 'PeriodName (e.g. LHC23c)', + id: 'periodNameFilter', + }, + { + type: FilterType.INPUT, + queryLabel: 'PassName', + placeholder: 'PassName (e.g. apass2)', + id: 'passNameFilter', + }, + ]; +}; /** * Returns a filter configuration object used to render dynamic filter in run mode. From eaf0900a045f2d730ecc5018ff3742892837f7af Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Mon, 5 Jan 2026 14:57:15 +0100 Subject: [PATCH 04/17] feat: add dropdown with the options --- QualityControl/public/app.css | 60 +++++++++++++++++++ .../public/common/filters/filter.js | 18 ++++-- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/QualityControl/public/app.css b/QualityControl/public/app.css index 9cd733a09..c4f760c81 100644 --- a/QualityControl/public/app.css +++ b/QualityControl/public/app.css @@ -230,3 +230,63 @@ flex: 1; } } + +.combobox-container { + position: relative; + display: flex; + flex-direction: column; + + /* The magic logic: Show list when input or container has focus */ + &:focus-within .combobox-list { + visibility: visible; + opacity: 1; + transform: translateY(0); + } + + & .combobox-input { + width: 100%; + position: relative; + z-index: 2; + } + + & .combobox-list { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 1000; + + margin: 4px 0 0; + padding: 0; + list-style: none; + background: #ffffff; + border: 1px solid #ced4da; + border-radius: 4px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); + max-height: 200px; + overflow-y: auto; + + /* Hidden by default */ + visibility: hidden; + opacity: 0; + transform: translateY(-5px); + transition: opacity 0.1s ease, transform 0.1s ease, visibility 0.1s; + + & .combobox-item { + padding: 8px 12px; + cursor: pointer; + font-size: 0.9rem; + color: #333; + transition: background 0.1s ease; + + &:hover { + background-color: #007bff; + color: #ffffff; + } + + &:not(:last-child) { + border-bottom: 1px solid #f0f0f0; + } + } + } +} diff --git a/QualityControl/public/common/filters/filter.js b/QualityControl/public/common/filters/filter.js index 61ad5fc56..456942494 100644 --- a/QualityControl/public/common/filters/filter.js +++ b/QualityControl/public/common/filters/filter.js @@ -198,7 +198,7 @@ export const ongoingRunsSelector = (config, filterMap, options, onChangeCallback }; export const combobox = ( - { id, queryLabel, placeholder, width = '.w-20' }, + { id, type, queryLabel, placeholder, width = '.w-20' }, filterMap, options, onEnterCallback, @@ -206,29 +206,35 @@ export const combobox = ( ) => { const filtered = filterMap[queryLabel] ? options.payload.filter((option) => - option.toLowerCase().includes(filterMap[queryLabel].toLowerCase())) + String(option).toLowerCase().includes(filterMap[queryLabel].toLowerCase())) : options.payload; return h(`${width}.combobox-container`, [ h('input.form-control', { id, placeholder, + type, min: 0, value: filterMap[queryLabel] || '', oninput: (event) => onInputCallback(queryLabel, event.target.value), - onkeydown: ({ keyCode }) => { - if (keyCode === 13) { + onkeydown: (e) => { + if (e.keyCode === 13) { onEnterCallback(); + e.target.blur(); } }, }), - filterMap[queryLabel]?.length > 0 && filtered.length > 0 && h( + h( 'ul.combobox-list', filtered.map((option) => h('li.combobox-item', { - onclick: () => { + onmousedown: (e) => { + e.preventDefault(); onInputCallback(queryLabel, option); + e.target.closest('.combobox-container') + .querySelector('input') + ?.blur() onEnterCallback(); }, }, option)), From 994e9d111cf469ca8accac2435f63cd5cecbf54b Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Mon, 5 Jan 2026 15:18:22 +0100 Subject: [PATCH 05/17] feat: add styling based upon the framework defined styles --- QualityControl/public/app.css | 52 ++++--------------- .../public/common/filters/filter.js | 9 ++-- 2 files changed, 15 insertions(+), 46 deletions(-) diff --git a/QualityControl/public/app.css b/QualityControl/public/app.css index c4f760c81..8a145ed49 100644 --- a/QualityControl/public/app.css +++ b/QualityControl/public/app.css @@ -233,59 +233,27 @@ .combobox-container { position: relative; - display: flex; - flex-direction: column; - /* The magic logic: Show list when input or container has focus */ &:focus-within .combobox-list { - visibility: visible; - opacity: 1; - transform: translateY(0); - } - - & .combobox-input { + display: block; width: 100%; - position: relative; - z-index: 2; } & .combobox-list { - position: absolute; - top: 100%; - left: 0; - right: 0; - z-index: 1000; - - margin: 4px 0 0; padding: 0; + margin: 0; + padding-inline-start: 0; list-style: none; - background: #ffffff; - border: 1px solid #ced4da; - border-radius: 4px; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); - max-height: 200px; + left: 0; + right: 0; + max-height: 250px; overflow-y: auto; - - /* Hidden by default */ - visibility: hidden; - opacity: 0; - transform: translateY(-5px); - transition: opacity 0.1s ease, transform 0.1s ease, visibility 0.1s; + margin-top: var(--space-xs); & .combobox-item { - padding: 8px 12px; - cursor: pointer; - font-size: 0.9rem; - color: #333; - transition: background 0.1s ease; - - &:hover { - background-color: #007bff; - color: #ffffff; - } - - &:not(:last-child) { - border-bottom: 1px solid #f0f0f0; + &:hover, .is-highlighted { + background-color: var(--color-primary); + color: var(--color-white); } } } diff --git a/QualityControl/public/common/filters/filter.js b/QualityControl/public/common/filters/filter.js index 456942494..673b23325 100644 --- a/QualityControl/public/common/filters/filter.js +++ b/QualityControl/public/common/filters/filter.js @@ -205,15 +205,16 @@ export const combobox = ( onInputCallback, ) => { const filtered = filterMap[queryLabel] - ? options.payload.filter((option) => + ? ['123', '78122', '17', '9856'].filter((option) => String(option).toLowerCase().includes(filterMap[queryLabel].toLowerCase())) - : options.payload; + : ['123', '78122', '17', '9856']; return h(`${width}.combobox-container`, [ h('input.form-control', { id, placeholder, type, + autocomplete: 'off', min: 0, value: filterMap[queryLabel] || '', oninput: (event) => onInputCallback(queryLabel, event.target.value), @@ -226,9 +227,9 @@ export const combobox = ( }), h( - 'ul.combobox-list', + 'ul.combobox-list.dropdown-menu', filtered.map((option) => - h('li.combobox-item', { + h('li.combobox-item.menu-item', { onmousedown: (e) => { e.preventDefault(); onInputCallback(queryLabel, option); From 9da30caab9c9d7dda31e953ae6dcfb719711e1a9 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Mon, 5 Jan 2026 15:40:38 +0100 Subject: [PATCH 06/17] feat: add keyboard navigation to the combobox --- QualityControl/public/app.css | 2 +- .../public/common/filters/filter.js | 72 ++++++++++++++++--- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/QualityControl/public/app.css b/QualityControl/public/app.css index 8a145ed49..010b6da27 100644 --- a/QualityControl/public/app.css +++ b/QualityControl/public/app.css @@ -251,7 +251,7 @@ margin-top: var(--space-xs); & .combobox-item { - &:hover, .is-highlighted { + &:hover, &.is-highlighted { background-color: var(--color-primary); color: var(--color-white); } diff --git a/QualityControl/public/common/filters/filter.js b/QualityControl/public/common/filters/filter.js index 673b23325..042d0804e 100644 --- a/QualityControl/public/common/filters/filter.js +++ b/QualityControl/public/common/filters/filter.js @@ -197,6 +197,22 @@ export const ongoingRunsSelector = (config, filterMap, options, onChangeCallback ]); }; +/** + * Renders a searchable Combobox with keyboard navigation using ALICE O2 design tokens. + * This component is a stateless function that leverages CSS `:focus-within` for visibility + * and direct DOM manipulation for arrow-key highlighting to avoid FilterModel pollution. + * @param {object} config - The configuration for the combobox field. + * @param {string} config.id - The unique HTML ID for the input element. + * @param {string} config.type - The HTML input type (e.g., 'text', 'number'). + * @param {string} config.queryLabel - The key name in the filterMap to update. + * @param {string} config.placeholder - The placeholder text for the input. + * @param {string} config.width - Width of the input container. + * @param {object} filterMap - Object containing current filter keys and values. + * @param {RemoteData} options - RemoteData object containing the list of available options. + * @param {Function} onEnterCallback - Callback to trigger filtering. + * @param {Function} onInputCallback - Callback to update the filter value. + * @returns {vnode} - A virtual node representing the combobox. + */ export const combobox = ( { id, type, queryLabel, placeholder, width = '.w-20' }, filterMap, @@ -205,9 +221,45 @@ export const combobox = ( onInputCallback, ) => { const filtered = filterMap[queryLabel] - ? ['123', '78122', '17', '9856'].filter((option) => + ? options.payload.filter((option) => String(option).toLowerCase().includes(filterMap[queryLabel].toLowerCase())) - : ['123', '78122', '17', '9856']; + : options.payload; + + const handleKeyNavigation = (e) => { + const container = e.target.closest('.combobox-container'); + const items = [...container.querySelectorAll('.combobox-item')]; + const current = container.querySelector('.is-highlighted'); + const index = items.indexOf(current); + + const move = (nextIndex) => { + if (current) { + current.classList.remove('is-highlighted') + } + if (items[nextIndex]) { + items[nextIndex].classList.add('is-highlighted'); + items[nextIndex].scrollIntoView({ block: 'nearest' }); + } + }; + + const keys = { + ArrowDown: () => move(Math.min(index + 1, items.length - 1)), + ArrowUp: () => move(Math.max(index - 1, 0)), + Enter: () => { + if (current) { + onInputCallback(queryLabel, current.innerText); + } + e.target.blur(); + onEnterCallback(); + }, + }; + + if (keys[e.key]) { + if (e.key !== 'Enter') { + e.preventDefault(); + } + keys[e.key](); + } + }; return h(`${width}.combobox-container`, [ h('input.form-control', { @@ -218,24 +270,26 @@ export const combobox = ( min: 0, value: filterMap[queryLabel] || '', oninput: (event) => onInputCallback(queryLabel, event.target.value), - onkeydown: (e) => { - if (e.keyCode === 13) { - onEnterCallback(); - e.target.blur(); - } - }, + onkeydown: handleKeyNavigation, }), h( 'ul.combobox-list.dropdown-menu', filtered.map((option) => h('li.combobox-item.menu-item', { + onmousemove: (e) => { + const prev = e.target.closest('.combobox-list').querySelector('.is-highlighted'); + if (prev) { + prev.classList.remove('is-highlighted'); + } + e.target.classList.add('is-highlighted'); + }, onmousedown: (e) => { e.preventDefault(); onInputCallback(queryLabel, option); e.target.closest('.combobox-container') .querySelector('input') - ?.blur() + ?.blur(); onEnterCallback(); }, }, option)), From cda1dad446ddcf02a026edd702c611bf9e7d1d07 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Mon, 5 Jan 2026 15:42:33 +0100 Subject: [PATCH 07/17] docs: rename service to filterService --- QualityControl/public/common/filters/filtersConfig.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/QualityControl/public/common/filters/filtersConfig.js b/QualityControl/public/common/filters/filtersConfig.js index c85070170..ca5c7abbb 100644 --- a/QualityControl/public/common/filters/filtersConfig.js +++ b/QualityControl/public/common/filters/filtersConfig.js @@ -17,14 +17,13 @@ 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} filterService.runTypes - run types to show in the filter * @returns {Array} Filter configuration array */ -export const filtersConfig = (service) => { - const { runTypes, ongoingRuns } = service; +export const filtersConfig = (filterService) => { + const { runTypes, ongoingRuns } = filterService; if (ongoingRuns.isNotAsked()) { - service.fetchOngoingRuns(); + filterService.fetchOngoingRuns(); } return [ From 25ea043a1c7914fc123e3bb7870dafd71d545254 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Mon, 5 Jan 2026 17:02:05 +0100 Subject: [PATCH 08/17] fix: call fetch ongoing runs once --- .../public/common/filters/filter.js | 6 +- .../public/common/filters/filtersConfig.js | 66 ++++++++----------- .../test/public/features/filterTest.test.js | 2 +- 3 files changed, 33 insertions(+), 41 deletions(-) diff --git a/QualityControl/public/common/filters/filter.js b/QualityControl/public/common/filters/filter.js index 042d0804e..dc00a6ba5 100644 --- a/QualityControl/public/common/filters/filter.js +++ b/QualityControl/public/common/filters/filter.js @@ -214,7 +214,7 @@ export const ongoingRunsSelector = (config, filterMap, options, onChangeCallback * @returns {vnode} - A virtual node representing the combobox. */ export const combobox = ( - { id, type, queryLabel, placeholder, width = '.w-20' }, + { id, inputType, queryLabel, placeholder, width = '.w-20' }, filterMap, options, onEnterCallback, @@ -233,7 +233,7 @@ export const combobox = ( const move = (nextIndex) => { if (current) { - current.classList.remove('is-highlighted') + current.classList.remove('is-highlighted'); } if (items[nextIndex]) { items[nextIndex].classList.add('is-highlighted'); @@ -265,7 +265,7 @@ export const combobox = ( h('input.form-control', { id, placeholder, - type, + type: inputType, autocomplete: 'off', min: 0, value: filterMap[queryLabel] || '', diff --git a/QualityControl/public/common/filters/filtersConfig.js b/QualityControl/public/common/filters/filtersConfig.js index ca5c7abbb..2a24ef1c5 100644 --- a/QualityControl/public/common/filters/filtersConfig.js +++ b/QualityControl/public/common/filters/filtersConfig.js @@ -19,43 +19,35 @@ import { FilterType } from './filterTypes.js'; * @param {FilterService} filterService - service to get the data to populate the filters * @returns {Array} Filter configuration array */ -export const filtersConfig = (filterService) => { - const { runTypes, ongoingRuns } = filterService; - - if (ongoingRuns.isNotAsked()) { - filterService.fetchOngoingRuns(); - } - - return [ - { - type: ongoingRuns.isSuccess() ? FilterType.COMBOBOX : FilterType.INPUT, - queryLabel: 'RunNumber', - placeholder: 'RunNumber (e.g. 546783)', - id: 'runNumberFilter', - inputType: 'number', - options: ongoingRuns, - }, - { - type: FilterType.DROPDOWN, - queryLabel: 'RunType', - placeholder: 'RunType (any)', - id: 'runTypeFilter', - options: runTypes, - }, - { - type: FilterType.INPUT, - queryLabel: 'PeriodName', - placeholder: 'PeriodName (e.g. LHC23c)', - id: 'periodNameFilter', - }, - { - type: FilterType.INPUT, - queryLabel: 'PassName', - placeholder: 'PassName (e.g. apass2)', - id: 'passNameFilter', - }, - ]; -}; +export const filtersConfig = ({ runTypes, ongoingRuns }) => [ + { + type: ongoingRuns.isSuccess() ? FilterType.COMBOBOX : FilterType.INPUT, + queryLabel: 'RunNumber', + placeholder: 'RunNumber (e.g. 546783)', + id: 'runNumberFilter', + inputType: 'number', + options: ongoingRuns, + }, + { + type: FilterType.DROPDOWN, + queryLabel: 'RunType', + placeholder: 'RunType (any)', + id: 'runTypeFilter', + options: runTypes, + }, + { + type: FilterType.INPUT, + queryLabel: 'PeriodName', + placeholder: 'PeriodName (e.g. LHC23c)', + id: 'periodNameFilter', + }, + { + type: FilterType.INPUT, + queryLabel: 'PassName', + placeholder: 'PassName (e.g. apass2)', + id: 'passNameFilter', + }, +]; /** * Returns a filter configuration object used to render dynamic filter in run mode. diff --git a/QualityControl/test/public/features/filterTest.test.js b/QualityControl/test/public/features/filterTest.test.js index 625a1bec7..568fc8878 100644 --- a/QualityControl/test/public/features/filterTest.test.js +++ b/QualityControl/test/public/features/filterTest.test.js @@ -43,7 +43,7 @@ export const filterTests = async (url, page, timeout = 5000, testParent) => { strictEqual(value, '0', 'RunNumber filter should still be set to 0 on objectView page'); // Navigate to layout show - await page.locator('.menu-item:nth-child(1)').click(); + await page.locator('nav .menu-item:nth-child(1)').click(); await page.waitForSelector('#runNumberFilter', { visible: true }); // Check that filter is still set to 0 From b81167de020893e26c8fe801ea2999b7e22b9cf9 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Mon, 5 Jan 2026 17:04:37 +0100 Subject: [PATCH 09/17] fix: change run number filter to combobox --- QualityControl/public/common/filters/filter.js | 7 ------- QualityControl/public/common/filters/filtersConfig.js | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/QualityControl/public/common/filters/filter.js b/QualityControl/public/common/filters/filter.js index dc00a6ba5..224234b13 100644 --- a/QualityControl/public/common/filters/filter.js +++ b/QualityControl/public/common/filters/filter.js @@ -277,13 +277,6 @@ export const combobox = ( 'ul.combobox-list.dropdown-menu', filtered.map((option) => h('li.combobox-item.menu-item', { - onmousemove: (e) => { - const prev = e.target.closest('.combobox-list').querySelector('.is-highlighted'); - if (prev) { - prev.classList.remove('is-highlighted'); - } - e.target.classList.add('is-highlighted'); - }, onmousedown: (e) => { e.preventDefault(); onInputCallback(queryLabel, option); diff --git a/QualityControl/public/common/filters/filtersConfig.js b/QualityControl/public/common/filters/filtersConfig.js index 2a24ef1c5..4cc2c795e 100644 --- a/QualityControl/public/common/filters/filtersConfig.js +++ b/QualityControl/public/common/filters/filtersConfig.js @@ -21,7 +21,7 @@ import { FilterType } from './filterTypes.js'; */ export const filtersConfig = ({ runTypes, ongoingRuns }) => [ { - type: ongoingRuns.isSuccess() ? FilterType.COMBOBOX : FilterType.INPUT, + type: FilterType.COMBOBOX, queryLabel: 'RunNumber', placeholder: 'RunNumber (e.g. 546783)', id: 'runNumberFilter', From 8c55622e5860eed0f69652578f1418185882eb23 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Tue, 6 Jan 2026 11:36:48 +0100 Subject: [PATCH 10/17] fix: empty list throwing errors for undefined --- QualityControl/public/common/filters/filter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/QualityControl/public/common/filters/filter.js b/QualityControl/public/common/filters/filter.js index 224234b13..0d9f29e54 100644 --- a/QualityControl/public/common/filters/filter.js +++ b/QualityControl/public/common/filters/filter.js @@ -203,7 +203,7 @@ export const ongoingRunsSelector = (config, filterMap, options, onChangeCallback * and direct DOM manipulation for arrow-key highlighting to avoid FilterModel pollution. * @param {object} config - The configuration for the combobox field. * @param {string} config.id - The unique HTML ID for the input element. - * @param {string} config.type - The HTML input type (e.g., 'text', 'number'). + * @param {string} config.inputType - The type for the HTML input element. * @param {string} config.queryLabel - The key name in the filterMap to update. * @param {string} config.placeholder - The placeholder text for the input. * @param {string} config.width - Width of the input container. @@ -221,7 +221,7 @@ export const combobox = ( onInputCallback, ) => { const filtered = filterMap[queryLabel] - ? options.payload.filter((option) => + ? options.payload?.filter((option) => String(option).toLowerCase().includes(filterMap[queryLabel].toLowerCase())) : options.payload; @@ -273,9 +273,9 @@ export const combobox = ( onkeydown: handleKeyNavigation, }), - h( + options.payload?.length > 0 && h( 'ul.combobox-list.dropdown-menu', - filtered.map((option) => + filtered?.map((option) => h('li.combobox-item.menu-item', { onmousedown: (e) => { e.preventDefault(); From daecc32a8d9509be2a43329978c3afde853645d0 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Fri, 9 Jan 2026 13:56:38 +0100 Subject: [PATCH 11/17] feat: add unselectable placeholder and onblur remove highlighted rows --- QualityControl/public/app.css | 7 ++++ .../public/common/filters/filter.js | 41 ++++++++++++------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/QualityControl/public/app.css b/QualityControl/public/app.css index 010b6da27..928b9ca44 100644 --- a/QualityControl/public/app.css +++ b/QualityControl/public/app.css @@ -256,5 +256,12 @@ color: var(--color-white); } } + + & .combobox-header { + padding: 4px 12px; + font-size: 0.85rem; + color: var(--color-gray-darker); + font-weight: bold; + } } } diff --git a/QualityControl/public/common/filters/filter.js b/QualityControl/public/common/filters/filter.js index 0d9f29e54..6893dd7c6 100644 --- a/QualityControl/public/common/filters/filter.js +++ b/QualityControl/public/common/filters/filter.js @@ -220,14 +220,15 @@ export const combobox = ( onEnterCallback, onInputCallback, ) => { + const ongoingRuns = options.payload ?? []; const filtered = filterMap[queryLabel] - ? options.payload?.filter((option) => + ? ongoingRuns?.filter((option) => String(option).toLowerCase().includes(filterMap[queryLabel].toLowerCase())) - : options.payload; + : ongoingRuns; const handleKeyNavigation = (e) => { const container = e.target.closest('.combobox-container'); - const items = [...container.querySelectorAll('.combobox-item')]; + const items = [...container.querySelectorAll('.combobox-item:not(.combobox-header)')]; const current = container.querySelector('.is-highlighted'); const index = items.indexOf(current); @@ -271,21 +272,31 @@ export const combobox = ( value: filterMap[queryLabel] || '', oninput: (event) => onInputCallback(queryLabel, event.target.value), onkeydown: handleKeyNavigation, + onblur: (e) => { + const container = e.target.closest('.combobox-container'); + container.querySelector('.is-highlighted')?.classList.remove('is-highlighted'); + }, }), - options.payload?.length > 0 && h( + filtered.length > 0 && h( 'ul.combobox-list.dropdown-menu', - filtered?.map((option) => - h('li.combobox-item.menu-item', { - onmousedown: (e) => { - e.preventDefault(); - onInputCallback(queryLabel, option); - e.target.closest('.combobox-container') - .querySelector('input') - ?.blur(); - onEnterCallback(); - }, - }, option)), + [ + h('li.combobox-header.dropdown-header', { + style: { pointerEvents: 'none', userSelect: 'none' }, + }, placeholder), + + ...filtered.map((option) => + h('li.combobox-item.menu-item', { + onmousedown: (e) => { + e.preventDefault(); + onInputCallback(queryLabel, option); + e.target.closest('.combobox-container') + .querySelector('input') + ?.blur(); + onEnterCallback(); + }, + }, option)), + ], ), ]); }; From acde26c1f2e0eb4cb1e482f88a877f62b668f2b0 Mon Sep 17 00:00:00 2001 From: Alex Janson Date: Thu, 15 Jan 2026 13:10:22 +0100 Subject: [PATCH 12/17] feat: add fetching ongoing runs at filter service initialisation --- QualityControl/public/common/filters/model/FilterModel.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/QualityControl/public/common/filters/model/FilterModel.js b/QualityControl/public/common/filters/model/FilterModel.js index d2f209732..33845f42e 100644 --- a/QualityControl/public/common/filters/model/FilterModel.js +++ b/QualityControl/public/common/filters/model/FilterModel.js @@ -49,6 +49,8 @@ export default class FilterModel extends Observable { this._runInformation = {}; this.ONGOING_RUN_INTERVAL_MS = 15000; + + this.filterService.fetchOngoingRuns(); } /** @@ -187,7 +189,6 @@ export default class FilterModel extends Observable { */ async activateRunsMode(viewModel) { this.isRunModeActivated = true; - await this.filterService.fetchOngoingRuns(); if (this._filterMap.RunNumber) { this._filterMap = { RunNumber: this._filterMap.RunNumber }; this.triggerFilter(viewModel); From 1a5ff8688b0f13330cfb9b7b8661e030d577cc2b Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 21:01:51 +0100 Subject: [PATCH 13/17] RunMode is not fetching data anymore from BKP --- QualityControl/test/setup/testSetupForBkp.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/QualityControl/test/setup/testSetupForBkp.js b/QualityControl/test/setup/testSetupForBkp.js index 33a44a610..9f7cff8b6 100644 --- a/QualityControl/test/setup/testSetupForBkp.js +++ b/QualityControl/test/setup/testSetupForBkp.js @@ -161,12 +161,6 @@ export const initializeNockForBkp = () => { }, }) .get(`/api/runs/500001${TOKEN_PATH}`) - .reply(200, { - data: { - timeO2End: null, - }, - }) - .get(`/api/runs/500001${TOKEN_PATH}`) .reply(200, { data: { timeO2End: '2023-12-01T10:30:00Z', From 3b2ddde96c1c5fe346dce61d0d2e2aaad65507b4 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 21:02:15 +0100 Subject: [PATCH 14/17] Use convention for ongoing runs variable --- QualityControl/public/services/Filter.service.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/QualityControl/public/services/Filter.service.js b/QualityControl/public/services/Filter.service.js index 0758cfaa6..d976ca6c8 100644 --- a/QualityControl/public/services/Filter.service.js +++ b/QualityControl/public/services/Filter.service.js @@ -31,7 +31,7 @@ export default class FilterService { this._detectors = RemoteData.notAsked(); this._dataPasses = RemoteData.notAsked(); - this.ongoingRuns = RemoteData.notAsked(); + this._ongoingRuns = RemoteData.notAsked(); } /** @@ -104,13 +104,13 @@ export default class FilterService { * @returns {void} assigns the remoteData object to ongoingRuns */ async fetchOngoingRuns() { - this.ongoingRuns = RemoteData.loading(); + this._ongoingRuns = RemoteData.loading(); this.filterModel.notify(); const { result, ok } = await this.loader.get('/api/filter/ongoingRuns'); if (ok) { - this.ongoingRuns = RemoteData.success(result?.ongoingRuns); + this._ongoingRuns = RemoteData.success(result?.ongoingRuns); } else { - this.ongoingRuns = RemoteData.failure('Error retrieving ongoing runs'); + this._ongoingRuns = RemoteData.failure('Error retrieving ongoing runs'); } this.filterModel.notify(); } @@ -138,4 +138,12 @@ export default class FilterService { get dataPasses() { return this._dataPasses; } + + /** + * Gets the list of ongoing runs. + * @returns {RemoteData} An array containing the ongoing run numbers. + */ + get ongoingRuns() { + return this._ongoingRuns; + } } From a66580964e7dca5bf7c9ddc71410604d19543c94 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 21:23:45 +0100 Subject: [PATCH 15/17] Fix combox functionality --- .../public/common/filters/filter.js | 19 ++++++++----------- .../common/filters/model/FilterModel.js | 12 ++++++++---- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/QualityControl/public/common/filters/filter.js b/QualityControl/public/common/filters/filter.js index 1dd11079a..35e6a524f 100644 --- a/QualityControl/public/common/filters/filter.js +++ b/QualityControl/public/common/filters/filter.js @@ -340,7 +340,7 @@ export const ongoingRunsSelector = (config, filterMap, options, onChangeCallback }; /** - * Renders a searchable Combobox with keyboard navigation using ALICE O2 design tokens. + * Renders a searchable Combobox with keyboard navigation using ALICE O2 design. * This component is a stateless function that leverages CSS `:focus-within` for visibility * and direct DOM manipulation for arrow-key highlighting to avoid FilterModel pollution. * @param {object} config - The configuration for the combobox field. @@ -351,8 +351,8 @@ export const ongoingRunsSelector = (config, filterMap, options, onChangeCallback * @param {string} config.width - Width of the input container. * @param {object} filterMap - Object containing current filter keys and values. * @param {RemoteData} options - RemoteData object containing the list of available options. - * @param {Function} onEnterCallback - Callback to trigger filtering. - * @param {Function} onInputCallback - Callback to update the filter value. + * @param {onenter} onEnterCallback - Callback to trigger filtering. + * @param {oninput} onInputCallback - Callback to update the filter value. * @returns {vnode} - A virtual node representing the combobox. */ export const combobox = ( @@ -362,7 +362,7 @@ export const combobox = ( onEnterCallback, onInputCallback, ) => { - const ongoingRuns = options.payload ?? []; + const ongoingRuns = options.isSuccess() ? options.payload : []; const filtered = filterMap[queryLabel] ? ongoingRuns?.filter((option) => String(option).toLowerCase().includes(filterMap[queryLabel].toLowerCase())) @@ -389,10 +389,9 @@ export const combobox = ( ArrowUp: () => move(Math.max(index - 1, 0)), Enter: () => { if (current) { - onInputCallback(queryLabel, current.innerText); + onInputCallback(queryLabel, current.innerText, true); } e.target.blur(); - onEnterCallback(); }, }; @@ -430,12 +429,10 @@ export const combobox = ( ...filtered.map((option) => h('li.combobox-item.menu-item', { onmousedown: (e) => { + // onmousedown to capture before blur event e.preventDefault(); - onInputCallback(queryLabel, option); - e.target.closest('.combobox-container') - .querySelector('input') - ?.blur(); - onEnterCallback(); + onInputCallback(queryLabel, option, true); + e.target.closest('.combobox-container').querySelector('input')?.blur(); }, }, option)), ], diff --git a/QualityControl/public/common/filters/model/FilterModel.js b/QualityControl/public/common/filters/model/FilterModel.js index f756effbc..574fa2bc3 100644 --- a/QualityControl/public/common/filters/model/FilterModel.js +++ b/QualityControl/public/common/filters/model/FilterModel.js @@ -105,10 +105,14 @@ export default class FilterModel extends Observable { * @returns {undefined} */ setFilterValue(key, value, setUrl = false) { - if (value?.trim()) { - this._filterMap[key] = value; - } else { - delete this._filterMap[key]; + if (typeof value === 'string') { + if (value && value?.trim()) { + this._filterMap[key] = value; + } else { + delete this._filterMap[key]; + } + } else if (typeof value === 'number') { + this._filterMap[key] = String(value); } if (setUrl) { From 77fb819f92be426c3ee2c9445309c6166b8b02e7 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 21:31:47 +0100 Subject: [PATCH 16/17] Revert test change --- QualityControl/test/setup/testSetupForBkp.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/QualityControl/test/setup/testSetupForBkp.js b/QualityControl/test/setup/testSetupForBkp.js index 9f7cff8b6..33a44a610 100644 --- a/QualityControl/test/setup/testSetupForBkp.js +++ b/QualityControl/test/setup/testSetupForBkp.js @@ -161,6 +161,12 @@ export const initializeNockForBkp = () => { }, }) .get(`/api/runs/500001${TOKEN_PATH}`) + .reply(200, { + data: { + timeO2End: null, + }, + }) + .get(`/api/runs/500001${TOKEN_PATH}`) .reply(200, { data: { timeO2End: '2023-12-01T10:30:00Z', From bdff5ca390c6e748077dd9ca973a8d6914876fe1 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Sat, 17 Jan 2026 22:06:57 +0100 Subject: [PATCH 17/17] Return normal inputfilter model if BKP no data --- QualityControl/public/common/filters/filter.js | 9 +++++++-- .../public/common/filters/model/FilterModel.js | 7 +++++-- .../notifications/model/NotificationRunStartModel.js | 2 +- .../test/public/components/profileHeader.test.js | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/QualityControl/public/common/filters/filter.js b/QualityControl/public/common/filters/filter.js index 35e6a524f..9ba2217ba 100644 --- a/QualityControl/public/common/filters/filter.js +++ b/QualityControl/public/common/filters/filter.js @@ -362,7 +362,12 @@ export const combobox = ( onEnterCallback, onInputCallback, ) => { - const ongoingRuns = options.isSuccess() ? options.payload : []; + const ongoingRuns = options.isSuccess() && options.payload.length > 0 ? options.payload : []; + if (!ongoingRuns.length) { + return filterInput({ + queryLabel, placeholder, id, filterMap, onInputCallback, onEnterCallback, type: inputType, width, + }); + } const filtered = filterMap[queryLabel] ? ongoingRuns?.filter((option) => String(option).toLowerCase().includes(filterMap[queryLabel].toLowerCase())) @@ -411,7 +416,7 @@ export const combobox = ( autocomplete: 'off', min: 0, value: filterMap[queryLabel] || '', - oninput: (event) => onInputCallback(queryLabel, event.target.value), + oninput: (event) => onInputCallback(queryLabel, event.target.value, true), onkeydown: handleKeyNavigation, onblur: (e) => { const container = e.target.closest('.combobox-container'); diff --git a/QualityControl/public/common/filters/model/FilterModel.js b/QualityControl/public/common/filters/model/FilterModel.js index 574fa2bc3..3cb0582dc 100644 --- a/QualityControl/public/common/filters/model/FilterModel.js +++ b/QualityControl/public/common/filters/model/FilterModel.js @@ -193,8 +193,11 @@ export default class FilterModel extends Observable { */ async activateRunsMode(viewModel) { this.isRunModeActivated = true; - if (this._filterMap.RunNumber) { - this._filterMap = { RunNumber: this._filterMap.RunNumber }; + const { RunNumber } = this._filterMap; + this.clearFilters(); + + if (RunNumber) { + this._filterMap = { RunNumber }; this.triggerFilter(viewModel); } else { const { ongoingRuns } = this.filterService; diff --git a/QualityControl/public/common/notifications/model/NotificationRunStartModel.js b/QualityControl/public/common/notifications/model/NotificationRunStartModel.js index 37e9ad8eb..2bd53506c 100644 --- a/QualityControl/public/common/notifications/model/NotificationRunStartModel.js +++ b/QualityControl/public/common/notifications/model/NotificationRunStartModel.js @@ -96,7 +96,7 @@ export default class NotificationRunStartModel extends Observable { // We select the given `runNumber` in RunMode. // We do not have to set the parameter in the URL, as this is already achieved on navigation. - this.model.filterModel.setFilterValue('RunNumber', runNumber?.toString()); + this.model.filterModel.setFilterValue('RunNumber', runNumber, true); }, }); } diff --git a/QualityControl/test/public/components/profileHeader.test.js b/QualityControl/test/public/components/profileHeader.test.js index 8c05e9524..5c017af2e 100644 --- a/QualityControl/test/public/components/profileHeader.test.js +++ b/QualityControl/test/public/components/profileHeader.test.js @@ -253,7 +253,7 @@ export const profileHeaderTests = async (url, page, timeout = 1000, testParent) }); await page.waitForFunction(() => document.querySelector('#run-mode-switch .switch input[type="checkbox"]')?.checked === true); - + await delay(500); // Wait a bit for the RunMode filter to update selectedRun = await page.evaluate(() => document.querySelector('select#ongoingRunsFilter')?.value); strictEqual( selectedRun,