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