From da583bde6903bc55b4da3832d9c30fa4de0f65fa Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 14:10:30 +0200 Subject: [PATCH 1/8] [O2B-1640] Merge columns of the run overview into denser ones - Run: run number with the definition and the quality below it - Timing: start, stop and duration - Context: LHC fill, LHC period and environment - Configuration: EPNs, FLPs, DCS and trigger - Merged columns are kept hidden to provide their filters and the export --- .../runsOverviewActiveColumns.js | 208 ++++++++++++++++++ .../views/Runs/Overview/RunsOverviewPage.js | 6 +- 2 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js diff --git a/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js new file mode 100644 index 0000000000..00ddfc99fb --- /dev/null +++ b/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js @@ -0,0 +1,208 @@ +/** + * @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. + */ + +import { h } from '/js/src/index.js'; +import { runsActiveColumns } from './runsActiveColumns.js'; +import { profiles } from '../../../components/common/table/profiles.js'; +import { overflowBalloon } from '../../../components/common/popover/overflowBalloon.js'; +import { frontLink } from '../../../components/common/navigation/frontLink.js'; +import { formatRunStart } from '../format/formatRunStart.js'; +import { formatRunEnd } from '../format/formatRunEnd.js'; +import { displayRunDuration } from '../format/displayRunDuration.js'; +import { coloredCalibrationStatusComponent } from '../coloredCalibrationStatusComponent.js'; +import { RunDefinition } from '../../../domain/enums/RunDefinition.js'; +import { BeamModes } from '../../../domain/enums/BeamModes.js'; + +/** + * Display a value preceded by a small label, on its own line + * + * @param {string} id the id of the line + * @param {string} label the label of the line + * @param {Component|string} content the content of the line + * @return {Component} the line + */ +const labelledLine = (id, label, content) => h('.flex-row.items-center.g1', [ + h('span.f7.gray-darker.flex-shrink-0', label), + h(`#${id}`, overflowBalloon(content)), +]); + +/** + * Return a copy of the given column, hidden from the table but still providing its filter (and export) to the filters panel + * + * @param {string} key the key of the column in the runs active columns + * @return {Column} the hidden column + */ +const hiddenColumn = (key) => ({ ...runsActiveColumns[key], visible: false, profiles: [profiles.none] }); + +/** + * Format the number of EPNs of a run: the count if known, ON if EPNs were used without a known count, OFF otherwise + * + * @param {boolean} epn true if the run used EPNs + * @param {number|null} nEpns the number of EPNs + * @return {string|number} the formatted value + */ +const formatEpnsCount = (epn, nEpns) => { + if (!epn) { + return 'OFF'; + } + return typeof nEpns === 'number' ? nEpns : 'ON'; +}; + +/** + * Display the definition (with the calibration status) and the quality of the run, below the run number + * + * @param {Run} run the run + * @return {Component} the lines + */ +const runDefinitionAndQuality = ({ id, definition, calibrationStatus, runQuality }) => h('.flex-column', [ + h(`#row${id}-definition.flex-row.g2.items-center.f7`, [ + h('', definition || '-'), + definition === RunDefinition.Calibration && coloredCalibrationStatusComponent(calibrationStatus), + ]), + h(`#row${id}-quality.f7.gray-darker`, runQuality), +]); + +/** + * List of active columns for the runs overview table + * + * The columns of the generic runs table are merged into denser ones, the merged ones are kept hidden to provide their filters and the export + */ +export const runsOverviewActiveColumns = { + id: runsActiveColumns.id, + runNumber: { + ...runsActiveColumns.runNumber, + name: 'Run', + classes: 'w-10 f6', + + /** + * Display the run number, with the definition and the quality of the run below it + * + * @param {number} runNumber the run number + * @param {Run} run the run + * @return {Component} the run number and its definition and quality + */ + format: (runNumber, run) => h('.flex-column.items-start', [ + h(`#row${run.id}-run-number`, runsActiveColumns.runNumber.format(runNumber, run)), + runDefinitionAndQuality(run), + ]), + }, + timing: { + name: 'Timing', + visible: true, + classes: 'w-15 f6', + noEllipsis: true, + + /** + * Display the start, the stop and the duration of the run, each in its own labelled line + * + * @param {*} _ unused, the times are read from the run + * @param {Run} run the run + * @return {Component} the start, stop and duration + */ + format: (_, run) => h('', [ + labelledLine(`row${run.id}-start`, 'Start', formatRunStart(run, { inline: true })), + labelledLine(`row${run.id}-stop`, 'Stop', formatRunEnd(run, { inline: true })), + labelledLine(`row${run.id}-runDuration`, 'Dur.', displayRunDuration(run)), + ]), + }, + context: { + name: 'Context', + visible: true, + classes: 'w-15 f6', + + /** + * Display the LHC fill, the LHC period and the environment of the run, each in its own labelled line + * + * @param {*} _ unused, the values are read from the run + * @param {Run} run the run + * @return {Component} the fill, period and environment + */ + format: (_, { id, fillNumber, lhcPeriod, lhcBeamMode, environmentId }) => { + const isStableBeams = lhcBeamMode === BeamModes.STABLE_BEAMS; + return h('', [ + labelledLine( + `row${id}-fillNumber`, + 'Fill', + fillNumber !== null && isStableBeams ? frontLink(fillNumber, 'lhc-fill-details', { fillNumber }) : '-', + ), + labelledLine(`row${id}-lhcPeriod`, 'Period', lhcPeriod && isStableBeams ? lhcPeriod : '-'), + labelledLine( + `row${id}-environmentId`, + 'Env', + environmentId ? frontLink(environmentId, 'env-details', { environmentId }) : '-', + ), + ]); + }, + }, + configuration: { + name: 'Configuration', + visible: true, + classes: 'w-10 f6', + + /** + * Display the number of EPNs and FLPs, the DCS state and the trigger of the run, two by line + * + * @param {*} _ unused, the values are read from the run + * @param {Run} run the run + * @return {Component} the EPNs, FLPs, DCS and trigger + */ + format: (_, { id, epn, nEpns, nFlps, dcs, triggerValue }) => h('', [ + h(`#row${id}-nodes.flex-row.g2`, [ + labelledLine(`row${id}-nEpns`, 'EPNs', formatEpnsCount(epn, nEpns)), + labelledLine(`row${id}-nFlps`, 'FLPs', nFlps ?? '-'), + ]), + h(`#row${id}-states.flex-row.g2`, [ + labelledLine(`row${id}-dcs`, 'DCS', dcs ? 'On' : 'Off'), + labelledLine(`row${id}-triggerValue`, 'TRG', triggerValue || '-'), + ]), + ]), + }, + detectors: { ...runsActiveColumns.detectors, classes: 'w-20 f6' }, + tags: { ...runsActiveColumns.tags, classes: 'w-10 f6' }, + eorReasons: { ...runsActiveColumns.eorReasons, classes: 'w-15 f6' }, + + // Columns displayed in a merged one, only providing their filters + definition: hiddenColumn('definition'), + runQuality: hiddenColumn('runQuality'), + timeO2Start: hiddenColumn('timeO2Start'), + timeO2End: hiddenColumn('timeO2End'), + runDuration: hiddenColumn('runDuration'), + fillNumber: hiddenColumn('fillNumber'), + lhcPeriod: hiddenColumn('lhcPeriod'), + environmentId: hiddenColumn('environmentId'), + nEpns: hiddenColumn('nEpns'), + nFlps: hiddenColumn('nFlps'), + dcs: hiddenColumn('dcs'), + triggerValue: hiddenColumn('triggerValue'), + + // Other columns, hidden by default + ...Object.fromEntries(Object.entries(runsActiveColumns).filter(([key]) => ![ + 'id', + 'runNumber', + 'detectors', + 'tags', + 'eorReasons', + 'definition', + 'runQuality', + 'timeO2Start', + 'timeO2End', + 'runDuration', + 'fillNumber', + 'lhcPeriod', + 'environmentId', + 'nEpns', + 'nFlps', + 'dcs', + 'triggerValue', + ].includes(key))), +}; diff --git a/lib/public/views/Runs/Overview/RunsOverviewPage.js b/lib/public/views/Runs/Overview/RunsOverviewPage.js index 4f76d417d9..d47d658d38 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewPage.js +++ b/lib/public/views/Runs/Overview/RunsOverviewPage.js @@ -15,7 +15,7 @@ import { h } from '/js/src/index.js'; import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; -import { runsActiveColumns } from '../ActiveColumns/runsActiveColumns.js'; +import { runsOverviewActiveColumns } from '../ActiveColumns/runsOverviewActiveColumns.js'; import { table } from '../../../components/common/table/table.js'; import { switchInput } from '../../../components/common/form/switchInput.js'; import { exportTriggerAndModal } from '../../../components/common/dataExport/exportTriggerAndModal.js'; @@ -52,14 +52,14 @@ export const RunsOverviewPage = ({ runs: { overviewModel: runsOverviewModel }, m return h('', [ h('.flex-row.header-container.g2.pv2', [ - filtersPanelPopover(runsOverviewModel, runsActiveColumns), + filtersPanelPopover(runsOverviewModel, runsOverviewActiveColumns), h('.pl2#runOverviewFilter', textInputFilter(runsOverviewModel.filteringModel, 'runNumbers', 'e.g. 534454, 534455...')), togglePhysicsOnlyFilter(filteringModel.get('definitions')), exportTriggerAndModal(exportModel, modalModel), ]), warningComponent(runsOverviewModel), h('.flex-column.w-100', [ - table(items, runsActiveColumns), + table(items, runsOverviewActiveColumns), paginationComponent(pagination), ]), ]); From 72d03f5ac125092ead9261a190286de748820745 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 14:10:30 +0200 Subject: [PATCH 2/8] [O2B-1640] Adapt the run overview tests to the merged columns --- test/public/runs/overview.test.js | 64 +++++++++++++++++++------------ 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 2a439b9d26..f9bdd63d31 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -31,13 +31,11 @@ const { waitForNavigation, waitForTableTotalRowsCountToEqual, expectInputValue, - expectColumnValues, waitForEmptyTable, waitForDownload, expectLink, expectUrlParams, expectAttributeValue, - getColumnCellsInnerTexts, resetFilters, openFilteringPanel, waitForButtonToBecomeActive, @@ -80,7 +78,25 @@ module.exports = () => { expect(title).to.equal('AliceO2 Bookkeeping'); }); - describe('General page behaviour', () => { + /** + * Expect the run numbers displayed in the overview table to be the given ones + * + * @param {puppeteer.Page} page the puppeteer page + * @param {string[]} expectedRunNumbers the expected run numbers, in the order of the rows + * @return {Promise} resolves once the run numbers are the expected ones + */ +const expectRunNumbers = async (page, expectedRunNumbers) => { + await page.waitForFunction( + (expected) => { + const cells = [...document.querySelectorAll('table tbody [id$="-run-number"]')]; + return cells.length === expected.length && cells.every((cell, index) => cell.innerText === expected[index]); + }, + {}, + expectedRunNumbers, + ); +}; + +describe('General page behaviour', () => { before(() => goToPage(page, 'run-overview')); beforeEach(async () => { await navigateToRunsOverview(page); @@ -148,12 +164,12 @@ module.exports = () => { }); it('successfully switch to raw timestamp display', async () => { - await expectInnerText(page, '#row106 td:nth-child(6)', '08/08/2019\n13:00:00'); - await expectInnerText(page, '#row106 td:nth-child(7)', '09/08/2019\n14:00:00'); + await expectInnerText(page, '#row106-start', '08/08/2019, 13:00:00'); + await expectInnerText(page, '#row106-stop', '09/08/2019, 14:00:00'); await pressElement(page, '#preferences-raw-timestamps', true); - await expectInnerText(page, '#row106 td:nth-child(6)', '1565269200000'); - await expectInnerText(page, '#row106 td:nth-child(7)', '1565359200000'); + await expectInnerText(page, '#row106-start', '1565269200000'); + await expectInnerText(page, '#row106-stop', '1565359200000'); // Go back to normal await pressElement(page, '#preferences-raw-timestamps', true); @@ -270,19 +286,19 @@ module.exports = () => { it('Should have balloon on detector, tags and eor column', async () => { // Run 106 has detectors and tags that overflow await fillInput(page, filterPanelRunNumbersInputSelector, '106', ['change']); - await expectColumnValues(page, 'runNumber', ['106']); + await expectRunNumbers(page, ['106']); - await checkColumnBalloon(page, 1, 2); - await checkColumnBalloon(page, 1, 3); + await checkColumnBalloon(page, 1, 5); + await checkColumnBalloon(page, 1, 6); // Run 1 has eor reasons that overflow await fillInput(page, filterPanelRunNumbersInputSelector, '1,1', ['change']); - await expectColumnValues(page, 'runNumber', ['1']); - await checkColumnBalloon(page, 1, 16); + await expectRunNumbers(page, ['1']); + await checkColumnBalloon(page, 1, 7); }); it('Should display balloon if the text overflows', async () => { - await checkColumnBalloon(page, 1, 2); + await checkColumnBalloon(page, 1, 5); }); it('should successfully display duration without warning popover when run has trigger OFF', async () => { @@ -419,7 +435,7 @@ module.exports = () => { await waitForTableLength(page, size); const definitions = await page.$$eval( - '.column-definition div div div:first-child', + 'tbody [id$="-definition"] > div:first-child', (rows) => rows.map((row) => row.innerText), ); @@ -428,7 +444,7 @@ module.exports = () => { } catch { const runNumbers = await page.$$eval('tbody tr', (rows) => rows.map((row) => { const rowId = row.id; - return document.querySelector(`#${rowId}-runNumber-text`).innerText; + return document.querySelector(`#${rowId}-run-number`).innerText; })); throw new Error(`Expect all run definitions ${definitions} to be one of ${authorizedRunDefinition}, for runs (${runNumbers})`); } @@ -515,7 +531,7 @@ module.exports = () => { await page.select(runDurationOperatorSelector, '='); await waitForTableLength(page, 4); - let runDurationList = await getColumnCellsInnerTexts(page, 'runDuration'); + let runDurationList = await page.$$eval('tbody [id$="-runDuration"]', (cells) => cells.map((cell) => cell.innerText)); expect(runDurationList.every((runDuration) => runDuration === '25:00:00')).to.be.true; @@ -528,7 +544,7 @@ module.exports = () => { // Expect only unknown runDurationList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { const rowId = row.id; - return document.querySelector(`#${rowId}-runDuration-text`)?.innerText; + return document.querySelector(`#${rowId}-runDuration`)?.innerText; })); expect(runDurationList.every((runDuration) => runDuration === 'UNKNOWN')).to.be.true; @@ -538,7 +554,7 @@ module.exports = () => { const popoverSelector = await getPopoverSelector(await page.waitForSelector('.aliceL3AndDipoleCurrent-filter .popover-trigger')); await pressElement(page, `${popoverSelector} .dropdown-option:last-child`, true); // Select 30003kA/0kA - await expectColumnValues(page, 'runNumber', ['54', '53', '52']); + await expectRunNumbers(page, ['54', '53', '52']); }); it('Should successfully filter runs by their run quality', async () => { @@ -557,7 +573,7 @@ module.exports = () => { for (const row of rows) { expect(await row.evaluate((rowItem) => { const rowId = rowItem.id; - return document.querySelector(`#${rowId}-runQuality-text`).innerText; + return document.querySelector(`#${rowId}-runQuality`).innerText; })).to.be.oneOf(authorizedRunQualities); } }; @@ -609,7 +625,7 @@ module.exports = () => { for (const row of rows) { expect(await row.evaluate((rowItem) => { const rowId = rowItem.id; - return document.querySelector(`#${rowId}-triggerValue-text`).innerText; + return document.querySelector(`#${rowId}-triggerValue`).innerText; })).to.be.oneOf(authorizedRunQualities); } }; @@ -679,10 +695,10 @@ module.exports = () => { const filterOnRun = async (selector) => { await expectAttributeValue(page, selector, 'placeholder', 'e.g. 534454, 534455...'); await fillInput(page, selector, inputValue, ['change']); - await expectColumnValues(page, 'runNumber', ['109', '108', '107', '106', '105', '104', '103', '102', '101', '100']); + await expectRunNumbers(page, ['109', '108', '107', '106', '105', '104', '103', '102', '101', '100']); await pressElement(page, '#pageMoveRight', true); - await expectColumnValues(page, 'runNumber', ['10']); + await expectRunNumbers(page, ['10']); }; await filterOnRun('#runOverviewFilter .runNumbers-textFilter'); @@ -748,7 +764,7 @@ module.exports = () => { const nFlpsList = await page.evaluate(() => Array.from(document.querySelectorAll('tbody tr')).map((row) => { const rowId = row.id; - return document.querySelector(`#${rowId}-nFlps-text`)?.innerText; + return document.querySelector(`#${rowId}-nFlps`)?.innerText; })); expect(nFlpsList.every((nFlps) => parseInt(nFlps, 10) <= 10)).to.be.true; }); @@ -760,7 +776,7 @@ module.exports = () => { await fillInput(page, '#nEpns-operand', '10', ['change']); await waitForTableLength(page, 5); - await expectColumnValues(page, 'nEpns', ['10', '10', 'OFF', 'OFF', '10']); + expect(await page.$$eval('tbody [id$="-nEpns"]', (cells) => cells.map((cell) => cell.innerText))).to.eql(['10', '10', 'OFF', 'OFF', '10']); }); it('should successfully filter on EPN on/off', async () => { From 008eaf72a5b3120518ab90bd237998c283221fb7 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 14:40:13 +0200 Subject: [PATCH 3/8] [O2B-1640] Extract run quality color, run actions and run end date helpers Notable changes for developers: - New `getRunQualityColor` utility, used by the run details quality badge and the runs table quality button, to be reused by any display of the run quality. - `runActionsComponents` is extracted from the run number column of `runsActiveColumns`, to display the run's actions in other layouts than the button with dropdown. - `formatRunEnd` has a new `hideDateIfSameDayAs` option (inline only): the date of the end is hidden, but keeps its space, when it is the same day as the given timestamp, so the times stay aligned. - `detectorStyleClass` of `formatRunDetectorsInline` is exported. --- .../Runs/ActiveColumns/runsActiveColumns.js | 52 +++++++++---------- .../Runs/format/formatRunDetectorsInline.js | 2 +- lib/public/views/Runs/format/formatRunEnd.js | 28 ++++++++-- .../views/Runs/format/formatRunQuality.js | 14 ++--- .../views/Runs/format/getRunQualityColor.js | 29 +++++++++++ 5 files changed, 84 insertions(+), 41 deletions(-) create mode 100644 lib/public/views/Runs/format/getRunQualityColor.js diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index d2302988fb..8b1808831f 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -34,7 +34,7 @@ import { formatAliceCurrent } from '../format/formatAliceCurrent.js'; import { formatTagsList } from '../../Tags/format/formatTagsList.js'; import { buttonLinkWithDropdown } from '../../../components/common/selection/infoLoggerButtonGroup/buttonLinkWithDropdown.js'; import { infologgerLinksComponents } from '../../../components/common/externalLinks/infologgerLinksComponents.js'; -import { RunQualities } from '../../../domain/enums/RunQualities.js'; +import { getRunQualityColor } from '../format/getRunQualityColor.js'; import { qcGuiLinkComponent } from '../../../components/common/externalLinks/qcGuiLinkComponent.js'; import { isRunConsideredRunning } from '../../../services/run/isRunConsideredRunning.js'; import { aliEcsEnvironmentLinkComponent } from '../../../components/common/externalLinks/aliEcsEnvironmentLinkComponent.js'; @@ -45,6 +45,29 @@ import { checkboxes } from '../../../components/Filters/common/filters/checkboxF import radioButtonFilter from '../../../components/Filters/common/filters/radioButtonFilter.js'; import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; +/** + * Returns the actions available on a run (copy of its number, links to InfoLogger, QC GUI and AliECS GUI) + * + * @param {number} runNumber the run number + * @param {Run} run the run + * @return {Component[]} the actions components + */ +export const runActionsComponents = (runNumber, run) => [ + h( + CopyToClipboardComponent, + { + value: runNumber, + id: runNumber, + }, + 'Copy Run Number', + ), + ...infologgerLinksComponents({ runNumbers: [runNumber], environmentId: run.environmentId }), + qcGuiLinkComponent(run), + isRunConsideredRunning(run) + ? aliEcsEnvironmentLinkComponent(run.environmentId) + : null, +]; + /** * List of active columns for a generic runs table */ @@ -72,21 +95,7 @@ export const runsActiveColumns = { runNumber, 'run-detail', { runNumber }, - [ - h( - CopyToClipboardComponent, - { - value: runNumber, - id: runNumber, - }, - 'Copy Run Number', - ), - ...infologgerLinksComponents({ runNumbers: [runNumber], environmentId: run.environmentId }), - qcGuiLinkComponent(run), - isRunConsideredRunning(run) - ? aliEcsEnvironmentLinkComponent(run.environmentId) - : null, - ], + runActionsComponents(runNumber, run), ), profiles: { home: true, @@ -422,16 +431,7 @@ export const runsActiveColumns = { environment: true, rcTemplate: { classes: '', - format: (runQuality) => h('.btn.white', { - class: (() => { - if (runQuality === RunQualities.GOOD) { - return 'bg-success'; - } else if (runQuality === RunQualities.BAD) { - return 'bg-danger'; - } - return 'bg-gray-darker'; - })(), - }, runQuality), + format: (runQuality) => h('.btn.white', { class: `bg-${getRunQualityColor(runQuality)}` }, runQuality), }, }, diff --git a/lib/public/views/Runs/format/formatRunDetectorsInline.js b/lib/public/views/Runs/format/formatRunDetectorsInline.js index ae0f4fe136..d6484232ce 100644 --- a/lib/public/views/Runs/format/formatRunDetectorsInline.js +++ b/lib/public/views/Runs/format/formatRunDetectorsInline.js @@ -45,5 +45,5 @@ export const formatRunDetectorsInline = (detectorsQualities, nDetectors) => { * @param {RunDetectorQuality} detectorsQuality The detector with its quality * @return {string} The class of the detector */ -const detectorStyleClass = (detectorsQuality) => 'd-inline-block bb3' +export const detectorStyleClass = (detectorsQuality) => 'd-inline-block bb3' + ` ${detectorsQuality.quality === RunDetectorQualities.GOOD ? 'b-good' : 'b-bad'}`; diff --git a/lib/public/views/Runs/format/formatRunEnd.js b/lib/public/views/Runs/format/formatRunEnd.js index 4ca87b5df2..d4ac8d4da7 100644 --- a/lib/public/views/Runs/format/formatRunEnd.js +++ b/lib/public/views/Runs/format/formatRunEnd.js @@ -14,6 +14,7 @@ import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; import { h, iconWarning } from '/js/src/index.js'; import { getLocaleDateAndTime, getLocaleDateAndTimeWithMilliseconds } from '../../../utilities/dateUtils.mjs'; +import { userPreferencesStore } from '../../../utilities/userPreferencesStore.js'; import { tooltip } from '../../../components/common/popover/tooltip.js'; import { TriggerValue } from '../../../domain/enums/TriggerValue.js'; @@ -27,10 +28,13 @@ const MISSING_TRIGGER_STOP_WARNING = 'O2 stop is displayed because trigger stop * @param {boolean} [configuration.inline] true if the date must be inlined * @param {boolean} [configuration.qc] true if QC run end must be displayed, i.e. if last TF timestamp must be used if available * @param {boolean} [configuration.milliseconds] true if the date must be displayed with milliseconds + * @param {number} [configuration.hideDateIfSameDayAs] if the end happens the same day as this timestamp, its date is hidden but still takes + * its space, so the time is aligned with the one of this timestamp when displayed below it (inline only, ignored when raw timestamps are + * displayed) * @return {Component} the formatted end date */ export const formatRunEnd = (run, configuration) => { - const { inline = false, qc = false, displayMilliseconds = false } = configuration || {}; + const { inline = false, qc = false, displayMilliseconds = false, hideDateIfSameDayAs } = configuration || {}; const { timeTrgEnd, timeO2End, lastTfTimestamp, triggerValue } = run; let runEnd = timeTrgEnd || timeO2End; @@ -38,15 +42,33 @@ export const formatRunEnd = (run, configuration) => { runEnd = lastTfTimestamp || runEnd; } - if (timeTrgEnd || triggerValue === TriggerValue.Off) { + /** + * Format the end timestamp, omitting its date if it is the same day as the given reference + * + * @return {string|Component} the formatted end + */ + const formatEnd = () => { + const isSameDay = inline + && hideDateIfSameDayAs + && runEnd + && !userPreferencesStore.rawTimestamps + && getLocaleDateAndTime(runEnd).date === getLocaleDateAndTime(hideDateIfSameDayAs).date; + if (isSameDay) { + const { date, time } = displayMilliseconds ? getLocaleDateAndTimeWithMilliseconds(runEnd) : getLocaleDateAndTime(runEnd); + return h('span', [h('span.invisible', `${date}, `), time]); + } return formatTimestamp(runEnd, inline, displayMilliseconds); + }; + + if (timeTrgEnd || triggerValue === TriggerValue.Off) { + return formatEnd(); } if (timeO2End) { if (inline) { return h('span', [ h('.flex-row.items-center.g2', [ - formatTimestamp(runEnd, inline, displayMilliseconds), + formatEnd(), tooltip(iconWarning(), MISSING_TRIGGER_STOP_WARNING), ]), ]); diff --git a/lib/public/views/Runs/format/formatRunQuality.js b/lib/public/views/Runs/format/formatRunQuality.js index 0da9721f7a..652e03a620 100644 --- a/lib/public/views/Runs/format/formatRunQuality.js +++ b/lib/public/views/Runs/format/formatRunQuality.js @@ -12,7 +12,8 @@ */ import { h } from '/js/src/index.js'; -import { RUN_QUALITIES, RunQualities } from '../../../domain/enums/RunQualities.js'; +import { RUN_QUALITIES } from '../../../domain/enums/RunQualities.js'; +import { getRunQualityColor } from './getRunQualityColor.js'; /** * Formatting for the run quality. @@ -51,16 +52,7 @@ export const formatRunQuality = (runDetailsModel, runQuality, run) => { } return ret; } else if (runQuality) { - return h('.badge.white', { - class: (() => { - if (runQuality === RunQualities.GOOD) { - return 'bg-success'; - } else if (runQuality === RunQualities.BAD) { - return 'bg-danger'; - } - return 'bg-gray-darker'; - })(), - }, runQuality); + return h('.badge.white', { class: `bg-${getRunQualityColor(runQuality)}` }, runQuality); } return '-'; }; diff --git a/lib/public/views/Runs/format/getRunQualityColor.js b/lib/public/views/Runs/format/getRunQualityColor.js new file mode 100644 index 0000000000..fb951c0b4a --- /dev/null +++ b/lib/public/views/Runs/format/getRunQualityColor.js @@ -0,0 +1,29 @@ +/** + * @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. + */ + +import { RunQualities } from '../../../domain/enums/RunQualities.js'; + +/** + * Returns the name of the color representing the given run quality, to be used as a suffix of the color css classes (e.g. `bg-success`) + * + * @param {('good'| 'bad' | 'test' | 'none')} runQuality the quality of the run + * @return {('success'|'danger'|'gray-darker')} the color name + */ +export const getRunQualityColor = (runQuality) => { + if (runQuality === RunQualities.GOOD) { + return 'success'; + } else if (runQuality === RunQualities.BAD) { + return 'danger'; + } + return 'gray-darker'; +}; From b2ae2bea35500723ca24b005cc830ddc97d2a26a Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 14:40:13 +0200 Subject: [PATCH 4/8] [O2B-1640] Improve the readability of the run overview columns Notable changes for users: - Run: the run number is a larger link, followed by a discreet caret opening the same actions as before. The definition and the quality of the run are displayed below it on one line, the quality in its color (good, bad, other). Each definition has its own neutral style (no color, as colors are reserved to states): PHYSICS dark badge, COSMICS outlined, TECHNICAL gray, SYNTHETIC dashed, CALIBRATION with a thick left bar (its calibration status is displayed below, in a compact badge) and COMMISSIONING dotted underline. An info icon next to the header of the column displays the legend of the definitions. - Timing: the labels are aligned and the time of the stop is aligned below the time of the start when the run stops the same day it started, the date being displayed when it is another day. - Context: narrower column. - Detectors and EOR reason are merged in one column: the detectors are wrapped on several lines instead of being truncated, with the EOR reasons below. Tags is its own column displaying the first tags followed by the number of remaining ones. - DCS is displayed in capital letters, as TRG. Notable changes for developers: - New `runsOverviewActiveColumns`, used by the run overview only. The columns merged into others are kept as hidden columns to provide their filters. - The estimation of the height of a row, used to compute the default number of rows, is updated. - New classes in `app.css`: `run-definition*`, `runs-calibration-status`, `runs-timing-label`, `bold`, `invisible` and `min-w-0`. --- lib/public/app.css | 81 +++++++++++ .../runsOverviewActiveColumns.js | 133 +++++++++++++++--- .../views/Runs/Overview/RunsOverviewPage.js | 2 +- .../views/Runs/format/runDefinitionLegend.js | 38 +++++ 4 files changed, 230 insertions(+), 24 deletions(-) create mode 100644 lib/public/views/Runs/format/runDefinitionLegend.js diff --git a/lib/public/app.css b/lib/public/app.css index 97516c94b8..c149cf8f88 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -864,6 +864,87 @@ label { } } +/* runs overview: labels of the timing lines have the same width so that the times are aligned, and digits have a fixed width */ +.runs-timing-label { + min-width: 2.8em; +} + +.runs-timing-label + * { + font-variant-numeric: tabular-nums; +} + +.invisible { + visibility: hidden; +} + +/* + * Run definitions of the runs overview: only shapes, weights and neutral shades are used, as colors are reserved to states + */ +.run-definition { + display: inline-block; + padding: 0.2em 0.45em; + border-radius: 0.25rem; + font-size: 0.6875rem; + font-weight: 500; + line-height: 1; + letter-spacing: 0.02em; + white-space: nowrap; +} + +.run-definition-physics { + background: color-mix(in srgb, var(--color-black) 75%, var(--color-white)); + color: var(--color-white); +} + +.run-definition-cosmics { + border: 1.5px solid var(--color-black); +} + +.run-definition-technical { + background: var(--color-gray); +} + +.run-definition-synthetic { + border: 1px dashed var(--color-gray-darker); + font-weight: 400; +} + +.run-definition-calibration { + border: 1px solid var(--color-gray-dark); + border-left: 4px solid var(--color-black); +} + +.run-definition-commissioning { + padding: 0; + text-decoration: underline dotted; + text-underline-offset: 3px; +} + +/* Legend of the run definitions: the labels have the same width and the descriptions are aligned on their left */ +.run-definition-legend-label { + width: 8rem; + flex-shrink: 0; +} + +.run-definition-legend-description { + text-align: left; +} + +/* Bold text */ +.bold { + font-weight: 700; +} + +/* Compact calibration status badge of the runs overview */ +.runs-calibration-status { + font-size: 0.625rem; +} + +/* Lets a flex item shrink below its content width, so its content can be truncated with an ellipsis */ +.min-w-0 { + min-width: 0; +} + /* logs table: the created, passes and tags columns have a content of a fixed size, so they leave more space to the title on wide screens */ .w-logs-title-column { width: 35%; diff --git a/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js index 00ddfc99fb..9ee2d1f71e 100644 --- a/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js @@ -11,8 +11,9 @@ * or submit itself to any jurisdiction. */ -import { h } from '/js/src/index.js'; -import { runsActiveColumns } from './runsActiveColumns.js'; +import { DropdownComponent, h } from '/js/src/index.js'; +import { iconCaretBottom } from '/js/src/icons.js'; +import { runActionsComponents, runsActiveColumns } from './runsActiveColumns.js'; import { profiles } from '../../../components/common/table/profiles.js'; import { overflowBalloon } from '../../../components/common/popover/overflowBalloon.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; @@ -20,20 +21,46 @@ import { formatRunStart } from '../format/formatRunStart.js'; import { formatRunEnd } from '../format/formatRunEnd.js'; import { displayRunDuration } from '../format/displayRunDuration.js'; import { coloredCalibrationStatusComponent } from '../coloredCalibrationStatusComponent.js'; +import { getRunQualityColor } from '../format/getRunQualityColor.js'; +import { runDefinitionLegend } from '../format/runDefinitionLegend.js'; import { RunDefinition } from '../../../domain/enums/RunDefinition.js'; +import { detectorStyleClass } from '../format/formatRunDetectorsInline.js'; +import { formatTagsList } from '../../Tags/format/formatTagsList.js'; +import { displayRunEorReasonsOverview } from '../format/displayRunEorReasonOverview.js'; import { BeamModes } from '../../../domain/enums/BeamModes.js'; +const TAGS_DISPLAY_LIMIT = 2; + +/** + * Display the number of detectors followed by the detectors underlined by their quality, wrapped on several lines if needed so that no + * detector is hidden + * + * @param {RunDetectorQuality[]} detectorsQualities the quality of each detector of the run + * @param {number} nDetectors the number of detectors of the run + * @return {Component|string} the detectors + */ +const formatDetectors = (detectorsQualities, nDetectors) => detectorsQualities?.length > 0 + ? h('', [ + h('.badge.bg-gray-light.mr1.nDetectors-badge', nDetectors), + detectorsQualities.map((detector, index) => [ + h(`span.${detectorStyleClass(detector)}`, detector.name), + h('span.mr1', index < detectorsQualities.length - 1 ? ',' : ''), + ]), + ]) + : '-'; + /** * Display a value preceded by a small label, on its own line * * @param {string} id the id of the line * @param {string} label the label of the line * @param {Component|string} content the content of the line + * @param {string} [labelClass] extra css classes of the label * @return {Component} the line */ -const labelledLine = (id, label, content) => h('.flex-row.items-center.g1', [ - h('span.f7.gray-darker.flex-shrink-0', label), - h(`#${id}`, overflowBalloon(content)), +const labelledLine = (id, label, content, labelClass = '') => h('.flex-row.items-baseline.g1', [ + h('span.f7.gray-darker.flex-shrink-0', { class: labelClass }, label), + h(`#${id}.min-w-0`, overflowBalloon(content)), ]); /** @@ -59,17 +86,37 @@ const formatEpnsCount = (epn, nEpns) => { }; /** - * Display the definition (with the calibration status) and the quality of the run, below the run number + * Display the run number as a link to the run, followed by a discreet caret opening the actions available on the run + * + * @param {number} runNumber the run number + * @param {Run} run the run + * @return {Component} the run number and its actions + */ +const runNumberWithActions = (runNumber, run) => h(`#row${run.id}-run-number.flex-row.items-center.g1.f5`, [ + frontLink(runNumber, 'run-detail', { runNumber }, { class: 'bold' }), + DropdownComponent( + h('.clickable.gray-darker.ph1', iconCaretBottom()), + h('.flex-column.p2.g2', runActionsComponents(runNumber, run)), + ), +]); + +/** + * Display the definition and the quality of the run on one line, separated by a dash, with the calibration status of calibration runs on + * its own line below * * @param {Run} run the run * @return {Component} the lines */ -const runDefinitionAndQuality = ({ id, definition, calibrationStatus, runQuality }) => h('.flex-column', [ - h(`#row${id}-definition.flex-row.g2.items-center.f7`, [ - h('', definition || '-'), - definition === RunDefinition.Calibration && coloredCalibrationStatusComponent(calibrationStatus), +const runDefinitionAndQuality = ({ id, definition, calibrationStatus, runQuality }) => h('.flex-column.items-start.g1', [ + h(`#row${id}-definition-and-quality.flex-row.flex-wrap.items-baseline.g1.f7`, [ + definition + ? h(`#row${id}-definition.run-definition.run-definition-${definition.toLowerCase()}`, definition) + : h(`#row${id}-definition`, '-'), + runQuality && h('span.gray-darker', '-'), + runQuality && h(`#row${id}-quality`, { class: getRunQualityColor(runQuality) }, runQuality), ]), - h(`#row${id}-quality.f7.gray-darker`, runQuality), + definition === RunDefinition.Calibration + && h(`#row${id}-calibration-status.runs-calibration-status`, coloredCalibrationStatusComponent(calibrationStatus)), ]); /** @@ -82,7 +129,9 @@ export const runsOverviewActiveColumns = { runNumber: { ...runsActiveColumns.runNumber, name: 'Run', - classes: 'w-10 f6', + information: runDefinitionLegend(), + classes: 'w-15 f6', + noEllipsis: true, /** * Display the run number, with the definition and the quality of the run below it @@ -92,7 +141,7 @@ export const runsOverviewActiveColumns = { * @return {Component} the run number and its definition and quality */ format: (runNumber, run) => h('.flex-column.items-start', [ - h(`#row${run.id}-run-number`, runsActiveColumns.runNumber.format(runNumber, run)), + runNumberWithActions(runNumber, run), runDefinitionAndQuality(run), ]), }, @@ -109,16 +158,19 @@ export const runsOverviewActiveColumns = { * @param {Run} run the run * @return {Component} the start, stop and duration */ - format: (_, run) => h('', [ - labelledLine(`row${run.id}-start`, 'Start', formatRunStart(run, { inline: true })), - labelledLine(`row${run.id}-stop`, 'Stop', formatRunEnd(run, { inline: true })), - labelledLine(`row${run.id}-runDuration`, 'Dur.', displayRunDuration(run)), - ]), + format: (_, run) => { + const hideDateIfSameDayAs = run.timeTrgStart || run.timeO2Start; + return h('', [ + labelledLine(`row${run.id}-start`, 'Start', formatRunStart(run, { inline: true }), 'runs-timing-label'), + labelledLine(`row${run.id}-stop`, 'Stop', formatRunEnd(run, { inline: true, hideDateIfSameDayAs }), 'runs-timing-label'), + labelledLine(`row${run.id}-runDuration`, 'Dur.', displayRunDuration(run), 'runs-timing-label'), + ]); + }, }, context: { name: 'Context', visible: true, - classes: 'w-15 f6', + classes: 'w-10 f6', /** * Display the LHC fill, the LHC period and the environment of the run, each in its own labelled line @@ -162,16 +214,51 @@ export const runsOverviewActiveColumns = { labelledLine(`row${id}-nFlps`, 'FLPs', nFlps ?? '-'), ]), h(`#row${id}-states.flex-row.g2`, [ - labelledLine(`row${id}-dcs`, 'DCS', dcs ? 'On' : 'Off'), + labelledLine(`row${id}-dcs`, 'DCS', dcs ? 'ON' : 'OFF'), labelledLine(`row${id}-triggerValue`, 'TRG', triggerValue || '-'), ]), ]), }, - detectors: { ...runsActiveColumns.detectors, classes: 'w-20 f6' }, - tags: { ...runsActiveColumns.tags, classes: 'w-10 f6' }, - eorReasons: { ...runsActiveColumns.eorReasons, classes: 'w-15 f6' }, + detectors: { + ...runsActiveColumns.detectors, + name: 'Detectors / EOR reason', + classes: 'w-35 f6', + balloon: false, + noEllipsis: true, + + /** + * Display the detectors of the run, with its end of run reasons below them + * + * @param {*} _ unused, the detectors are read from the run + * @param {Run} run the run + * @return {Component} the detectors and end of run reasons + */ + format: (_, { id, detectorsQualities, nDetectors, eorReasons }) => h('.flex-column.g1', [ + h(`#row${id}-detectors`, formatDetectors(detectorsQualities, nDetectors)), + eorReasons?.length > 0 && h('.flex-row.items-baseline.g1', [ + h('span.f7.gray-darker.flex-shrink-0', 'EOR'), + h(`#row${id}-eorReasons`, displayRunEorReasonsOverview(eorReasons)), + ]), + ]), + }, + tags: { + ...runsActiveColumns.tags, + classes: 'w-10 f6', + balloon: false, + noEllipsis: true, + + /** + * Display the first tags of the run, followed by the number of remaining ones, all the tags being displayed in a popover on hover + * + * @param {Tag[]} tags the tags of the run + * @param {Run} run the run + * @return {Component} the tags + */ + format: (tags, { id }) => h(`#row${id}-tags`, formatTagsList(tags, { flex: true, limit: TAGS_DISPLAY_LIMIT })), + }, // Columns displayed in a merged one, only providing their filters + eorReasons: hiddenColumn('eorReasons'), definition: hiddenColumn('definition'), runQuality: hiddenColumn('runQuality'), timeO2Start: hiddenColumn('timeO2Start'), diff --git a/lib/public/views/Runs/Overview/RunsOverviewPage.js b/lib/public/views/Runs/Overview/RunsOverviewPage.js index d47d658d38..b326593a16 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewPage.js +++ b/lib/public/views/Runs/Overview/RunsOverviewPage.js @@ -22,7 +22,7 @@ import { exportTriggerAndModal } from '../../../components/common/dataExport/exp import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; import { warningComponent } from '../../../components/common/messages/warningComponent.js'; -const TABLEROW_HEIGHT = 59; +const TABLEROW_HEIGHT = 81; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; const PAGE_USED_HEIGHT = 215; diff --git a/lib/public/views/Runs/format/runDefinitionLegend.js b/lib/public/views/Runs/format/runDefinitionLegend.js new file mode 100644 index 0000000000..1c09613ec7 --- /dev/null +++ b/lib/public/views/Runs/format/runDefinitionLegend.js @@ -0,0 +1,38 @@ +/** + * @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. + */ + +import { h } from '/js/src/index.js'; +import { RunDefinition } from '../../../domain/enums/RunDefinition.js'; + +const DEFINITIONS_DESCRIPTIONS = [ + [RunDefinition.Physics, 'Run taken during stable beams, with the detectors and the components required for physics data taking'], + [RunDefinition.Cosmics, 'Run taken without beam, with the components required for data taking'], + [RunDefinition.Technical, 'Run reading the front-end electronics'], + [RunDefinition.Synthetic, 'Run at the level of readout, emulating the readout'], + [RunDefinition.Calibration, 'Calibration, pedestal, laser, pulser or noise run, displayed with its calibration status below'], + [RunDefinition.Commissioning, 'Any run that does not fit the other definitions'], +]; + +/** + * Render the legend of the run definitions, as displayed in the run column of the runs overview + * + * @return {Component} the legend + */ +export const runDefinitionLegend = () => h('.flex-column.g3', [ + h('h3', 'Run definition legend'), + h('.flex-column.g2', DEFINITIONS_DESCRIPTIONS.map(([definition, description]) => h('.flex-row.items-baseline.g3', [ + h('.run-definition-legend-label.flex-row', h(`.run-definition.run-definition-${definition.toLowerCase()}`, definition)), + h('.run-definition-legend-description', description), + ]))), + h('span.f7.gray-darker', 'The definition is displayed before the quality of the run'), +]); From dc261e1bc26646dad38ae84f9efc7c8861b202a9 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 14:40:18 +0200 Subject: [PATCH 5/8] [O2B-1640] Update and add tests of the run overview for its new columns - Tests of the tags, EOR reasons, definition, quality, duration, start and stop, and run numbers use the elements of the new merged columns. - New tests for the legend of the run definitions, the hidden date of the stop of a run ending the same day it started, and the calibration status displayed below the definition of calibration runs. They were not run locally. --- test/public/runs/overview.test.js | 50 +++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index f9bdd63d31..394e54d940 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -21,7 +21,6 @@ const { pressElement, getFirstRow, goToPage, - checkColumnBalloon, fillInput, getPopoverContent, getInnerText, @@ -43,6 +42,7 @@ const { const { RUN_QUALITIES, RunQualities } = require('../../../lib/domain/enums/RunQualities.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); const { RunDefinition } = require('../../../lib/domain/enums/RunDefinition.js'); +const { RunCalibrationStatus } = require('../../../lib/domain/enums/RunCalibrationStatus.js'); const { runService } = require('../../../lib/server/services/run/RunService.js'); const { navigateToRunsOverview } = require('./navigationUtils.js'); const RunRepository = require('../../../lib/database/repositories/RunRepository.js'); @@ -283,22 +283,41 @@ describe('General page behaviour', () => { expect(urlParameters).to.contain(`runNumber=${expectedRunNumber}`); }); - it('Should have balloon on detector, tags and eor column', async () => { - // Run 106 has detectors and tags that overflow + it('Should display the eor reasons of the run', async () => { + await fillInput(page, filterPanelRunNumbersInputSelector, '1,1', ['change']); + await expectRunNumbers(page, ['1']); + await page.waitForSelector('#row1-eorReasons'); + }); + + it('Should display the first tags with a popover listing all of them when there are more', async () => { + // Run 106 has more tags than the displayed ones await fillInput(page, filterPanelRunNumbersInputSelector, '106', ['change']); await expectRunNumbers(page, ['106']); + await page.waitForSelector('#row106-tags .popover-trigger'); + }); - await checkColumnBalloon(page, 1, 5); - await checkColumnBalloon(page, 1, 6); + it('should display the legend of the run definitions in the header of the run column', async () => { + const legendTrigger = await page.waitForSelector('thead th:first-child .popover-trigger'); + const legend = await getPopoverContent(legendTrigger); - // Run 1 has eor reasons that overflow - await fillInput(page, filterPanelRunNumbersInputSelector, '1,1', ['change']); - await expectRunNumbers(page, ['1']); - await checkColumnBalloon(page, 1, 7); + for (const definition of Object.values(RunDefinition)) { + expect(legend).to.include(definition); + } }); - it('Should display balloon if the text overflows', async () => { - await checkColumnBalloon(page, 1, 5); + it('should only display the time of the stop of a run ending the same day as it started', async () => { + // Run 40 starts and stops the same day, run 106 stops the day after its start + await fillInput(page, filterPanelRunNumbersInputSelector, '40', ['change']); + await expectRunNumbers(page, ['40']); + await expectInnerText(page, '#row40-start', '08/08/2019, 13:00:00'); + await expectInnerText(page, '#row40-stop', '13:00:00'); + + await fillInput(page, filterPanelRunNumbersInputSelector, '106', ['change']); + await expectRunNumbers(page, ['106']); + await expectInnerText(page, '#row106-stop', '09/08/2019, 14:00:00'); + + // Reset the filters for the next tests + await navigateToRunsOverview(page); }); it('should successfully display duration without warning popover when run has trigger OFF', async () => { @@ -435,7 +454,7 @@ describe('General page behaviour', () => { await waitForTableLength(page, size); const definitions = await page.$$eval( - 'tbody [id$="-definition"] > div:first-child', + 'tbody [id$="-definition"]', (rows) => rows.map((row) => row.innerText), ); @@ -478,6 +497,11 @@ describe('General page behaviour', () => { await pressElement(page, calibrationFilterSelector, true); await checkTableSizeAndDefinition(2, [RunDefinition.TECHNICAL, RunDefinition.CALIBRATION]); + // The calibration status is displayed below the definition of calibration runs only + await page.waitForSelector('#row40-definition.run-definition-calibration'); + await expectInnerText(page, '#row40-calibration-status', RunCalibrationStatus.NO_STATUS); + expect(await page.$$('tbody [id$="-calibration-status"]')).to.have.lengthOf(1); + await pressElement(page, commissioningFilterSelector, true); await checkTableSizeAndDefinition(20, [RunDefinition.COMMISSIONING]); @@ -573,7 +597,7 @@ describe('General page behaviour', () => { for (const row of rows) { expect(await row.evaluate((rowItem) => { const rowId = rowItem.id; - return document.querySelector(`#${rowId}-runQuality`).innerText; + return document.querySelector(`#${rowId}-quality`).innerText; })).to.be.oneOf(authorizedRunQualities); } }; From db8d62bffe56c5c9f841c3a4d89f9190ebee841a Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 14:45:52 +0200 Subject: [PATCH 6/8] [O2B-1640] Do not wrap the duration of the run overview in a balloon The duration already has its own popover (warning on the missing trigger times), a second balloon around it hid it. --- .../ActiveColumns/runsOverviewActiveColumns.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js index 9ee2d1f71e..0044e94ffc 100644 --- a/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js @@ -49,18 +49,22 @@ const formatDetectors = (detectorsQualities, nDetectors) => detectorsQualities?. ]) : '-'; +const TIMING_LINE = { labelClass: 'runs-timing-label' }; + /** * Display a value preceded by a small label, on its own line * * @param {string} id the id of the line * @param {string} label the label of the line * @param {Component|string} content the content of the line - * @param {string} [labelClass] extra css classes of the label + * @param {object} [options] the display options + * @param {string} [options.labelClass] extra css classes of the label + * @param {boolean} [options.balloon] if false, the content is not wrapped in a balloon, as it already has its own popover (default true) * @return {Component} the line */ -const labelledLine = (id, label, content, labelClass = '') => h('.flex-row.items-baseline.g1', [ +const labelledLine = (id, label, content, { labelClass = '', balloon = true } = {}) => h('.flex-row.items-baseline.g1', [ h('span.f7.gray-darker.flex-shrink-0', { class: labelClass }, label), - h(`#${id}.min-w-0`, overflowBalloon(content)), + h(`#${id}.min-w-0`, balloon ? overflowBalloon(content) : content), ]); /** @@ -161,9 +165,9 @@ export const runsOverviewActiveColumns = { format: (_, run) => { const hideDateIfSameDayAs = run.timeTrgStart || run.timeO2Start; return h('', [ - labelledLine(`row${run.id}-start`, 'Start', formatRunStart(run, { inline: true }), 'runs-timing-label'), - labelledLine(`row${run.id}-stop`, 'Stop', formatRunEnd(run, { inline: true, hideDateIfSameDayAs }), 'runs-timing-label'), - labelledLine(`row${run.id}-runDuration`, 'Dur.', displayRunDuration(run), 'runs-timing-label'), + labelledLine(`row${run.id}-start`, 'Start', formatRunStart(run, { inline: true }), TIMING_LINE), + labelledLine(`row${run.id}-stop`, 'Stop', formatRunEnd(run, { inline: true, hideDateIfSameDayAs }), TIMING_LINE), + labelledLine(`row${run.id}-runDuration`, 'Dur.', displayRunDuration(run), { ...TIMING_LINE, balloon: false }), ]); }, }, From 693f36b25c8b88cc7eb9f222a856d266cc9e736f Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 14:45:52 +0200 Subject: [PATCH 7/8] [O2B-1640] Fix the run tests for the new run overview columns - The run overview displays 6 rows by default, as rows are taller. - The EOR reasons and the run number are read from the elements of the merged columns, including from the run details tests. --- test/public/runs/detail.test.js | 4 ++-- test/public/runs/overview.test.js | 17 +++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/test/public/runs/detail.test.js b/test/public/runs/detail.test.js index 83f35e862a..a39302db34 100644 --- a/test/public/runs/detail.test.js +++ b/test/public/runs/detail.test.js @@ -518,7 +518,7 @@ module.exports = () => { it('should successfully expose a button to create a new log related to the displayed environment', async () => { await waitForNavigation(page, () => pressElement(page, '#run-overview')); - await waitForNavigation(page, () => pressElement(page, '#row106-runNumber-text > div > a')); + await waitForNavigation(page, () => pressElement(page, '#row106-run-number a')); await waitForNavigation(page, () => pressElement(page, '#create-log')); expectUrlParams(page, { page: 'log-create', runNumbers: '106', lhcFillNumbers: '1' }); @@ -529,7 +529,7 @@ module.exports = () => { it('should not display the LHC Data when beam is not stable', async () => { await waitForNavigation(page, () => pressElement(page, '#run-overview')); - await waitForNavigation(page, () => pressElement(page, '#row107-runNumber-text > div > a')); + await waitForNavigation(page, () => pressElement(page, '#row107-run-number a')); await expectInnerText(page, '#non-stable-beam-message', 'No LHC Fill information, beam mode was: UNSTABLE BEAMS'); }); diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 394e54d940..92dd8b117e 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -159,7 +159,7 @@ describe('General page behaviour', () => { await navigateToRunsOverview(page); await expectInnerText(page, '#firstRowIndex', '1'); - await expectInnerText(page, '#lastRowIndex', '8'); + await expectInnerText(page, '#lastRowIndex', '6'); await expectInnerText(page, '#totalRowsCount', '109'); }); @@ -409,8 +409,9 @@ describe('General page behaviour', () => { await pressElement(page, '#detector-filter-dropdown-option-FT0', true); await waitForTableLength(page, 4); + // 8 runs match, but only the first page, of 6 rows, is displayed await pressElement(page, '#detector-filter-combination-operator-radio-button-or', true); - await waitForTableLength(page, 8); + await waitForTableLength(page, 6); await pressElement(page, '#detector-filter-combination-operator-radio-button-none', true); await waitForTableLength(page, 2); @@ -830,7 +831,7 @@ describe('General page behaviour', () => { * The correct number of runs should be displayed in the table. * Furthermore, each of the displayed EOR reasons should contain 'DETECTORS' */ - let eorReasons = await page.$$('table td[id$="eorReasons"]'); + let eorReasons = await page.$$('table [id$="-eorReasons"]'); expect(eorReasons).has.lengthOf(3); let eorReasonTexts = await Promise.all(eorReasons.map(async (element) => (await element.getProperty('innerText')).jsonValue())); @@ -846,7 +847,7 @@ describe('General page behaviour', () => { * The correct number of runs should be displayed in the table. * Furthermore, each of the displayed EOR reasons should contain 'DETECTORS - CPV' */ - eorReasons = await page.$$('table td[id$="eorReasons"]'); + eorReasons = await page.$$('table [id$="-eorReasons"]'); expect(eorReasons).has.lengthOf(2); eorReasonTexts = await Promise.all(eorReasons.map(async (element) => (await element.getProperty('innerText')).jsonValue())); @@ -861,7 +862,7 @@ describe('General page behaviour', () => { expect(detectorTitleElements).has.lengthOf(1); // There should be many items in the run details table - eorReasons = await page.$$('table td[id$="eorReasons"]'); + eorReasons = await page.$$('table [id$="-eorReasons"]'); expect(eorReasons.length).to.be.greaterThan(3); }); @@ -871,7 +872,7 @@ describe('General page behaviour', () => { await fillInput(page, '#eorDescription', descriptionInput, ['change']); await waitForTableLength(page, 2); - let eorReasons = await page.$$('table td[id$="eorReasons"]'); + let eorReasons = await page.$$('table [id$="-eorReasons"]'); expect(eorReasons).has.lengthOf(2); const eorReasonText = await (await eorReasons[0].getProperty('innerText')).jsonValue(); expect(eorReasonText.toLowerCase()).to.include(descriptionInput); @@ -879,13 +880,13 @@ describe('General page behaviour', () => { // Assuming this result had the category DETECTORS, when we select a different category it should disappear. await page.select('#eorCategories', 'OTHER'); await waitForEmptyTable(page); - eorReasons = await page.$$('table td[id$="eorReasons"]'); + eorReasons = await page.$$('table [id$="-eorReasons"]'); expect(eorReasons).has.lengthOf(0); // When we reset the filters, the input field should be empty await resetFilters(page); await waitForTableLength(page, 10); - eorReasons = await page.$$('table td[id$="eorReasons"]'); + eorReasons = await page.$$('table [id$="-eorReasons"]'); expect(eorReasons.length).to.be.greaterThan(1); await expectInputValue(page, '#eorDescription', ''); From 84c27febcbbc7d362eb5b62fb001e1ba01474966 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 14:52:35 +0200 Subject: [PATCH 8/8] [O2B-1640] Count the rows instead of the EOR reasons after resetting the filters of the run overview Only the runs having EOR reasons display them, so the first page may not have any. --- test/public/runs/overview.test.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 92dd8b117e..5ced52981c 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -862,8 +862,8 @@ describe('General page behaviour', () => { expect(detectorTitleElements).has.lengthOf(1); // There should be many items in the run details table - eorReasons = await page.$$('table [id$="-eorReasons"]'); - expect(eorReasons.length).to.be.greaterThan(3); + const rows = await page.$$('table tbody tr'); + expect(rows.length).to.be.greaterThan(3); }); it('should correctly filter by EOR reason description', async () => { @@ -886,8 +886,7 @@ describe('General page behaviour', () => { // When we reset the filters, the input field should be empty await resetFilters(page); await waitForTableLength(page, 10); - eorReasons = await page.$$('table [id$="-eorReasons"]'); - expect(eorReasons.length).to.be.greaterThan(1); + expect((await page.$$('table tbody tr')).length).to.be.greaterThan(1); await expectInputValue(page, '#eorDescription', ''); });