From acbfb68756c45d1c3c40acab13bf8fef168000c9 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 16:46:51 +0200 Subject: [PATCH 1/9] [O2B-1642] Add the formatting of the status history and of the configuration of the environments Notable changes for developers: - New `formatEnvironmentStatusHistory`: the acronym of each status in its color, with the time the environment stayed in it below it, separated by chevrons. Hovering a status displays its message and the time it was reached. - New `parseEnvironmentConfiguration`: extracts the run type, the beam type, the detectors and the number of FLPs and EPNs of the raw configuration of an environment. --- .../format/formatEnvironmentStatusHistory.js | 47 +++++++++++++ .../format/parseEnvironmentConfiguration.js | 67 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 lib/public/views/Environments/format/formatEnvironmentStatusHistory.js create mode 100644 lib/public/views/Environments/format/parseEnvironmentConfiguration.js diff --git a/lib/public/views/Environments/format/formatEnvironmentStatusHistory.js b/lib/public/views/Environments/format/formatEnvironmentStatusHistory.js new file mode 100644 index 0000000000..3faa649985 --- /dev/null +++ b/lib/public/views/Environments/format/formatEnvironmentStatusHistory.js @@ -0,0 +1,47 @@ +/** + * @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 { coloredEnvironmentStatusComponent } from '../ColoredEnvironmentStatusComponent.js'; +import { StatusAcronym } from '../../../domain/enums/statusAcronym.mjs'; +import { tooltip } from '../../../components/common/popover/tooltip.js'; +import { formatDuration } from '../../../utilities/formatting/formatDuration.mjs'; +import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; + +/** + * Display the status history of an environment: the acronym of each status in its color, with the time the environment stayed in it below it, + * separated by chevrons. Hovering a status displays its message and the time it was reached + * + * @param {{status: string, statusMessage: string, createdAt: number}[]} historyItems the history items of the environment, sorted + * @return {Component} the status history + */ +export const formatEnvironmentStatusHistory = (historyItems) => h( + '.flex-row.flex-wrap.items-start.g1', + historyItems.flatMap(({ status, statusMessage, createdAt }, index) => { + const nextItem = historyItems[index + 1]; + return [ + h('span.gray-dark', '\u203A'), + tooltip( + h('.flex-column.items-center', [ + h('strong', coloredEnvironmentStatusComponent(status, StatusAcronym[status])), + h('.f7.gray-darker', nextItem ? formatDuration(nextItem.createdAt - createdAt) : '\u00A0'), + ]), + h('.flex-column', [ + h('strong', status), + statusMessage && h('', statusMessage), + h('.f7', formatTimestamp(createdAt, true)), + ]), + ), + ]; + }).slice(1), +); diff --git a/lib/public/views/Environments/format/parseEnvironmentConfiguration.js b/lib/public/views/Environments/format/parseEnvironmentConfiguration.js new file mode 100644 index 0000000000..d934cd2514 --- /dev/null +++ b/lib/public/views/Environments/format/parseEnvironmentConfiguration.js @@ -0,0 +1,67 @@ +/** + * @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. + */ + +/** + * Parse a value of the environment configuration that may be a JSON serialized list (e.g. `["FT0", "ITS"]`) or an already parsed one + * + * @param {*} value the value to parse + * @return {string[]} the list + */ +const parseList = (value) => { + if (Array.isArray(value)) { + return value; + } + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +}; + +/** + * @typedef EnvironmentConfigurationSummary + * @property {string|null} runType the type of the runs of the environment + * @property {string|null} beamType the PDP beam type + * @property {string[]} detectors the detectors of the environment + * @property {number|null} flpsCount the number of FLPs (hosts) + * @property {number|null} epnsCount the number of EPNs + */ + +/** + * Extract the most relevant information of the raw configuration of an environment + * + * @param {string|null} rawConfiguration the raw configuration of the environment (JSON serialized) + * @return {EnvironmentConfigurationSummary|null} the summary, or null if there is no valid configuration + */ +export const parseEnvironmentConfiguration = (rawConfiguration) => { + if (!rawConfiguration) { + return null; + } + + try { + const configuration = JSON.parse(rawConfiguration); + const hosts = parseList(configuration.hosts); + const epnsCount = parseInt(configuration.odc_n_epns, 10); + + return { + runType: configuration.run_type ?? null, + beamType: configuration.pdp_beam_type ?? null, + detectors: parseList(configuration.detectors), + flpsCount: hosts.length > 0 ? hosts.length : null, + epnsCount: isNaN(epnsCount) ? null : epnsCount, + }; + } catch { + return null; + } +}; From f9fd2a4499d594c2d0ec86094a63846c5549cbd5 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 16:46:51 +0200 Subject: [PATCH 2/9] [O2B-1642] Improve the readability of the environments overview columns Notable changes for users: - The environments overview has fixed widths fitting their content for its columns, the configuration taking the remaining width: - **Environment**: the id is a larger link followed by a discreet caret opening the same actions as before. - **Current Status**: the current status (with the warning icon if the environment has been in error) with its message below it. - **Timing**: the creation and the last update on labelled and aligned lines. When both happen the same day, only the time of the update is displayed. - **Status History**: the status are displayed with the time spent in each of them, hovering one displays its message and when it was reached. - **Runs**: narrower, as an environment has few runs. - **Configuration** (new): the run type, the beam type, the detectors and the number of FLPs and EPNs of the environment. - The default number of rows of the page is estimated for the new height of the rows. - All the filters are still available, in the same filters panel, with unchanged URL parameters. The filter on runs is displayed after the ones on the status and the status history. Notable changes for developers: - The `environmentsActiveColumns` columns merged into others are kept as hidden columns to provide their filters. - New `w-environments-*-column` css classes. - Elements have stable ids to be used in UI tests: `#row-id`, `-currentStatus`, `-statusMessage`, `-createdAt`, `-updatedAt`, `-runType`, `-beamType`, `-detectors`, `-flps`, `-epns`. - The tests of the environments overview are not updated yet. --- lib/public/app.css | 24 +++ .../environmentsActiveColumns.js | 187 +++++++++++++----- .../Overview/environmentOverviewComponent.js | 4 +- 3 files changed, 164 insertions(+), 51 deletions(-) diff --git a/lib/public/app.css b/lib/public/app.css index cccddff024..e0a25f1f90 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -962,6 +962,30 @@ label { width: 8rem; } +/* + * Columns of the environments overview: all of them but the configuration, which takes the remaining width, have a fixed width fitting their + * content + */ +.w-environments-id-column { + width: 10rem; +} + +.w-environments-status-column { + width: 12rem; +} + +.w-environments-timing-column { + width: 14rem; +} + +.w-environments-history-column { + width: 20rem; +} + +.w-environments-runs-column { + width: 8rem; +} + /* * Beam types of the LHC fills overview, by what collides: only shapes and neutral shades are used, as colors are reserved to states */ diff --git a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js index b7bb400bc1..399b116e3b 100644 --- a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js +++ b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js @@ -14,30 +14,103 @@ import { CopyToClipboardComponent, h } from '/js/src/index.js'; import { iconPlus } from '/js/src/icons.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; +import { formatTimestampAlignedWith } from '../../../utilities/formatting/formatTimestampAlignedWith.js'; +import { labelledLine } from '../../../components/common/formatting/labelledLine.js'; +import { linkWithActionsDropdown } from '../../../components/common/selection/infoLoggerButtonGroup/linkWithActionsDropdown.js'; +import { profiles } from '../../../components/common/table/profiles.js'; import { formatRunsList } from '../../Runs/format/formatRunsList.js'; import { displayEnvironmentStatus } from '../format/displayEnvironmentStatus.js'; -import { buttonLinkWithDropdown } from '../../../components/common/selection/infoLoggerButtonGroup/buttonLinkWithDropdown.js'; import { infologgerLinksComponents } from '../../../components/common/externalLinks/infologgerLinksComponents.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js';; -import { coloredEnvironmentStatusComponent } from '../ColoredEnvironmentStatusComponent.js'; import { environmentStatusHistoryLegendComponent } from '../../../components/environments/environmentStatusHistoryComponent.js'; +import { overflowBalloon } from '../../../components/common/popover/overflowBalloon.js'; import { infoTooltip } from '../../../components/common/popover/infoTooltip.js'; import { aliEcsEnvironmentLinkComponent } from '../../../components/common/externalLinks/aliEcsEnvironmentLinkComponent.js'; -import { StatusAcronym } from '../../../domain/enums/statusAcronym.mjs'; +import { formatEnvironmentStatusHistory } from '../format/formatEnvironmentStatusHistory.js'; +import { parseEnvironmentConfiguration } from '../format/parseEnvironmentConfiguration.js'; import { timeRangeFilter } from '../../../components/Filters/common/filters/timeRangeFilter.js'; import { checkboxes } from '../../../components/Filters/common/filters/checkboxFilter.js'; import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; +const TIMING_LINE = { labelClass: 'timing-label' }; + +/** + * Return a copy of the given column, hidden from the table but still providing its filter to the filters panel + * + * @param {Column} column the column + * @return {Column} the hidden column + */ +const hiddenColumn = (column) => ({ ...column, visible: false, profiles: [profiles.none] }); + +/** + * Display the configuration of the environment: its run type and beam type, its detectors and its number of FLPs and EPNs + * + * @param {*} _ unused, the configuration is read from the environment + * @param {Environment} environment the environment + * @return {Component|string} the summary of the configuration, or a dash if the environment has no valid configuration + */ +const formatConfiguration = (_, { id, rawConfiguration }) => { + const configuration = parseEnvironmentConfiguration(rawConfiguration); + if (!configuration) { + return '-'; + } + + const { runType, beamType, detectors, flpsCount, epnsCount } = configuration; + if (!runType && !beamType && detectors.length === 0 && flpsCount === null && epnsCount === null) { + return '-'; + } + + return h('', [ + h('.flex-row.g3', [ + labelledLine(`row${id}-runType`, 'Type', runType ?? '-'), + labelledLine(`row${id}-beamType`, 'Beam', beamType ?? '-'), + ]), + labelledLine(`row${id}-detectors`, 'Detectors', detectors.length > 0 ? detectors.join(', ') : '-'), + h('.flex-row.g3', [ + labelledLine(`row${id}-flps`, 'FLPs', flpsCount ?? '-'), + labelledLine(`row${id}-epns`, 'EPNs', epnsCount ?? '-'), + ]), + ]); +}; + +const createdAtColumn = { + name: 'Created At', + visible: true, + sortable: false, + + /** + * CreatedAt filter component + * + * @param {EnvironmentOverviewModel} environmentOverviewModel the environment overview model + * @return {Component} the filter component + */ + filter: (environmentOverviewModel) => timeRangeFilter(environmentOverviewModel.filteringModel.get('created')), +}; + /** - * List of active columns for a generic Environments component + * List of active columns for the environments overview + * + * The creation and update dates are displayed together in the timing column. The created at column is kept hidden to provide its filter, + * in the same order as before */ export const environmentsActiveColumns = { id: { - name: 'Id', - size: 'w-10 f6 w-wrapped', + name: 'Environment', + size: 'w-environments-id-column', + classes: 'f6', visible: true, primary: true, - format: (environmentId, { status, runs }) => buttonLinkWithDropdown( + noEllipsis: true, + + /** + * Display the id of the environment as a link with its actions + * + * @param {string} environmentId the id of the environment + * @param {Environment} environment the environment + * @return {Component} the environment id + */ + format: (environmentId, { status, runs }) => linkWithActionsDropdown( + `row${environmentId}-id`, environmentId, 'env-details', { environmentId }, @@ -54,7 +127,6 @@ export const environmentsActiveColumns = { { id: 'add-log-link', class: 'btn btn-primary h2' }, ), ], - '125px', ), /** @@ -66,72 +138,63 @@ export const environmentsActiveColumns = { */ filter: ({ filteringModel }) => textInputFilter(filteringModel, 'ids', 'e.g. CmCvjNbg, TDI59So3d...'), }, - runs: { - name: 'Runs', + createdAt: hiddenColumn(createdAtColumn), + status: { + name: 'Current Status', visible: true, sortable: false, - size: 'w-10', - format: formatRunsList, - balloon: true, + size: 'w-environments-status-column', + classes: 'f6', + noEllipsis: true, /** - * Run numbers filter component + * Display the current status of the environment, with its message below it * - * @param {object} environmentOverviewModel the overview model - * @param {FilteringModel} environmentOverviewModel.filteringModel the filtering model - * @return {Component} the filter component + * @param {*} _ unused, the status is read from the environment + * @param {Environment} environment the environment + * @return {Component} the current status and its message */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'runNumbers', 'e.g. 553203, 553221, ...'), - }, - updatedAt: { - name: 'Last Update', - visible: true, - sortable: false, - size: 'w-10', - format: (timestamp) => formatTimestamp(timestamp, false), - }, - createdAt: { - name: 'Created At', - visible: true, - sortable: false, - size: 'w-10', - format: (timestamp) => formatTimestamp(timestamp, false), + format: (_, environment) => h('', [ + h(`#row${environment.id}-currentStatus`, displayEnvironmentStatus(environment)), + h(`#row${environment.id}-statusMessage.f7.gray-darker`, overflowBalloon(environment.statusMessage || '-')), + ]), /** - * CreatedAt filter component + * Status filter component * * @param {EnvironmentOverviewModel} environmentOverviewModel the environment overview model * @return {Component} the filter component */ - filter: (environmentOverviewModel) => timeRangeFilter(environmentOverviewModel.filteringModel.get('created')), + filter: (environmentOverviewModel) => checkboxes(environmentOverviewModel.filteringModel.get('currentStatus')), }, - status: { - name: 'Current Status', + timing: { + name: 'Timing', visible: true, sortable: false, - size: 'w-10', + size: 'w-environments-timing-column', + classes: 'f6', noEllipsis: true, - format: (_, environment) => displayEnvironmentStatus(environment), /** - * Status filter component + * Display the creation and the last update of the environment, each in its own labelled line * - * @param {EnvironmentOverviewModel} environmentOverviewModel the environment overview model - * @return {Component} the filter component + * @param {*} _ unused, the dates are read from the environment + * @param {Environment} environment the environment + * @return {Component} the creation and the last update */ - filter: (environmentOverviewModel) => checkboxes(environmentOverviewModel.filteringModel.get('currentStatus')), + format: (_, { id, createdAt, updatedAt }) => h('', [ + labelledLine(`row${id}-createdAt`, 'Created', formatTimestamp(createdAt, true), TIMING_LINE), + labelledLine(`row${id}-updatedAt`, 'Updated', formatTimestampAlignedWith(updatedAt, createdAt), TIMING_LINE), + ]), }, historyItems: { name: h('.flex-row.g2.items-center', ['Status History', infoTooltip(environmentStatusHistoryLegendComponent())]), visible: true, sortable: false, - size: 'w-20', - format: (historyItems) => historyItems - .flatMap(({ status }) => [ - '-', - coloredEnvironmentStatusComponent(status, StatusAcronym[status]), - ]).slice(1), - balloon: true, + size: 'w-environments-history-column', + classes: 'f6', + noEllipsis: true, + format: (historyItems) => formatEnvironmentStatusHistory(historyItems), /** * Status history filter component @@ -142,4 +205,30 @@ export const environmentsActiveColumns = { */ filter: ({ filteringModel }) => textInputFilter(filteringModel, 'statusHistory', 'e.g. D-R-X'), }, + runs: { + name: 'Runs', + visible: true, + sortable: false, + size: 'w-environments-runs-column', + classes: 'f6', + format: formatRunsList, + balloon: true, + + /** + * Run numbers filter component + * + * @param {object} environmentOverviewModel the overview model + * @param {FilteringModel} environmentOverviewModel.filteringModel the filtering model + * @return {Component} the filter component + */ + filter: ({ filteringModel }) => textInputFilter(filteringModel, 'runNumbers', 'e.g. 553203, 553221, ...'), + }, + configuration: { + name: 'Configuration', + visible: true, + sortable: false, + classes: 'f6', + noEllipsis: true, + format: formatConfiguration, + }, }; diff --git a/lib/public/views/Environments/Overview/environmentOverviewComponent.js b/lib/public/views/Environments/Overview/environmentOverviewComponent.js index df8f5a332d..70d9118b15 100644 --- a/lib/public/views/Environments/Overview/environmentOverviewComponent.js +++ b/lib/public/views/Environments/Overview/environmentOverviewComponent.js @@ -19,9 +19,9 @@ import { paginationComponent } from '../../../components/Pagination/paginationCo import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; import { warningComponent } from '../../../components/common/messages/warningComponent.js'; -const TABLEROW_HEIGHT = 58; +const TABLEROW_HEIGHT = 60; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; -const PAGE_USED_HEIGHT = 181; +const PAGE_USED_HEIGHT = 215; /** * The shows the environment table From 72df817bba205f8a1d6e8045003dd0bf2f4fe2b0 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 16:53:49 +0200 Subject: [PATCH 3/9] [O2B-1642] Display the status history of the environments on one line with its total and running times Notable changes for users: - The status history displays the acronym of each status on a single line, followed by the total time of the environment (from its first to its last status) and the time it spent running. - Hovering a status displays its message, the time it was reached and the time spent in it. Notable changes for developers: - `formatEnvironmentStatusHistory` computes the durations of the history. --- .../format/formatEnvironmentStatusHistory.js | 71 +++++++++++++------ 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/lib/public/views/Environments/format/formatEnvironmentStatusHistory.js b/lib/public/views/Environments/format/formatEnvironmentStatusHistory.js index 3faa649985..5a5da4ebf5 100644 --- a/lib/public/views/Environments/format/formatEnvironmentStatusHistory.js +++ b/lib/public/views/Environments/format/formatEnvironmentStatusHistory.js @@ -18,30 +18,59 @@ import { tooltip } from '../../../components/common/popover/tooltip.js'; import { formatDuration } from '../../../utilities/formatting/formatDuration.mjs'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; +const RUNNING_STATUS = 'RUNNING'; + /** - * Display the status history of an environment: the acronym of each status in its color, with the time the environment stayed in it below it, - * separated by chevrons. Hovering a status displays its message and the time it was reached + * Compute the time spent in each status of the history (the duration of the last one is unknown), the total time spent running and the time + * between the first and the last status + * + * @param {{status: string, createdAt: number}[]} historyItems the history items of the environment, sorted + * @return {{durations: (number|null)[], runningDuration: number, totalDuration: number}} the durations + */ +const computeDurations = (historyItems) => { + const durations = historyItems.map(({ createdAt }, index) => { + const nextItem = historyItems[index + 1]; + return nextItem ? nextItem.createdAt - createdAt : null; + }); + const runningDuration = historyItems.reduce( + (total, { status }, index) => status === RUNNING_STATUS ? total + (durations[index] ?? 0) : total, + 0, + ); + const totalDuration = historyItems.length > 1 ? historyItems[historyItems.length - 1].createdAt - historyItems[0].createdAt : 0; + + return { durations, runningDuration, totalDuration }; +}; + +/** + * Display the status history of an environment on two lines: the acronym of each status in its color, separated by chevrons, then the total + * time of the environment and the time it spent running. Hovering a status displays its message, the time it was reached and the time spent + * in it * * @param {{status: string, statusMessage: string, createdAt: number}[]} historyItems the history items of the environment, sorted * @return {Component} the status history */ -export const formatEnvironmentStatusHistory = (historyItems) => h( - '.flex-row.flex-wrap.items-start.g1', - historyItems.flatMap(({ status, statusMessage, createdAt }, index) => { - const nextItem = historyItems[index + 1]; - return [ - h('span.gray-dark', '\u203A'), - tooltip( - h('.flex-column.items-center', [ +export const formatEnvironmentStatusHistory = (historyItems) => { + const { durations, runningDuration, totalDuration } = computeDurations(historyItems); + + return h('', [ + h( + '.flex-row.items-baseline.g1', + historyItems.flatMap(({ status, statusMessage, createdAt }, index) => [ + h('span.gray-dark', '\u203A'), + tooltip( h('strong', coloredEnvironmentStatusComponent(status, StatusAcronym[status])), - h('.f7.gray-darker', nextItem ? formatDuration(nextItem.createdAt - createdAt) : '\u00A0'), - ]), - h('.flex-column', [ - h('strong', status), - statusMessage && h('', statusMessage), - h('.f7', formatTimestamp(createdAt, true)), - ]), - ), - ]; - }).slice(1), -); + h('.flex-column', [ + h('strong', status), + statusMessage && h('', statusMessage), + h('.f7', formatTimestamp(createdAt, true)), + durations[index] !== null && h('.f7', `Stayed ${formatDuration(durations[index])}`), + ]), + ), + ]).slice(1), + ), + h('.flex-row.g3.f7.gray-darker', [ + h('span', ['Total ', h('span.black', formatDuration(totalDuration))]), + h('span', ['Running ', h('span.black', formatDuration(runningDuration))]), + ]), + ]); +}; From 6bef9fb51c59210b1813dfd511b0c3ee1bf3e521 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 16:53:49 +0200 Subject: [PATCH 4/9] [O2B-1642] Replace the timing of the environments overview by its creation and fit its widths Notable changes for users: - The Timing column is replaced by the Created At column, as the last update of an environment is the same as its creation in most cases. The time range filter is displayed on this column. - The status history column is wider to display the longest histories on one line. - The default number of rows of the page is estimated for the new height of the rows. Notable changes for developers: - The created at hidden column and the `w-environments-timing-column` css class are removed, `w-environments-created-column` is added. - The tests of the environments overview are not updated yet. --- lib/public/app.css | 6 +-- .../environmentsActiveColumns.js | 49 ++++--------------- .../Overview/environmentOverviewComponent.js | 2 +- 3 files changed, 13 insertions(+), 44 deletions(-) diff --git a/lib/public/app.css b/lib/public/app.css index e0a25f1f90..b8600b7688 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -974,12 +974,12 @@ label { width: 12rem; } -.w-environments-timing-column { - width: 14rem; +.w-environments-created-column { + width: 12rem; } .w-environments-history-column { - width: 20rem; + width: 18rem; } .w-environments-runs-column { diff --git a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js index 399b116e3b..f2e0ef320e 100644 --- a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js +++ b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js @@ -14,10 +14,8 @@ import { CopyToClipboardComponent, h } from '/js/src/index.js'; import { iconPlus } from '/js/src/icons.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; -import { formatTimestampAlignedWith } from '../../../utilities/formatting/formatTimestampAlignedWith.js'; import { labelledLine } from '../../../components/common/formatting/labelledLine.js'; import { linkWithActionsDropdown } from '../../../components/common/selection/infoLoggerButtonGroup/linkWithActionsDropdown.js'; -import { profiles } from '../../../components/common/table/profiles.js'; import { formatRunsList } from '../../Runs/format/formatRunsList.js'; import { displayEnvironmentStatus } from '../format/displayEnvironmentStatus.js'; import { infologgerLinksComponents } from '../../../components/common/externalLinks/infologgerLinksComponents.js'; @@ -32,16 +30,6 @@ import { timeRangeFilter } from '../../../components/Filters/common/filters/time import { checkboxes } from '../../../components/Filters/common/filters/checkboxFilter.js'; import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; -const TIMING_LINE = { labelClass: 'timing-label' }; - -/** - * Return a copy of the given column, hidden from the table but still providing its filter to the filters panel - * - * @param {Column} column the column - * @return {Column} the hidden column - */ -const hiddenColumn = (column) => ({ ...column, visible: false, profiles: [profiles.none] }); - /** * Display the configuration of the environment: its run type and beam type, its detectors and its number of FLPs and EPNs * @@ -73,25 +61,10 @@ const formatConfiguration = (_, { id, rawConfiguration }) => { ]); }; -const createdAtColumn = { - name: 'Created At', - visible: true, - sortable: false, - - /** - * CreatedAt filter component - * - * @param {EnvironmentOverviewModel} environmentOverviewModel the environment overview model - * @return {Component} the filter component - */ - filter: (environmentOverviewModel) => timeRangeFilter(environmentOverviewModel.filteringModel.get('created')), -}; - /** * List of active columns for the environments overview * - * The creation and update dates are displayed together in the timing column. The created at column is kept hidden to provide its filter, - * in the same order as before + * The current status is displayed with its message and the status history with the time spent running by the environment */ export const environmentsActiveColumns = { id: { @@ -138,7 +111,6 @@ export const environmentsActiveColumns = { */ filter: ({ filteringModel }) => textInputFilter(filteringModel, 'ids', 'e.g. CmCvjNbg, TDI59So3d...'), }, - createdAt: hiddenColumn(createdAtColumn), status: { name: 'Current Status', visible: true, @@ -167,25 +139,22 @@ export const environmentsActiveColumns = { */ filter: (environmentOverviewModel) => checkboxes(environmentOverviewModel.filteringModel.get('currentStatus')), }, - timing: { - name: 'Timing', + createdAt: { + name: 'Created At', visible: true, sortable: false, - size: 'w-environments-timing-column', + size: 'w-environments-created-column', classes: 'f6', noEllipsis: true, + format: (timestamp) => formatTimestamp(timestamp, true), /** - * Display the creation and the last update of the environment, each in its own labelled line + * CreatedAt filter component * - * @param {*} _ unused, the dates are read from the environment - * @param {Environment} environment the environment - * @return {Component} the creation and the last update + * @param {EnvironmentOverviewModel} environmentOverviewModel the environment overview model + * @return {Component} the filter component */ - format: (_, { id, createdAt, updatedAt }) => h('', [ - labelledLine(`row${id}-createdAt`, 'Created', formatTimestamp(createdAt, true), TIMING_LINE), - labelledLine(`row${id}-updatedAt`, 'Updated', formatTimestampAlignedWith(updatedAt, createdAt), TIMING_LINE), - ]), + filter: (environmentOverviewModel) => timeRangeFilter(environmentOverviewModel.filteringModel.get('created')), }, historyItems: { name: h('.flex-row.g2.items-center', ['Status History', infoTooltip(environmentStatusHistoryLegendComponent())]), diff --git a/lib/public/views/Environments/Overview/environmentOverviewComponent.js b/lib/public/views/Environments/Overview/environmentOverviewComponent.js index 70d9118b15..3017638703 100644 --- a/lib/public/views/Environments/Overview/environmentOverviewComponent.js +++ b/lib/public/views/Environments/Overview/environmentOverviewComponent.js @@ -19,7 +19,7 @@ import { paginationComponent } from '../../../components/Pagination/paginationCo import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; import { warningComponent } from '../../../components/common/messages/warningComponent.js'; -const TABLEROW_HEIGHT = 60; +const TABLEROW_HEIGHT = 46; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; const PAGE_USED_HEIGHT = 215; From c3e86581dadb1a94ac859fce994739aea8e6141d Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 16:59:06 +0200 Subject: [PATCH 5/9] [O2B-1642] Fit the row estimate of the environments overview to the height of its rows The rows of the environments overview are 72px high, as the configuration is displayed on three lines, so the default number of rows is estimated with this height. The elements of the status history have ids to be used in UI tests. --- .../ActiveColumns/environmentsActiveColumns.js | 2 +- .../Environments/Overview/environmentOverviewComponent.js | 2 +- .../Environments/format/formatEnvironmentStatusHistory.js | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js index f2e0ef320e..8ff582135f 100644 --- a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js +++ b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js @@ -163,7 +163,7 @@ export const environmentsActiveColumns = { size: 'w-environments-history-column', classes: 'f6', noEllipsis: true, - format: (historyItems) => formatEnvironmentStatusHistory(historyItems), + format: (historyItems, { id }) => formatEnvironmentStatusHistory(historyItems, id), /** * Status history filter component diff --git a/lib/public/views/Environments/Overview/environmentOverviewComponent.js b/lib/public/views/Environments/Overview/environmentOverviewComponent.js index 3017638703..d34836ddf5 100644 --- a/lib/public/views/Environments/Overview/environmentOverviewComponent.js +++ b/lib/public/views/Environments/Overview/environmentOverviewComponent.js @@ -19,7 +19,7 @@ import { paginationComponent } from '../../../components/Pagination/paginationCo import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; import { warningComponent } from '../../../components/common/messages/warningComponent.js'; -const TABLEROW_HEIGHT = 46; +const TABLEROW_HEIGHT = 72; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; const PAGE_USED_HEIGHT = 215; diff --git a/lib/public/views/Environments/format/formatEnvironmentStatusHistory.js b/lib/public/views/Environments/format/formatEnvironmentStatusHistory.js index 5a5da4ebf5..29faf084a6 100644 --- a/lib/public/views/Environments/format/formatEnvironmentStatusHistory.js +++ b/lib/public/views/Environments/format/formatEnvironmentStatusHistory.js @@ -47,14 +47,15 @@ const computeDurations = (historyItems) => { * in it * * @param {{status: string, statusMessage: string, createdAt: number}[]} historyItems the history items of the environment, sorted + * @param {string} id the id of the environment, used to generate the ids of the elements * @return {Component} the status history */ -export const formatEnvironmentStatusHistory = (historyItems) => { +export const formatEnvironmentStatusHistory = (historyItems, id) => { const { durations, runningDuration, totalDuration } = computeDurations(historyItems); return h('', [ h( - '.flex-row.items-baseline.g1', + `#row${id}-statusChain.flex-row.items-baseline.g1`, historyItems.flatMap(({ status, statusMessage, createdAt }, index) => [ h('span.gray-dark', '\u203A'), tooltip( @@ -68,7 +69,7 @@ export const formatEnvironmentStatusHistory = (historyItems) => { ), ]).slice(1), ), - h('.flex-row.g3.f7.gray-darker', [ + h(`#row${id}-statusSummary.flex-row.g3.f7.gray-darker`, [ h('span', ['Total ', h('span.black', formatDuration(totalDuration))]), h('span', ['Running ', h('span.black', formatDuration(runningDuration))]), ]), From 57e8bc9c87c9bd3b39b8bd41cbc1a5ab1c52950b Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 16:59:06 +0200 Subject: [PATCH 6/9] [O2B-1642] Adapt the environments overview tests to its new columns The tests of the datatypes, the status colors, the balloon of the runs and the number of rows per page use the elements of the new columns, and tests are added for the message of the current status, the total and running times of the status history, and the configuration. --- test/public/envs/overview.test.js | 121 +++++++++++++++--------------- 1 file changed, 60 insertions(+), 61 deletions(-) diff --git a/test/public/envs/overview.test.js b/test/public/envs/overview.test.js index d2c58d927c..bd218a681d 100644 --- a/test/public/envs/overview.test.js +++ b/test/public/envs/overview.test.js @@ -17,11 +17,9 @@ const { pressElement, checkColumnBalloon, expectLink, - validateTableData, expectInnerText, expectUrlParams, waitForNavigation, - getInnerText, waitForTableLength, getPopoverSelector, goToPage, @@ -68,84 +66,85 @@ module.exports = () => { const statusNames = new Set(Object.keys(StatusAcronym)); - // eslint-disable-next-line jsdoc/require-param - const checkDate = (date) => !isNaN(dateAndTime.parse(date, 'DD/MM/YYYY hh:mm:ss')); - const tableDataValidators = { - id: (id) => /[A-Za-z0-9]+/.test(id), - runs: (runs) => runs === '-' || runs.split(',').every((run) => !isNaN(run)), - updatedAt: checkDate, - status: (currentStatus) => statusNames.has(currentStatus), - historyItems: (history) => history.split('-').every((statusAcronym) => STATUS_ACRONYMS.includes(statusAcronym)), - }; - await validateTableData(page, new Map(Object.entries(tableDataValidators))); + const checkDate = (date) => !isNaN(dateAndTime.parse(date, 'DD/MM/YYYY, HH:mm:ss')); + const durationRegex = /^\d{2,}:\d{2}:\d{2}$/; + + await page.waitForSelector('tbody tr:nth-of-type(1) [id$="-statusChain"]'); + const rows = await page.$$eval('tbody tr', (trs) => trs.map((tr) => { + const text = (suffix) => tr.querySelector(`[id$="-${suffix}"]`)?.innerText; + return { + id: text('id'), + currentStatus: text('currentStatus'), + createdAt: text('createdAt'), + statusChain: tr.querySelector('[id$="-statusChain"]')?.textContent, + total: tr.querySelector('[id$="-statusSummary"] span:nth-child(1) span')?.innerText, + running: tr.querySelector('[id$="-statusSummary"] span:nth-child(2) span')?.innerText, + runs: text('runs'), + }; + })); + + expect(rows).to.have.length.greaterThan(0); + for (const row of rows) { + expect(row.id, `id of ${JSON.stringify(row)}`).to.match(/^[A-Za-z0-9]+$/); + expect(statusNames.has(row.currentStatus), `status of ${row.id}`).to.be.true; + expect(checkDate(row.createdAt), `creation date of ${row.id}: ${row.createdAt}`).to.be.true; + expect( + row.statusChain.split('\u203A').every((statusAcronym) => STATUS_ACRONYMS.includes(statusAcronym)), + `status history of ${row.id}: ${row.statusChain}`, + ).to.be.true; + expect(row.total, `total time of ${row.id}`).to.match(durationRegex); + expect(row.running, `running time of ${row.id}`).to.match(durationRegex); + expect(row.runs === '-' || row.runs.split(',').every((run) => !isNaN(run)), `runs of ${row.id}: ${row.runs}`).to.be.true; + } }); it('Should display the correct items counter at the bottom of the page', async () => { await expectInnerText(page, '#firstRowIndex', '1'); - await expectInnerText(page, '#lastRowIndex', '9'); + await expectInnerText(page, '#lastRowIndex', '7'); await expectInnerText(page, '#totalRowsCount', '9'); }); it('Should have balloon on runs column', async () => { - await checkColumnBalloon(page, 1, 2); - await checkColumnBalloon(page, 1, 6); + await checkColumnBalloon(page, 1, 5); }); it('Should have correct status color in the overview page', async () => { - /** - * Check that a given cell of the given column displays the correct color depending on the status - * - * @param {number} rowIndex the index of the row to look for status color - * @param {number} columnIndex the index of the column to look for status color - * @returns {Promise} void promise - */ - const checkEnvironmentStatusColor = async (rowIndex, columnIndex) => { - const cellSelector = `tbody tr:nth-of-type(${rowIndex}) td:nth-of-type(${columnIndex})`; - const cell = await page.waitForSelector(cellSelector); - const cellContent = await getInnerText(cell); - - switch (cellContent) { - case 'RUNNING': - await page.waitForSelector(`${cellSelector}.success`); - break; - case 'ERROR': - await page.waitForSelector(`${cellSelector}.danger`); - break; - case 'CONFIGURED': - await page.waitForSelector(`${cellSelector}.primary`); - break; - case 'DONE': - await page.waitForSelector(`${cellSelector}.black`); - break; - case 'DEPLOYED': - await page.waitForSelector(`${cellSelector}.gray`); - break; - case 'PENDING': - await page.waitForSelector(`${cellSelector}.gray`); - break; - case 'STANDBY': - await page.waitForSelector(`${cellSelector}.gray`); - break; - case 'UNKNOWN': - await page.waitForSelector(`${cellSelector}.gray-dark`); - break; - } - + const expectedColors = { + CmCvjNbg: ['RUNNING', 'success'], + EIDO13i3D: ['ERROR', 'danger'], + TDI59So3d: ['DONE', 'black'], }; - await checkEnvironmentStatusColor(1, 4); - await checkEnvironmentStatusColor(2, 4); - await checkEnvironmentStatusColor(3, 4); - await checkEnvironmentStatusColor(6, 4); - await checkEnvironmentStatusColor(9, 4); + for (const [environmentId, [status, colorClass]] of Object.entries(expectedColors)) { + await expectInnerText(page, `#row${environmentId}-currentStatus`, status); + await page.waitForSelector(`#row${environmentId}-currentStatus span.${colorClass}`); + } + }); + + it('Should display the message of the current status below it', async () => { + await expectInnerText(page, '#rowEIDO13i3D-statusMessage', 'This one is really broken'); + }); + + it('Should display the total and running times of the status history, and its statuses on one line', async () => { + // TDI59So3d: configured, running during one hour, deployed then done + await expectInnerText(page, '#rowTDI59So3d-statusChain', 'C\n\u203A\nR\n\u203A\nD\n\u203A\nX'); + await expectInnerText(page, '#rowTDI59So3d-statusSummary', 'Total 02:55:00\nRunning 01:00:00'); + }); + + it('Should display the configuration of the environment', async () => { + await expectInnerText(page, '#rowCmCvjNbg-runType', 'PHYSICS'); + await expectInnerText(page, '#rowCmCvjNbg-beamType', 'pp'); + await expectInnerText(page, '#rowCmCvjNbg-detectors', 'FT0, ITS'); + await expectInnerText(page, '#rowCmCvjNbg-flps', '2'); + await expectInnerText(page, '#rowCmCvjNbg-epns', '2'); }); it('can set how many environments are available per page', async () => { - // Expect the amount selector to currently be set to 9 (because of the defined page height) + // Expect the amount selector to currently be set to 7 (because of the defined page height) const amountSelectorId = '#amountSelector'; const amountSelectorButton = await page.waitForSelector(`${amountSelectorId} button`); const amountSelectorButtonText = await page.evaluate((element) => element.innerText, amountSelectorButton); - expect(amountSelectorButtonText.trim().endsWith('9')).to.be.true; + expect(amountSelectorButtonText.trim().endsWith('7')).to.be.true; // Expect the dropdown options to be visible when it is selected await pressElement(page, `${amountSelectorId} button`); From 816ca8aea3558b58ace8e7545e5f45f5d6d27262 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 17:06:36 +0200 Subject: [PATCH 7/9] [O2B-1642] Extract the data taking configuration displayed in the run overview Notable changes for developers: - New `dataTakingConfiguration` component (the EPNs, the FLPs, the DCS state and the trigger, two by line), extracted from the run overview columns to be reused by the environments overview. The ids of its elements are unchanged. --- .../formatting/dataTakingConfiguration.js | 52 +++++++++++++++++++ .../runsOverviewActiveColumns.js | 26 +--------- 2 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 lib/public/components/common/formatting/dataTakingConfiguration.js diff --git a/lib/public/components/common/formatting/dataTakingConfiguration.js b/lib/public/components/common/formatting/dataTakingConfiguration.js new file mode 100644 index 0000000000..1915c6a4ce --- /dev/null +++ b/lib/public/components/common/formatting/dataTakingConfiguration.js @@ -0,0 +1,52 @@ +/** + * @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 { labelledLine } from './labelledLine.js'; + +/** + * Format the number of EPNs: the count if known, ON if EPNs are used without a known count, OFF otherwise + * + * @param {boolean} epn true if EPNs are used + * @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 data taking configuration of a run or of an environment: the number of EPNs and FLPs, the DCS state and the trigger, two by line + * + * @param {string} idPrefix the prefix of the ids of the elements (e.g. `row123`) + * @param {object} configuration the configuration to display + * @param {boolean} configuration.epn true if EPNs are used + * @param {number|null} configuration.nEpns the number of EPNs + * @param {number|null} configuration.nFlps the number of FLPs + * @param {boolean} configuration.dcs true if DCS is enabled + * @param {string|null} configuration.triggerValue the trigger (OFF, LTU or CTP) + * @return {Component} the configuration + */ +export const dataTakingConfiguration = (idPrefix, { epn, nEpns, nFlps, dcs, triggerValue }) => h('', [ + h(`#${idPrefix}-nodes.flex-row.g2`, [ + labelledLine(`${idPrefix}-nEpns`, 'EPNs', formatEpnsCount(epn, nEpns)), + labelledLine(`${idPrefix}-nFlps`, 'FLPs', nFlps ?? '-'), + ]), + h(`#${idPrefix}-states.flex-row.g2`, [ + labelledLine(`${idPrefix}-dcs`, 'DCS', dcs ? 'ON' : 'OFF'), + labelledLine(`${idPrefix}-triggerValue`, 'TRG', triggerValue || '-'), + ]), +]); diff --git a/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js index 596588ea03..4eca6854d6 100644 --- a/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsOverviewActiveColumns.js @@ -14,6 +14,7 @@ import { h } from '/js/src/index.js'; import { runActionsComponents, runsActiveColumns } from './runsActiveColumns.js'; import { profiles } from '../../../components/common/table/profiles.js'; +import { dataTakingConfiguration } from '../../../components/common/formatting/dataTakingConfiguration.js'; import { labelledLine } from '../../../components/common/formatting/labelledLine.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { formatRunStart } from '../format/formatRunStart.js'; @@ -59,20 +60,6 @@ const TIMING_LINE = { labelClass: 'timing-label' }; */ const hiddenColumn = (key) => ({ ...runsActiveColumns[key], visible: false, profiles: [profiles.none] }); -/** - * Format the number of EPNs of a run: the count if known, ON if EPNs were used without a known count, OFF otherwise - * - * @param {boolean} epn true if the run used EPNs - * @param {number|null} nEpns the number of EPNs - * @return {string|number} the formatted value - */ -const formatEpnsCount = (epn, nEpns) => { - if (!epn) { - return 'OFF'; - } - return typeof nEpns === 'number' ? nEpns : 'ON'; -}; - /** * Display the definition 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 @@ -181,16 +168,7 @@ export const runsOverviewActiveColumns = { * @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 || '-'), - ]), - ]), + format: (_, run) => dataTakingConfiguration(`row${run.id}`, run), }, detectors: { ...runsActiveColumns.detectors, From 4c815cc56be6b5c2dbf220cd823c2ef0db92ae90 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 17:06:36 +0200 Subject: [PATCH 8/9] [O2B-1642] Display the configuration of the environments as for the runs, and their detectors in their own column Notable changes for users: - The Configuration column of the environments overview displays the number of EPNs and FLPs, the DCS state and the trigger (OFF, LTU or CTP) of the environment on two lines, as the run overview does. They are read from the configuration of the environment the same way the runs started in it are. - The detectors of the environment are displayed in their own column, on a single line, preceded by their number. - The default number of rows of the page is estimated for the new height of the rows (51px). Notable changes for developers: - `parseEnvironmentConfiguration` returns the same data as the runs (`epn`, `nEpns`, `nFlps`, `dcs`, `triggerValue`) and the detectors, instead of the run type and the beam type. - New `w-environments-configuration-column` css class. --- lib/public/app.css | 6 +- .../environmentsActiveColumns.js | 71 ++++++++++--------- .../Overview/environmentOverviewComponent.js | 2 +- .../format/parseEnvironmentConfiguration.js | 46 +++++++++--- 4 files changed, 80 insertions(+), 45 deletions(-) diff --git a/lib/public/app.css b/lib/public/app.css index b8600b7688..92a684e58a 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -963,7 +963,7 @@ label { } /* - * Columns of the environments overview: all of them but the configuration, which takes the remaining width, have a fixed width fitting their + * Columns of the environments overview: all of them but the detectors, which take the remaining width, have a fixed width fitting their * content */ .w-environments-id-column { @@ -982,6 +982,10 @@ label { width: 18rem; } +.w-environments-configuration-column { + width: 11rem; +} + .w-environments-runs-column { width: 8rem; } diff --git a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js index 8ff582135f..96f963f6b4 100644 --- a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js +++ b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js @@ -14,7 +14,7 @@ import { CopyToClipboardComponent, h } from '/js/src/index.js'; import { iconPlus } from '/js/src/icons.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; -import { labelledLine } from '../../../components/common/formatting/labelledLine.js'; +import { dataTakingConfiguration } from '../../../components/common/formatting/dataTakingConfiguration.js'; import { linkWithActionsDropdown } from '../../../components/common/selection/infoLoggerButtonGroup/linkWithActionsDropdown.js'; import { formatRunsList } from '../../Runs/format/formatRunsList.js'; import { displayEnvironmentStatus } from '../format/displayEnvironmentStatus.js'; @@ -30,37 +30,6 @@ import { timeRangeFilter } from '../../../components/Filters/common/filters/time import { checkboxes } from '../../../components/Filters/common/filters/checkboxFilter.js'; import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; -/** - * Display the configuration of the environment: its run type and beam type, its detectors and its number of FLPs and EPNs - * - * @param {*} _ unused, the configuration is read from the environment - * @param {Environment} environment the environment - * @return {Component|string} the summary of the configuration, or a dash if the environment has no valid configuration - */ -const formatConfiguration = (_, { id, rawConfiguration }) => { - const configuration = parseEnvironmentConfiguration(rawConfiguration); - if (!configuration) { - return '-'; - } - - const { runType, beamType, detectors, flpsCount, epnsCount } = configuration; - if (!runType && !beamType && detectors.length === 0 && flpsCount === null && epnsCount === null) { - return '-'; - } - - return h('', [ - h('.flex-row.g3', [ - labelledLine(`row${id}-runType`, 'Type', runType ?? '-'), - labelledLine(`row${id}-beamType`, 'Beam', beamType ?? '-'), - ]), - labelledLine(`row${id}-detectors`, 'Detectors', detectors.length > 0 ? detectors.join(', ') : '-'), - h('.flex-row.g3', [ - labelledLine(`row${id}-flps`, 'FLPs', flpsCount ?? '-'), - labelledLine(`row${id}-epns`, 'EPNs', epnsCount ?? '-'), - ]), - ]); -}; - /** * List of active columns for the environments overview * @@ -196,8 +165,44 @@ export const environmentsActiveColumns = { name: 'Configuration', visible: true, sortable: false, + size: 'w-environments-configuration-column', + classes: 'f6', + noEllipsis: true, + + /** + * Display the number of EPNs and FLPs, the DCS state and the trigger of the environment, two by line, as for the runs + * + * @param {*} _ unused, the configuration is read from the environment + * @param {Environment} environment the environment + * @return {Component|string} the configuration, or a dash if the environment has no valid configuration + */ + format: (_, { id, rawConfiguration }) => { + const configuration = parseEnvironmentConfiguration(rawConfiguration); + return configuration ? dataTakingConfiguration(`row${id}`, configuration) : '-'; + }, + }, + detectors: { + name: 'Detectors', + visible: true, + sortable: false, classes: 'f6', noEllipsis: true, - format: formatConfiguration, + + /** + * Display the number of detectors of the environment followed by their names, on a single line + * + * @param {*} _ unused, the detectors are read from the configuration of the environment + * @param {Environment} environment the environment + * @return {Component|string} the detectors, or a dash if the environment has none + */ + format: (_, { id, rawConfiguration }) => { + const detectors = parseEnvironmentConfiguration(rawConfiguration)?.detectors ?? []; + return detectors.length > 0 + ? h(`#row${id}-detectors`, overflowBalloon(h('', [ + h('.badge.bg-gray-light.mr1', detectors.length), + detectors.join(', '), + ]))) + : '-'; + }, }, }; diff --git a/lib/public/views/Environments/Overview/environmentOverviewComponent.js b/lib/public/views/Environments/Overview/environmentOverviewComponent.js index d34836ddf5..20ff5eac6e 100644 --- a/lib/public/views/Environments/Overview/environmentOverviewComponent.js +++ b/lib/public/views/Environments/Overview/environmentOverviewComponent.js @@ -19,7 +19,7 @@ import { paginationComponent } from '../../../components/Pagination/paginationCo import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; import { warningComponent } from '../../../components/common/messages/warningComponent.js'; -const TABLEROW_HEIGHT = 72; +const TABLEROW_HEIGHT = 51; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; const PAGE_USED_HEIGHT = 215; diff --git a/lib/public/views/Environments/format/parseEnvironmentConfiguration.js b/lib/public/views/Environments/format/parseEnvironmentConfiguration.js index d934cd2514..5751442efd 100644 --- a/lib/public/views/Environments/format/parseEnvironmentConfiguration.js +++ b/lib/public/views/Environments/format/parseEnvironmentConfiguration.js @@ -11,6 +11,8 @@ * or submit itself to any jurisdiction. */ +import { TriggerValue } from '../../../domain/enums/TriggerValue.js'; + /** * Parse a value of the environment configuration that may be a JSON serialized list (e.g. `["FT0", "ITS"]`) or an already parsed one * @@ -19,7 +21,7 @@ */ const parseList = (value) => { if (Array.isArray(value)) { - return value; + return [...value]; } try { const parsed = JSON.parse(value); @@ -29,13 +31,28 @@ const parseList = (value) => { } }; +/** + * Parse a boolean value of the environment configuration, that may be serialized (e.g. `"true"`) + * + * @param {*} value the value to parse + * @return {boolean} the boolean, false if the value is missing or invalid + */ +const parseBoolean = (value) => { + try { + return JSON.parse(value) === true; + } catch { + return false; + } +}; + /** * @typedef EnvironmentConfigurationSummary - * @property {string|null} runType the type of the runs of the environment - * @property {string|null} beamType the PDP beam type * @property {string[]} detectors the detectors of the environment - * @property {number|null} flpsCount the number of FLPs (hosts) - * @property {number|null} epnsCount the number of EPNs + * @property {boolean} epn true if EPNs are used + * @property {number|null} nEpns the number of EPNs + * @property {number|null} nFlps the number of FLPs (hosts, with the CTP readout host if it is enabled) + * @property {boolean} dcs true if DCS is enabled + * @property {string} triggerValue the trigger (OFF, LTU or CTP), computed as for the runs started in the environment */ /** @@ -52,14 +69,23 @@ export const parseEnvironmentConfiguration = (rawConfiguration) => { try { const configuration = JSON.parse(rawConfiguration); const hosts = parseList(configuration.hosts); - const epnsCount = parseInt(configuration.odc_n_epns, 10); + if (parseBoolean(configuration.ctp_readout_enabled) && configuration.ctp_readout_host) { + hosts.push(configuration.ctp_readout_host); + } + const nEpns = parseInt(configuration.odc_n_epns, 10); + + let triggerValue = TriggerValue.Off; + if (parseBoolean(configuration.trg_enabled)) { + triggerValue = parseBoolean(configuration.trg_global_run_enabled) ? TriggerValue.CTP : TriggerValue.LTU; + } return { - runType: configuration.run_type ?? null, - beamType: configuration.pdp_beam_type ?? null, detectors: parseList(configuration.detectors), - flpsCount: hosts.length > 0 ? hosts.length : null, - epnsCount: isNaN(epnsCount) ? null : epnsCount, + epn: parseBoolean(configuration.odc_enabled), + nEpns: isNaN(nEpns) ? null : nEpns, + nFlps: hosts.length, + dcs: parseBoolean(configuration.dcs_enabled), + triggerValue, }; } catch { return null; From dce720fd49911bd3460879a5f36cadeb48f6c1f0 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Fri, 23 Oct 2026 17:06:36 +0200 Subject: [PATCH 9/9] [O2B-1642] Adapt the environments overview tests to its configuration and detectors columns --- test/public/envs/overview.test.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/test/public/envs/overview.test.js b/test/public/envs/overview.test.js index bd218a681d..38756dee26 100644 --- a/test/public/envs/overview.test.js +++ b/test/public/envs/overview.test.js @@ -18,6 +18,7 @@ const { checkColumnBalloon, expectLink, expectInnerText, + expectInnerTextTo, expectUrlParams, waitForNavigation, waitForTableLength, @@ -100,7 +101,7 @@ module.exports = () => { it('Should display the correct items counter at the bottom of the page', async () => { await expectInnerText(page, '#firstRowIndex', '1'); - await expectInnerText(page, '#lastRowIndex', '7'); + await expectInnerText(page, '#lastRowIndex', '9'); await expectInnerText(page, '#totalRowsCount', '9'); }); @@ -131,20 +132,21 @@ module.exports = () => { await expectInnerText(page, '#rowTDI59So3d-statusSummary', 'Total 02:55:00\nRunning 01:00:00'); }); - it('Should display the configuration of the environment', async () => { - await expectInnerText(page, '#rowCmCvjNbg-runType', 'PHYSICS'); - await expectInnerText(page, '#rowCmCvjNbg-beamType', 'pp'); - await expectInnerText(page, '#rowCmCvjNbg-detectors', 'FT0, ITS'); - await expectInnerText(page, '#rowCmCvjNbg-flps', '2'); - await expectInnerText(page, '#rowCmCvjNbg-epns', '2'); + it('Should display the configuration of the environment, as for the runs, and its detectors', async () => { + // The CTP readout host is counted in the FLPs + await expectInnerText(page, '#rowCmCvjNbg-nEpns', '2'); + await expectInnerText(page, '#rowCmCvjNbg-nFlps', '3'); + await expectInnerText(page, '#rowCmCvjNbg-dcs', 'ON'); + await expectInnerText(page, '#rowCmCvjNbg-triggerValue', 'CTP'); + await expectInnerTextTo(page, '#rowCmCvjNbg-detectors', (text) => text.includes('FT0, ITS') && text.startsWith('2')); }); it('can set how many environments are available per page', async () => { - // Expect the amount selector to currently be set to 7 (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.waitForSelector(`${amountSelectorId} button`); const amountSelectorButtonText = await page.evaluate((element) => element.innerText, amountSelectorButton); - expect(amountSelectorButtonText.trim().endsWith('7')).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`);