From 5daf775a0a4aca669211d005379717b840da5d07 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 15:19:57 +0200 Subject: [PATCH 01/11] [O2B-1641] Extract the labelled line, the link with actions and the aligned timestamp of the run overview Notable changes for developers: - New `labelledLine` component (a value preceded by a small label), `linkWithActionsDropdown` component (a bold link followed by a discreet caret opening its actions) and `formatTimestampAlignedWith` utility (the date is hidden, but keeps its space, when it is the same day as a reference timestamp), extracted from the run overview columns to be reused by other overview pages. - `formatRunEnd` uses `formatTimestampAlignedWith`. - `runs-timing-label` css class is renamed `timing-label`. --- lib/public/app.css | 6 +-- .../common/formatting/labelledLine.js | 31 ++++++++++++++ .../linkWithActionsDropdown.js | 34 +++++++++++++++ .../formatting/formatTimestampAlignedWith.js | 37 +++++++++++++++++ .../runsOverviewActiveColumns.js | 41 +++---------------- lib/public/views/Runs/format/formatRunEnd.js | 19 +++------ 6 files changed, 115 insertions(+), 53 deletions(-) create mode 100644 lib/public/components/common/formatting/labelledLine.js create mode 100644 lib/public/components/common/selection/infoLoggerButtonGroup/linkWithActionsDropdown.js create mode 100644 lib/public/utilities/formatting/formatTimestampAlignedWith.js diff --git a/lib/public/app.css b/lib/public/app.css index c149cf8f88..e25786c4ca 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -864,12 +864,12 @@ 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 { +/* runs and LHC fills overviews: labels of the timing lines have the same width so that the times are aligned, and digits have a fixed width */ +.timing-label { min-width: 2.8em; } -.runs-timing-label + * { +.timing-label + * { font-variant-numeric: tabular-nums; } diff --git a/lib/public/components/common/formatting/labelledLine.js b/lib/public/components/common/formatting/labelledLine.js new file mode 100644 index 0000000000..e0d81985ca --- /dev/null +++ b/lib/public/components/common/formatting/labelledLine.js @@ -0,0 +1,31 @@ +/** + * @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 { overflowBalloon } from '../popover/overflowBalloon.js'; + +/** + * Display a value preceded by a small label, on its own line + * + * @param {string} id the id of the element containing the value + * @param {string} label the label of the line + * @param {Component|string} content the content of the line + * @param {object} [options] the display options + * @param {string} [options.labelClass] extra css classes of the label, to align the values of several lines for example + * @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 + */ +export 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`, balloon ? overflowBalloon(content) : content), +]); diff --git a/lib/public/components/common/selection/infoLoggerButtonGroup/linkWithActionsDropdown.js b/lib/public/components/common/selection/infoLoggerButtonGroup/linkWithActionsDropdown.js new file mode 100644 index 0000000000..571865a948 --- /dev/null +++ b/lib/public/components/common/selection/infoLoggerButtonGroup/linkWithActionsDropdown.js @@ -0,0 +1,34 @@ +/** + * @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 { DropdownComponent, h } from '/js/src/index.js'; +import { iconCaretBottom } from '/js/src/icons.js'; +import { frontLink } from '../../navigation/frontLink.js'; + +/** + * Display a bold link, followed by a discreet caret opening a dropdown with the actions available on the target of the link + * + * @param {string} id the id of the component + * @param {string|number} linkContent the content of the link + * @param {string} page the target page of the link + * @param {Object} parameters the url parameters of the link + * @param {Component[]} actions the actions displayed in the dropdown + * @return {Component} the link with its actions + */ +export const linkWithActionsDropdown = (id, linkContent, page, parameters, actions) => h(`#${id}.flex-row.items-center.g1.f5`, [ + frontLink(linkContent, page, parameters, { class: 'bold' }), + DropdownComponent( + h('.clickable.gray-darker.ph1', iconCaretBottom()), + h('.flex-column.p2.g2', actions), + ), +]); diff --git a/lib/public/utilities/formatting/formatTimestampAlignedWith.js b/lib/public/utilities/formatting/formatTimestampAlignedWith.js new file mode 100644 index 0000000000..3fcea38494 --- /dev/null +++ b/lib/public/utilities/formatting/formatTimestampAlignedWith.js @@ -0,0 +1,37 @@ +/** + * @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 { formatTimestamp } from './formatTimestamp.js'; +import { userPreferencesStore } from '../userPreferencesStore.js'; +import { getLocaleDateAndTime, getLocaleDateAndTimeWithMilliseconds } from '../dateUtils.mjs'; + +/** + * Format the given timestamp inline, hiding its date, but keeping its space, if it is the same day as a reference timestamp, so that when + * displayed below the formatted reference, the times are aligned and a different day is visible. Raw timestamps are displayed as is + * + * @param {number|null} timestamp the timestamp to format (in milliseconds) + * @param {number|null} [referenceTimestamp] the timestamp to compare the day to + * @param {boolean} [displayMilliseconds] if true, the time will include milliseconds + * @return {Component|string} the formatted timestamp + */ +export const formatTimestampAlignedWith = (timestamp, referenceTimestamp, displayMilliseconds = false) => { + if (timestamp && referenceTimestamp && !userPreferencesStore.rawTimestamps) { + const getDateAndTime = displayMilliseconds ? getLocaleDateAndTimeWithMilliseconds : getLocaleDateAndTime; + const { date, time } = getDateAndTime(timestamp); + if (date === getLocaleDateAndTime(referenceTimestamp).date) { + return h('span', [h('span.invisible', `${date}, `), time]); + } + } + return formatTimestamp(timestamp, true, displayMilliseconds); +}; diff --git a/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js index 0044e94ffc..596588ea03 100644 --- a/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js @@ -11,17 +11,17 @@ * or submit itself to any jurisdiction. */ -import { DropdownComponent, h } from '/js/src/index.js'; -import { iconCaretBottom } from '/js/src/icons.js'; +import { h } from '/js/src/index.js'; import { runActionsComponents, runsActiveColumns } from './runsActiveColumns.js'; import { profiles } from '../../../components/common/table/profiles.js'; -import { overflowBalloon } from '../../../components/common/popover/overflowBalloon.js'; +import { labelledLine } from '../../../components/common/formatting/labelledLine.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 { getRunQualityColor } from '../format/getRunQualityColor.js'; +import { linkWithActionsDropdown } from '../../../components/common/selection/infoLoggerButtonGroup/linkWithActionsDropdown.js'; import { runDefinitionLegend } from '../format/runDefinitionLegend.js'; import { RunDefinition } from '../../../domain/enums/RunDefinition.js'; import { detectorStyleClass } from '../format/formatRunDetectorsInline.js'; @@ -49,23 +49,7 @@ 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 {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 = '', balloon = true } = {}) => h('.flex-row.items-baseline.g1', [ - h('span.f7.gray-darker.flex-shrink-0', { class: labelClass }, label), - h(`#${id}.min-w-0`, balloon ? overflowBalloon(content) : content), -]); +const TIMING_LINE = { labelClass: 'timing-label' }; /** * Return a copy of the given column, hidden from the table but still providing its filter (and export) to the filters panel @@ -89,21 +73,6 @@ const formatEpnsCount = (epn, nEpns) => { return typeof nEpns === 'number' ? nEpns : 'ON'; }; -/** - * 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 @@ -145,7 +114,7 @@ export const runsOverviewActiveColumns = { * @return {Component} the run number and its definition and quality */ format: (runNumber, run) => h('.flex-column.items-start', [ - runNumberWithActions(runNumber, run), + linkWithActionsDropdown(`row${run.id}-run-number`, runNumber, 'run-detail', { runNumber }, runActionsComponents(runNumber, run)), runDefinitionAndQuality(run), ]), }, diff --git a/lib/public/views/Runs/format/formatRunEnd.js b/lib/public/views/Runs/format/formatRunEnd.js index d4ac8d4da7..babda6f50a 100644 --- a/lib/public/views/Runs/format/formatRunEnd.js +++ b/lib/public/views/Runs/format/formatRunEnd.js @@ -14,7 +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 { formatTimestampAlignedWith } from '../../../utilities/formatting/formatTimestampAlignedWith.js'; import { tooltip } from '../../../components/common/popover/tooltip.js'; import { TriggerValue } from '../../../domain/enums/TriggerValue.js'; @@ -43,22 +43,13 @@ export const formatRunEnd = (run, configuration) => { } /** - * Format the end timestamp, omitting its date if it is the same day as the given reference + * Format the end timestamp, hiding 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); - }; + const formatEnd = () => inline && hideDateIfSameDayAs + ? formatTimestampAlignedWith(runEnd, hideDateIfSameDayAs, displayMilliseconds) + : formatTimestamp(runEnd, inline, displayMilliseconds); if (timeTrgEnd || triggerValue === TriggerValue.Off) { return formatEnd(); From dffa110cf5a2de78fab5955d46a44f6e7df1c61f Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 15:20:07 +0200 Subject: [PATCH 02/11] [O2B-1641] Improve the readability of the LHC fills overview columns Notable changes for users: - The LHC fills overview has fewer, denser columns: - **Fill #**: the fill number is a larger link followed by a discreet caret opening the same actions as before, with its beam type below it. An info icon next to the header displays the legend of the beam types. - **Stable beams**: the start and the end, on labelled and aligned lines. When the stable beams end the same day they started, only the time of the end is displayed, aligned with the time of the start. - **SB Duration**: unchanged. - **Efficiency**: the efficiency with the time lost before the first run below it. - **Runs duration**: the mean duration of the runs and their total duration. - **Scheme name**: the name of the filling scheme with the number of colliding bunches below it. - **Runs**: unchanged. - The beam types are displayed in a badge whose shape depends on what collides, without colors as they are reserved to states: lead-lead (the main collisions of ALICE) in a dark badge, proton-proton with a thick outline, proton-ion with a thin outline and other ions with a thin outline and a thick left bar. - The default number of rows of the page is estimated for the new height of the rows, to fill the page. - All the filters are still available, in the same filters panel and in the same order, with unchanged URL parameters. Notable changes for developers: - New `lhcFillsOverviewActiveColumns`, used by the LHC fills overview only. The columns merged into others are kept as hidden columns to provide their filters. - New `getBeamTypeFamily`, `formatBeamTypeBadge` and `beamTypeLegend`. - Elements have stable ids to be used in UI tests: `#row-fill-number`, `-beamType`, `-stableBeamsStart`, `-stableBeamsEnd`, `-stableBeamsDuration`, `-efficiencyValue`, `-timeLossAtStart`, `-meanRunDuration`, `-runsCoverage`, `-schemeName`, `-collidingBunches`. --- lib/public/app.css | 53 +++++- .../lhcFillsOverviewActiveColumns.js | 179 ++++++++++++++++++ lib/public/views/LhcFills/Overview/index.js | 10 +- .../views/LhcFills/format/beamTypeFamily.js | 48 +++++ .../views/LhcFills/format/beamTypeLegend.js | 37 ++++ .../LhcFills/format/formatBeamTypeBadge.js | 25 +++ 6 files changed, 345 insertions(+), 7 deletions(-) create mode 100644 lib/public/views/LhcFills/ActiveColumns/lhcFillsOverviewActiveColumns.js create mode 100644 lib/public/views/LhcFills/format/beamTypeFamily.js create mode 100644 lib/public/views/LhcFills/format/beamTypeLegend.js create mode 100644 lib/public/views/LhcFills/format/formatBeamTypeBadge.js diff --git a/lib/public/app.css b/lib/public/app.css index e25786c4ca..a8bd988949 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -6,7 +6,7 @@ html, body { .cell-xs { max-width: 2rem; } .cell-s { max-width: 4rem; } -.cell-m { max-width: 8rem; } +.cell-m { max-width: 9rem; } .cell-l { max-width: 16rem; } .cell-xl { max-width: 32rem; } .cell-f { width: 40%; } @@ -873,6 +873,10 @@ label { font-variant-numeric: tabular-nums; } +.efficiency-label { + min-width: 6.5em; +} + .invisible { visibility: hidden; } @@ -922,7 +926,7 @@ label { /* 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; + width: 9rem; flex-shrink: 0; } @@ -930,6 +934,51 @@ label { text-align: left; } +/* + * Beam types of the LHC fills overview, by what collides: only shapes and neutral shades are used, as colors are reserved to states + */ +.beam-type { + 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; +} + +.beam-type-lead-lead { + background: color-mix(in srgb, var(--color-black) 75%, var(--color-white)); + color: var(--color-white); +} + +.beam-type-proton-proton { + border: 1.5px solid var(--color-black); +} + +.beam-type-proton-ion { + border: 1px solid var(--color-gray-dark); +} + +.beam-type-ion-ion { + border: 1px solid var(--color-gray-dark); + border-left: 4px solid var(--color-black); +} + +.beam-type-unknown { + padding: 0; +} + +.beam-type-legend-label { + width: 9rem; + flex-shrink: 0; +} + +.beam-type-legend-description { + text-align: left; +} + /* Bold text */ .bold { font-weight: 700; diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsOverviewActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsOverviewActiveColumns.js new file mode 100644 index 0000000000..c6fff9ea6b --- /dev/null +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsOverviewActiveColumns.js @@ -0,0 +1,179 @@ +/** + * @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 { CopyToClipboardComponent, h } from '/js/src/index.js'; +import { iconPlus } from '/js/src/icons.js'; +import { lhcFillsActiveColumns } from './lhcFillsActiveColumns.js'; +import { profiles } from '../../../components/common/table/profiles.js'; +import { labelledLine } from '../../../components/common/formatting/labelledLine.js'; +import { linkWithActionsDropdown } from '../../../components/common/selection/infoLoggerButtonGroup/linkWithActionsDropdown.js'; +import { infologgerLinksComponents } from '../../../components/common/externalLinks/infologgerLinksComponents.js'; +import { frontLink } from '../../../components/common/navigation/frontLink.js'; +import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; +import { formatTimestampAlignedWith } from '../../../utilities/formatting/formatTimestampAlignedWith.js'; +import { formatDuration } from '../../../utilities/formatting/formatDuration.mjs'; +import { formatPercentage } from '../../../utilities/formatting/formatPercentage.js'; +import { formatLhcFillsTimeLoss } from '../format/formatLhcFillsTimeLoss.js'; +import { formatBeamTypeBadge } from '../format/formatBeamTypeBadge.js'; +import { beamTypeLegend } from '../format/beamTypeLegend.js'; + +const TIMING_LINE = { labelClass: 'timing-label' }; +const DURATION_LINE = { labelClass: 'efficiency-label' }; + +/** + * Return a copy of the given column, hidden from the table but still providing its filter to the filters panel + * + * @param {string} key the key of the column in the LHC fills active columns + * @return {Column} the hidden column + */ +const hiddenColumn = (key) => ({ ...lhcFillsActiveColumns[key], visible: false, profiles: [profiles.none] }); + +/** + * List of active columns for the LHC fills overview table + * + * The columns of the generic LHC fills table are merged into denser ones, the merged ones are kept hidden to provide their filters. The hidden + * columns are interleaved with the displayed ones so that the filters keep the order of the generic table + */ +export const lhcFillsOverviewActiveColumns = { + fillNumber: { + ...lhcFillsActiveColumns.fillNumber, + name: 'Fill #', + information: beamTypeLegend(), + size: 'w-10', + noEllipsis: true, + + /** + * Display the fill number as a link to the fill with its actions, with its beam type below + * + * @param {number} fillNumber the fill number + * @param {LhcFill} lhcFill the LHC fill + * @return {Component} the fill number and its beam type + */ + format: (fillNumber, { runs = [], beamType }) => h('.flex-column.items-start.g1', [ + linkWithActionsDropdown( + `row${fillNumber}-fill-number`, + fillNumber, + 'lhc-fill-details', + { fillNumber }, + [ + h(CopyToClipboardComponent, { value: fillNumber, id: fillNumber }, 'Copy Fill Number'), + ...infologgerLinksComponents({ runNumbers: runs.map(({ runNumber }) => runNumber) }), + frontLink( + [iconPlus(), ' Add log to this fill'], + 'log-create', + { lhcFillNumbers: [fillNumber] }, + { id: 'create-log', class: 'btn btn-primary h2' }, + ), + ], + ), + h(`#row${fillNumber}-beamType`, formatBeamTypeBadge(beamType)), + ]), + }, + stableBeamsStartAndEnd: { + name: 'Stable beams', + visible: true, + size: 'w-20', + classes: 'f6', + noEllipsis: true, + + /** + * Display the start and the end of the stable beams, each in its own labelled line + * + * @param {*} _ unused, the values are read from the fill + * @param {LhcFill} lhcFill the LHC fill + * @return {Component} the start and the end + */ + format: (_, { fillNumber, stableBeamsStart, stableBeamsEnd }) => h('', [ + labelledLine(`row${fillNumber}-stableBeamsStart`, 'Start', formatTimestamp(stableBeamsStart, true), TIMING_LINE), + labelledLine( + `row${fillNumber}-stableBeamsEnd`, + 'End', + formatTimestampAlignedWith(stableBeamsEnd, stableBeamsStart), + TIMING_LINE, + ), + ]), + }, + stableBeamsStart: hiddenColumn('stableBeamsStart'), + stableBeamsEnd: hiddenColumn('stableBeamsEnd'), + stableBeams: lhcFillsActiveColumns.stableBeams, + stableBeamsDuration: { ...lhcFillsActiveColumns.stableBeamsDuration, name: 'SB Duration', size: 'w-10', classes: 'f6' }, + efficiency: { + ...lhcFillsActiveColumns.efficiency, + name: 'Efficiency', + size: 'w-15', + classes: 'f6', + noEllipsis: true, + + /** + * Display the efficiency of the fill, with the time lost before the first run below it + * + * @param {number} efficiency the efficiency of the fill + * @param {LhcFill} lhcFill the LHC fill + * @return {Component} the efficiency and the time loss + */ + format: (efficiency, { fillNumber, timeLossAtStart, efficiencyLossAtStart }) => h('', [ + labelledLine(`row${fillNumber}-efficiencyValue`, 'Efficiency', formatPercentage(efficiency), DURATION_LINE), + labelledLine( + `row${fillNumber}-timeLossAtStart`, + 'Before 1st run', + formatLhcFillsTimeLoss(timeLossAtStart, efficiencyLossAtStart), + DURATION_LINE, + ), + ]), + }, + timeLossAtStart: hiddenColumn('timeLossAtStart'), + timeLossAtEnd: lhcFillsActiveColumns.timeLossAtEnd, + runsDurations: { + name: 'Runs duration', + visible: true, + size: 'w-15', + classes: 'f6', + noEllipsis: true, + + /** + * Display the mean duration of the runs of the fill and their total duration, each in its own labelled line + * + * @param {*} _ unused, the values are read from the fill + * @param {LhcFill} lhcFill the LHC fill + * @return {Component} the mean and total durations + */ + format: (_, { fillNumber, meanRunDuration, runsCoverage }) => h('', [ + labelledLine(`row${fillNumber}-meanRunDuration`, 'Mean', formatDuration(meanRunDuration), DURATION_LINE), + labelledLine(`row${fillNumber}-runsCoverage`, 'Total', formatDuration(runsCoverage), DURATION_LINE), + ]), + }, + meanRunDuration: hiddenColumn('meanRunDuration'), + runsCoverage: hiddenColumn('runsCoverage'), + beamType: hiddenColumn('beamType'), + collidingBunches: hiddenColumn('collidingBunches'), + fillingSchemeName: { + ...lhcFillsActiveColumns.fillingSchemeName, + size: 'w-15', + classes: 'f6', + balloon: false, + noEllipsis: true, + + /** + * Display the name of the filling scheme with the number of colliding bunches below it + * + * @param {string|null} name the name of the filling scheme + * @param {LhcFill} lhcFill the LHC fill + * @return {Component} the filling scheme and the colliding bunches + */ + format: (name, { fillNumber, collidingBunchesCount }) => h('', [ + labelledLine(`row${fillNumber}-schemeName`, 'Scheme', name || '-'), + labelledLine(`row${fillNumber}-collidingBunches`, 'Bunches', collidingBunchesCount || '-'), + ]), + }, + runs: { ...lhcFillsActiveColumns.runs, size: 'w-10', classes: 'f6' }, +}; diff --git a/lib/public/views/LhcFills/Overview/index.js b/lib/public/views/LhcFills/Overview/index.js index f790bb9957..778e11f998 100644 --- a/lib/public/views/LhcFills/Overview/index.js +++ b/lib/public/views/LhcFills/Overview/index.js @@ -14,16 +14,16 @@ import { h } from '/js/src/index.js'; import { table } from '../../../components/common/table/table.js'; -import { lhcFillsActiveColumns } from '../ActiveColumns/lhcFillsActiveColumns.js'; +import { lhcFillsOverviewActiveColumns } from '../ActiveColumns/lhcFillsOverviewActiveColumns.js'; import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; import { toggleFilter } from '../../../components/Filters/common/filters/toggleFilter.js'; import { warningComponent } from '../../../components/common/messages/warningComponent.js'; -const TABLEROW_HEIGHT = 53.3; +const TABLEROW_HEIGHT = 62; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; -const PAGE_USED_HEIGHT = 230; +const PAGE_USED_HEIGHT = 205; /** * The function to load the lhcFills overview @@ -47,12 +47,12 @@ const showLhcFillsTable = (lhcFillsOverviewModel) => { return [ h('.flex-row.header-container.g2.pv2', [ - filtersPanelPopover(lhcFillsOverviewModel, lhcFillsActiveColumns), + filtersPanelPopover(lhcFillsOverviewModel, lhcFillsOverviewActiveColumns), toggleFilter(filteringModel.get('hasStableBeams'), 'STABLE BEAM ONLY'), ]), warningComponent(lhcFillsOverviewModel), h('.w-100.flex-column', [ - table(items, lhcFillsActiveColumns, null, { tableClasses: '.table-sm' }), + table(items, lhcFillsOverviewActiveColumns, null, { tableClasses: '.table-sm' }), paginationComponent(pagination), ]), ]; diff --git a/lib/public/views/LhcFills/format/beamTypeFamily.js b/lib/public/views/LhcFills/format/beamTypeFamily.js new file mode 100644 index 0000000000..ed9ac83009 --- /dev/null +++ b/lib/public/views/LhcFills/format/beamTypeFamily.js @@ -0,0 +1,48 @@ +/** + * @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. + */ + +/** + * Families of beam types, depending on what collides. Lead-lead collisions have their own family as they are the main ones of ALICE + * + * @enum {string} + */ +export const BeamTypeFamilies = Object.freeze({ + LEAD_LEAD: 'lead-lead', + PROTON_PROTON: 'proton-proton', + PROTON_ION: 'proton-ion', + ION_ION: 'ion-ion', + UNKNOWN: 'unknown', +}); + +const PROTON_NAMES = ['p', 'proton']; +const LEAD_NAMES = ['pb', 'pb82']; + +/** + * Returns the family of the given beam type, depending on what collides in it + * + * @param {string|null} beamType the beam type as sent by the LHC (e.g. `p-p`, `PROTON-PROTON`, `p-Pb`, `Pb-Pb`) + * @return {BeamTypeFamilies} the family of the beam type + */ +export const getBeamTypeFamily = (beamType) => { + const beams = beamType?.split('-').map((beam) => beam.trim().toLowerCase()) ?? []; + if (beams.length !== 2 || beams.some((beam) => beam === '')) { + return BeamTypeFamilies.UNKNOWN; + } + + if (beams.every((beam) => LEAD_NAMES.includes(beam))) { + return BeamTypeFamilies.LEAD_LEAD; + } + + const protonsCount = beams.filter((beam) => PROTON_NAMES.includes(beam)).length; + return [BeamTypeFamilies.ION_ION, BeamTypeFamilies.PROTON_ION, BeamTypeFamilies.PROTON_PROTON][protonsCount]; +}; diff --git a/lib/public/views/LhcFills/format/beamTypeLegend.js b/lib/public/views/LhcFills/format/beamTypeLegend.js new file mode 100644 index 0000000000..a554efb5d0 --- /dev/null +++ b/lib/public/views/LhcFills/format/beamTypeLegend.js @@ -0,0 +1,37 @@ +/** + * @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 { formatBeamTypeBadge } from './formatBeamTypeBadge.js'; + +const BEAM_TYPES_DESCRIPTIONS = [ + ['PB82 - PB82', 'Lead ions colliding with lead ions, the main collisions of ALICE'], + ['PROTON - PROTON', 'Protons colliding with protons'], + ['PROTON - O8', 'Protons colliding with oxygen ions'], + ['O8 - O8', 'Oxygen ions colliding with oxygen ions'], + ['NE10 - NE10', 'Neon ions colliding with neon ions'], +]; + +/** + * Render the legend of the beam types, as displayed in the fill column of the LHC fills overview + * + * @return {Component} the legend + */ +export const beamTypeLegend = () => h('.flex-column.g3', [ + h('h3', 'Beam type legend'), + h('.flex-column.g2', BEAM_TYPES_DESCRIPTIONS.map(([beamType, description]) => h('.flex-row.items-baseline.g3', [ + h('.beam-type-legend-label.flex-row', formatBeamTypeBadge(beamType)), + h('.beam-type-legend-description', description), + ]))), + h('span.f7.gray-darker', 'Lead against lead, protons against protons, protons against ions and other ions each have their own shape'), +]); diff --git a/lib/public/views/LhcFills/format/formatBeamTypeBadge.js b/lib/public/views/LhcFills/format/formatBeamTypeBadge.js new file mode 100644 index 0000000000..94148758f2 --- /dev/null +++ b/lib/public/views/LhcFills/format/formatBeamTypeBadge.js @@ -0,0 +1,25 @@ +/** + * @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 { getBeamTypeFamily } from './beamTypeFamily.js'; + +/** + * Display the beam type in a badge whose shape depends on what collides (no colors, as they are reserved to states) + * + * @param {string|null} beamType the beam type as sent by the LHC + * @return {Component|string} the beam type badge, or a dash if there is no beam type + */ +export const formatBeamTypeBadge = (beamType) => beamType + ? h(`.beam-type.beam-type-${getBeamTypeFamily(beamType)}`, beamType) + : '-'; From 2c094ed82ebcae436f546f045aea9e1add6ad788 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 15:20:07 +0200 Subject: [PATCH 03/11] [O2B-1641] Use the beam types of production in the LHC fills seeders The beam types of the seeded LHC fills are the ones displayed in production, with the tests adapted: - p-p becomes PROTON - PROTON, p-Pb becomes PROTON - O8, Pb-Pb becomes PB82 - PB82 and PROTON-PROTON becomes O8 - O8 - the fill without beam type has now NE10 - NE10 --- .../seeders/20220503120937-lhc-fills.js | 11 +++++----- test/api/lhcFills.test.js | 21 ++++++++++--------- test/api/runs.test.js | 10 ++++----- .../lhcFill/CreateLhcFillUseCase.test.js | 4 ++-- .../lhcFill/GetAllLhcFillsUseCase.test.js | 16 +++++++------- .../usecases/run/GetAllRunsUseCase.test.js | 4 ++-- test/public/Filters/FilteringModel.test.js | 2 +- test/public/Filters/filtersToUrl.test.js | 4 ++-- test/public/Filters/urlToFilter.test.js | 4 ++-- test/public/lhcFills/detail.test.js | 2 +- 10 files changed, 40 insertions(+), 38 deletions(-) diff --git a/lib/database/seeders/20220503120937-lhc-fills.js b/lib/database/seeders/20220503120937-lhc-fills.js index f65e38ba63..a313a44904 100644 --- a/lib/database/seeders/20220503120937-lhc-fills.js +++ b/lib/database/seeders/20220503120937-lhc-fills.js @@ -21,7 +21,7 @@ module.exports = { stable_beams_start: '2022-03-21 13:00:00', stable_beams_end: '2022-03-22 15:00:00', stable_beams_duration: 100, - beam_type: 'p-p', + beam_type: 'PROTON - PROTON', filling_scheme_name: '25ns_2352b_2340_2004_2133_108bpi_24inj', created_at: new Date('2019-08-09 16:00:00'), updated_at: new Date('2019-08-09 16:00:00'), @@ -32,7 +32,7 @@ module.exports = { stable_beams_start: '2022-03-21 13:00:00', stable_beams_end: '2022-03-22 15:00:00', stable_beams_duration: 100, - beam_type: 'p-Pb', + beam_type: 'PROTON - O8', filling_scheme_name: 'schemename', created_at: new Date('2019-08-09 17:00:00'), updated_at: new Date('2019-08-09 17:00:00'), @@ -42,20 +42,21 @@ module.exports = { stable_beams_start: '2022-03-21 13:00:00', stable_beams_end: '2022-03-22 15:00:00', stable_beams_duration: 100, - beam_type: 'Pb-Pb', + beam_type: 'PB82 - PB82', filling_scheme_name: 'schemename', created_at: new Date('2019-08-09 18:00:00'), updated_at: new Date('2019-08-09 18:00:00'), }, { fill_number: 4, - beam_type: 'p-p', + beam_type: 'PROTON - PROTON', filling_scheme_name: 'schemename', created_at: new Date('2019-08-09 19:00:00'), updated_at: new Date('2019-08-09 19:00:00'), }, { fill_number: 5, + beam_type: 'NE10 - NE10', stable_beams_start: '2022-03-21 13:00:00', filling_scheme_name: 'schemename', created_at: new Date('2019-08-09 20:00:00'), @@ -63,7 +64,7 @@ module.exports = { }, { fill_number: 6, - beam_type: 'PROTON-PROTON ', + beam_type: 'O8 - O8', stable_beams_start: '2019-08-08 11:00:00', stable_beams_end: '2019-08-08 23:00:00', stable_beams_duration: 60 * 60 * 12, diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index 65214e52db..7f3c8670f7 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -694,7 +694,7 @@ module.exports = () => { it('should return 200 and an LHCFill array for beam types filter, correct', (done) => { request(server) - .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamTypes]=Pb-Pb') + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamTypes]=PB82%20-%20PB82') .expect(200) .end((err, res) => { if (err) { @@ -711,7 +711,7 @@ module.exports = () => { it('should return 200 and an LHCFill array for beam types filter, multiple correct', (done) => { request(server) - .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamTypes]=Pb-Pb,p-p,p-Pb') + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamTypes]=PB82%20-%20PB82,PROTON%20-%20PROTON,PROTON%20-%20O8') .expect(200) .end((err, res) => { if (err) { @@ -729,7 +729,7 @@ module.exports = () => { // API accepts filters that do not exist, this is because it does not affect the results it('should return 200 for beam types filter, one wrong', (done) => { request(server) - .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamTypes]=Pb-Pb,Hello-World,p-p,p-Pb') + .get('/api/lhcFills?page[offset]=0&page[limit]=15&filter[beamTypes]=PB82%20-%20PB82,Hello-World,PROTON%20-%20PROTON,PROTON%20-%20O8') .expect(200) .end((err, res) => { if (err) { @@ -787,10 +787,11 @@ module.exports = () => { } expect(res.body.data).to.deep.equal([ - { beam_type: 'p-p' }, - { beam_type: 'p-Pb' }, - { beam_type: 'Pb-Pb' }, - { beam_type: 'PROTON-PROTON' }, + { beam_type: 'NE10 - NE10' }, + { beam_type: 'O8 - O8' }, + { beam_type: 'PB82 - PB82' }, + { beam_type: 'PROTON - O8' }, + { beam_type: 'PROTON - PROTON' }, ]); done(); @@ -804,14 +805,14 @@ module.exports = () => { .post('/api/lhcFills') .send({ fillNumber: 544455, - beamType: 'Pb-Pb', + beamType: 'PB82 - PB82', fillingSchemeName: 'schemename', }); expect(response.status).to.equal(201); const { data } = response.body; - expect(data.beamType).to.equal('Pb-Pb'); + expect(data.beamType).to.equal('PB82 - PB82'); expect(data.fillingSchemeName).to.equal('schemename'); }); it('should return 409 if the fillNumber is duplicate', (done) => { @@ -913,7 +914,7 @@ module.exports = () => { expect(data.stableBeamsStart).to.equal(1647961200000); expect(data.stableBeamsEnd).to.equal(1647961200000); expect(data.stableBeamsDuration).to.equal(600); - expect(data.beamType).to.equal('p-p'); + expect(data.beamType).to.equal('PROTON - PROTON'); expect(data.fillingSchemeName).to.equal('schemename'); expect(data.fillNumber).to.equal(1); expect(data.runs.map(({ lhcPeriod }) => lhcPeriod)).to.eql([undefined, 'LHC22a', 'LHC22b', 'LHC22b', 'LHC22b']); diff --git a/test/api/runs.test.js b/test/api/runs.test.js index 17f74ed5d2..e608896823 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -161,8 +161,8 @@ module.exports = () => { }); it('should successfully filter with single beamType', async () => { - const beamType = 'p-p'; - const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamType}`); + const beamType = 'PROTON - PROTON'; + const response = await request(server).get(`/api/runs?filter[beamTypes]=${encodeURIComponent(beamType)}`); expect(response.status).to.equal(200); const { data: runs } = response.body; @@ -173,8 +173,8 @@ module.exports = () => { }); it('should successfully filter with multiple beamTypes', async () => { - const beamTypes = 'p-p,Pb-Pb'; - const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamTypes}`); + const beamTypes = 'PROTON - PROTON,PB82 - PB82'; + const response = await request(server).get(`/api/runs?filter[beamTypes]=${encodeURIComponent(beamTypes)}`); expect(response.status).to.equal(200); const { data: runs } = response.body; @@ -186,7 +186,7 @@ module.exports = () => { it('should return 400 if beamTypes filter has the incorrect format', async () => { const beamTypes = 'DOES NOT EXIST'; - const response = await request(server).get(`/api/runs?filter[beamTypes]=${beamTypes}`); + const response = await request(server).get(`/api/runs?filter[beamTypes]=${encodeURIComponent(beamTypes)}`); expect(response.status).to.equal(400); diff --git a/test/lib/usecases/lhcFill/CreateLhcFillUseCase.test.js b/test/lib/usecases/lhcFill/CreateLhcFillUseCase.test.js index d21eb0c441..7dec4b1780 100644 --- a/test/lib/usecases/lhcFill/CreateLhcFillUseCase.test.js +++ b/test/lib/usecases/lhcFill/CreateLhcFillUseCase.test.js @@ -24,7 +24,7 @@ module.exports = () => { createLhcFillDto = await CreateLhcFillDto.validateAsync({ body: { fillNumber: 123123123, - beamType: 'Pb-Pb', + beamType: 'PB82 - PB82', fillingSchemeName: 'schemename', }, }); @@ -49,7 +49,7 @@ module.exports = () => { .execute(createLhcFillDto); expect(result.fillNumber).to.equal(123123); - expect(result.beamType).to.equal('Pb-Pb'); + expect(result.beamType).to.equal('PB82 - PB82'); expect(result.fillingSchemeName).to.equal('schemename'); }); it('should not be able to create with the same id', async () => { diff --git a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js index 8060e95e71..c54e91d5e5 100644 --- a/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js +++ b/test/lib/usecases/lhcFill/GetAllLhcFillsUseCase.test.js @@ -277,18 +277,18 @@ module.exports = () => { }); }) - it('should only contain specified beam type, {p-p}', async () => { - getAllLhcFillsDto.query = { filter: { beamTypes: ['p-p'] } }; + it('should only contain specified beam type, {PROTON - PROTON}', async () => { + getAllLhcFillsDto.query = { filter: { beamTypes: ['PROTON - PROTON'] } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(2) lhcFills.forEach((lhcFill) => { - expect(lhcFill.beamType).to.equal('p-p') + expect(lhcFill.beamType).to.equal('PROTON - PROTON') }); }) - it('should only contain specified beam types, {p-p, PROTON-PROTON, Pb-Pb}', async () => { - const beamTypes = ['p-p', 'PROTON-PROTON', 'Pb-Pb'] + it('should only contain specified beam types, {PROTON - PROTON, O8 - O8, PB82 - PB82}', async () => { + const beamTypes = ['PROTON - PROTON', 'O8 - O8', 'PB82 - PB82'] getAllLhcFillsDto.query = { filter: { beamTypes: beamTypes } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) @@ -299,15 +299,15 @@ module.exports = () => { }); }) - it('should ignore unknown beam types, {p-p, Hello-world, Pb-Pb}', async () => { - const beamTypes = ['p-p', 'Hello-world', 'Pb-Pb'] + it('should ignore unknown beam types, {PROTON - PROTON, Hello-world, PB82 - PB82}', async () => { + const beamTypes = ['PROTON - PROTON', 'Hello-world', 'PB82 - PB82'] getAllLhcFillsDto.query = { filter: { beamTypes: beamTypes } }; const { lhcFills } = await new GetAllLhcFillsUseCase().execute(getAllLhcFillsDto) expect(lhcFills).to.be.an('array').and.lengthOf(3) lhcFills.forEach((lhcFill) => { - expect(lhcFill.beamType).oneOf(['p-p', 'Pb-Pb']) + expect(lhcFill.beamType).oneOf(['PROTON - PROTON', 'PB82 - PB82']) }); }) diff --git a/test/lib/usecases/run/GetAllRunsUseCase.test.js b/test/lib/usecases/run/GetAllRunsUseCase.test.js index bbaea9edaf..a04d301f14 100644 --- a/test/lib/usecases/run/GetAllRunsUseCase.test.js +++ b/test/lib/usecases/run/GetAllRunsUseCase.test.js @@ -210,8 +210,8 @@ module.exports = () => { }); it('should successfully filter on beamTypes', async () => { - const singleBeamType = ['p-p']; - const multipleBeamTypes = ['p-p', 'Pb-Pb']; + const singleBeamType = ['PROTON - PROTON']; + const multipleBeamTypes = ['PROTON - PROTON', 'PB82 - PB82']; const nonExistentBeamType = ['DOES-NOT-EXIST']; getAllRunsDto.query = { filter: { beamTypes: singleBeamType }, page: { limit: 200 } }; diff --git a/test/public/Filters/FilteringModel.test.js b/test/public/Filters/FilteringModel.test.js index 6c563d259d..5992273b70 100644 --- a/test/public/Filters/FilteringModel.test.js +++ b/test/public/Filters/FilteringModel.test.js @@ -49,7 +49,7 @@ module.exports = () => { const lhcFillsSelectionFiltersChecks = { 'hasStableBeams': [{ count: 6, selector: '.switch > input' }], - 'beamTypes': [{ count: 1, selector: '#beam-types-checkbox-p-p' }, { count: 2, selector: '#beam-types-checkbox-p-Pb' }] + 'beamTypes': [{ count: 1, selector: '[id="beam-types-checkbox-PROTON - PROTON"]' }, { count: 2, selector: '[id="beam-types-checkbox-PROTON - O8"]' }] }; const checkSelectionFilters = async (selectionFilterObject, baseRowCount) => { diff --git a/test/public/Filters/filtersToUrl.test.js b/test/public/Filters/filtersToUrl.test.js index df32f10913..d858f7447f 100644 --- a/test/public/Filters/filtersToUrl.test.js +++ b/test/public/Filters/filtersToUrl.test.js @@ -124,7 +124,7 @@ module.exports = () => { await openFilteringPanel(page); await fillInput(page, '#beam-duration-filter-operand', '00:01:40', ['change']); await fillInput(page, '#run-duration-filter-operand', '00:00:00', ['change']); - await pressElement(page, '#beam-types-checkbox-p-Pb'); + await pressElement(page, '[id="beam-types-checkbox-PROTON - O8"]'); await fillInput(page, sbStartFromDateTimeSelector, '2019-08-08T10:00', ['change']); await fillInput(page, sbStartToDateTimeSelector, '2019-08-08T12:00', ['change']); await fillInput(page, sbEndFromDateTimeSelector, '2022-03-22T01:00', ['change']); @@ -143,7 +143,7 @@ module.exports = () => { "filter[stableBeamsEnd][to]": "1647993540000", "filter[stableBeamsStart][from]": "1565258400000", "filter[stableBeamsStart][to]": "1565265600000", - "filter[beamTypes]": "p-Pb", + "filter[beamTypes]": "PROTON - O8", "filter[schemeName]": "Single_12b_8_1024_8_2018" }); }); diff --git a/test/public/Filters/urlToFilter.test.js b/test/public/Filters/urlToFilter.test.js index 51daf71519..107eba8fce 100644 --- a/test/public/Filters/urlToFilter.test.js +++ b/test/public/Filters/urlToFilter.test.js @@ -80,7 +80,7 @@ module.exports = () => { it('should set filters from LhcFillsOverview to the URL', async () => { const url = 'http://localhost:4000/?page=lhc-fill-overview&filter[beamDuration][operator]=%3D&filter[beamDuration][limit]=00%3A01%3A40&' + 'filter[runDuration][operator]=%3D&filter[runDuration][limit]=00%3A00%3A00&filter[hasStableBeams]=true&filter[stableBeamsStart][from]=1565251200000&' + - 'filter[stableBeamsStart][to]=1565258400000&filter[stableBeamsEnd][from]=1647907200000&filter[stableBeamsEnd][to]=1647989940000&filter[beamTypes]=p-Pb&filter[schemeName]=Single_12b_8_1024_8_2018'; + 'filter[stableBeamsStart][to]=1565258400000&filter[stableBeamsEnd][from]=1647907200000&filter[stableBeamsEnd][to]=1647989940000&filter[beamTypes]=PROTON%20-%20O8&filter[schemeName]=Single_12b_8_1024_8_2018'; await page.goto(url, { waitUntil: 'load' }); @@ -109,7 +109,7 @@ module.exports = () => { await expectInputValue(page, sbEndFromDateTimeSelector, '2022-03-22T00:00'); await expectInputValue(page, sbEndToDateTimeSelector, '2022-03-22T22:59'); await expectInputValue(page, filterSchemeNameInputField, 'Single_12b_8_1024_8_2018'); - await page.waitForSelector('#beam-types-checkbox-p-Pb:checked'); + await page.waitForSelector('[id="beam-types-checkbox-PROTON - O8"]:checked'); }); it('should set filters from runsOverview to the URL', async () => { diff --git a/test/public/lhcFills/detail.test.js b/test/public/lhcFills/detail.test.js index 1e4d33dfc9..992c6f6a11 100644 --- a/test/public/lhcFills/detail.test.js +++ b/test/public/lhcFills/detail.test.js @@ -43,7 +43,7 @@ module.exports = () => { await expectInnerText(page, '#lhc-fill-stableBeamsEnd', 'Stable beams end:\n08/08/2019, 23:00:00'); await expectInnerText(page, '#lhc-fill-stableBeamsDuration', 'Beams duration:\n12:00:00'); - await expectInnerText(page, '#lhc-fill-beamType', 'Beam type:\nPROTON-PROTON'); + await expectInnerText(page, '#lhc-fill-beamType', 'Beam type:\nO8 - O8'); await expectInnerText(page, '#lhc-fill-fillingSchemeName', 'Scheme name:\nSingle_12b_8_1024_8_2018'); await expectInnerText(page, '#lhc-fill-collidingBunchesCount', 'Colliding bunches:\n1,024'); await expectInnerText(page, '#lhc-fill-deliveredLuminosity', 'Delivered lumi:\n420 nb-1'); From a6ce005162b9e0001287d5ee58d36f87ec361a10 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 15:20:07 +0200 Subject: [PATCH 04/11] [O2B-1641] Adapt the LHC fills overview tests to its new columns The tests of the statistics, the stable beams, the fill dropdown, the ONGOING information and the number of rows per page use the elements of the new columns. --- test/public/lhcFills/overview.test.js | 57 +++++++++++++-------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 4f0115bcc9..c96d452434 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -17,7 +17,6 @@ const { defaultAfter, pressElement, goToPage, - checkColumnBalloon, waitForNavigation, expectUrlParams, expectInnerText, @@ -155,15 +154,8 @@ module.exports = () => { await expectInnerText(page, '#totalRowsCount', '5'); }); - it('Should have balloon on runs column', async () => { - await goToPage(page, 'lhc-fill-overview'); - await waitForTableLength(page, 5); - - await checkColumnBalloon(page, 1, 11); - }); - it('fill dropdown menu should be correct', async() => { - const popoverTrigger = '#row6-fillNumber-text > div:nth-child(1) > div:nth-child(2)'; + const popoverTrigger = '#row6-fill-number .popover-trigger'; await pressElement(page, popoverTrigger); await expectInnerText(page, '#copy-6 > div:nth-child(1)', 'Copy Fill Number'); @@ -182,11 +174,11 @@ module.exports = () => { // turn off Stable Beams Only filter await pressElement(page, '.slider.round'); - // Expect the amount selector to currently be set to 10 (because of the defined page height) + // Expect the amount selector to currently be set to 9 (because of the defined page height) const amountSelectorId = '#amountSelector'; const amountSelectorButton = await page.$(`${amountSelectorId} button`); const amountSelectorButtonText = await page.evaluate((element) => element.innerText, amountSelectorButton); - expect(amountSelectorButtonText.trim().endsWith('10')).to.be.true; + expect(amountSelectorButtonText.trim().endsWith('9')).to.be.true; // Expect the dropdown options to be visible when it is selected await pressElement(page, `${amountSelectorId} button`); @@ -248,7 +240,7 @@ module.exports = () => { it('should successfully display ONGOING information', async () => { await goToPage(page, 'lhc-fill-overview'); - const stableBeamsDurationText = await page.waitForSelector('#row5-stableBeamsDuration-text div'); + const stableBeamsDurationText = await page.waitForSelector('#row5-stableBeamsDuration .badge'); expect(await stableBeamsDurationText.evaluate((element) => element.classList.contains('bg-success'))).to.be.true; expect(await stableBeamsDurationText.evaluate((element) => element.innerText)).to.equal('ONGOING'); @@ -261,23 +253,28 @@ module.exports = () => { }); it('should successfully display some statistics', async () => { - const beamTypeExpect = { selector: 'tbody tr:nth-child(1) td:nth-child(9)', value: 'PROTON\nPROTON' }; - const beforeFirststRunExpect = { selector: 'tbody tr:nth-child(1) td:nth-child(5)', value: '03:00:00\n(25.00%)' }; - const collidingBunchesExpect = { selector: 'tbody tr:nth-child(1) td:nth-child(10)', value: '1024' }; - const meanRunDurationExpect = { selector: 'tbody tr:nth-child(1) td:nth-child(6)', value: '01:40:00' }; - const totalRunsDurationExpect = { selector: 'tbody tr:nth-child(1) td:nth-child(7)', value: '05:00:00' }; - const efficiencyExpect = { selector: 'tbody tr:nth-child(1) td:nth-child(8)', value: '41.67%' }; - const schemeNameExpect = { selector: '#row6-fillingSchemeName > div:nth-child(1) > div:nth-child(1)', value: 'Single_12b_8_1024_8_2018'}; + await goToPage(page, 'lhc-fill-overview'); + + await expectInnerText(page, '#row6-beamType', 'O8 - O8'); + await expectInnerText(page, '#row6-timeLossAtStart', '03:00:00 (25.00%)'); + await expectInnerText(page, '#row6-collidingBunches', '1024'); + await expectInnerText(page, '#row6-meanRunDuration', '01:40:00'); + await expectInnerText(page, '#row6-runsCoverage', '05:00:00'); + await expectInnerText(page, '#row6-efficiencyValue', '41.67%'); + await expectInnerText(page, '#row6-schemeName', 'Single_12b_8_1024_8_2018'); + }); + it('should successfully display the start, the end and the duration of the stable beams', async () => { await goToPage(page, 'lhc-fill-overview'); - await expectInnerText(page, beamTypeExpect.selector, beamTypeExpect.value); - await expectInnerText(page, beforeFirststRunExpect.selector, beforeFirststRunExpect.value); - await expectInnerText(page, collidingBunchesExpect.selector, collidingBunchesExpect.value); - await expectInnerText(page, meanRunDurationExpect.selector, meanRunDurationExpect.value); - await expectInnerText(page, totalRunsDurationExpect.selector, totalRunsDurationExpect.value); - await expectInnerText(page, efficiencyExpect.selector, efficiencyExpect.value); - await expectInnerText(page, schemeNameExpect.selector, schemeNameExpect.value); + // Fill 6 ends the same day it started: only the time of its end is displayed, aligned with the time of its start + await expectInnerText(page, '#row6-stableBeamsStart', '08/08/2019, 11:00:00'); + await expectInnerText(page, '#row6-stableBeamsEnd', '23:00:00'); + await expectInnerText(page, '#row6-stableBeamsDuration', '12:00:00'); + + // Fill 3 ends the day after its start: the date of its end is displayed + await expectInnerText(page, '#row3-stableBeamsStart', '21/03/2022, 13:00:00'); + await expectInnerText(page, '#row3-stableBeamsEnd', '22/03/2022, 15:00:00'); }); it('should successfully display filter elements', async () => { @@ -355,8 +352,8 @@ module.exports = () => { }); it('should successfully apply beam types filter', async () => { - const filterBeamTypeP_Pb = '#beam-types-dropdown-option-p-Pb'; - const filterBeamTypePb_Pb = '#beam-types-dropdown-option-Pb-Pb'; + const filterBeamTypeP_Pb = '[id="beam-types-dropdown-option-PROTON - O8"]'; + const filterBeamTypePb_Pb = '[id="beam-types-dropdown-option-PB82 - PB82"]'; await goToPage(page, 'lhc-fill-overview'); await waitForTableLength(page, 5); @@ -375,7 +372,7 @@ module.exports = () => { await goToPage(page, 'lhc-fill-overview'); await waitForTableLength(page, 5); - await page.waitForSelector('.column-stableBeamsStart'); + await page.waitForSelector('#row6-stableBeamsStart'); await openFilteringPanel(page); const popOverSelector = await getPopoverSelector(await page.$(popoverTrigger)); @@ -394,7 +391,7 @@ module.exports = () => { await goToPage(page, 'lhc-fill-overview'); await waitForTableLength(page, 5); - await page.waitForSelector('.column-stableBeamsEnd'); + await page.waitForSelector('#row6-stableBeamsEnd'); await openFilteringPanel(page); const popOverSelector = await getPopoverSelector(await page.$(popoverTrigger)); From 83b2f82b0eb157d26fc9c3eadc4667c2c784c675 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 15:31:32 +0200 Subject: [PATCH 05/11] [O2B-1641] Display the beam types of the filters as they are in the tables Notable changes for users: - The options and the selected items of the beam type filter (LHC fills and runs overviews) are displayed in the same badge as the beam types of the tables. Notable changes for developers: - New `beamTypeSelectionDropdown`, with unchanged DOM ids. --- .../ActiveColumns/lhcFillsActiveColumns.js | 7 +-- .../format/beamTypeSelectionDropdown.js | 49 +++++++++++++++++++ .../Runs/ActiveColumns/runsActiveColumns.js | 6 +-- 3 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 lib/public/views/LhcFills/format/beamTypeSelectionDropdown.js diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index 4abb608912..00ebbc9834 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -21,12 +21,12 @@ import { formatRunsList } from '../../Runs/format/formatRunsList.js'; import { formatLhcFillsTimeLoss } from '../format/formatLhcFillsTimeLoss.js'; import { buttonLinkWithDropdown } from '../../../components/common/selection/infoLoggerButtonGroup/buttonLinkWithDropdown.js'; import { infologgerLinksComponents } from '../../../components/common/externalLinks/infologgerLinksComponents.js'; -import { selectionDropdown } from '../../../components/common/selection/dropdown/selectionDropdown.js'; import { formatBeamType } from '../../../utilities/formatting/formatBeamType.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { toggleFilter } from '../../../components/Filters/common/filters/toggleFilter.js'; import { durationFilter } from '../../../components/Filters/LhcFillsFilter/durationFilter.js'; import { timeRangeFilter } from '../../../components/Filters/common/filters/timeRangeFilter.js'; +import { beamTypeSelectionDropdown } from '../format/beamTypeSelectionDropdown.js'; import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; /** @@ -188,10 +188,7 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-8', format: (value) => formatBeamType(value), - filter: (lhcFillModel) => selectionDropdown( - lhcFillModel.filteringModel.get('beamTypes'), - { selectorPrefix: 'beam-types' }, - ), + filter: (lhcFillModel) => beamTypeSelectionDropdown(lhcFillModel.filteringModel.get('beamTypes')), }, collidingBunches: { name: 'Colliding bunches', diff --git a/lib/public/views/LhcFills/format/beamTypeSelectionDropdown.js b/lib/public/views/LhcFills/format/beamTypeSelectionDropdown.js new file mode 100644 index 0000000000..c47a062b31 --- /dev/null +++ b/lib/public/views/LhcFills/format/beamTypeSelectionDropdown.js @@ -0,0 +1,49 @@ +/** + * @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 { selectionDropdown } from '../../../components/common/selection/dropdown/selectionDropdown.js'; +import { formatBeamTypeBadge } from './formatBeamTypeBadge.js'; + +/** + * Display the dropdown selecting beam types, whose options and selected items are displayed as the beam types are in the tables + * + * @param {SelectionDropdownModel} beamTypesSelectionModel the model storing the state of the dropdown + * @return {Component} the dropdown + */ +export const beamTypeSelectionDropdown = (beamTypesSelectionModel) => selectionDropdown(beamTypesSelectionModel, { + selectorPrefix: 'beam-types', + + /** + * Display an option of the dropdown, with the same selectors as the default display + * + * @param {SelectionOption} option the option to display + * @param {boolean} checked true if the option is selected + * @param {Function} onChange function called with the option and its new state when it changes + * @param {string} selectorPrefix the prefix of the DOM selectors + * @return {Component} the option + */ + displayOption: (option, checked, onChange, selectorPrefix) => { + const key = option.selector ?? option.value; + return h('label.dropdown-option.form-check-label.flex-row.g2.ph2.pv1', { key }, [ + h(`input#${selectorPrefix}dropdown-option-${key}`, { + type: 'checkbox', + name: `${selectorPrefix}dropdown-option-${key}`, + checked, + onchange: (e) => onChange(option, e.target.checked), + }), + formatBeamTypeBadge(option.label || option.value), + ]); + }, + displaySelectionItem: ({ label, value }) => h('', { key: value }, formatBeamTypeBadge(label || value)), +}); diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 8b1808831f..0d80e73e4e 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -43,6 +43,7 @@ import { timeRangeFilter } from '../../../components/Filters/common/filters/time import { numericalComparisonFilter } from '../../../components/Filters/common/filters/numericalComparisonFilter.js'; import { checkboxes } from '../../../components/Filters/common/filters/checkboxFilter.js'; import radioButtonFilter from '../../../components/Filters/common/filters/radioButtonFilter.js'; +import { beamTypeSelectionDropdown } from '../../LhcFills/format/beamTypeSelectionDropdown.js'; import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; /** @@ -601,10 +602,7 @@ export const runsActiveColumns = { beamType: { name: 'Beam Type', visible: false, - filter: (runsOverviewModel) => selectionDropdown( - runsOverviewModel.filteringModel.get('beamTypes'), - { selectorPrefix: 'beam-types' }, - ), + filter: (runsOverviewModel) => beamTypeSelectionDropdown(runsOverviewModel.filteringModel.get('beamTypes')), filterTooltip: 'As sent by LHC to Bookkeeping-LHC Plugin.', }, readoutCfgUri: { From dc1607057e59fed8e2f1d90e96f2500243242248 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 16:20:13 +0200 Subject: [PATCH 06/11] [O2B-1641] Check the datatypes of the LHC fills overview values on their own elements The test reads the value of each merged cell (fill number, efficiency, time lost before the first run, mean and total duration of the runs) instead of the whole cell, so that the labels do not interfere with the datatype checks and the cells are checked once the table is loaded. --- test/public/lhcFills/overview.test.js | 46 ++++++--------------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index c96d452434..85a2eb9916 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -101,48 +101,22 @@ module.exports = () => { it('shows correct datatypes in respective columns', async () => { - // Expectations of header texts being of a certain datatype - const headerDatatypes = { - fillNumber: (fill) => !isNaN(parseInt(fill, 10)), - // Seems to not exist anymore - createdAt: (date) => !isNaN(Date.parse(date)), - // Seems to not exist anymore - updatedAt: (date) => !isNaN(Date.parse(date)), - // Seems to not exist anymore - status: (date) => !isNaN(Date.parse(date)), - // Seems to not exist anymore - statusMessage: (string) => typeof string == 'string', - efficiency: (efficiency) => efficiency === '-' || efficiency.match(percentageRegex) !== null, + // Expectations of the datatype of the values displayed in the merged cells, identified by the suffix of the id of their element + const valuesDatatypes = { + 'fill-number': (fill) => !isNaN(parseInt(fill, 10)), + efficiencyValue: (efficiency) => efficiency === '-' || efficiency.match(percentageRegex) !== null, timeLossAtStart: (data) => data === '-' - || data.match(new RegExp(`${durationRegex.source}\n\\(${percentageRegex.source}\\)`)) !== null, - timeLossAtEnd: (data) => data === '-' - || data.match(new RegExp(`${durationRegex.source}\n\\(${percentageRegex.source}\\)`)) !== null, + || data.match(new RegExp(`${durationRegex.source} \\(${percentageRegex.source}\\)`)) !== null, meanRunDuration: (duration) => duration === '-' || duration.match(durationRegex) !== null, runsCoverage: (duration) => duration === '-' || duration.match(durationRegex) !== null, - runs: (string) => typeof string == 'string', }; - // We find the headers matching the datatype keys - const headers = await page.$$('th'); - const headerIndices = {}; - for (const [index, header] of headers.entries()) { - const headerContent = await page.evaluate((element) => element.id, header); - const matchingDatatype = Object.keys(headerDatatypes).find((key) => headerContent === key); - if (matchingDatatype !== undefined) { - headerIndices[index] = matchingDatatype; - } - } - - // We expect every value of a header matching a datatype key to actually be of that datatype + // Use the third row because it is where statistics are present, once the table is loaded + await page.waitForSelector('tbody tr:nth-of-type(3) [id$="-fill-number"]'); - // Use the third row because it is where statistics are present - const firstRowCells = await page.$$('tr:nth-of-type(3) td'); - for (const [index, cell] of firstRowCells.entries()) { - if (index in headerIndices) { - const cellContent = await page.evaluate((element) => element.innerText, cell); - const expectedDatatype = headerDatatypes[headerIndices[index]](cellContent); - expect(expectedDatatype).to.be.true; - } + for (const [suffix, isOfExpectedDatatype] of Object.entries(valuesDatatypes)) { + const value = await page.$eval(`tbody tr:nth-of-type(3) [id$="-${suffix}"]`, (element) => element.innerText); + expect(isOfExpectedDatatype(value), `${suffix} has an unexpected value: ${value}`).to.be.true; } }); From a7aa1240b40cbe5705c28b77eecac1c570a32e3d Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 16:30:34 +0200 Subject: [PATCH 07/11] [O2B-1641] Add the efficiency level, its badge and its legend of the LHC fills Notable changes for developers: - New `getEfficiencyLevel`: great from 95%, good from 80% to 95% excluded, bad below 80%, no level for a missing or null efficiency. - New `formatEfficiency` and `efficiencyLegend`. - Unit tests of `getEfficiencyLevel` and `getBeamTypeFamily`. --- .../views/LhcFills/format/efficiencyLegend.js | 40 +++++++++++++++ .../views/LhcFills/format/efficiencyLevel.js | 42 ++++++++++++++++ .../views/LhcFills/format/formatEfficiency.js | 29 +++++++++++ test/lib/public/index.js | 2 + .../views/LhcFills/beamTypeFamily.test.js | 49 +++++++++++++++++++ .../views/LhcFills/efficiencyLevel.test.js | 46 +++++++++++++++++ test/lib/public/views/LhcFills/index.js | 20 ++++++++ test/lib/public/views/index.js | 18 +++++++ 8 files changed, 246 insertions(+) create mode 100644 lib/public/views/LhcFills/format/efficiencyLegend.js create mode 100644 lib/public/views/LhcFills/format/efficiencyLevel.js create mode 100644 lib/public/views/LhcFills/format/formatEfficiency.js create mode 100644 test/lib/public/views/LhcFills/beamTypeFamily.test.js create mode 100644 test/lib/public/views/LhcFills/efficiencyLevel.test.js create mode 100644 test/lib/public/views/LhcFills/index.js create mode 100644 test/lib/public/views/index.js diff --git a/lib/public/views/LhcFills/format/efficiencyLegend.js b/lib/public/views/LhcFills/format/efficiencyLegend.js new file mode 100644 index 0000000000..b2a68c2f7c --- /dev/null +++ b/lib/public/views/LhcFills/format/efficiencyLegend.js @@ -0,0 +1,40 @@ +/** + * @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 { EfficiencyLevels } from './efficiencyLevel.js'; + +const EFFICIENCY_LEVELS_DESCRIPTIONS = [ + [EfficiencyLevels.GREAT, '95.00%', '95% and above'], + [EfficiencyLevels.GOOD, '80.00%', 'From 80% to below 95%'], + [EfficiencyLevels.BAD, '50.00%', 'Below 80%'], +]; + +/** + * Render the legend of the efficiency, as displayed in the efficiency column of the LHC fills overview + * + * @return {Component} the legend + */ +export const efficiencyLegend = () => h('.flex-column.g3', [ + h('h3', 'Efficiency legend'), + h('.flex-column.g2', [ + ...EFFICIENCY_LEVELS_DESCRIPTIONS.map(([level, example, description]) => h('.flex-row.items-baseline.g3', [ + h('.efficiency-legend-label.flex-row', h(`.efficiency.efficiency-${level}`, example)), + h('.efficiency-legend-description', `${level[0].toUpperCase()}${level.slice(1)}: ${description}`), + ])), + h('.flex-row.items-baseline.g3', [ + h('.efficiency-legend-label.flex-row', '0.00%'), + h('.efficiency-legend-description', 'No efficiency, displayed without style'), + ]), + ]), +]); diff --git a/lib/public/views/LhcFills/format/efficiencyLevel.js b/lib/public/views/LhcFills/format/efficiencyLevel.js new file mode 100644 index 0000000000..22ff6c383b --- /dev/null +++ b/lib/public/views/LhcFills/format/efficiencyLevel.js @@ -0,0 +1,42 @@ +/** + * @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. + */ + +/** + * Levels of efficiency of a LHC fill + * + * @enum {string} + */ +export const EfficiencyLevels = Object.freeze({ + GREAT: 'great', + GOOD: 'good', + BAD: 'bad', +}); + +export const GREAT_EFFICIENCY_THRESHOLD = 0.95; +export const GOOD_EFFICIENCY_THRESHOLD = 0.8; + +/** + * Returns the level of the given efficiency: great from 95%, good from 80% to 95% excluded, else bad + * + * @param {number|null|undefined} efficiency the efficiency of the fill, normalized between 0 and 1 + * @return {EfficiencyLevels|null} the level, or null if the efficiency is unknown or null (no level is displayed for them) + */ +export const getEfficiencyLevel = (efficiency) => { + if (typeof efficiency !== 'number' || isNaN(efficiency) || efficiency === 0) { + return null; + } + if (efficiency >= GREAT_EFFICIENCY_THRESHOLD) { + return EfficiencyLevels.GREAT; + } + return efficiency >= GOOD_EFFICIENCY_THRESHOLD ? EfficiencyLevels.GOOD : EfficiencyLevels.BAD; +}; diff --git a/lib/public/views/LhcFills/format/formatEfficiency.js b/lib/public/views/LhcFills/format/formatEfficiency.js new file mode 100644 index 0000000000..f71d16300b --- /dev/null +++ b/lib/public/views/LhcFills/format/formatEfficiency.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 { h } from '/js/src/index.js'; +import { formatPercentage } from '../../../utilities/formatting/formatPercentage.js'; +import { getEfficiencyLevel } from './efficiencyLevel.js'; + +/** + * Display the efficiency of a fill as a percentage, in a badge whose color depends on its level (a null efficiency has no styling) + * + * @param {number|null|undefined} efficiency the efficiency of the fill, normalized between 0 and 1 + * @return {Component|string} the formatted efficiency + */ +export const formatEfficiency = (efficiency) => { + const level = getEfficiencyLevel(efficiency); + return level + ? h(`.efficiency.efficiency-${level}`, formatPercentage(efficiency)) + : formatPercentage(efficiency); +}; diff --git a/test/lib/public/index.js b/test/lib/public/index.js index c1a3fd5322..96acc0c93a 100644 --- a/test/lib/public/index.js +++ b/test/lib/public/index.js @@ -12,7 +12,9 @@ */ const UtilitiesSuite = require('./utilities/index.js'); +const ViewsSuite = require('./views/index.js'); module.exports = () => { describe('utilities', UtilitiesSuite); + describe('views', ViewsSuite); }; diff --git a/test/lib/public/views/LhcFills/beamTypeFamily.test.js b/test/lib/public/views/LhcFills/beamTypeFamily.test.js new file mode 100644 index 0000000000..a4a6cba477 --- /dev/null +++ b/test/lib/public/views/LhcFills/beamTypeFamily.test.js @@ -0,0 +1,49 @@ +/** + * @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. + */ + +const { expect } = require('chai'); + +let getBeamTypeFamily; + +module.exports = () => { + before(async () => { + ({ getBeamTypeFamily } = await import('../../../../../lib/public/views/LhcFills/format/beamTypeFamily.js')); + }); + + it('should recognize the lead-lead collisions', () => { + expect(getBeamTypeFamily('PB82 - PB82')).to.equal('lead-lead'); + expect(getBeamTypeFamily('Pb-Pb')).to.equal('lead-lead'); + }); + + it('should recognize the proton-proton collisions', () => { + expect(getBeamTypeFamily('PROTON - PROTON')).to.equal('proton-proton'); + expect(getBeamTypeFamily('p-p')).to.equal('proton-proton'); + }); + + it('should recognize the proton-ion collisions', () => { + expect(getBeamTypeFamily('PROTON - O8')).to.equal('proton-ion'); + expect(getBeamTypeFamily('p-Pb')).to.equal('proton-ion'); + }); + + it('should recognize the other ion-ion collisions', () => { + expect(getBeamTypeFamily('O8 - O8')).to.equal('ion-ion'); + expect(getBeamTypeFamily('NE10 - NE10')).to.equal('ion-ion'); + }); + + it('should not recognize malformed beam types', () => { + expect(getBeamTypeFamily(null)).to.equal('unknown'); + expect(getBeamTypeFamily('')).to.equal('unknown'); + expect(getBeamTypeFamily('PROTON')).to.equal('unknown'); + expect(getBeamTypeFamily('PROTON - ')).to.equal('unknown'); + }); +}; diff --git a/test/lib/public/views/LhcFills/efficiencyLevel.test.js b/test/lib/public/views/LhcFills/efficiencyLevel.test.js new file mode 100644 index 0000000000..015611a964 --- /dev/null +++ b/test/lib/public/views/LhcFills/efficiencyLevel.test.js @@ -0,0 +1,46 @@ +/** + * @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. + */ + +const { expect } = require('chai'); + +let getEfficiencyLevel; + +module.exports = () => { + before(async () => { + ({ getEfficiencyLevel } = await import('../../../../../lib/public/views/LhcFills/format/efficiencyLevel.js')); + }); + + it('should have no level for a missing or null efficiency', () => { + expect(getEfficiencyLevel(null)).to.be.null; + expect(getEfficiencyLevel(undefined)).to.be.null; + expect(getEfficiencyLevel(NaN)).to.be.null; + expect(getEfficiencyLevel(0)).to.be.null; + }); + + it('should have a great level from 95%', () => { + expect(getEfficiencyLevel(0.95)).to.equal('great'); + expect(getEfficiencyLevel(0.9999)).to.equal('great'); + expect(getEfficiencyLevel(1)).to.equal('great'); + }); + + it('should have a good level from 80% to 95% excluded', () => { + expect(getEfficiencyLevel(0.8)).to.equal('good'); + expect(getEfficiencyLevel(0.9499)).to.equal('good'); + }); + + it('should have a bad level below 80%', () => { + expect(getEfficiencyLevel(0.7999)).to.equal('bad'); + expect(getEfficiencyLevel(0.4167)).to.equal('bad'); + expect(getEfficiencyLevel(0.0001)).to.equal('bad'); + }); +}; diff --git a/test/lib/public/views/LhcFills/index.js b/test/lib/public/views/LhcFills/index.js new file mode 100644 index 0000000000..1c89930e30 --- /dev/null +++ b/test/lib/public/views/LhcFills/index.js @@ -0,0 +1,20 @@ +/** + * @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. + */ + +const efficiencyLevelTest = require('./efficiencyLevel.test.js'); +const beamTypeFamilyTest = require('./beamTypeFamily.test.js'); + +module.exports = () => { + describe('efficiencyLevel', efficiencyLevelTest); + describe('beamTypeFamily', beamTypeFamilyTest); +}; diff --git a/test/lib/public/views/index.js b/test/lib/public/views/index.js new file mode 100644 index 0000000000..520c711a57 --- /dev/null +++ b/test/lib/public/views/index.js @@ -0,0 +1,18 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const LhcFillsSuite = require('./LhcFills/index.js'); + +module.exports = () => { + describe('LhcFills', LhcFillsSuite); +}; From 0991fb0665db3a8dd7a5d1aaa664a8384ae4c160 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 16:30:35 +0200 Subject: [PATCH 08/11] [O2B-1641] Style the efficiency of the LHC fills overview and adjust the width of its columns Notable changes for users: - The efficiency is displayed in a green solid badge from 95% (great), a soft green badge from 80% to 95% excluded (good) and a soft red badge below 80% (bad). A null efficiency (0.00%) has no style. An info icon next to the header of the efficiency column displays the legend of the levels. - The columns Fill #, Stable beams, SB Duration and Runs duration are narrower, to leave more space to the Scheme name and the Runs columns. The beam type badge is truncated if it does not fit in the Fill # column. Notable changes for developers: - New css classes in `app.css`: `efficiency*`, `runs-duration-label`. - The tests check the style of the efficiency and its legend. --- lib/public/app.css | 43 +++++++++++++++++++ .../lhcFillsOverviewActiveColumns.js | 27 ++++++------ test/public/lhcFills/overview.test.js | 16 +++++++ 3 files changed, 74 insertions(+), 12 deletions(-) diff --git a/lib/public/app.css b/lib/public/app.css index a8bd988949..8e4c81f3cd 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -877,6 +877,10 @@ label { min-width: 6.5em; } +.runs-duration-label { + min-width: 3em; +} + .invisible { visibility: hidden; } @@ -946,6 +950,10 @@ label { line-height: 1; letter-spacing: 0.02em; white-space: nowrap; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; } .beam-type-lead-lead { @@ -979,6 +987,41 @@ label { text-align: left; } +/* + * Efficiency of the LHC fills overview: from the best to the worst level, a solid, a soft and a pale color. No efficiency has no style + */ +.efficiency { + display: inline-block; + padding: 0.1em 0.45em; + border-radius: 0.25rem; + font-weight: 500; + line-height: 1.4; +} + +.efficiency-great { + background: var(--color-success); + color: var(--color-white); +} + +.efficiency-good { + background: color-mix(in srgb, var(--color-success) 25%, var(--color-white)); + color: var(--color-success-dark); +} + +.efficiency-bad { + background: color-mix(in srgb, var(--color-danger) 15%, var(--color-white)); + color: var(--color-danger-dark); +} + +.efficiency-legend-label { + width: 6rem; + flex-shrink: 0; +} + +.efficiency-legend-description { + text-align: left; +} + /* Bold text */ .bold { font-weight: 700; diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsOverviewActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsOverviewActiveColumns.js index c6fff9ea6b..4d576c3a7e 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsOverviewActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsOverviewActiveColumns.js @@ -22,13 +22,15 @@ import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; import { formatTimestampAlignedWith } from '../../../utilities/formatting/formatTimestampAlignedWith.js'; import { formatDuration } from '../../../utilities/formatting/formatDuration.mjs'; -import { formatPercentage } from '../../../utilities/formatting/formatPercentage.js'; import { formatLhcFillsTimeLoss } from '../format/formatLhcFillsTimeLoss.js'; import { formatBeamTypeBadge } from '../format/formatBeamTypeBadge.js'; +import { formatEfficiency } from '../format/formatEfficiency.js'; +import { efficiencyLegend } from '../format/efficiencyLegend.js'; import { beamTypeLegend } from '../format/beamTypeLegend.js'; const TIMING_LINE = { labelClass: 'timing-label' }; -const DURATION_LINE = { labelClass: 'efficiency-label' }; +const EFFICIENCY_LINE = { labelClass: 'efficiency-label' }; +const RUNS_DURATION_LINE = { labelClass: 'runs-duration-label' }; /** * Return a copy of the given column, hidden from the table but still providing its filter to the filters panel @@ -49,7 +51,7 @@ export const lhcFillsOverviewActiveColumns = { ...lhcFillsActiveColumns.fillNumber, name: 'Fill #', information: beamTypeLegend(), - size: 'w-10', + size: 'w-7', noEllipsis: true, /** @@ -82,7 +84,7 @@ export const lhcFillsOverviewActiveColumns = { stableBeamsStartAndEnd: { name: 'Stable beams', visible: true, - size: 'w-20', + size: 'w-15', classes: 'f6', noEllipsis: true, @@ -106,10 +108,11 @@ export const lhcFillsOverviewActiveColumns = { stableBeamsStart: hiddenColumn('stableBeamsStart'), stableBeamsEnd: hiddenColumn('stableBeamsEnd'), stableBeams: lhcFillsActiveColumns.stableBeams, - stableBeamsDuration: { ...lhcFillsActiveColumns.stableBeamsDuration, name: 'SB Duration', size: 'w-10', classes: 'f6' }, + stableBeamsDuration: { ...lhcFillsActiveColumns.stableBeamsDuration, name: 'SB Duration', size: 'w-7', classes: 'f6' }, efficiency: { ...lhcFillsActiveColumns.efficiency, name: 'Efficiency', + information: efficiencyLegend(), size: 'w-15', classes: 'f6', noEllipsis: true, @@ -122,12 +125,12 @@ export const lhcFillsOverviewActiveColumns = { * @return {Component} the efficiency and the time loss */ format: (efficiency, { fillNumber, timeLossAtStart, efficiencyLossAtStart }) => h('', [ - labelledLine(`row${fillNumber}-efficiencyValue`, 'Efficiency', formatPercentage(efficiency), DURATION_LINE), + labelledLine(`row${fillNumber}-efficiencyValue`, 'Efficiency', formatEfficiency(efficiency), EFFICIENCY_LINE), labelledLine( `row${fillNumber}-timeLossAtStart`, 'Before 1st run', formatLhcFillsTimeLoss(timeLossAtStart, efficiencyLossAtStart), - DURATION_LINE, + EFFICIENCY_LINE, ), ]), }, @@ -136,7 +139,7 @@ export const lhcFillsOverviewActiveColumns = { runsDurations: { name: 'Runs duration', visible: true, - size: 'w-15', + size: 'w-10', classes: 'f6', noEllipsis: true, @@ -148,8 +151,8 @@ export const lhcFillsOverviewActiveColumns = { * @return {Component} the mean and total durations */ format: (_, { fillNumber, meanRunDuration, runsCoverage }) => h('', [ - labelledLine(`row${fillNumber}-meanRunDuration`, 'Mean', formatDuration(meanRunDuration), DURATION_LINE), - labelledLine(`row${fillNumber}-runsCoverage`, 'Total', formatDuration(runsCoverage), DURATION_LINE), + labelledLine(`row${fillNumber}-meanRunDuration`, 'Mean', formatDuration(meanRunDuration), RUNS_DURATION_LINE), + labelledLine(`row${fillNumber}-runsCoverage`, 'Total', formatDuration(runsCoverage), RUNS_DURATION_LINE), ]), }, meanRunDuration: hiddenColumn('meanRunDuration'), @@ -158,7 +161,7 @@ export const lhcFillsOverviewActiveColumns = { collidingBunches: hiddenColumn('collidingBunches'), fillingSchemeName: { ...lhcFillsActiveColumns.fillingSchemeName, - size: 'w-15', + size: 'w-20', classes: 'f6', balloon: false, noEllipsis: true, @@ -175,5 +178,5 @@ export const lhcFillsOverviewActiveColumns = { labelledLine(`row${fillNumber}-collidingBunches`, 'Bunches', collidingBunchesCount || '-'), ]), }, - runs: { ...lhcFillsActiveColumns.runs, size: 'w-10', classes: 'f6' }, + runs: { ...lhcFillsActiveColumns.runs, size: 'w-20', classes: 'f6' }, }; diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 85a2eb9916..faf7d92bef 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -27,6 +27,7 @@ const { fillInput, getPeriodInputsSelectors, getPopoverSelector, + getPopoverContent, } = require('../defaults.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -238,6 +239,21 @@ module.exports = () => { await expectInnerText(page, '#row6-schemeName', 'Single_12b_8_1024_8_2018'); }); + it('should display the efficiency with a style depending on its level, and the legend of the levels', async () => { + await goToPage(page, 'lhc-fill-overview'); + + // Fill 6 has an efficiency of 41.67%, which is bad. The fills without efficiency (0.00%) have no style + await page.waitForSelector('#row6-efficiencyValue .efficiency-bad'); + await page.waitForSelector('#row3-efficiencyValue'); + expect(await page.$('#row3-efficiencyValue .efficiency')).to.be.null; + + const legendTrigger = await page.waitForSelector('thead th#efficiency .popover-trigger'); + const legend = await getPopoverContent(legendTrigger); + for (const level of ['Great', 'Good', 'Bad']) { + expect(legend).to.include(level); + } + }); + it('should successfully display the start, the end and the duration of the stable beams', async () => { await goToPage(page, 'lhc-fill-overview'); From 9b49a747eb525ff391d52f5b0225ad8414f57fd9 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 16:33:01 +0200 Subject: [PATCH 09/11] [O2B-1641] Extend the scale of the efficiency of the LHC fills with fair and poor levels Notable changes for users: - The efficiency has five levels instead of three: great (95% and above, solid green), good (80% to below 95%, soft green), fair (70% to below 80%, yellow), poor (50% to below 70%, orange) and bad (below 50%, red). A null efficiency (0.00%) still has no style. The legend of the efficiency column displays the five levels. Notable changes for developers: - The thresholds of the levels are listed in `EFFICIENCY_LEVELS_THRESHOLDS`. - Unit and UI tests are updated for the new levels. They were not run locally. --- lib/public/app.css | 12 ++++++++++- .../views/LhcFills/format/efficiencyLegend.js | 10 +++++---- .../views/LhcFills/format/efficiencyLevel.js | 21 ++++++++++++------- .../views/LhcFills/efficiencyLevel.test.js | 15 +++++++++++-- test/public/lhcFills/overview.test.js | 2 +- 5 files changed, 45 insertions(+), 15 deletions(-) diff --git a/lib/public/app.css b/lib/public/app.css index 8e4c81f3cd..e0e0c035b4 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -988,7 +988,7 @@ label { } /* - * Efficiency of the LHC fills overview: from the best to the worst level, a solid, a soft and a pale color. No efficiency has no style + * Efficiency of the LHC fills overview: from the best to the worst level, solid green, soft green, yellow, orange and red. No efficiency has no style */ .efficiency { display: inline-block; @@ -1008,6 +1008,16 @@ label { color: var(--color-success-dark); } +.efficiency-fair { + background: #fbf0b8; + color: #6b5600; +} + +.efficiency-poor { + background: color-mix(in srgb, var(--color-warning) 28%, var(--color-white)); + color: var(--color-warning-darker); +} + .efficiency-bad { background: color-mix(in srgb, var(--color-danger) 15%, var(--color-white)); color: var(--color-danger-dark); diff --git a/lib/public/views/LhcFills/format/efficiencyLegend.js b/lib/public/views/LhcFills/format/efficiencyLegend.js index b2a68c2f7c..06565f179c 100644 --- a/lib/public/views/LhcFills/format/efficiencyLegend.js +++ b/lib/public/views/LhcFills/format/efficiencyLegend.js @@ -15,9 +15,11 @@ import { h } from '/js/src/index.js'; import { EfficiencyLevels } from './efficiencyLevel.js'; const EFFICIENCY_LEVELS_DESCRIPTIONS = [ - [EfficiencyLevels.GREAT, '95.00%', '95% and above'], - [EfficiencyLevels.GOOD, '80.00%', 'From 80% to below 95%'], - [EfficiencyLevels.BAD, '50.00%', 'Below 80%'], + [EfficiencyLevels.GREAT, '95.00%', 'Great: 95% and above'], + [EfficiencyLevels.GOOD, '80.00%', 'Good: from 80% to below 95%'], + [EfficiencyLevels.FAIR, '70.00%', 'Fair: from 70% to below 80%'], + [EfficiencyLevels.POOR, '50.00%', 'Poor: from 50% to below 70%'], + [EfficiencyLevels.BAD, '25.00%', 'Bad: below 50%'], ]; /** @@ -30,7 +32,7 @@ export const efficiencyLegend = () => h('.flex-column.g3', [ h('.flex-column.g2', [ ...EFFICIENCY_LEVELS_DESCRIPTIONS.map(([level, example, description]) => h('.flex-row.items-baseline.g3', [ h('.efficiency-legend-label.flex-row', h(`.efficiency.efficiency-${level}`, example)), - h('.efficiency-legend-description', `${level[0].toUpperCase()}${level.slice(1)}: ${description}`), + h('.efficiency-legend-description', description), ])), h('.flex-row.items-baseline.g3', [ h('.efficiency-legend-label.flex-row', '0.00%'), diff --git a/lib/public/views/LhcFills/format/efficiencyLevel.js b/lib/public/views/LhcFills/format/efficiencyLevel.js index 22ff6c383b..b4dbc140a2 100644 --- a/lib/public/views/LhcFills/format/efficiencyLevel.js +++ b/lib/public/views/LhcFills/format/efficiencyLevel.js @@ -19,14 +19,23 @@ export const EfficiencyLevels = Object.freeze({ GREAT: 'great', GOOD: 'good', + FAIR: 'fair', + POOR: 'poor', BAD: 'bad', }); -export const GREAT_EFFICIENCY_THRESHOLD = 0.95; -export const GOOD_EFFICIENCY_THRESHOLD = 0.8; +/** + * Minimal efficiency (normalized between 0 and 1) of each level, from the best to the worst one. Below the last threshold, the level is bad + */ +export const EFFICIENCY_LEVELS_THRESHOLDS = Object.freeze([ + [EfficiencyLevels.GREAT, 0.95], + [EfficiencyLevels.GOOD, 0.8], + [EfficiencyLevels.FAIR, 0.7], + [EfficiencyLevels.POOR, 0.5], +]); /** - * Returns the level of the given efficiency: great from 95%, good from 80% to 95% excluded, else bad + * Returns the level of the given efficiency: great from 95%, good from 80%, fair from 70%, poor from 50%, else bad * * @param {number|null|undefined} efficiency the efficiency of the fill, normalized between 0 and 1 * @return {EfficiencyLevels|null} the level, or null if the efficiency is unknown or null (no level is displayed for them) @@ -35,8 +44,6 @@ export const getEfficiencyLevel = (efficiency) => { if (typeof efficiency !== 'number' || isNaN(efficiency) || efficiency === 0) { return null; } - if (efficiency >= GREAT_EFFICIENCY_THRESHOLD) { - return EfficiencyLevels.GREAT; - } - return efficiency >= GOOD_EFFICIENCY_THRESHOLD ? EfficiencyLevels.GOOD : EfficiencyLevels.BAD; + const [level] = EFFICIENCY_LEVELS_THRESHOLDS.find(([, threshold]) => efficiency >= threshold) ?? [EfficiencyLevels.BAD]; + return level; }; diff --git a/test/lib/public/views/LhcFills/efficiencyLevel.test.js b/test/lib/public/views/LhcFills/efficiencyLevel.test.js index 015611a964..1712cd57db 100644 --- a/test/lib/public/views/LhcFills/efficiencyLevel.test.js +++ b/test/lib/public/views/LhcFills/efficiencyLevel.test.js @@ -38,8 +38,19 @@ module.exports = () => { expect(getEfficiencyLevel(0.9499)).to.equal('good'); }); - it('should have a bad level below 80%', () => { - expect(getEfficiencyLevel(0.7999)).to.equal('bad'); + it('should have a fair level from 70% to 80% excluded', () => { + expect(getEfficiencyLevel(0.7)).to.equal('fair'); + expect(getEfficiencyLevel(0.76)).to.equal('fair'); + expect(getEfficiencyLevel(0.7999)).to.equal('fair'); + }); + + it('should have a poor level from 50% to 70% excluded', () => { + expect(getEfficiencyLevel(0.5)).to.equal('poor'); + expect(getEfficiencyLevel(0.6999)).to.equal('poor'); + }); + + it('should have a bad level below 50%', () => { + expect(getEfficiencyLevel(0.4999)).to.equal('bad'); expect(getEfficiencyLevel(0.4167)).to.equal('bad'); expect(getEfficiencyLevel(0.0001)).to.equal('bad'); }); diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index faf7d92bef..c52d913cff 100644 --- a/test/public/lhcFills/overview.test.js +++ b/test/public/lhcFills/overview.test.js @@ -249,7 +249,7 @@ module.exports = () => { const legendTrigger = await page.waitForSelector('thead th#efficiency .popover-trigger'); const legend = await getPopoverContent(legendTrigger); - for (const level of ['Great', 'Good', 'Bad']) { + for (const level of ['Great', 'Good', 'Fair', 'Poor', 'Bad']) { expect(legend).to.include(level); } }); From ecc75bfd3474a4f340f426bbda42c86d18231e6e Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 16:37:04 +0200 Subject: [PATCH 10/11] [O2B-1641] Give the free space of the LHC fills overview to its runs Notable changes for users: - The columns Fill #, Stable beams, SB Duration, Efficiency and Runs duration have a fixed width fitting their content, instead of a part of the screen leaving unused space around it. The Scheme name takes 20% of the screen and the Runs column all the remaining width, so more runs are displayed (and the dates are not truncated on smaller screens). Notable changes for developers: - New `w-lhc-fills-*-column` css classes. --- lib/public/app.css | 24 +++++++++++++++++++ .../lhcFillsOverviewActiveColumns.js | 17 ++++++++----- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/lib/public/app.css b/lib/public/app.css index e0e0c035b4..cccddff024 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -938,6 +938,30 @@ label { text-align: left; } +/* + * Columns of the LHC fills overview: the columns whose content has a known width have a fixed one, the scheme name takes a part of the screen + * and the runs, which have the most variable content, take all the remaining width + */ +.w-lhc-fills-fill-column { + width: 9.5rem; +} + +.w-lhc-fills-stable-beams-column { + width: 14rem; +} + +.w-lhc-fills-sb-duration-column { + width: 7rem; +} + +.w-lhc-fills-efficiency-column { + width: 16rem; +} + +.w-lhc-fills-runs-duration-column { + width: 8rem; +} + /* * Beam types of the LHC fills overview, by what collides: only shapes and neutral shades are used, as colors are reserved to states */ diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsOverviewActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsOverviewActiveColumns.js index 4d576c3a7e..c4f6006bac 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsOverviewActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsOverviewActiveColumns.js @@ -51,7 +51,7 @@ export const lhcFillsOverviewActiveColumns = { ...lhcFillsActiveColumns.fillNumber, name: 'Fill #', information: beamTypeLegend(), - size: 'w-7', + size: 'w-lhc-fills-fill-column', noEllipsis: true, /** @@ -84,7 +84,7 @@ export const lhcFillsOverviewActiveColumns = { stableBeamsStartAndEnd: { name: 'Stable beams', visible: true, - size: 'w-15', + size: 'w-lhc-fills-stable-beams-column', classes: 'f6', noEllipsis: true, @@ -108,12 +108,17 @@ export const lhcFillsOverviewActiveColumns = { stableBeamsStart: hiddenColumn('stableBeamsStart'), stableBeamsEnd: hiddenColumn('stableBeamsEnd'), stableBeams: lhcFillsActiveColumns.stableBeams, - stableBeamsDuration: { ...lhcFillsActiveColumns.stableBeamsDuration, name: 'SB Duration', size: 'w-7', classes: 'f6' }, + stableBeamsDuration: { + ...lhcFillsActiveColumns.stableBeamsDuration, + name: 'SB Duration', + size: 'w-lhc-fills-sb-duration-column', + classes: 'f6', + }, efficiency: { ...lhcFillsActiveColumns.efficiency, name: 'Efficiency', information: efficiencyLegend(), - size: 'w-15', + size: 'w-lhc-fills-efficiency-column', classes: 'f6', noEllipsis: true, @@ -139,7 +144,7 @@ export const lhcFillsOverviewActiveColumns = { runsDurations: { name: 'Runs duration', visible: true, - size: 'w-10', + size: 'w-lhc-fills-runs-duration-column', classes: 'f6', noEllipsis: true, @@ -178,5 +183,5 @@ export const lhcFillsOverviewActiveColumns = { labelledLine(`row${fillNumber}-collidingBunches`, 'Bunches', collidingBunchesCount || '-'), ]), }, - runs: { ...lhcFillsActiveColumns.runs, size: 'w-20', classes: 'f6' }, + runs: { ...lhcFillsActiveColumns.runs, size: '', classes: 'f6' }, }; From d754c719bd37913bacdf12352573cd5089e509c0 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 26 Oct 2026 11:23:36 +0100 Subject: [PATCH 11/11] [O2B-1641] Generate the legend of the efficiency from its scale Notable changes for developers: - The scale of the efficiency is defined once, in `EFFICIENCY_SCALE`: both `getEfficiencyLevel` and `efficiencyLegend` are computed from it, so changing a threshold or adding a level only needs to be done there. The `EfficiencyLevels` enum is removed. - The legend labels the levels with their names and the ranges are generated from the thresholds. --- .../views/LhcFills/format/efficiencyLegend.js | 42 +++++++++++++------ .../views/LhcFills/format/efficiencyLevel.js | 34 ++++++--------- .../views/LhcFills/efficiencyLevel.test.js | 9 +++- 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/lib/public/views/LhcFills/format/efficiencyLegend.js b/lib/public/views/LhcFills/format/efficiencyLegend.js index 06565f179c..efdf9186c6 100644 --- a/lib/public/views/LhcFills/format/efficiencyLegend.js +++ b/lib/public/views/LhcFills/format/efficiencyLegend.js @@ -12,27 +12,45 @@ */ import { h } from '/js/src/index.js'; -import { EfficiencyLevels } from './efficiencyLevel.js'; +import { formatPercentage } from '../../../utilities/formatting/formatPercentage.js'; +import { EFFICIENCY_SCALE } from './efficiencyLevel.js'; -const EFFICIENCY_LEVELS_DESCRIPTIONS = [ - [EfficiencyLevels.GREAT, '95.00%', 'Great: 95% and above'], - [EfficiencyLevels.GOOD, '80.00%', 'Good: from 80% to below 95%'], - [EfficiencyLevels.FAIR, '70.00%', 'Fair: from 70% to below 80%'], - [EfficiencyLevels.POOR, '50.00%', 'Poor: from 50% to below 70%'], - [EfficiencyLevels.BAD, '25.00%', 'Bad: below 50%'], -]; +/** + * Format a limit of the scale of the efficiency (e.g. 95%) + * + * @param {number} efficiency the efficiency, normalized between 0 and 1 + * @return {string} the formatted limit + */ +const formatLimit = (efficiency) => formatPercentage(efficiency, 0); + +/** + * Describe the range of efficiencies of the level at the given index of the scale + * + * @param {number} index the index of the level in the scale + * @return {string} the description of the range + */ +const describeRange = (index) => { + const { minimum } = EFFICIENCY_SCALE[index]; + if (index === 0) { + return `${formatLimit(minimum)} and above`; + } + + const upperLimit = formatLimit(EFFICIENCY_SCALE[index - 1].minimum); + return index === EFFICIENCY_SCALE.length - 1 ? `Below ${upperLimit}` : `From ${formatLimit(minimum)} to below ${upperLimit}`; +}; /** - * Render the legend of the efficiency, as displayed in the efficiency column of the LHC fills overview + * Render the legend of the efficiency, as displayed in the efficiency column of the LHC fills overview. It is generated from the scale of the + * efficiency * * @return {Component} the legend */ export const efficiencyLegend = () => h('.flex-column.g3', [ h('h3', 'Efficiency legend'), h('.flex-column.g2', [ - ...EFFICIENCY_LEVELS_DESCRIPTIONS.map(([level, example, description]) => h('.flex-row.items-baseline.g3', [ - h('.efficiency-legend-label.flex-row', h(`.efficiency.efficiency-${level}`, example)), - h('.efficiency-legend-description', description), + ...EFFICIENCY_SCALE.map(({ level }, index) => h('.flex-row.items-baseline.g3', [ + h('.efficiency-legend-label.flex-row', h(`.efficiency.efficiency-${level}`, `${level[0].toUpperCase()}${level.slice(1)}`)), + h('.efficiency-legend-description', describeRange(index)), ])), h('.flex-row.items-baseline.g3', [ h('.efficiency-legend-label.flex-row', '0.00%'), diff --git a/lib/public/views/LhcFills/format/efficiencyLevel.js b/lib/public/views/LhcFills/format/efficiencyLevel.js index b4dbc140a2..6f699051c9 100644 --- a/lib/public/views/LhcFills/format/efficiencyLevel.js +++ b/lib/public/views/LhcFills/format/efficiencyLevel.js @@ -12,38 +12,30 @@ */ /** - * Levels of efficiency of a LHC fill + * Scale of the efficiency of a LHC fill, from the best level to the worst one. This is the only definition of the scale: the level of an + * efficiency and the legend are both computed from it * - * @enum {string} + * @type {{level: string, minimum: number}[]} the levels, with the minimal efficiency (normalized between 0 and 1) to reach them. The last one + * has no real minimum, it is the level of everything below the previous one */ -export const EfficiencyLevels = Object.freeze({ - GREAT: 'great', - GOOD: 'good', - FAIR: 'fair', - POOR: 'poor', - BAD: 'bad', -}); - -/** - * Minimal efficiency (normalized between 0 and 1) of each level, from the best to the worst one. Below the last threshold, the level is bad - */ -export const EFFICIENCY_LEVELS_THRESHOLDS = Object.freeze([ - [EfficiencyLevels.GREAT, 0.95], - [EfficiencyLevels.GOOD, 0.8], - [EfficiencyLevels.FAIR, 0.7], - [EfficiencyLevels.POOR, 0.5], +export const EFFICIENCY_SCALE = Object.freeze([ + { level: 'great', minimum: 0.95 }, + { level: 'good', minimum: 0.8 }, + { level: 'fair', minimum: 0.7 }, + { level: 'poor', minimum: 0.5 }, + { level: 'bad', minimum: 0 }, ]); /** - * Returns the level of the given efficiency: great from 95%, good from 80%, fair from 70%, poor from 50%, else bad + * Returns the level of the given efficiency according to the scale * * @param {number|null|undefined} efficiency the efficiency of the fill, normalized between 0 and 1 - * @return {EfficiencyLevels|null} the level, or null if the efficiency is unknown or null (no level is displayed for them) + * @return {string|null} the level, or null if the efficiency is unknown or null (no level is displayed for them) */ export const getEfficiencyLevel = (efficiency) => { if (typeof efficiency !== 'number' || isNaN(efficiency) || efficiency === 0) { return null; } - const [level] = EFFICIENCY_LEVELS_THRESHOLDS.find(([, threshold]) => efficiency >= threshold) ?? [EfficiencyLevels.BAD]; + const { level } = EFFICIENCY_SCALE.find(({ minimum }) => efficiency >= minimum) ?? EFFICIENCY_SCALE[EFFICIENCY_SCALE.length - 1]; return level; }; diff --git a/test/lib/public/views/LhcFills/efficiencyLevel.test.js b/test/lib/public/views/LhcFills/efficiencyLevel.test.js index 1712cd57db..70a6674b66 100644 --- a/test/lib/public/views/LhcFills/efficiencyLevel.test.js +++ b/test/lib/public/views/LhcFills/efficiencyLevel.test.js @@ -14,10 +14,17 @@ const { expect } = require('chai'); let getEfficiencyLevel; +let EFFICIENCY_SCALE; module.exports = () => { before(async () => { - ({ getEfficiencyLevel } = await import('../../../../../lib/public/views/LhcFills/format/efficiencyLevel.js')); + ({ getEfficiencyLevel, EFFICIENCY_SCALE } = await import('../../../../../lib/public/views/LhcFills/format/efficiencyLevel.js')); + }); + + it('should have a scale sorted from the best level to the worst one, ending at 0', () => { + const minimums = EFFICIENCY_SCALE.map(({ minimum }) => minimum); + expect(minimums).to.deep.equal([...minimums].sort((a, b) => b - a)); + expect(minimums[minimums.length - 1]).to.equal(0); }); it('should have no level for a missing or null efficiency', () => {