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/lib/public/app.css b/lib/public/app.css index c149cf8f88..cccddff024 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%; } @@ -864,15 +864,23 @@ 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; } +.efficiency-label { + min-width: 6.5em; +} + +.runs-duration-label { + min-width: 3em; +} + .invisible { visibility: hidden; } @@ -922,7 +930,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 +938,124 @@ 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 + */ +.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; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; +} + +.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; +} + +/* + * 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; + 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-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); +} + +.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/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/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/ActiveColumns/lhcFillsOverviewActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsOverviewActiveColumns.js new file mode 100644 index 0000000000..c4f6006bac --- /dev/null +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsOverviewActiveColumns.js @@ -0,0 +1,187 @@ +/** + * @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 { 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 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 + * + * @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-lhc-fills-fill-column', + 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-lhc-fills-stable-beams-column', + 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-lhc-fills-sb-duration-column', + classes: 'f6', + }, + efficiency: { + ...lhcFillsActiveColumns.efficiency, + name: 'Efficiency', + information: efficiencyLegend(), + size: 'w-lhc-fills-efficiency-column', + 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', formatEfficiency(efficiency), EFFICIENCY_LINE), + labelledLine( + `row${fillNumber}-timeLossAtStart`, + 'Before 1st run', + formatLhcFillsTimeLoss(timeLossAtStart, efficiencyLossAtStart), + EFFICIENCY_LINE, + ), + ]), + }, + timeLossAtStart: hiddenColumn('timeLossAtStart'), + timeLossAtEnd: lhcFillsActiveColumns.timeLossAtEnd, + runsDurations: { + name: 'Runs duration', + visible: true, + size: 'w-lhc-fills-runs-duration-column', + 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), RUNS_DURATION_LINE), + labelledLine(`row${fillNumber}-runsCoverage`, 'Total', formatDuration(runsCoverage), RUNS_DURATION_LINE), + ]), + }, + meanRunDuration: hiddenColumn('meanRunDuration'), + runsCoverage: hiddenColumn('runsCoverage'), + beamType: hiddenColumn('beamType'), + collidingBunches: hiddenColumn('collidingBunches'), + fillingSchemeName: { + ...lhcFillsActiveColumns.fillingSchemeName, + size: 'w-20', + 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: '', 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/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/LhcFills/format/efficiencyLegend.js b/lib/public/views/LhcFills/format/efficiencyLegend.js new file mode 100644 index 0000000000..efdf9186c6 --- /dev/null +++ b/lib/public/views/LhcFills/format/efficiencyLegend.js @@ -0,0 +1,60 @@ +/** + * @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 { EFFICIENCY_SCALE } from './efficiencyLevel.js'; + +/** + * 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. 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_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%'), + 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..6f699051c9 --- /dev/null +++ b/lib/public/views/LhcFills/format/efficiencyLevel.js @@ -0,0 +1,41 @@ +/** + * @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. + */ + +/** + * 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 + * + * @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 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 according to the scale + * + * @param {number|null|undefined} efficiency the efficiency of the fill, normalized between 0 and 1 + * @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_SCALE.find(({ minimum }) => efficiency >= minimum) ?? EFFICIENCY_SCALE[EFFICIENCY_SCALE.length - 1]; + return level; +}; 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) + : '-'; 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/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: { 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(); 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/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..70a6674b66 --- /dev/null +++ b/test/lib/public/views/LhcFills/efficiencyLevel.test.js @@ -0,0 +1,64 @@ +/** + * @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; +let EFFICIENCY_SCALE; + +module.exports = () => { + before(async () => { + ({ 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', () => { + 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 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/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); +}; 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'); diff --git a/test/public/lhcFills/overview.test.js b/test/public/lhcFills/overview.test.js index 4f0115bcc9..c52d913cff 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, @@ -28,6 +27,7 @@ const { fillInput, getPeriodInputsSelectors, getPopoverSelector, + getPopoverContent, } = require('../defaults.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -102,48 +102,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; } }); @@ -155,15 +129,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 +149,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 +215,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 +228,43 @@ 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 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', 'Fair', 'Poor', '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'); - 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 +342,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 +362,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 +381,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));