diff --git a/CHANGELOG.md b/CHANGELOG.md index 03a5f684fe..284f9e9959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.4.1](https://github.com/AliceO2Group/Bookkeeping/releases/tag/%40aliceo2%2Fbookkeeping%401.4.1) +* Notable changes for developers: + * Fixed the runs start/stop extraction from AliECS kafka messages + * Added more logs to external service synchronization (CCDB, MonALISA) + +## [1.4.0](https://github.com/AliceO2Group/Bookkeeping/releases/tag/%40aliceo2%2Fbookkeeping%401.4.0) +* Notable changes for users: + * Fixed tag color not being updated when switching to run details after updating a tag + * Environment variable ENABLE_HOUSEKEEPING must be set to true (case-insensitive) to actually enable housekeeping + * Use time range picker for runs start & stop filtering + * Run details page has been modernized +* Notable changes for developers: + * Runs and QC flags timestamps now store milliseconds + * Fixed users that start/stop runs not being extracted from kafka message + * Fixed TF timestamps being ignored when creating QC flags + * Fixed randomly failing test in FLP frontend tests + * Use coalesced run time in raw SQL querries + * Removed the max number of retries for kafka connection + ## [1.3.0](https://github.com/AliceO2Group/Bookkeeping/releases/tag/%40aliceo2%2Fbookkeeping%401.3.0) * Notable changes for users: * Fixed physical constants values which resulted in wrong AVG center of mass energy diff --git a/Dockerfile b/Dockerfile index 110faf7a63..c54f057c90 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN apk add --no-cache \ freetype=2.13.0-r5 \ freetype-dev=2.13.0-r5 \ harfbuzz=7.3.0-r0 \ - ca-certificates=20240226-r0 + ca-certificates=20241121-r0 # Tell Puppeteer to skip installing Chrome. We'll be using the installed package. ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true diff --git a/database/CHANGELOG.md b/database/CHANGELOG.md index 527caeed98..3c4e8f1e57 100644 --- a/database/CHANGELOG.md +++ b/database/CHANGELOG.md @@ -1,3 +1,24 @@ +## [1.4.0](https://github.com/AliceO2Group/Bookkeeping/releases/tag/%40aliceo2%2Fbookkeeping%401.4.0) +* Changes made to the database + * Following columns in runs table has been changed to Datetime(3): + * time_o2_start + * time_trg_start + * first_tf_timestamp + * last_tf_timestamp + * time_trg_end + * time_o2_end + * Following columns in quality_control_flags table has been changed to Datetime(3): + * from + * to + * created_at + * updated_at + * Following columns in quality_control_flag_effective_periods table has been changed to Datetime(3): + * from + * to + * created_at + * updated_at + * Added two more virtual columns to runs, rct_time_start and rct_time_end that coalesce first/last TF timestamps with run start/stop + ## [1.3.0](https://github.com/AliceO2Group/Bookkeeping/releases/tag/%40aliceo2%2Fbookkeeping%401.3.0) * Changes made to the database * Fixed physical constants values in database diff --git a/docker-compose.yml b/docker-compose.yml index 1960683bc7..a6947ce3af 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,7 +24,8 @@ services: FLP_INFOLOGGER_URL: "${ALI_ECS_GUI_URL:-http://localhost:8081}" QC_GUI_URL: "${ALI_ECS_GUI_URL:-http://localhost:8082}" ALI_FLP_INDEX_URL: "${ALI_ECS_GUI_URL:-http://localhost:80}" - CCDB_ENABLE_SYNCHRONIZATION: false + CCDB_ENABLE_SYNCHRONIZATION: "${CCDB_ENABLE_SYNCHRONIZATION:-false}" + CCDB_RUN_INFO_URL: "${CCDB_RUN_INFO_URL:-}" links: - database restart: unless-stopped diff --git a/lib/application.js b/lib/application.js index 6c867fa4f6..733fab3514 100644 --- a/lib/application.js +++ b/lib/application.js @@ -48,7 +48,6 @@ class BookkeepingApplication { const kafkaClient = new Kafka({ clientId: 'bookkeeping', brokers: KafkaConfig.brokers, - retry: { retries: 3 }, logLevel: logLevel.NOTHING, }); @@ -94,15 +93,9 @@ class BookkeepingApplication { const ccdbSynchronizer = new CcdbSynchronizer(ccdbConfig.runInfoUrl); this.scheduledProcessesManager.schedule( // Sync runs a few sync period ago in case some synchronization failed - async () => { - try { - await ccdbSynchronizer.syncFirstAndLastTf(new Date(Date.now() - ccdbConfig.synchronizationPeriod * 5)); - } catch (e) { - this._logger.errorMessage(`Failed to synchronize runs first and last TF:\n ${e.stack}`); - } - }, + () => ccdbSynchronizer.syncFirstAndLastTf(new Date(Date.now() - ccdbConfig.synchronizationPeriod * 5)), { - wait: 3 * 1000, + wait: 10 * 1000, every: ccdbConfig.synchronizationPeriod, }, ); diff --git a/lib/config/services.js b/lib/config/services.js index 571f37b574..052f4a79b8 100644 --- a/lib/config/services.js +++ b/lib/config/services.js @@ -26,7 +26,7 @@ const { } = process.env ?? {}; exports.services = { - enableHousekeeping: process.env?.ENABLE_HOUSEKEEPING ?? false, + enableHousekeeping: process.env?.ENABLE_HOUSEKEEPING?.toLowerCase() === 'true', aliEcsGui: { url: process.env?.ALI_ECS_GUI_URL || null, token: process.env?.ALI_ECS_GUI_TOKEN || null, @@ -64,7 +64,7 @@ exports.services = { ccdb: { enableSynchronization: CCDB_ENABLE_SYNCHRONIZATION?.toLowerCase() === 'true', - synchronizationPeriod: Number(CCDB_SYNCHRONIZATION_PERIOD) || 24 * 60 * 60 * 1000, // 1h in milliseconds + synchronizationPeriod: Number(CCDB_SYNCHRONIZATION_PERIOD) || 24 * 60 * 60 * 1000, // 1d in milliseconds runInfoUrl: CCDB_RUN_INFO_URL, }, }; diff --git a/lib/database/adapters/RunAdapter.js b/lib/database/adapters/RunAdapter.js index cbddb9d3d8..289ba0dab7 100644 --- a/lib/database/adapters/RunAdapter.js +++ b/lib/database/adapters/RunAdapter.js @@ -140,6 +140,7 @@ class RunAdapter { tfFileSize, otherFileCount, otherFileSize, + nTfOrbits, lhcFill, flpRoles, logs, @@ -214,6 +215,7 @@ class RunAdapter { tfFileSize: tfFileSize !== null ? String(tfFileSize) : null, otherFileCount, otherFileSize: otherFileSize !== null ? String(otherFileSize) : null, + nTfOrbits: nTfOrbits !== null ? String(nTfOrbits) : null, lhcFill, crossSection, triggerEfficiency, @@ -322,6 +324,7 @@ class RunAdapter { tfFileSize: entityObject.tfFileSize, otherFileCount: entityObject.otherFileCount, otherFileSize: entityObject.otherFileSize, + nTfOrbits: entityObject.nTfOrbits, concatenatedDetectors: entityObject.detectors, definition: entityObject.definition, calibrationStatus: entityObject.calibrationStatus, diff --git a/lib/database/migrations/20241212074001-create-rct-run-start-and-stop.js b/lib/database/migrations/20241212074001-create-rct-run-start-and-stop.js new file mode 100644 index 0000000000..750400bae6 --- /dev/null +++ b/lib/database/migrations/20241212074001-create-rct-run-start-and-stop.js @@ -0,0 +1,17 @@ +'use strict'; + +const ADD_RUN_RCT_START_AND_STOP = ` + ALTER TABLE runs + ADD COLUMN qc_time_start DATETIME(3) AS (COALESCE(first_tf_timestamp, time_trg_start, time_o2_start)) VIRTUAL AFTER time_start, + ADD COLUMN qc_time_end DATETIME(3) AS (COALESCE(last_tf_timestamp, time_trg_end, time_o2_end)) VIRTUAL AFTER qc_time_start; +`; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface) => queryInterface.sequelize.query(ADD_RUN_RCT_START_AND_STOP), + + down: async (queryInterface) => queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.removeColumn('runs', 'qc_time_start', { transaction }); + await queryInterface.removeColumn('runs', 'qc_time_end', { transaction }); + }), +}; diff --git a/lib/database/migrations/20241218095948-add-n-tf-orbits.js b/lib/database/migrations/20241218095948-add-n-tf-orbits.js new file mode 100644 index 0000000000..992ad12b68 --- /dev/null +++ b/lib/database/migrations/20241218095948-add-n-tf-orbits.js @@ -0,0 +1,17 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface, Sequelize) => await queryInterface.addColumn( + 'runs', + 'n_tf_orbits', + { + type: Sequelize.DataTypes.BIGINT, + allowNull: true, + default: null, + after: 'other_file_size', + }, + ), + + down: async (queryInterface) => await queryInterface.removeColumn('runs', 'n_tf_orbits'), +}; diff --git a/lib/database/models/run.js b/lib/database/models/run.js index ab258b7dbb..0f11a2aca5 100644 --- a/lib/database/models/run.js +++ b/lib/database/models/run.js @@ -203,6 +203,10 @@ module.exports = (sequelize) => { type: Sequelize.BIGINT, default: null, }, + nTfOrbits: { + type: Sequelize.BIGINT, + default: null, + }, definition: { type: Sequelize.ENUM(...RUN_DEFINITIONS), }, diff --git a/lib/database/models/typedefs/SequelizeRun.js b/lib/database/models/typedefs/SequelizeRun.js index d8633b6d15..ad299110f3 100644 --- a/lib/database/models/typedefs/SequelizeRun.js +++ b/lib/database/models/typedefs/SequelizeRun.js @@ -67,6 +67,7 @@ * @property {string|number|null} tfFileSize * @property {string|null} otherFileCount * @property {string|number|null} otherFileSize + * @property {string|number|null} nTfOrbits * @property {number|null} inelasticInteractionRateAvg * @property {number|null} inelasticInteractionRateAtStart * @property {number|null} inelasticInteractionRateAtMid diff --git a/lib/database/repositories/QcFlagRepository.js b/lib/database/repositories/QcFlagRepository.js index 04b94b12bf..d031982c25 100644 --- a/lib/database/repositories/QcFlagRepository.js +++ b/lib/database/repositories/QcFlagRepository.js @@ -203,32 +203,17 @@ class QcFlagRepository extends Repository { GROUP_CONCAT(effectivePeriods.flagsList) AS flagsList, IF( - ( - COALESCE(run.time_trg_end, run.time_o2_end ) IS NULL - OR COALESCE(run.time_trg_start, run.time_o2_start) IS NULL - ), + run.time_start IS NULL OR run.time_end IS NULL, IF( - SUM( - COALESCE(effectivePeriods.\`to\` , 0) - + COALESCE(effectivePeriods.\`from\`, 0) - ) = 0, + effectivePeriods.\`from\` IS NULL AND effectivePeriods.\`to\` IS NULL, 1, null ), SUM( - COALESCE( - effectivePeriods.\`to\`, - UNIX_TIMESTAMP(run.time_trg_end), - UNIX_TIMESTAMP(run.time_o2_end) - ) - - COALESCE( - effectivePeriods.\`from\`, - UNIX_TIMESTAMP(run.time_trg_start), - UNIX_TIMESTAMP(run.time_o2_start) - ) + COALESCE(effectivePeriods.\`to\`, UNIX_TIMESTAMP(run.time_end)) + - COALESCE(effectivePeriods.\`from\`, UNIX_TIMESTAMP(run.time_start)) ) / ( - UNIX_TIMESTAMP(COALESCE(run.time_trg_end, run.time_o2_end)) - - UNIX_TIMESTAMP(COALESCE(run.time_trg_start, run.time_o2_start)) + UNIX_TIMESTAMP(run.time_end) - UNIX_TIMESTAMP(run.time_start) ) ) AS effectiveRunCoverage diff --git a/lib/database/seeders/20200713103855-runs.js b/lib/database/seeders/20200713103855-runs.js index 1eeda8e0a8..7f28469612 100644 --- a/lib/database/seeders/20200713103855-runs.js +++ b/lib/database/seeders/20200713103855-runs.js @@ -53,6 +53,7 @@ module.exports = { tf_file_size: '214920239535280', other_file_count: 50, other_file_size: '9999999999999999', + n_tf_orbits: '9999999999999999', definition: RunDefinition.PHYSICS, cross_section: 1.23, trigger_efficiency: 2.34, diff --git a/lib/domain/dtos/UpdateRunByRunNumberDto.js b/lib/domain/dtos/UpdateRunByRunNumberDto.js index fe1426d6a3..1440c49943 100644 --- a/lib/domain/dtos/UpdateRunByRunNumberDto.js +++ b/lib/domain/dtos/UpdateRunByRunNumberDto.js @@ -37,6 +37,7 @@ const BodyDto = Joi.object({ tfFileSize: Joi.string().optional(), otherFileCount: Joi.number().optional(), otherFileSize: Joi.string().optional(), + nTfOrbits: Joi.string().optional(), crossSection: Joi.number().optional(), triggerEfficiency: Joi.number().optional(), triggerAcceptance: Joi.number().optional(), diff --git a/lib/domain/entities/Run.js b/lib/domain/entities/Run.js index 7f7bcab11c..4083389bbc 100644 --- a/lib/domain/entities/Run.js +++ b/lib/domain/entities/Run.js @@ -65,6 +65,7 @@ * @property {string|null} tfFileSize * @property {string|null} otherFileCount * @property {string|null} otherFileSize + * @property {string|null} nTfOrbits * @property {number|null} muInelasticInteractionRate * @property {number|null} inelasticInteractionRateAvg * @property {number|null} inelasticInteractionRateAtStart diff --git a/lib/public/views/Runs/Details/editTagsPanel.js b/lib/public/components/Filters/RunsFilter/o2StartFilter.js similarity index 50% rename from lib/public/views/Runs/Details/editTagsPanel.js rename to lib/public/components/Filters/RunsFilter/o2StartFilter.js index 7b7ae423c4..ab10978cfa 100644 --- a/lib/public/views/Runs/Details/editTagsPanel.js +++ b/lib/public/components/Filters/RunsFilter/o2StartFilter.js @@ -11,17 +11,12 @@ * or submit itself to any jurisdiction. */ -import { h } from '/js/src/index.js'; -import { tagPicker } from '../../../components/tag/tagPicker.js'; +import { timeRangeFilter } from '../common/filters/timeRangeFilter.js'; /** - * A panel containing: - * a list of potential tags to add to a RUN - * a button to update the tag selection - * @param {RunDetailsModel} runDetailsModel the details model - * @return {vnode} virtual node with representation of the panel + * Returns a filter to be applied on run start + * + * @param {RunsOverviewModel} runsOverviewModel the run overview model object + * @return {Component} the filter component */ -export const editTagsPanel = (runDetailsModel) => h( - '#tags-selection.flex-column.w-30.p2.g2', - tagPicker(runDetailsModel.editionTagPickerModel), -); +export const o2StartFilter = (runsOverviewModel) => timeRangeFilter(runsOverviewModel.o2StartFilterModel); diff --git a/lib/public/components/Filters/RunsFilter/o2StopFilter.js b/lib/public/components/Filters/RunsFilter/o2StopFilter.js new file mode 100644 index 0000000000..74f00d4c39 --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/o2StopFilter.js @@ -0,0 +1,21 @@ +/** + * @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 { timeRangeFilter } from '../common/filters/timeRangeFilter.js'; + +/** + * Returns a filter to be applied on run stop + * + * @param {RunsOverviewModel} runsOverviewModel the run overview model object + * @return {Component} the filter component + */ +export const o2StopFilter = (runsOverviewModel) => timeRangeFilter(runsOverviewModel.o2stopFilterModel); diff --git a/lib/public/components/Filters/RunsFilter/o2start.js b/lib/public/components/Filters/RunsFilter/o2start.js deleted file mode 100644 index da13099448..0000000000 --- a/lib/public/components/Filters/RunsFilter/o2start.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @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'; - -const DATE_FORMAT = 'YYYY-MM-DD'; -let today = new Date(); -today.setMinutes(today.getMinutes() - today.getTimezoneOffset()); -[today] = today.toISOString().split('T'); - -/** - * Returns the creation date filter components - * @param {RunsOverviewModel} runModel the run model object - * @return {vnode} Two date selection boxes to control the minimum and maximum creation dates for the log filters - */ -const o2startFilter = (runModel) => { - const date = new Date(); - const o2from = runModel.getO2startFilterFrom(); - const o2to = runModel.getO2startFilterTo(); - const o2fromTime = runModel.getO2startFilterFromTime(); - const o2toTime = runModel.getO2startFilterToTime(); - // FIXME No leading 0 in hours leads to no max date defined in the inputs if the date is bellow 9 - const now = `${date.getHours()}:${(date.getMinutes() < 10 ? '0' : '') + date.getMinutes()}`; - - return h('', [ - h('.f6', 'Started From:'), - h('input.w-50.mv1', { - type: 'date', - id: 'o2startFilterFrom', - placeholder: DATE_FORMAT, - max: o2to || today, - value: o2from, - oninput: (e) => runModel.setO2startFilter('From', e.target.value, e.target.validity.valid), - }, ''), - h('input.w-50.mv1', { - type: 'time', - id: 'o2startFilterFromTime', - max: today == o2from - ? now - : o2from == o2to ? o2toTime : '23:59', - value: o2fromTime, - oninput: (e) => { - const time = e.target.value ? e.target.value : '00:00'; - if (!o2from) { - runModel.setO2startFilter('From', today, true); - runModel.setO2startFilter('FromTime', time, e.target.value <= now); - } else { - runModel.setO2startFilter('FromTime', time, e.target.validity.valid); - } - }, - - }, ''), - h('.f6', 'Started Till:'), - h('input.w-50.mv1', { - type: 'date', - id: 'o2startFilterTo', - placeholder: DATE_FORMAT, - min: o2from, - max: today, - value: o2to, - oninput: (e) => runModel.setO2startFilter('To', e.target.value, e.target.validity.valid), - }, ''), - h('input.w-50.mv1', { - type: 'time', - id: 'o2startFilterToTime', - min: o2from == o2to ? o2fromTime : '00:00', - // Fixme I suppose that here it should be today == o2to ? - max: today == o2from ? now : '23:59', - value: o2toTime, - oninput: (e) => { - const time = e.target.value ? e.target.value : - today == o2from ? now : '23:59'; - if (!o2to) { - runModel.setO2startFilter('To', today, true); - } - runModel.setO2startFilter('ToTime', time, e.target.validity.valid); - }, - }, ''), - ]); -}; - -export default o2startFilter; diff --git a/lib/public/components/Filters/RunsFilter/o2stop.js b/lib/public/components/Filters/RunsFilter/o2stop.js deleted file mode 100644 index 9ea8175933..0000000000 --- a/lib/public/components/Filters/RunsFilter/o2stop.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * @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'; - -const DATE_FORMAT = 'YYYY-MM-DD'; - -let today = new Date(); -today.setMinutes(today.getMinutes() - today.getTimezoneOffset()); -[today] = today.toISOString().split('T'); - -/** - * Returns the creation date filter components - * @param {RunsOverviewModel} runsOverviewModel the run model object - * @return {vnode} Two date selection boxes to control the minimum and maximum creation dates for the log filters - */ -const o2endFilter = (runsOverviewModel) => { - const date = new Date(); - const o2from = runsOverviewModel.getO2endFilterFrom(); - const o2to = runsOverviewModel.getO2endFilterTo(); - const o2fromTime = runsOverviewModel.getO2endFilterFromTime(); - const o2toTime = runsOverviewModel.getO2endFilterToTime(); - const now = `${date.getHours()}:${(date.getMinutes() < 10 ? '0' : '') + date.getMinutes()}`; - return h('', [ - h('.f6', 'Ended from:'), - h('input.w-50.mv1', { - type: 'date', - id: 'o2endFilterFrom', - placeholder: DATE_FORMAT, - max: o2to || today, - value: o2from, - oninput: (e) => runsOverviewModel.setO2endFilter('From', e.target.value, e.target.validity.valid), - }, ''), - h('input.w-50.mv1', { - type: 'time', - id: 'o2endFilterFromTime', - max: today == o2from ? - now : - o2from == o2to ? o2toTime : '23:59', - value: o2fromTime, - oninput: (e) => { - const time = e.target.value ? e.target.value : '00:00'; - if (!o2from) { - runsOverviewModel.setO2endFilter('From', today, true); - runsOverviewModel.setO2endFilter('FromTime', time, e.target.value <= now); - } else { - runsOverviewModel.setO2endFilter('FromTime', time, e.target.validity.valid); - } - }, - }, ''), - h('.f6', 'Ended Till:'), - h('input.w-50.mv1', { - type: 'date', - id: 'o2endFilterTo', - min: o2from, - max: today, - value: o2to, - oninput: (e) => runsOverviewModel.setO2endFilter('To', e.target.value, e.target.validity.valid), - }, ''), - - h('input.w-50.mv1', { - type: 'time', - id: 'o2endFilterToTime', - min: o2from == o2to ? o2fromTime : '00:00', - max: '23:59', - value: o2toTime, - oninput: (e) => { - const time = e.target.value ? e.target.value : - today == o2from ? now : '23:59'; - if (!o2to) { - runsOverviewModel.setO2endFilter('To', today, true); - } - runsOverviewModel.setO2endFilter('ToTime', time, e.target.validity.valid); - }, - }, ''), - ]); -}; - -export default o2endFilter; diff --git a/lib/public/components/Filters/common/filters/TimeRangeInputModel.js b/lib/public/components/Filters/common/filters/TimeRangeInputModel.js index 0aa78df8f5..99a7587168 100644 --- a/lib/public/components/Filters/common/filters/TimeRangeInputModel.js +++ b/lib/public/components/Filters/common/filters/TimeRangeInputModel.js @@ -52,7 +52,7 @@ export class TimeRangeInputModel extends FilterModel { this._toTimeInputModel.observe(() => this._periodLabel = null); this._toTimeInputModel.bubbleTo(this); - this.setValue(value, periodLabel, false); + this.setValue(value, periodLabel, true); } /** @@ -132,7 +132,7 @@ export class TimeRangeInputModel extends FilterModel { * @override */ get isEmpty() { - return this._fromTimeInputModel.value === null && this._toTimeInputModel.value === null; + return this._fromTimeInputModel.isEmpty && this._toTimeInputModel.isEmpty; } // eslint-disable-next-line valid-jsdoc diff --git a/lib/public/components/Pagination/amountSelector.js b/lib/public/components/Pagination/amountSelector.js index 3d2da543f8..4986b60cbb 100644 --- a/lib/public/components/Pagination/amountSelector.js +++ b/lib/public/components/Pagination/amountSelector.js @@ -50,12 +50,7 @@ const perPageAmountInputComponent = (onCustomChoiceConfirm, paginationModel) => oninput: ({ target }) => { paginationModel.customItemsPerPage = target.value; }, - onkeyup: ({ key }) => { - if (key === 'Enter') { - onCustomChoiceConfirm(); - } - }, - onblur: onCustomChoiceConfirm, + onchange: onCustomChoiceConfirm, }); /** diff --git a/lib/public/components/common/chart/rendering/ChartRenderer.js b/lib/public/components/common/chart/rendering/ChartRenderer.js index 4a57856dcb..12753c5378 100644 --- a/lib/public/components/common/chart/rendering/ChartRenderer.js +++ b/lib/public/components/common/chart/rendering/ChartRenderer.js @@ -60,7 +60,7 @@ export class ChartRenderer { * - and if index axis is 'y', 'x' must contain an array in the same manner */ constructor(configuration, data) { - if (!data.length) { + if (!data?.length) { throw new Error('The data list can not be empty'); } this._data = data; diff --git a/lib/public/components/common/externalLinks/aliEcsEnvironmentLinkComponent.js b/lib/public/components/common/externalLinks/aliEcsEnvironmentLinkComponent.js index d9ceaf7aaf..2302cddfda 100644 --- a/lib/public/components/common/externalLinks/aliEcsEnvironmentLinkComponent.js +++ b/lib/public/components/common/externalLinks/aliEcsEnvironmentLinkComponent.js @@ -23,7 +23,7 @@ export const aliEcsEnvironmentLinkComponent = (environmentId) => { const aliEcsEnvironmentUrl = getAliEcsUrl(environmentId); return aliEcsEnvironmentUrl ? h( - 'a', + 'a.external-link', { target: '_blank', href: aliEcsEnvironmentUrl }, 'ECS', ) diff --git a/lib/public/components/common/externalLinks/infologgerLinksComponents.js b/lib/public/components/common/externalLinks/infologgerLinksComponents.js index 191823cb56..18d2d0ff00 100644 --- a/lib/public/components/common/externalLinks/infologgerLinksComponents.js +++ b/lib/public/components/common/externalLinks/infologgerLinksComponents.js @@ -21,7 +21,7 @@ import { h } from '/js/src/index.js'; * @param {string} label The label for the link. * @return {Component} The infologger link component */ -const infoLoggerLink = (url, label) => h('a', { href: url, target: '_blank' }, label); +const infoLoggerLink = (url, label) => h('a.external-link', { href: url, target: '_blank' }, label); /** * Returns an FLP infologger link component for the given infologger filter and label. diff --git a/lib/public/components/common/externalLinks/qcGuiLinkComponent.js b/lib/public/components/common/externalLinks/qcGuiLinkComponent.js index 3cd0428015..9f3218fe8f 100644 --- a/lib/public/components/common/externalLinks/qcGuiLinkComponent.js +++ b/lib/public/components/common/externalLinks/qcGuiLinkComponent.js @@ -27,5 +27,5 @@ export const qcGuiLinkComponent = (run) => { return null; } - return h('a', { href: qcGuiLinkUrl, target: '_blank' }, 'QCG'); + return h('a.external-link', { href: qcGuiLinkUrl, target: '_blank' }, 'QCG'); }; diff --git a/lib/public/components/common/form/inputs/DateTimeInputComponent.js b/lib/public/components/common/form/inputs/DateTimeInputComponent.js index fac8acab2e..16bc5d9c5e 100644 --- a/lib/public/components/common/form/inputs/DateTimeInputComponent.js +++ b/lib/public/components/common/form/inputs/DateTimeInputComponent.js @@ -59,7 +59,7 @@ export class DateTimeInputComponent extends StatefulComponent { * @property {DateTimeInputComponentVnode} vnode the component's vnode * @return {void} */ - onupdate({ attrs }) { + onbeforeupdate({ attrs }) { this._updateAttrs(attrs); } @@ -68,8 +68,8 @@ export class DateTimeInputComponent extends StatefulComponent { * @return {Component} the component */ view() { - const inputsMin = this._getInputsMin(this._min, this._value); - const inputsMax = this._getInputsMax(this._max, this._value); + const inputsMin = this._getInputsMin(this._minTimestamp, this._value); + const inputsMax = this._getInputsMax(this._maxTimestamp, this._value); return h('.flex-row.items-center.g2', [ h( @@ -81,8 +81,9 @@ export class DateTimeInputComponent extends StatefulComponent { required: this._required, value: this._value.date, onchange: (e) => this._patchValue({ date: e.target.value }), - min: inputsMin ? inputsMin.date : undefined, - max: inputsMax ? inputsMax.date : undefined, + // Mithril do not remove min/max if previously set... + min: inputsMin?.date ?? '', + max: inputsMax?.date ?? '', }, ), h( @@ -93,8 +94,9 @@ export class DateTimeInputComponent extends StatefulComponent { value: this._value.time, step: this._seconds ? 1 : undefined, onchange: (e) => this._patchValue({ time: e.target.value }), - min: inputsMin ? inputsMin.time : undefined, - max: inputsMax ? inputsMax.time : undefined, + // Mithril do not remove min/max if previously set... + min: inputsMin?.time ?? '', + max: inputsMax?.time ?? '', }, ), h( @@ -132,8 +134,8 @@ export class DateTimeInputComponent extends StatefulComponent { time: defaultTime, }; - this._min = min; - this._max = max; + this._minTimestamp = min; + this._maxTimestamp = max; } /** @@ -163,59 +165,61 @@ export class DateTimeInputComponent extends StatefulComponent { /** * Returns the min values to apply to the inputs * - * @param {number|null} min the minimal timestamp to represent in the inputs + * @param {number|null} minTimestamp the minimal timestamp to represent in the inputs * @param {DateTimeInputRawData} raw the current raw values * @return {(Partial<{date: string, time: string}>|null)} the min values to apply to date and time inputs */ - _getInputsMin(min, raw) { - if (min === null) { + _getInputsMin(minTimestamp, raw) { + if (minTimestamp === null) { return null; } - const minDateDayAfter = new Date(min + MILLISECONDS_IN_ONE_DAY); + const rawDate = raw.date || null; + const rawTime = raw.time || null; - const minDateAndTime = formatTimestampForDateTimeInput(min, this._seconds); - const ret = {}; + const minDateAndTime = formatTimestampForDateTimeInput(minTimestamp, this._seconds); + const inputsMin = {}; - if (raw.date !== null && raw.date === minDateAndTime.date) { - ret.time = minDateAndTime.time; + if (rawDate !== null && rawDate === minDateAndTime.date) { + inputsMin.time = minDateAndTime.time; } - if (raw.time !== null && raw.time < minDateAndTime.time) { - ret.date = formatTimestampForDateTimeInput(minDateDayAfter.getTime(), this._seconds).date; + if (rawTime !== null && rawTime < minDateAndTime.time) { + inputsMin.date = formatTimestampForDateTimeInput(minTimestamp + MILLISECONDS_IN_ONE_DAY, this._seconds).date; } else { - ret.date = minDateAndTime.date; + inputsMin.date = minDateAndTime.date; } - return ret; + return inputsMin; } /** * Returns the max values to apply to the inputs * - * @param {number|null} max the maximal timestamp to represent in the inputs + * @param {number|null} maxTimestamp the maximal timestamp to represent in the inputs * @param {DateTimeInputRawData} raw the current raw values * @return {(Partial<{date: string, time: string}>|null)} the max values */ - _getInputsMax(max, raw) { - if (max === null) { + _getInputsMax(maxTimestamp, raw) { + if (maxTimestamp === null) { return null; } - const maxDateDayBefore = new Date(max - MILLISECONDS_IN_ONE_DAY); + const rawDate = raw.date || null; + const rawTime = raw.time || null; - const maxDateAndTime = formatTimestampForDateTimeInput(max, this._seconds); - const ret = {}; + const maxDateAndTime = formatTimestampForDateTimeInput(maxTimestamp, this._seconds); + const inputsMax = {}; - if (raw.date !== null && raw.date === maxDateAndTime.date) { - ret.time = maxDateAndTime.time; + if (rawDate !== null && rawDate === maxDateAndTime.date) { + inputsMax.time = maxDateAndTime.time; } - if (raw.time !== null && raw.time > maxDateAndTime.time) { - ret.date = formatTimestampForDateTimeInput(maxDateDayBefore.getTime(), this._seconds).date; + if (rawTime !== null && rawTime > maxDateAndTime.time) { + inputsMax.date = formatTimestampForDateTimeInput(maxTimestamp - MILLISECONDS_IN_ONE_DAY, this._seconds).date; } else { - ret.date = maxDateAndTime.date; + inputsMax.date = maxDateAndTime.date; } - return ret; + return inputsMax; } } diff --git a/lib/public/components/common/form/inputs/DateTimeInputModel.js b/lib/public/components/common/form/inputs/DateTimeInputModel.js index 0ff6aeef8f..51113ba2f0 100644 --- a/lib/public/components/common/form/inputs/DateTimeInputModel.js +++ b/lib/public/components/common/form/inputs/DateTimeInputModel.js @@ -79,9 +79,16 @@ export class DateTimeInputModel extends Observable { * @return {void} */ clear() { - this._raw = { date: '', time: '' }; - this._value = null; - this.notify(); + this.setValue(null, true); + } + + /** + * States if the input model is empty (has no defined value) + * + * @return {boolean} true if the model is empty + */ + get isEmpty() { + return this._value === null; } /** diff --git a/lib/public/components/common/formatting/formatTimeRange.js b/lib/public/components/common/formatting/formatTimeRange.js index a9e7c86456..103254153e 100644 --- a/lib/public/components/common/formatting/formatTimeRange.js +++ b/lib/public/components/common/formatting/formatTimeRange.js @@ -12,6 +12,7 @@ */ import { formatTimestamp as defaultFormatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; +import { h } from '/js/src/index.js'; /** * Format the current timestamp range display @@ -34,7 +35,7 @@ export const formatTimeRange = ({ from, to }, configuration) => { let parts = []; if (from === undefined && to === undefined) { - parts = ['-']; + parts = [h('.badge', '-')]; } else if (from === undefined) { parts = [formatText('Before'), formatTimestamp(to)]; } else if (to === undefined) { diff --git a/lib/public/components/common/selection/dropdown/selectionDropdown.js b/lib/public/components/common/selection/dropdown/selectionDropdown.js index a4046d76d4..c3db98eacf 100644 --- a/lib/public/components/common/selection/dropdown/selectionDropdown.js +++ b/lib/public/components/common/selection/dropdown/selectionDropdown.js @@ -117,6 +117,7 @@ const dropdownOptions = (dropdownModel, selectorPrefix, displayOption, openingOb * @param {SelectionDropdownModel} selectionDropdownModel the model storing the state of the dropdown * @param {Object} [configuration] the component's configuration * @param {string} [configuration.selectorPrefix=''] a selector prefix used to generate DOM selectors + * @param {Component} [configuration.placeholder='-'] component used as trigger content when no option are selected * @param {DisplayDropdownOption} [configuration.displayOption=null] function used to generate the option's view * @param {DisplaySelectionItem} [configuration.displaySelectionItem=null] function called with the selected option to generate the current * selection's items @@ -126,7 +127,7 @@ const dropdownOptions = (dropdownModel, selectorPrefix, displayOption, openingOb */ export const selectionDropdown = (selectionDropdownModel, configuration) => { let { displayOption = null, displaySelectionItem = null } = configuration || {}; - const { searchEnabled = true } = configuration || {}; + const { searchEnabled = true, placeholder = '-' } = configuration || {}; const selectorPrefix = cleanPrefix(configuration.selectorPrefix); @@ -149,14 +150,14 @@ export const selectionDropdown = (selectionDropdownModel, configuration) => { } if (displaySelectionItem === null) { - displaySelectionItem = ({ label, value }) => h('small.badge.bg-gray-light', label || value); + displaySelectionItem = ({ label, value }) => h('small.badge.bg-gray-light', { key: value }, label || value); } const selectedPills = h( '.flex-row.flex-wrap.dropdown-selection.g2', selectionDropdownModel.selectedOptions.length > 0 ? selectionDropdownModel.selectedOptions.map(displaySelectionItem) - : h('small.badge', '-'), + : h('small.badge', placeholder), ); // Create an observable notified any time the dropdown is opened diff --git a/lib/public/components/tag/TagSelectionDropdownModel.js b/lib/public/components/tag/TagSelectionDropdownModel.js index 5de6abcf34..d6a6ec23d5 100644 --- a/lib/public/components/tag/TagSelectionDropdownModel.js +++ b/lib/public/components/tag/TagSelectionDropdownModel.js @@ -22,12 +22,13 @@ import { tagToOption } from './tagToOption.js'; export class TagSelectionDropdownModel extends SelectionDropdownModel { /** * Constructor - * @param {{includeArchived: boolean}} [configuration={}] the dropdown configuration + * @param {SelectionModelConfiguration} [configuration={}] the dropdown configuration * @param {boolean} [configuration.includeArchived] if true, all tags including archived will be available as selection */ constructor(configuration) { - super({ availableOptions: RemoteData.notAsked() }); - const { includeArchived } = configuration || {}; + super({ ...configuration || {}, availableOptions: RemoteData.notAsked() }); + const { includeArchived = false } = configuration || {}; + this._includeArchived = includeArchived; } diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index b6f35d0f15..0ec18fb4b0 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -13,8 +13,6 @@ import { h } from '/js/src/index.js'; import runNumberFilter from '../../../components/Filters/RunsFilter/runNumber.js'; -import o2startFilter from '../../../components/Filters/RunsFilter/o2start.js'; -import o2endFilter from '../../../components/Filters/RunsFilter/o2stop.js'; import environmentIdFilter from '../../../components/Filters/RunsFilter/environmentId.js'; import nDetectorsFilter from '../../../components/Filters/RunsFilter/nDetectors.js'; import nFlpsFilter from '../../../components/Filters/RunsFilter/nFlps.js'; @@ -54,6 +52,8 @@ import { CopyToClipboardComponent } from '../../../components/common/selection/i import { infologgerLinksComponents } from '../../../components/common/externalLinks/infologgerLinksComponents.js'; import { RunQualities } from '../../../domain/enums/RunQualities.js'; import { qcGuiLinkComponent } from '../../../components/common/externalLinks/qcGuiLinkComponent.js'; +import { o2StartFilter } from '../../../components/Filters/RunsFilter/o2StartFilter.js'; +import { o2StopFilter } from '../../../components/Filters/RunsFilter/o2StopFilter.js'; import { isRunConsideredRunning } from '../../../services/run/isRunConsideredRunning.js'; import { aliEcsEnvironmentLinkComponent } from '../../../components/common/externalLinks/aliEcsEnvironmentLinkComponent.js'; @@ -174,7 +174,7 @@ export const runsActiveColumns = { noEllipsis: true, format: (_, run) => formatRunStart(run, false), exportFormat: (timestamp) => formatTimestamp(timestamp), - filter: o2startFilter, + filter: o2StartFilter, profiles: { lhcFill: true, environment: true, @@ -200,7 +200,7 @@ export const runsActiveColumns = { noEllipsis: true, format: (_, run) => formatRunEnd(run, false), exportFormat: (timestamp) => formatTimestamp(timestamp), - filter: o2endFilter, + filter: o2StopFilter, profiles: { lhcFill: true, environment: true, @@ -502,6 +502,6 @@ export const runsActiveColumns = { name: 'L3 / Dipole', visible: false, filter: ({ aliceL3AndDipoleCurrentFilter }) => selectionDropdown(aliceL3AndDipoleCurrentFilter, { selectorPrefix: 'l3-dipole-current' }), - profiles: ['runsPerLhcPeriod', 'runsPerDataPass', 'runsPerSimulationPass'], + profiles: ['runsPerLhcPeriod', 'runsPerDataPass', 'runsPerSimulationPass', profiles.none], }, }; diff --git a/lib/public/views/Runs/Details/RunDetailsModel.js b/lib/public/views/Runs/Details/RunDetailsModel.js index 2693787ca8..ee7d1d2dcc 100644 --- a/lib/public/views/Runs/Details/RunDetailsModel.js +++ b/lib/public/views/Runs/Details/RunDetailsModel.js @@ -15,11 +15,11 @@ import { Observable, RemoteData } from '/js/src/index.js'; import { getRemoteData } from '../../../utilities/fetch/getRemoteData.js'; import { jsonFetch } from '../../../utilities/fetch/jsonFetch.js'; import { RunPatch } from './RunPatch.js'; -import { TagPickerModel } from '../../../components/tag/TagPickerModel.js'; import { TabbedPanelModel } from '../../../components/TabbedPanel/TabbedPanelModel.js'; import { CollapsibleTreeNodeModel } from '../../../models/CollapsibleTreeNodeModel.js'; import { detectorsProvider } from '../../../services/detectors/detectorsProvider.js'; import { tagToOption } from '../../../components/tag/tagToOption.js'; +import { TagSelectionDropdownModel } from '../../../components/tag/TagSelectionDropdownModel.js'; export const RUN_DETAILS_PANELS_KEYS = { LOGS: 'logs', @@ -45,7 +45,7 @@ export class RunDetailsModel extends Observable { this._detectors$ = detectorsProvider.dataTaking$; this._detectors$.bubbleTo(this); - this.editionTagPickerModel = new TagPickerModel(); + this.editionTagPickerModel = new TagSelectionDropdownModel(); this._runPatch = new RunPatch(); this._runPatch.bubbleTo(this); @@ -266,7 +266,7 @@ export class RunDetailsModel extends Observable { this._runDetails = RemoteData.success(run); this._runPatch = new RunPatch(run); this._runPatch.bubbleTo(this); - this.editionTagPickerModel = new TagPickerModel({ defaultSelection: run.tags.map(tagToOption) }); + this.editionTagPickerModel = new TagSelectionDropdownModel({ defaultSelection: run.tags.map(tagToOption) }); this._tabbedPanelModel.runNumber = run.runNumber; this._runNumber = run.runNumber; } catch (error) { diff --git a/lib/public/views/Runs/Details/lhcFillPanel.js b/lib/public/views/Runs/Details/lhcFillPanel.js deleted file mode 100644 index 958e54e09b..0000000000 --- a/lib/public/views/Runs/Details/lhcFillPanel.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @license - * Copyright 2019-2020 CERN and copyright holders of ALICE O2. - * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. - * All rights not expressly granted are reserved. - * - * This software is distributed under the terms of the GNU General Public - * License v3 (GPL Version 3), copied verbatim in the file "COPYING". - * - * 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 errorAlert from '../../../components/common/errorAlert.js'; -import spinner from '../../../components/common/spinner.js'; -import { h } from '/js/src/index.js'; -import { BeamModes } from '../../../domain/enums/BeamModes.js'; -import { commonLhcFillDisplayConfiguration } from '../../LhcFills/Detail/commonLhcFillDisplayConfiguration.js'; -import { contextualInfo } from '../../../components/common/contextualMessage/contextualMessage.js'; -import { detailsList } from '../../../components/Detail/detailsList.js'; -import { runLhcFillDisplayConfiguration } from './runLhcFillDisplayConfiguration.js'; - -/** - * Show the lhc data panel - * @param {RemoteData} run the run for which lhc fill information have to be displayed - * @returns {Component} run's LHC fill information - */ -export const lhcFillPanel = (run) => [ - h('strong', 'LHC Data'), - run.match({ - NotAsked: () => null, - Loading: () => spinner({ size: 5, absolute: false }), - Success: (runPayload) => runPayload.lhcBeamMode === BeamModes.STABLE_BEAMS ? [ - detailsList(commonLhcFillDisplayConfiguration, runPayload.lhcFill || {}, { selector: 'lhc-fill' }), - detailsList(runLhcFillDisplayConfiguration, runPayload, { selector: 'run-lhc-fill' }), - ] : h('div', h('em#NoLHCDataNotStable', contextualInfo(`No LHC Fill information, beam mode was: ${runPayload.lhcBeamMode}`))), - Failure: (error) => error.map(errorAlert), - }), -]; diff --git a/lib/public/views/Runs/Details/runDetailsComponent.js b/lib/public/views/Runs/Details/runDetailsComponent.js index 02737848f4..02118fe0f5 100644 --- a/lib/public/views/Runs/Details/runDetailsComponent.js +++ b/lib/public/views/Runs/Details/runDetailsComponent.js @@ -11,9 +11,7 @@ * or submit itself to any jurisdiction. */ -import { h } from '/js/src/index.js'; -import { lhcFillPanel } from './lhcFillPanel.js'; -import { editTagsPanel } from './editTagsPanel.js'; +import { h, iconChevronRight, iconExternalLink } from '/js/src/index.js'; import { tabbedPanelComponent } from '../../../components/TabbedPanel/tabbedPanelComponent.js'; import { RUN_DETAILS_PANELS_KEYS } from './RunDetailsModel.js'; import { table } from '../../../components/common/table/table.js'; @@ -22,14 +20,33 @@ import { flpsActiveColumns } from '../../Flps/ActiveColumns/flpsActiveColumns.js import spinner from '../../../components/common/spinner.js'; import errorAlert from '../../../components/common/errorAlert.js'; import { collapsibleTreeNode } from '../../../components/common/tree/collapsibleTreeNode.js'; -import { detailsList } from '../../../components/Detail/detailsList.js'; -import { runDetailsConfiguration } from './runDetailsConfiguration.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { qcGuiLinkComponent } from '../../../components/common/externalLinks/qcGuiLinkComponent.js'; import { aliEcsEnvironmentLinkComponent } from '../../../components/common/externalLinks/aliEcsEnvironmentLinkComponent.js'; import { isRunConsideredRunning } from '../../../services/run/isRunConsideredRunning.js'; import { epnInfologgerLinkComponent, flpInfologgerLinkComponent } from '../../../components/common/externalLinks/infologgerLinksComponents.js'; import { formatItemsCount } from '../../../utilities/formatting/formatItemsCount.js'; +import { formatRunDetectors } from '../format/formatRunDetectors.js'; +import { PanelComponent } from '../../../components/common/panel/PanelComponent.js'; +import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; +import { displayRunDuration } from '../format/displayRunDuration.js'; +import { formatDuration } from '../../../utilities/formatting/formatDuration.mjs'; +import { formatRunQuality } from '../format/formatRunQuality.js'; +import { formatTagsList } from '../../Tags/format/formatTagsList.js'; +import { formatRunType } from '../../../utilities/formatting/formatRunType.js'; +import { formatBoolean } from '../../../utilities/formatting/formatBoolean.js'; +import { formatFileSize } from '../../../utilities/formatting/formatFileSize.js'; +import { RunDefinition } from '../../../domain/enums/RunDefinition.js'; +import { formatFloat } from '../../../utilities/formatting/formatFloat.js'; +import { formatEditableNumber } from '../format/formatEditableNumber.js'; +import { PdpBeamType } from '../../../domain/enums/PdpBeamType.js'; +import { editRunEorReasons } from '../format/editRunEorReasons.js'; +import { formatEorReason } from '../format/formatEorReason.mjs'; +import { selectionDropdown } from '../../../components/common/selection/dropdown/selectionDropdown.js'; +import { formatRunCalibrationStatus } from '../format/formatRunCalibrationStatus.js'; +import { BeamModes } from '../../../domain/enums/BeamModes.js'; + +const GREEK_LOWER_MU_ENCODING = '\u03BC'; /** * Returns a component displaying details for a given run @@ -66,56 +83,411 @@ export const runDetailsComponent = (runDetailsModel, router) => runDetailsModel. ...run.fillNumber ? { lhcFillNumbers: [run.fillNumber] } : {}, }; + const environmentLink = run.environmentId + ? frontLink(run.environmentId, 'env-details', { environmentId: run.environmentId }) + : null; + return [ h('.flex-column.w-100', [ - h('.w-100.flex-row', [ - h('h2.w-40', `Run #${run.runNumber}`), - h('.w-60.flex-row.items-center', { - style: 'justify-content: flex-end;', - }, h('.btn-group.', [ - !runDetailsModel.isEditModeEnabled ? - [ - frontLink( - 'Add log to this run', - 'log-create', - queryParameters, - { id: 'create-log', class: 'btn btn-primary' }, - ), - h('button.btn#edit-run', { - onclick: () => { - runDetailsModel.isEditModeEnabled = true; - }, - }, 'Edit Run'), - ] - : [ - h('button.btn.btn-success#save-run', { - disabled: !runDetailsModel.isEditionPatchValid, - onclick: () => { - runDetailsModel.updateOneRun(); - runDetailsModel.clearAllEditors(); - }, - }, 'Save'), - h('button.btn#cancel-run', { - onclick: () => { - runDetailsModel.clearAllEditors(); - }, - }, 'Revert'), - ], - ])), - ]), - h('h3.external-links.flex-row.w-100.g2.items-baseline.mb3', [ - (flpInfologgerLink || epnInfologgerLink) && [h('', 'Infologger: '), flpInfologgerLink, epnInfologgerLink], - [qcGuiLinkComponent(run)], - isRunConsideredRunning(run) && aliEcsEnvironmentLink && [aliEcsEnvironmentLink], - ].filter((elements) => elements).flatMap((elements) => ['-', ...elements]).slice(1)), - errorAlert(runDetailsModel.updateErrors), - h('.flex-row.w-100', [ + h('.w-100.flex-row.justify-between', [ + h( + '.flex-row.items-center.g3', + [ + h('h2', `Run #${run.runNumber}`), + h( + '#tags.f4', + runDetailsModel.isEditModeEnabled + ? selectionDropdown( + runDetailsModel.editionTagPickerModel, + { selectorPrefix: 'tags', placeholder: 'No tags' }, + ) + : formatTagsList(run.tags, { flex: true, description: true, placeholder: null }), + ), + ], + ), h( - '.w-40.ph1', - detailsList(runDetailsConfiguration(runDetailsModel), run, { selector: 'Run' }), + '.flex-row.items-center', + { style: 'justify-content: flex-end;' }, + h('.btn-group.', [ + !runDetailsModel.isEditModeEnabled ? + [ + frontLink( + 'Add log to this run', + 'log-create', + queryParameters, + { id: 'create-log', class: 'btn btn-primary' }, + ), + h('button.btn#edit-run', { + onclick: () => { + runDetailsModel.isEditModeEnabled = true; + }, + }, 'Edit Run'), + ] + : [ + h('button.btn.btn-success#save-run', { + disabled: !runDetailsModel.isEditionPatchValid, + onclick: () => { + runDetailsModel.updateOneRun(); + runDetailsModel.clearAllEditors(); + }, + }, 'Save'), + h('button.btn#cancel-run', { + onclick: () => { + runDetailsModel.clearAllEditors(); + }, + }, 'Revert'), + ], + ]), ), - h('.w-30.ph3', lhcFillPanel(runDetailsModel.runDetails)), - runDetailsModel.isEditModeEnabled && editTagsPanel(runDetailsModel), + ]), + h( + 'h3.flex-row.w-100.g2.items-baseline.mb3', + [ + environmentLink && [h('', 'Environment: '), environmentLink], + (flpInfologgerLink || epnInfologgerLink) && [h('', 'Infologger: '), flpInfologgerLink, epnInfologgerLink], + [qcGuiLinkComponent(run)], + isRunConsideredRunning(run) && aliEcsEnvironmentLink && [aliEcsEnvironmentLink], + ] + .filter((elements) => elements && elements.filter((item) => item).length) + .flatMap((elements) => ['-', ...elements]) + .slice(1), + ), + errorAlert(runDetailsModel.updateErrors), + h('.flex-column.g2', [ + h(PanelComponent, [ + h('.panel-header', 'Timeline'), + h('.flex-row.flex-wrap.p2.items-center.g3', [ + h('.flex-column.items-center.flex-grow', [ + h('strong', 'O2 Start'), + formatTimestamp(run.timeO2Start), + ]), + iconChevronRight(), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'Trigger Start'), + formatTimestamp(run.timeTrgStart), + ]), + iconChevronRight(), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'Data Transfer Start'), + formatTimestamp(run.startOfDataTransfer), + ]), + iconChevronRight(), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'Trigger End'), + formatTimestamp(run.timeTrgEnd), + ]), + iconChevronRight(), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'O2 End'), + formatTimestamp(run.timeO2End), + ]), + iconChevronRight(), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'Data Transfer End'), + formatTimestamp(run.endOfDataTransfer), + ]), + h('strong', '|'), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'Run duration'), + h('#runDurationValue', displayRunDuration(run)), + ]), + ]), + ]), + h(PanelComponent, [ + h('.panel-header', h('.flex-row.flex-wrap.items-center.g2', [ + 'Detectors', + h('.badge.bg-white', run.nDetectors), + h('', '-'), + h('strong', 'Global quality: '), + formatRunQuality(runDetailsModel, run.runQuality, run), + ])), + h('#detectors.p2', formatRunDetectors(run, runDetailsModel)), + ]), + h('.flex-row.items-start.g2', [ + h(PanelComponent, { class: 'flex-grow' }, [ + h('.panel-header', 'Components'), + h('.flex-row.p2.items-center.g3', [ + h('#n-epns.flex-column.items-center.flex-grow', [ + h('strong', '# EPNs'), + run.epn + ? typeof run.nEpns === 'number' ? run.nEpns : 'ON' + : 'OFF', + ]), + h('#n-flps.flex-column.items-center.flex-grow', [ + h('strong', '# FLPs'), + run.nFlps, + ]), + h('strong', '|'), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'Trigger'), + run.triggerValue, + ]), + h('strong', '|'), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'DD FLP'), + formatBoolean(run.dd_flp), + ]), + h('strong', '|'), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'DCS'), + formatBoolean(run.dcs), + ]), + ]), + ]), + h(PanelComponent, { class: 'flex-grow' }, [ + h('.panel-header', 'ALICE Magnets'), + h('.flex-row.p2.items-center.g3', [ + h('.flex-column.items-center.flex-grow', [ + h('strong', 'Dipole Current'), + run.aliceDipoleCurrent, + ]), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'Dipole Polarity'), + run.aliceDipolePolarity, + ]), + h('strong', '|'), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'L3 Current'), + run.aliceL3Current, + ]), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'L3 Polarity'), + run.aliceL3Polarity, + ]), + ]), + ]), + ]), + h(PanelComponent, [ + h('.panel-header', 'Metrics'), + h('.flex-row.p2.items-center.g3', [ + h('.flex-column.items-center.flex-grow', [ + h('strong', 'CTF Files count'), + formatItemsCount(run.ctfFileCount), + ]), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'CTF Files size'), + formatFileSize(run.ctfFileSize), + ]), + h('strong', '|'), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'TF Files count'), + formatItemsCount(run.tfFileCount), + ]), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'TF Files size'), + formatFileSize(run.tfFileSize), + ]), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'TF Orbits count'), + formatItemsCount(run.nTfOrbits), + ]), + h('strong', '|'), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'Other Files count'), + formatItemsCount(run.otherFileCount), + ]), + ]), + ]), + run.definition === RunDefinition.Physics && h(PanelComponent, [ + h('.panel-header', 'INEL'), + h('.flex-row.p2.items-center.g3', [ + run.pdpBeamType === PdpBeamType.PROTON_PROTON + ? [ + h('.flex-column.items-center.flex-grow', [ + h('strong', `${GREEK_LOWER_MU_ENCODING}(INEL)`), + h('#mu-inelastic-interaction-rate', formatFloat(run.muInelasticInteractionRate)), + ]), + h('strong', '|'), + ] + : [], + h('.flex-column.items-center.flex-grow', [ + h('strong', 'Average'), + h('#inelastic-interaction-rate-avg', formatEditableNumber( + runDetailsModel.isEditModeEnabled, + runDetailsModel.runPatch.formData.inelasticInteractionRateAvg, + ({ target: { value: inelasticInteractionRateAvg } }) => + runDetailsModel.runPatch.patchFormData({ inelasticInteractionRateAvg }), + { unit: 'Hz' }, + )), + ]), + run.pdpBeamType === PdpBeamType.LEAD_LEAD + ? [ + h('strong', '|'), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'At start'), + h('#inelastic-interaction-rate-at-start', formatEditableNumber( + runDetailsModel.isEditModeEnabled, + runDetailsModel.runPatch.formData.inelasticInteractionRateAtStart, + ({ target: { value: inelasticInteractionRateAtStart } }) => + runDetailsModel.runPatch.patchFormData({ inelasticInteractionRateAtStart }), + { unit: 'Hz' }, + )), + ]), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'At Mid'), + h('#inelastic-interaction-rate-at-mid', formatEditableNumber( + runDetailsModel.isEditModeEnabled, + runDetailsModel.runPatch.formData.inelasticInteractionRateAtMid, + ({ target: { value: inelasticInteractionRateAtMid } }) => + runDetailsModel.runPatch.patchFormData({ inelasticInteractionRateAtMid }), + { unit: 'Hz' }, + )), + ]), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'At end'), + h('#inelastic-interaction-rate-at-end', formatEditableNumber( + runDetailsModel.isEditModeEnabled, + runDetailsModel.runPatch.formData.inelasticInteractionRateAtEnd, + ({ target: { value: inelasticInteractionRateAtEnd } }) => + runDetailsModel.runPatch.patchFormData({ inelasticInteractionRateAtEnd }), + { unit: 'Hz' }, + )), + ]), + ] + : [], + ]), + ]), + h('.flex-row.items-start.g2', [ + h(PanelComponent, { class: 'flex-grow' }, [ + h('.panel-header', h('.flex-row.items-center.justify-between', 'Configuration')), + h('.flex-column.p2.g2', [ + h('.flex-row', [ + h('.flex-column.items-center.flex-grow', [ + h('strong', 'Run Definition'), + h( + '#definition.flex-row.flex-wrap.items-center.justify-center.g2', + [ + run.definition || '-', + h('#calibration-status', formatRunCalibrationStatus(runDetailsModel, run)), + ], + ), + ]), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'Run Type'), + formatRunType(run.runType), + ]), + ]), + ]), + h('.flex-column.p2.g2', [ + h('', [ + h('strong', 'PDP Configuration Option: '), + run.pdpConfigOption || '-', + ]), + h('', [ + h('strong', 'PDP Topology Description Library File: '), + run.pdpTopologyDescriptionLibraryFile || '-', + ]), + h('', [ + h('strong', 'PDP Workflow Parameters: '), + run.pdpWorkflowParameters || '-', + ]), + h('', [ + h('strong', 'PDP Beam Type: '), + run.pdpBeamType || '-', + ]), + h('', [ + h('strong', 'TFB DD Mode: '), + run.tfbDdMode || '-', + ]), + h('', [ + h('strong', 'Topology: '), + run.epnTopology || '-', + ]), + h('', [ + h('strong', 'Topology full name: '), + run.odcTopologyFullName || '-', + ]), + h('', [ + h('strong', 'Readout config URI: '), + run.readoutCfgUri || '-', + ]), + ]), + ]), + h( + PanelComponent, + { class: 'flex-grow' }, + [ + h('.panel-header', h('.flex-row.items-center.justify-between', [ + 'LHC', + h( + 'span#fill-number', + [ + 'Fill ', + frontLink( + run.lhcFill?.fillNumber, + 'lhc-fill-details', + { fillNumber: run.lhcFill?.fillNumber }, + ), + ], + ), + frontLink( + h('.flex-row.g2', [iconExternalLink(), 'Details']), + 'lhc-fill-details', + { fillNumber: run.lhcFill?.fillNumber }, + { id: 'create-log', class: 'btn btn-primary btn-sm' }, + ), + ])), + run.lhcBeamMode === BeamModes.STABLE_BEAMS + ? [ + h('.flex-row.p2.items-center.g3', [ + h('.flex-column.items-center.flex-grow', [ + h('strong', 'SB Start'), + formatTimestamp(run.lhcFill?.stableBeamsStart), + ]), + iconChevronRight(), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'SB End'), + formatTimestamp(run.lhcFill?.stableBeamsEnd), + ]), + h('strong', '|'), + h('.flex-column.items-center.flex-grow', [ + h('strong', 'Beams duration'), + h('#runDurationValue', formatDuration(1000 * run.lhcFill?.stableBeamsDuration)), + ]), + ]), + h('.flex-column.p2.g2', [ + h('.flex-row', [ + h('.flex-grow', [ + h('strong', 'Beams type: '), + run.lhcFill?.beamType, + ]), + h('.flex-grow', [ + h('strong', 'Beam mode: '), + run.lhcBeamMode, + ]), + ]), + h('', [ + h('strong', 'Beam Energy: '), + run.lhcBeamEnergy ? `${run.lhcBeamEnergy} GeV` : '-', + ]), + h('', [ + h('strong', 'Filling scheme: '), + run.lhcFill?.fillingSchemeName, + ]), + h('', [ + h('strong', 'Beta Star: '), + run.lhcBetaStar ?? '-', + ]), + h('', [ + h('strong', 'LHC Period: '), + run.lhcPeriod ?? '-', + ]), + ]), + ] + : h('#non-stable-beam-message.p2', `No LHC Fill information, beam mode was: ${run.lhcBeamMode}`), + ], + ), + ]), + h(PanelComponent, [ + h('.panel-header', 'End of run reasons'), + h('.flex-column.p2.g2', [ + h('#eor-reasons.flex-row', [ + runDetailsModel.isEditModeEnabled + ? editRunEorReasons(runDetailsModel) + : h('.flex-column.g2', run.eorReasons.map((eorReason) => h('.eor-reason', formatEorReason(eorReason)))), + ]), + ]), + ]), ]), ]), h('', tabbedPanelComponent( diff --git a/lib/public/views/Runs/Details/runDetailsConfiguration.js b/lib/public/views/Runs/Details/runDetailsConfiguration.js deleted file mode 100644 index 912782f7dd..0000000000 --- a/lib/public/views/Runs/Details/runDetailsConfiguration.js +++ /dev/null @@ -1,259 +0,0 @@ -/** - * @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 { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; -import { formatItemsCount } from '../../../utilities/formatting/formatItemsCount.js'; -import { h } from '/js/src/index.js'; -import { displayRunDuration } from '../format/displayRunDuration.js'; -import { frontLink } from '../../../components/common/navigation/frontLink.js'; -import { formatRunType } from '../../../utilities/formatting/formatRunType.js'; -import { formatBoolean } from '../../../utilities/formatting/formatBoolean.js'; -import { editRunEorReasons } from '../format/editRunEorReasons.js'; -import { formatRunQuality } from '../format/formatRunQuality.js'; -import { formatRunDetectors } from '../format/formatRunDetectors.js'; -import { displayRunEorReasons } from '../format/displayRunEorReasons.js'; -import { formatFileSize } from '../../../utilities/formatting/formatFileSize.js'; -import { formatRunCalibrationStatus } from '../format/formatRunCalibrationStatus.js'; -import { formatTagsList } from '../../Tags/format/formatTagsList.js'; -import { formatEditableNumber } from '../format/formatEditableNumber.js'; -import { formatFloat } from '../../../utilities/formatting/formatFloat.js'; -import { RunDefinition } from '../../../domain/enums/RunDefinition.js'; -import { PdpBeamType } from '../../../domain/enums/PdpBeamType.js'; - -const GREEK_LOWER_MU_ENCODING = '\u03BC'; - -/** - * Returns the configuration to display a given run - * - * @param {RunDetailsModel} runDetailsModel the model storing the run details state - * @return {Object} A collection of data with parameters for the Run detail page. - */ -export const runDetailsConfiguration = (runDetailsModel) => ({ - detectors: { - name: 'Detectors', - visible: true, - format: (_, run) => formatRunDetectors(run, runDetailsModel), - }, - tags: { - name: 'Tags', - visible: true, - format: (tags) => formatTagsList(tags, { flex: true, description: true }), - }, - timeO2Start: { - name: 'O2 Start', - visible: true, - format: (timestamp) => formatTimestamp(timestamp), - }, - timeO2End: { - name: 'O2 Stop', - visible: true, - format: (timestamp) => formatTimestamp(timestamp), - }, - timeTrgStart: { - name: 'TRG Start', - visible: true, - format: (timestamp) => formatTimestamp(timestamp), - }, - timeTrgEnd: { - name: 'TRG Stop', - visible: true, - format: (timestamp) => formatTimestamp(timestamp), - }, - runDuration: { - name: 'Run Duration', - visible: true, - format: (_duration, run) => - h('#runDurationValue', displayRunDuration(run)), - }, - environmentId: { - name: 'Environment Id', - visible: true, - format: (id) => id ? frontLink(id, 'env-details', { environmentId: id }) : '-', - }, - runQuality: { - name: 'Run Quality', - visible: true, - format: (runQuality, run) => formatRunQuality(runDetailsModel, runQuality, run), - }, - definition: { - name: 'Definition', - visible: true, - format: (definition) => definition || '-', - }, - calibrationStatus: { - name: 'Calibration status', - visible: (calibrationStatus) => calibrationStatus !== null, - format: (calibrationStatus, run) => formatRunCalibrationStatus(runDetailsModel, run), - }, - runType: { - name: 'Run Type', - visible: true, - format: formatRunType, - }, - nDetectors: { - name: 'Number of Detectors', - visible: true, - }, - nEpns: { - name: 'Number of EPNs', - visible: true, - // eslint-disable-next-line no-extra-parens - format: (nEpns, { epn }) => epn ? (typeof nEpns === 'number' ? nEpns : 'ON') : 'OFF', - }, - nFlps: { - name: 'Number of FLPs', - visible: true, - }, - triggerValue: { - name: 'Trigger Value', - visible: true, - }, - pdpConfigOption: { - name: 'PDP Configuration Option', - visible: true, - }, - pdpTopologyDescriptionLibraryFile: { - name: 'PDP Topology Description Library File', - }, - pdpWorkflowParameters: { - name: 'PDP Workflow Parameters', - visible: true, - }, - pdpBeamType: { - name: 'PDP Beam Type', - visible: true, - }, - muInelasticInteractionRate: { - name: `${GREEK_LOWER_MU_ENCODING}(INEL)`, - visible: (_, { pdpBeamType, definition }) => pdpBeamType === PdpBeamType.PROTON_PROTON && definition === RunDefinition.Physics, - format: formatFloat, - }, - inelasticInteractionRateAvg: { - name: ['INEL', h('sub', 'avg')], - visible: (_, { definition }) => definition === RunDefinition.Physics, - format: () => formatEditableNumber( - runDetailsModel.isEditModeEnabled, - runDetailsModel.runPatch.formData.inelasticInteractionRateAvg, - ({ target: { value: inelasticInteractionRateAvg } }) => - runDetailsModel.runPatch.patchFormData({ inelasticInteractionRateAvg }), - { unit: 'Hz' }, - ), - }, - inelasticInteractionRateAtStart: { - name: ['INEL', h('sub', 'start')], - visible: (_, { pdpBeamType, definition }) => pdpBeamType === PdpBeamType.LEAD_LEAD && definition === RunDefinition.Physics, - format: () => formatEditableNumber( - runDetailsModel.isEditModeEnabled, - runDetailsModel.runPatch.formData.inelasticInteractionRateAtStart, - ({ target: { value: inelasticInteractionRateAtStart } }) => - runDetailsModel.runPatch.patchFormData({ inelasticInteractionRateAtStart }), - { unit: 'Hz' }, - ), - }, - inelasticInteractionRateAtMid: { - name: ['INEL', h('sub', 'mid')], - visible: (_, { pdpBeamType, definition }) => pdpBeamType === PdpBeamType.LEAD_LEAD && definition === RunDefinition.Physics, - format: () => formatEditableNumber( - runDetailsModel.isEditModeEnabled, - runDetailsModel.runPatch.formData.inelasticInteractionRateAtMid, - ({ target: { value: inelasticInteractionRateAtMid } }) => - runDetailsModel.runPatch.patchFormData({ inelasticInteractionRateAtMid }), - { unit: 'Hz' }, - ), - }, - inelasticInteractionRateAtEnd: { - name: ['INEL', h('sub', 'end')], - visible: (_, { pdpBeamType, definition }) => pdpBeamType === PdpBeamType.LEAD_LEAD && definition === RunDefinition.Physics, - format: () => formatEditableNumber( - runDetailsModel.isEditModeEnabled, - runDetailsModel.runPatch.formData.inelasticInteractionRateAtEnd, - ({ target: { value: inelasticInteractionRateAtEnd } }) => - runDetailsModel.runPatch.patchFormData({ inelasticInteractionRateAtEnd }), - { unit: 'Hz' }, - ), - }, - tfbDdMode: { - name: 'TFB DD Mode', - visible: true, - }, - dd_flp: { - name: 'Data Distribution (FLP)', - visible: true, - format: formatBoolean, - }, - dcs: { - name: 'DCS', - visible: true, - format: formatBoolean, - }, - epnTopology: { - name: 'Topology', - visible: true, - }, - odcTopologyFullName: { - name: 'Topology Full Name', - visible: true, - }, - readoutCfgUri: { - name: 'Readout Config URI', - visible: true, - }, - startOfDataTransfer: { - name: 'Start of Data Transfer', - visible: true, - format: formatTimestamp, - - }, - endOfDataTransfer: { - name: 'End of Data Transfer', - visible: true, - format: formatTimestamp, - - }, - ctfFileCount: { - name: 'CTF File Count', - visible: true, - format: formatItemsCount, - }, - ctfFileSize: { - name: 'CTF File Size', - visible: true, - format: formatFileSize, - }, - tfFileCount: { - name: 'TF File Count', - visible: true, - format: formatItemsCount, - }, - tfFileSize: { - name: 'TF File Size', - visible: true, - format: formatFileSize, - }, - otherFileCount: { - name: 'Other File Count', - visible: true, - }, - otherFileSize: { - name: 'Other File Size', - visible: false, - format: formatFileSize, - }, - eorReasons: { - name: 'EOR Reasons', - visible: true, - format: (eorReasons) => runDetailsModel.isEditModeEnabled - ? editRunEorReasons(runDetailsModel) - : displayRunEorReasons(eorReasons), - }, -}); diff --git a/lib/public/views/Runs/Details/runLhcFillDisplayConfiguration.js b/lib/public/views/Runs/Details/runLhcFillDisplayConfiguration.js deleted file mode 100644 index f326d2944e..0000000000 --- a/lib/public/views/Runs/Details/runLhcFillDisplayConfiguration.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @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. - */ - -/** - * Display configuration of LHC fills data specific for a given run - */ -export const runLhcFillDisplayConfiguration = { - aliceDipoleCurrent: { - name: 'ALICE Dipole Current', - }, - aliceDipolePolarity: { - name: 'ALICE Dipole Polarity', - }, - aliceL3Current: { - name: 'ALICE L3 Current', - }, - aliceL3Polarity: { - name: 'ALICE L3 Polarity', - }, - lhcBeamEnergy: { - name: 'LHC Beam Energy', - format: (value) => value ? `${value} GeV` : '-', - }, - lhcBeamMode: { - name: 'LHC Beam Mode', - }, - lhcBetaStar: { - name: 'LHC Beta Star', - }, - lhcPeriod: { - name: 'LHC Period', - }, -}; diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index eaeb86ec54..25084a524e 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -22,9 +22,10 @@ import pick from '../../../utilities/pick.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { getRemoteDataSlice } from '../../../utilities/fetch/getRemoteDataSlice.js'; import { CombinationOperator } from '../../../components/Filters/common/CombinationOperatorChoiceModel.js'; -import { AliceL3AndDipoleFilteringModel } from '../../../components/Filters/RunsFilter/AliceL3AndDipoleFilteringModel.js'; -import { detectorsProvider } from '../../../services/detectors/detectorsProvider.js'; +import { TimeRangeInputModel } from '../../../components/Filters/common/filters/TimeRangeInputModel.js'; import { FloatComparisonFilterModel } from '../../../components/Filters/common/filters/FloatComparisonFilterModel.js'; +import { detectorsProvider } from '../../../services/detectors/detectorsProvider.js'; +import { AliceL3AndDipoleFilteringModel } from '../../../components/Filters/RunsFilter/AliceL3AndDipoleFilteringModel.js'; /** * Model representing handlers for runs page @@ -57,7 +58,15 @@ export class RunsOverviewModel extends OverviewPageModel { this._eorReasonsFilterModel = new EorReasonFilterModel(); this._eorReasonsFilterModel.observe(() => this._applyFilters()); - this._eorReasonsFilterModel.visualChange$.observe(() => this.notify()); + this._eorReasonsFilterModel.visualChange$.bubbleTo(this); + + this._o2StartFilterModel = new TimeRangeInputModel(); + this._o2StartFilterModel.observe(() => this._applyFilters(true)); + this._o2StartFilterModel.visualChange$.bubbleTo(this); + + this._o2StopFilterModel = new TimeRangeInputModel(); + this._o2StopFilterModel.observe(() => this._applyFilters(true)); + this._o2StopFilterModel.visualChange$.bubbleTo(this); this._aliceL3AndDipoleCurrentFilter = new AliceL3AndDipoleFilteringModel(); this._aliceL3AndDipoleCurrentFilter.observe(() => this._applyFilters()); @@ -115,7 +124,7 @@ export class RunsOverviewModel extends OverviewPageModel { * Create the export with the variables set in the model, handling errors appropriately * @param {object[]} runs The source content. * @param {string} fileName The name of the file including the output format. - * @param {Object>} exportFormats defines how particular fields of data units will be formatted + * @param {Object>} exportFormats defines how particular fields of data units will be formated * @return {void} */ async createRunsExport(runs, fileName, exportFormats) { @@ -184,20 +193,13 @@ export class RunsOverviewModel extends OverviewPageModel { this._listingTagsFilterModel.reset(); this._listingRunTypesFilterModel.reset(); this._eorReasonsFilterModel.reset(); + this._o2StartFilterModel.reset(); + this._o2StopFilterModel.reset(); + this._aliceL3AndDipoleCurrentFilter.reset(); this._fillNumbersFilter = ''; - this.o2startFilterFrom = ''; - this.o2startFilterTo = ''; - this.o2startFilterFromTime = '00:00'; - this.o2startFilterToTime = '23:59'; - - this.o2endFilterFrom = ''; - this.o2endFilterTo = ''; - this.o2endFilterFromTime = '00:00'; - this.o2endFilterToTime = '23:59'; - this._runDurationFilter = null; this._lhcPeriodsFilter = null; @@ -238,23 +240,16 @@ export class RunsOverviewModel extends OverviewPageModel { * @return {Boolean} If any filter is active */ isAnyFilterActive() { - return ( - this.runFilterValues !== '' + return this.runFilterValues !== '' || this._runDefinitionFilter.length > 0 || !this._eorReasonsFilterModel.isEmpty() + || !this._o2StartFilterModel.isEmpty + || !this._o2StopFilterModel.isEmpty || !this._detectorsFilterModel.isEmpty() || !this._listingTagsFilterModel.isEmpty() || this._listingRunTypesFilterModel.selected.length !== 0 || this._aliceL3AndDipoleCurrentFilter.selected.length !== 0 || this._fillNumbersFilter !== '' - || this.o2startFilterFrom !== '' - || this.o2startFilterTo !== '' - || this.o2startFilterToTime !== '23:59' - || this.o2startFilterFromTime !== '00:00' - || this.o2endFilterFrom !== '' - || this.o2endFilterTo !== '' - || this.o2endFilterToTime !== '23:59' - || this.o2endFilterFromTime !== '00:00' || this._runDurationFilter !== null || this._lhcPeriodsFilter !== null || this.environmentIdsFilter !== '' @@ -271,8 +266,7 @@ export class RunsOverviewModel extends OverviewPageModel { || this._inelasticInteractionRateAvgFilterModel.isEmpty || this._inelasticInteractionRateAtStartFilterModel.isEmpty || this._inelasticInteractionRateAtMidFilterModel.isEmpty - || this._inelasticInteractionRateAtEndFilterModel.isEmpty - ); + || this._inelasticInteractionRateAtEndFilterModel.isEmpty; } /** @@ -392,99 +386,6 @@ export class RunsOverviewModel extends OverviewPageModel { this._applyFilters(); } - /** - * Returns the current minimum creation date - * @return {number} The current minimum creation date - */ - getO2startFilterFrom() { - return this.o2startFilterFrom; - } - - /** - * Returns the current maximum creation date - * @return {number} The current maximum creation date - */ - getO2startFilterTo() { - return this.o2startFilterTo; - } - - /** - * Returns the current minimum creation time - * @return {number} The current minimum creation time - */ - getO2startFilterFromTime() { - return this.o2startFilterFromTime; - } - - /** - * Returns the current maximum creation time - * @return {number} The current maximum creation time - */ - getO2startFilterToTime() { - return this.o2startFilterToTime; - } - - /** - * Set a datetime for the start datetime filter - * - * @param {string} comparisonType comparison type - * @param {string} date The datetime to be applied to the datetime filter - * @param {boolean} valid states whether provided date is valid - * @return {void} - */ - setO2startFilter(comparisonType, date, valid) { - if (valid) { - this[`o2startFilter${comparisonType}`] = date; - this._applyFilters(); - } - } - - /** - * Returns the current minimum creation datetime - * @return {number} The current minimum creation datetime - */ - getO2endFilterFrom() { - return this.o2endFilterFrom; - } - - /** - * Returns the current maximum creation datetime - * @return {number} The current maximum creation datetime - */ - getO2endFilterTo() { - return this.o2endFilterTo; - } - - /** - * Returns the current minimum creation datetime - * @return {number} The current minimum creation datetime - */ - getO2endFilterFromTime() { - return this.o2endFilterFromTime; - } - - /** - * Returns the current maximum creation datetime - * @return {number} The current maximum creation datetime - */ - getO2endFilterToTime() { - return this.o2endFilterToTime; - } - - /** - * Set a datetime for the creation datetime filter - * @param {string} key The filter value to apply the datetime to - * @param {object} date The datetime to be applied to the creation datetime filter - * @param {boolean} valid Whether the inserted date passes validity check - * @return {undefined} - */ - setO2endFilter(key, date, valid) { - if (valid) { - this[`o2endFilter${key}`] = date; - this._applyFilters(); - } - } - /** * Returns the run duration filter (filter is defined in minutes) * @return {{operator: string, limit: (number|null)}|null} The current run duration filter @@ -811,12 +712,30 @@ export class RunsOverviewModel extends OverviewPageModel { /** * Return the model handling the filtering on run types * - * @return {EorReasonsFilterModel} the run type filtering model + * @return {EorReasonFilterModel} the run type filtering model */ get eorReasonsFilterModel() { return this._eorReasonsFilterModel; } + /** + * Returns the model for o2 start filter + * + * @return {TimeRangeInputModel} the filter model + */ + get o2StartFilterModel() { + return this._o2StartFilterModel; + } + + /** + * Returns the model for o2 stop filter + * + * @return {TimeRangeInputModel} the filter model + */ + get o2stopFilterModel() { + return this._o2StopFilterModel; + } + /** * Set the model handling the filtering on run types * @param {EorReasonFilterModel} model the model to set @@ -894,7 +813,6 @@ export class RunsOverviewModel extends OverviewPageModel { inelasticInteractionRateAtMid: this._inelasticInteractionRateAtMidFilterModel, inelasticInteractionRateAtEnd: this._inelasticInteractionRateAtEndFilterModel, }; - return { ...this.runFilterValues && { 'filter[runNumbers]': this.runFilterValues, @@ -914,21 +832,17 @@ export class RunsOverviewModel extends OverviewPageModel { ...this._fillNumbersFilter && { 'filter[fillNumbers]': this._fillNumbersFilter, }, - ...this.o2startFilterFrom && { - 'filter[o2start][from]': - new Date(`${this.o2startFilterFrom.replace(/\//g, '-')}T${this.o2startFilterFromTime}:00.000`).getTime(), + ...!this._o2StartFilterModel.fromTimeInputModel.isEmpty && { + 'filter[o2start][from]': this._o2StartFilterModel.normalized.from, }, - ...this.o2startFilterTo && { - 'filter[o2start][to]': - new Date(`${this.o2startFilterTo.replace(/\//g, '-')}T${this.o2startFilterToTime}:59.999`).getTime(), + ...!this._o2StartFilterModel.toTimeInputModel.isEmpty && { + 'filter[o2start][to]': this._o2StartFilterModel.normalized.to, }, - ...this.o2endFilterFrom && { - 'filter[o2end][from]': - new Date(`${this.o2endFilterFrom.replace(/\//g, '-')}T${this.o2endFilterFromTime}:00.000`).getTime(), + ...!this._o2StopFilterModel.fromTimeInputModel.isEmpty && { + 'filter[o2end][from]': this._o2StopFilterModel.normalized.from, }, - ...this.o2endFilterTo && { - 'filter[o2end][to]': - new Date(`${this.o2endFilterTo.replace(/\//g, '-')}T${this.o2endFilterToTime}:59.999`).getTime(), + ...!this._o2StopFilterModel.toTimeInputModel.isEmpty && { + 'filter[o2end][to]': this._o2StopFilterModel.normalized.to, }, ...this._runDurationFilter && this._runDurationFilter.limit !== null && { 'filter[runDuration][operator]': this._runDurationFilter.operator, diff --git a/lib/public/views/Runs/format/editRunEorReasons.js b/lib/public/views/Runs/format/editRunEorReasons.js index db5f0961a3..236d06ec12 100644 --- a/lib/public/views/Runs/format/editRunEorReasons.js +++ b/lib/public/views/Runs/format/editRunEorReasons.js @@ -29,7 +29,7 @@ export const editRunEorReasons = (runDetailsModel) => { let newEorReasonComponent = null; return eorReasonTypesRemoteData.match({ NotAsked: () => null, - Loading: () => spinner({ size: 2, absolute: false, justify: 'right' }), + Loading: () => spinner({ size: 2, absolute: false }), /** * Displays the EOR reasons form @@ -45,8 +45,8 @@ export const editRunEorReasons = (runDetailsModel) => { } } - newEorReasonComponent = h('.flex-row.items-center', [ - h('select.w-30.form-control', { + newEorReasonComponent = h('.flex-row.g2.items-center', [ + h('select.form-control', { onchange: ({ target }) => { runDetailsModel.newEorReason.category = target.value; runDetailsModel.newEorReason.title = ''; @@ -58,7 +58,7 @@ export const editRunEorReasons = (runDetailsModel) => { value: category, }, category)), ]), - h('select.w-30.form-control', { + h('select.form-control', { onchange: ({ target }) => { runDetailsModel.newEorReason.title = target.value; runDetailsModel.notify(); @@ -72,7 +72,7 @@ export const editRunEorReasons = (runDetailsModel) => { reason.title || '(empty)', )), ]), - h('input.w-40.form-control', { + h('input.form-control', { placeholder: 'Description', type: 'text', oninput: ({ target }) => { @@ -85,7 +85,7 @@ export const editRunEorReasons = (runDetailsModel) => { }, iconPlus()), ]); - return h('.w-80.flex-column.items-end', [ + return h('.flex-column.g2.flex-grow', [ newEorReasonComponent, /* @@ -100,15 +100,15 @@ export const editRunEorReasons = (runDetailsModel) => { const titleString = title ? ` - ${title}` : ''; const descriptionString = description ? ` - ${description}` : ''; return h( - '.flex-row', + '.flex-row.items-center', { key: `${category} ${titleString} ${descriptionString}`, }, [ - h('w-wrapped', `${category} ${titleString} ${descriptionString}`), h('label.remove-eor-reason.danger.ph1.actionable-icon', { onclick: () => runDetailsModel.runPatch.removeEorReason(eorReason), }, iconTrash()), + h('.w-wrapped', `${category} ${titleString} ${descriptionString}`), ], ); }) diff --git a/lib/public/views/Runs/format/formatEditableNumber.js b/lib/public/views/Runs/format/formatEditableNumber.js index d69b7ec4ae..c5b0d58605 100644 --- a/lib/public/views/Runs/format/formatEditableNumber.js +++ b/lib/public/views/Runs/format/formatEditableNumber.js @@ -32,7 +32,7 @@ export const formatEditableNumber = ( { inputDecimals = 3, displayDecimals = 3, unit = null } = {}, ) => { if (isEditionEnabled) { - return h('.flex-row.g1', [ + return h('.flex-row.g1.items-center', [ h('input.form-control', { type: 'number', step: Math.pow(10, -inputDecimals), diff --git a/lib/public/views/Runs/format/formatRunCalibrationStatus.js b/lib/public/views/Runs/format/formatRunCalibrationStatus.js index d9e47ae5c2..8fd77d7def 100644 --- a/lib/public/views/Runs/format/formatRunCalibrationStatus.js +++ b/lib/public/views/Runs/format/formatRunCalibrationStatus.js @@ -47,7 +47,7 @@ export const formatRunCalibrationStatus = (runDetailsModel, run) => { ), ]; if (runDetailsModel.runPatch.requireCalibrationStatusChangeReason) { - ret.push(h('textarea.form-control.v-resize', { + ret.push(h('textarea.form-control.w-100.v-resize', { placeholder: 'Reason of the calibration status change', value: runDetailsModel.runPatch.calibrationStatusChangeReason, // eslint-disable-next-line no-return-assign @@ -55,7 +55,7 @@ export const formatRunCalibrationStatus = (runDetailsModel, run) => { })); } - return h('', { class: 'flex-column flex-grow g2' }, ret); + return ret; } else { return coloredCalibrationStatusComponent(calibrationStatus); } diff --git a/lib/public/views/Runs/format/formatRunDetectors.js b/lib/public/views/Runs/format/formatRunDetectors.js index f3a10dc8cf..7e21500f3f 100644 --- a/lib/public/views/Runs/format/formatRunDetectors.js +++ b/lib/public/views/Runs/format/formatRunDetectors.js @@ -14,7 +14,7 @@ import { h } from '/js/src/index.js'; import { RUN_DETECTOR_QUALITIES, RunDetectorQualities } from '../../../domain/enums/RunDetectorQualities.js'; import { dropdown } from '../../../components/common/popover/dropdown.js'; -import { iconCheck, iconX, iconBan } from '/js/src/icons.js'; +import { iconBan, iconCheck, iconX } from '/js/src/icons.js'; /** * Format a list of detectors @@ -38,7 +38,7 @@ export const formatRunDetectors = (run, detailsModel) => { const detectorsBadges = (detectors) => h( '', - { class: 'g2 flex-row flex-grow flex-wrap justify-end' }, + { class: 'g2 flex-row flex-wrap' }, detectors.map(({ id, name }) => { let color; let icon; @@ -80,45 +80,42 @@ export const formatRunDetectors = (run, detailsModel) => { Success: (allDataTakingDetectors) => { if (detailsModel.isEditModeEnabled && (run.timeTrgEnd || run.timeO2End)) { const ret = [ - h( - '.self-end', - dropdown( - h( - '.dropdown-trigger.form-control', + dropdown( + h( + '.dropdown-trigger.form-control', + [ + h('.flex-grow', detectorsBadges(detectorsQualities)), + h('.dropdown-trigger-symbol', ''), + ], + ), + h( + '', + detectorsQualities.map(({ id, name, quality }) => h( + '.flex-row.justify-between.g4.p2', [ - h('.flex-grow', detectorsBadges(detectorsQualities)), - h('.dropdown-trigger-symbol', ''), - ], - ), - h( - '', - detectorsQualities.map(({ id, name, quality }) => h( - '.flex-row.justify-between.g4.p2', - [ - name, - h( - '.flex-row.g2.items-center', - RUN_DETECTOR_QUALITIES.map((qualityOption) => h('label.form-check-label.flex-row.g2', [ - h( - `input#detector-quality-${id}-${qualityOption.toLowerCase()}`, - { - type: 'radio', - checked: (detailsModel.runPatch.getDetectorQuality(id) - || quality) === qualityOption.toLowerCase(), - name: `detector-quality-${id}`, - onchange: () => { - detailsModel.runPatch.setDetectorQuality(id, qualityOption.toLowerCase()); - }, + name, + h( + '.flex-row.g2.items-center', + RUN_DETECTOR_QUALITIES.map((qualityOption) => h('label.form-check-label.flex-row.g2', [ + h( + `input#detector-quality-${id}-${qualityOption.toLowerCase()}`, + { + type: 'radio', + checked: (detailsModel.runPatch.getDetectorQuality(id) + || quality) === qualityOption.toLowerCase(), + name: `detector-quality-${id}`, + onchange: () => { + detailsModel.runPatch.setDetectorQuality(id, qualityOption.toLowerCase()); }, - ), - qualityOption, - ])), - ), - ], - )), - ), - { alignment: 'right' }, + }, + ), + qualityOption, + ])), + ), + ], + )), ), + // { alignment: 'right' }, ), ]; if (detailsModel.runPatch.hasAnyDetectorsQualityChange()) { diff --git a/lib/public/views/Runs/format/formatRunQuality.js b/lib/public/views/Runs/format/formatRunQuality.js index d3d782995f..0da9721f7a 100644 --- a/lib/public/views/Runs/format/formatRunQuality.js +++ b/lib/public/views/Runs/format/formatRunQuality.js @@ -42,14 +42,14 @@ export const formatRunQuality = (runDetailsModel, runQuality, run) => { ), ]; if (runDetailsModel.runPatch.requireRunQualityChangeReason()) { - ret.push(h('textarea.form-control.v-resize', { + ret.push(h('textarea.form-control.v-resize.w-100', { placeholder: 'Reason of the detector(s) quality change', value: runDetailsModel.runPatch.runQualityChangeReason, // eslint-disable-next-line no-return-assign oninput: (event) => runDetailsModel.runPatch.runQualityChangeReason = event.target.value, })); } - return h('', { class: 'flex-column flex-grow g2' }, ret); + return ret; } else if (runQuality) { return h('.badge.white', { class: (() => { diff --git a/lib/public/views/Tags/format/formatTagsList.js b/lib/public/views/Tags/format/formatTagsList.js index 27e67ac66b..739b0a5eab 100644 --- a/lib/public/views/Tags/format/formatTagsList.js +++ b/lib/public/views/Tags/format/formatTagsList.js @@ -21,10 +21,11 @@ import { h } from '/js/src/index.js'; * @param {object} [configuration] formatting configuration * @param {boolean} [configuration.flex] if true, tags list will be displayed in a flex component * @param {boolean} [configuration.description] if true, tag will have the description tooltip enabled + * @param {Component} [configuration.placeholder] return used as placeholder if no tags are present * @return {Component|string} Tag with its correct color or empty value */ export const formatTagsList = (tags, configuration) => { - const { flex, description: enableDescription } = configuration || {}; + const { flex, description: enableDescription, placeholder = '-' } = configuration || {}; /** * Wrap the tag badge component to include the description @@ -41,7 +42,7 @@ export const formatTagsList = (tags, configuration) => { }; if (!tags?.length) { - return '-'; + return placeholder; } if (flex) { diff --git a/lib/server/externalServicesSynchronization/ccdb/CcdbSynchronizer.js b/lib/server/externalServicesSynchronization/ccdb/CcdbSynchronizer.js index 7a9ee7e160..3acb210468 100644 --- a/lib/server/externalServicesSynchronization/ccdb/CcdbSynchronizer.js +++ b/lib/server/externalServicesSynchronization/ccdb/CcdbSynchronizer.js @@ -1,5 +1,6 @@ const { getGoodPhysicsRunsWithMissingTfTimestamps } = require('../../services/run/getRunsMissingTfTimestamps.js'); const { runService } = require('../../services/run/RunService.js'); +const { LogManager } = require('@aliceo2/web-ui'); /** * Synchronizer for CCDB data @@ -10,6 +11,7 @@ class CcdbSynchronizer { * @param {string} runInfoUrl the CCDB URL where run information is to be retrieved */ constructor(runInfoUrl) { + this._logger = LogManager.getLogger(CcdbSynchronizer.name); this._runInfoUrl = runInfoUrl; } @@ -20,14 +22,21 @@ class CcdbSynchronizer { * @return {Promise} resolves once all runs have been updated */ async syncFirstAndLastTf(synchronizeAfter) { - const runs = await getGoodPhysicsRunsWithMissingTfTimestamps(synchronizeAfter); + try { + this._logger.debugMessage('Starting to sync runs TF with CCDB'); + const runNumbers = (await getGoodPhysicsRunsWithMissingTfTimestamps(synchronizeAfter)).map(({ runNumber }) => runNumber); + this._logger.debugMessage(`Runs to sync: ${runNumbers.join(', ')}`); - for (const { runNumber } of runs) { - const timeframes = await this._getRunStartAndEndTimeframes(runNumber); + for (const runNumber of runNumbers) { + const timeframes = await this._getRunStartAndEndTimeframes(runNumber); - if (timeframes.firstTfTimestamp || timeframes.lastTfTimestamp) { - await runService.update({ runNumber }, { runPatch: timeframes }); + if (timeframes.firstTfTimestamp || timeframes.lastTfTimestamp) { + await runService.update({ runNumber }, { runPatch: timeframes }); + } } + this._logger.debugMessage('Synchronization of TF done'); + } catch (e) { + this._logger.errorMessage(`Synchronization failed:\n ${e.stack}`); } } diff --git a/lib/server/externalServicesSynchronization/monalisa/MonAlisaClient.js b/lib/server/externalServicesSynchronization/monalisa/MonAlisaClient.js index 77e8e93d90..4c528a0bb3 100644 --- a/lib/server/externalServicesSynchronization/monalisa/MonAlisaClient.js +++ b/lib/server/externalServicesSynchronization/monalisa/MonAlisaClient.js @@ -186,7 +186,7 @@ class MonAlisaClient { /** * Get data passes from MonAlisa associated with at least one data pass - * @return {{properties: SimulationPass, associations: SimulationPassAssociations}[]} simulation passes list + * @return {Promise<{properties: SimulationPass, associations: SimulationPassAssociations}[]>} simulation passes list */ async getSimulationPasses() { const payload = await this._fetchSimulationPasses(); diff --git a/lib/server/externalServicesSynchronization/monalisa/MonAlisaSynchronizer.js b/lib/server/externalServicesSynchronization/monalisa/MonAlisaSynchronizer.js index 4494451720..6acafd32cf 100644 --- a/lib/server/externalServicesSynchronization/monalisa/MonAlisaSynchronizer.js +++ b/lib/server/externalServicesSynchronization/monalisa/MonAlisaSynchronizer.js @@ -59,6 +59,7 @@ class MonAlisaSynchronizer { */ async _synchronizeDataPassesFromMonAlisa() { try { + this._logger.debugMessage('Starting synchronization of Data Passes'); const descriptionToLastSeenAndIdAndStatus = await this._getAllDataPassVersionsLastSeenAndIdAndLastStatus(); const allMonAlisaDataPassesVersions = await this._monAlisaInterface.getDataPassesVersions(); const monAlisaDataPassesVersionsToBeUpdated = allMonAlisaDataPassesVersions @@ -271,6 +272,7 @@ class MonAlisaSynchronizer { */ async _synchronizeSimulationPassesFromMonAlisa() { try { + this._logger.debugMessage('Starting synchronization of Simulation Passes'); const simulationPasses = await this._monAlisaInterface.getSimulationPasses(); for (const simulationPass of simulationPasses) { try { diff --git a/lib/server/kafka/AliEcsSynchronizer.js b/lib/server/kafka/AliEcsSynchronizer.js index f0b78303c0..8ec4f73472 100644 --- a/lib/server/kafka/AliEcsSynchronizer.js +++ b/lib/server/kafka/AliEcsSynchronizer.js @@ -45,7 +45,6 @@ class AliEcsSynchronizer { this.ecsEnvironmentsConsumer = new AliEcsEventMessagesConsumer(kafkaClient, ENVIRONMENT_CONSUMER_GROUP, ENVIRONMENT_TOPICS); this.ecsEnvironmentsConsumer.onMessageReceived(async (eventMessage) => { const { timestampNano, environmentEvent: { environmentId, state, message, vars = {} } } = eventMessage; - this._logger.infoMessage('Got timestamp: ', timestampNano); const bigIntTimestamp = longToBigInt(timestampNano); const millisecondsTimestamp = Number(bigIntTimestamp / BigInt(1e6)); @@ -73,10 +72,10 @@ class AliEcsSynchronizer { this.ecsRunConsumer = new AliEcsEventMessagesConsumer(kafkaClient, RUN_CONSUMER_GROUP, RUN_TOPICS); this.ecsRunConsumer.onMessageReceived(async (eventMessage) => { - const { timestamp, runEvent: { environmentId, runNumber, state, transition, lastRequestUser } } = eventMessage; + const { timestamp, runEvent: { environmentId, runNumber, transition, lastRequestUser } } = eventMessage; const { externalId: externalUserId, name: userName } = lastRequestUser ?? {}; - if (state === 'CONFIGURED' && transition === 'START_ACTIVITY') { + if (transition === 'START_ACTIVITY') { runService .createOrUpdate( runNumber, @@ -87,7 +86,7 @@ class AliEcsSynchronizer { .catch((error) => this._logger.errorMessage(`Failed to save run start for ${runNumber}: ${error.message}`)); } - if (state === 'RUNNING' && transition === 'STOP_ACTIVITY') { + if (transition === 'STOP_ACTIVITY' || transition === 'GO_ERROR') { runService .createOrUpdate( runNumber, diff --git a/lib/server/services/qualityControlFlag/QcFlagService.js b/lib/server/services/qualityControlFlag/QcFlagService.js index 4481618791..87416847eb 100644 --- a/lib/server/services/qualityControlFlag/QcFlagService.js +++ b/lib/server/services/qualityControlFlag/QcFlagService.js @@ -264,35 +264,20 @@ class QcFlagService { [ sequelize.literal(` IF( - ( - COALESCE(run.time_trg_end, run.time_o2_end ) IS NULL - OR COALESCE(run.time_trg_start, run.time_o2_start) IS NULL - ), + run.time_start IS NULL OR run.time_end IS NULL, IF( - SUM( - COALESCE(UNIX_TIMESTAMP(effectivePeriods.\`to\` ), 0) - + COALESCE(UNIX_TIMESTAMP(effectivePeriods.\`from\`), 0) - ) = 0, + effectivePeriods.\`from\` IS NULL AND effectivePeriods.\`to\` IS NULL, 1, null ), SUM( - UNIX_TIMESTAMP(COALESCE( - effectivePeriods.\`to\`, - run.time_trg_end, - run.time_o2_end - )) - - UNIX_TIMESTAMP(COALESCE( - effectivePeriods.\`from\`, - run.time_trg_start, - run.time_o2_start - )) + UNIX_TIMESTAMP(COALESCE(effectivePeriods.\`to\`, run.time_end)) + - UNIX_TIMESTAMP(COALESCE(effectivePeriods.\`from\`, run.time_start)) ) / ( - UNIX_TIMESTAMP(COALESCE(run.time_trg_end, run.time_o2_end)) - - UNIX_TIMESTAMP(COALESCE(run.time_trg_start, run.time_o2_start)) + UNIX_TIMESTAMP(run.time_end) - UNIX_TIMESTAMP(run.time_start) ) ) - `), + `), 'effectiveRunCoverage', ], [ diff --git a/lib/server/services/run/setO2StopOfLostRuns.js b/lib/server/services/run/setO2StopOfLostRuns.js index 439ae79e68..282ee166db 100644 --- a/lib/server/services/run/setO2StopOfLostRuns.js +++ b/lib/server/services/run/setO2StopOfLostRuns.js @@ -28,9 +28,9 @@ exports.setO2StopOfLostRuns = async (runNumbersOfRunningRuns, modificationTimePe FROM runs WHERE time_o2_end IS NULL AND time_trg_end IS NULL - AND COALESCE(time_trg_start, time_o2_start) IS NOT NULL - AND COALESCE(time_trg_start, time_o2_start) >= '${timestampToMysql(modificationTimePeriod.from, true)}' - AND COALESCE(time_trg_start, time_o2_start) < '${timestampToMysql(modificationTimePeriod.to, true)}' + AND time_start IS NOT NULL + AND time_start >= '${timestampToMysql(modificationTimePeriod.from, true)}' + AND time_start < '${timestampToMysql(modificationTimePeriod.to, true)}' `; if (runNumbersOfRunningRuns.length > 0) { fetchQuery += ` AND run_number NOT IN (${runNumbersOfRunningRuns.join(',')})`; diff --git a/package-lock.json b/package-lock.json index 0b65c4c0fc..23f85b70e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aliceo2/bookkeeping", - "version": "1.3.0", + "version": "1.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aliceo2/bookkeeping", - "version": "1.3.0", + "version": "1.4.1", "bundleDependencies": [ "@aliceo2/web-ui", "@grpc/grpc-js", @@ -50,7 +50,7 @@ "mocha": "11.0.1", "nodemon": "3.1.3", "nyc": "17.1.0", - "puppeteer": "23.9.0", + "puppeteer": "23.10.2", "puppeteer-to-istanbul": "1.4.0", "sequelize-cli": "6.6.0", "sinon": "19.0.2", @@ -569,13 +569,19 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@grpc/grpc-js": { @@ -1109,15 +1115,15 @@ "inBundle": true }, "node_modules/@puppeteer/browsers": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.1.tgz", - "integrity": "sha512-0kdAbmic3J09I6dT8e9vE2JOCSt13wHCW5x/ly8TSt2bDtuIWe2TgLZZDHdcziw9AVCzflMAXCrVyRIhIs44Ng==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.0.tgz", + "integrity": "sha512-jESwj3APl78YUWHf28s2EjL0OIxcvl1uLU6Ge68KQ9ZXNsekUcbdr9dCi6vEO8naXS18lWXCV56shVkPStzXSQ==", "dev": true, "dependencies": { - "debug": "^4.3.7", + "debug": "^4.4.0", "extract-zip": "^2.0.1", "progress": "^2.0.3", - "proxy-agent": "^6.4.0", + "proxy-agent": "^6.5.0", "semver": "^7.6.3", "tar-fs": "^3.0.6", "unbzip2-stream": "^1.4.3", @@ -1803,13 +1809,10 @@ } }, "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, "engines": { "node": ">= 14" } @@ -3205,9 +3208,9 @@ "dev": true }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "inBundle": true, "dependencies": { "ms": "^2.1.3" @@ -3693,6 +3696,7 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -3772,6 +3776,30 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/esniff": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", @@ -4291,20 +4319,6 @@ } ] }, - "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4405,15 +4419,14 @@ } }, "node_modules/get-uri": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", - "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", "dev": true, "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4", - "fs-extra": "^11.2.0" + "debug": "^4.3.4" }, "engines": { "node": ">= 14" @@ -4512,21 +4525,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -4696,12 +4694,12 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { @@ -6500,19 +6498,19 @@ } }, "node_modules/pac-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", - "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", + "integrity": "sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==", "dev": true, "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.5", + "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.4" + "socks-proxy-agent": "^8.0.5" }, "engines": { "node": ">= 14" @@ -6859,19 +6857,19 @@ } }, "node_modules/proxy-agent": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", - "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", "dev": true, "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.3", + "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.1", + "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.2" + "socks-proxy-agent": "^8.0.5" }, "engines": { "node": ">= 14" @@ -6918,17 +6916,17 @@ } }, "node_modules/puppeteer": { - "version": "23.9.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.9.0.tgz", - "integrity": "sha512-WfB8jGwFV+qrD9dcJJVvWPFJBU6kxeu2wxJz9WooDGfM3vIiKLgzImEDBxUQnCBK/2cXB3d4dV6gs/LLpgfLDg==", + "version": "23.10.2", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.10.2.tgz", + "integrity": "sha512-Iii2ZwdukXzEGeCxs2/GG8G+dbVCylnlBrTxZnMxLW/7w/ftoGq4VB2Bt1vwrbMIn1XwFqxYEWNEkZpIkcVfwg==", "dev": true, "hasInstallScript": true, "dependencies": { - "@puppeteer/browsers": "2.4.1", + "@puppeteer/browsers": "2.6.0", "chromium-bidi": "0.8.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1367902", - "puppeteer-core": "23.9.0", + "puppeteer-core": "23.10.2", "typed-query-selector": "^2.12.0" }, "bin": { @@ -6939,14 +6937,14 @@ } }, "node_modules/puppeteer-core": { - "version": "23.9.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.9.0.tgz", - "integrity": "sha512-hLVrav2HYMVdK0YILtfJwtnkBAwNOztUdR4aJ5YKDvgsbtagNr6urUJk9HyjRA9e+PaLI3jzJ0wM7A4jSZ7Qxw==", + "version": "23.10.2", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.10.2.tgz", + "integrity": "sha512-SEPjEbhPxRlzjGRCs8skwfnzFQj6XrZZmoMz0JIQbanj0fBpQ5HOGgyQTyh4YOW33q+461plJc5GfsQ+ErVBgQ==", "dev": true, "dependencies": { - "@puppeteer/browsers": "2.4.1", + "@puppeteer/browsers": "2.6.0", "chromium-bidi": "0.8.0", - "debug": "^4.3.7", + "debug": "^4.4.0", "devtools-protocol": "0.0.1367902", "typed-query-selector": "^2.12.0", "ws": "^8.18.0" @@ -7782,12 +7780,12 @@ } }, "node_modules/socks-proxy-agent": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", - "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, "dependencies": { - "agent-base": "^7.1.1", + "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" }, @@ -7870,9 +7868,9 @@ } }, "node_modules/streamx": { - "version": "2.20.2", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.2.tgz", - "integrity": "sha512-aDGDLU+j9tJcUdPGOaHmVF1u/hhI+CsGkT02V3OKlHDV7IukOI+nTWAGkiZEKCO35rWN1wIr4tS7YFr1f4qSvA==", + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.0.tgz", + "integrity": "sha512-Qz6MsDZXJ6ur9u+b+4xCG18TluU7PGlRfXVAAjNiGsFrBUt/ioyLkxbFaKJygoPs+/kW4VyBj0bSj89Qu0IGyg==", "dev": true, "dependencies": { "fast-fifo": "^1.3.2", @@ -8112,10 +8110,13 @@ } }, "node_modules/text-decoder": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", - "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==", - "dev": true + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.2.tgz", + "integrity": "sha512-/MDslo7ZyWTA2vnk1j7XoDVfXsGk3tp+zFEJHJGm0UjIlQifonVFwlVbQDFh8KJzTBnT8ie115TYqir6bclddA==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4" + } }, "node_modules/text-hex": { "version": "1.0.0", @@ -8281,6 +8282,21 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/umzug": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/umzug/-/umzug-3.8.0.tgz", diff --git a/package.json b/package.json index 4b29c04cb4..7feaef98dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aliceo2/bookkeeping", - "version": "1.3.0", + "version": "1.4.1", "author": "ALICEO2", "scripts": { "coverage": "nyc npm test && npm run coverage:report", @@ -53,7 +53,7 @@ "mocha": "11.0.1", "nodemon": "3.1.3", "nyc": "17.1.0", - "puppeteer": "23.9.0", + "puppeteer": "23.10.2", "puppeteer-to-istanbul": "1.4.0", "sequelize-cli": "6.6.0", "sinon": "19.0.2", diff --git a/test/api/runs.test.js b/test/api/runs.test.js index 8b1ccbc36e..53e8868d11 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -1203,6 +1203,7 @@ module.exports = () => { tfFileSize: BIG_INT_NUMBER, otherFileCount: 123156132, otherFileSize: BIG_INT_NUMBER, + nTfOrbits: BIG_INT_NUMBER, crossSection: 0.1, triggerEfficiency: 0.2, triggerAcceptance: 0.3, @@ -1235,6 +1236,7 @@ module.exports = () => { expect(data.tfFileSize).to.equal(BIG_INT_NUMBER); expect(data.otherFileCount).to.equal(123156132); expect(data.otherFileSize).to.equal(BIG_INT_NUMBER); + expect(data.nTfOrbits).to.equal(BIG_INT_NUMBER); expect(data.triggerEfficiency).to.equal(0.2); expect(data.triggerAcceptance).to.equal(0.3); expect(data.phaseShiftAtStartBeam1).to.equal(0.4); diff --git a/test/lib/usecases/run/UpdateRunUseCase.test.js b/test/lib/usecases/run/UpdateRunUseCase.test.js index 913aabb14c..838283a46e 100644 --- a/test/lib/usecases/run/UpdateRunUseCase.test.js +++ b/test/lib/usecases/run/UpdateRunUseCase.test.js @@ -58,6 +58,7 @@ module.exports = () => { tfFileSize: BIG_INT_NUMBER, otherFileCount: 123156132, otherFileSize: BIG_INT_NUMBER, + nTfOrbits: BIG_INT_NUMBER, crossSection: 0.1, triggerEfficiency: 0.2, triggerAcceptance: 0.3, @@ -324,6 +325,7 @@ module.exports = () => { expect(result.tfFileSize).to.equal(BIG_INT_NUMBER); expect(result.otherFileCount).to.equal(123156132); expect(result.otherFileSize).to.equal(BIG_INT_NUMBER); + expect(result.nTfOrbits).to.equal(BIG_INT_NUMBER); expect(result.crossSection).to.equal(0.1); expect(result.triggerEfficiency).to.equal(0.2); expect(result.triggerAcceptance).to.equal(0.3); diff --git a/test/public/defaults.js b/test/public/defaults.js index 35403816df..04b5d9c8d6 100644 --- a/test/public/defaults.js +++ b/test/public/defaults.js @@ -396,7 +396,7 @@ module.exports.getInnerText = getInnerText; * @return {Promise} resolves once the text has been checked */ module.exports.expectInnerText = async (page, selector, innerText) => { - const element = await page.waitForSelector(selector); + const elementHandle = await page.waitForSelector(selector); try { await page.waitForFunction( (selector, innerText) => document.querySelector(selector).innerText === innerText, @@ -405,8 +405,38 @@ module.exports.expectInnerText = async (page, selector, innerText) => { innerText, ); } catch (_) { - throw new Error(`Expected innerText for ${selector} to be "${innerText}", got "${await getInnerText(element)}"`); + const actualInnerText = await getInnerText(elementHandle); + await elementHandle.dispose(); + throw new Error(`Expected innerText for ${selector} to be "${innerText}", got "${actualInnerText}"`); + } + await elementHandle.dispose(); +}; + +/** + * Expect a given attribute of an element to have a given value + * + * @param {puppeteer.Page} page the puppeteer page + * @param {string} selector the selector of the element to look for attribute + * @param {string} attributeKey the key of the attribute to check + * @param {string} attributeValue the expected value + * @return {Promise} resolves once the attribute has the expected value + */ +module.exports.expectAttributeValue = async (page, selector, attributeKey, attributeValue) => { + const elementHandle = await page.waitForSelector(selector); + try { + await page.waitForFunction( + (selector, attributeKey, attributeValue) => document.querySelector(selector).getAttribute(attributeKey) === attributeValue, + {}, + selector, + attributeKey, + attributeValue, + ); + } catch (_) { + const actualAttributeValue = await elementHandle.evaluate((element, attributeKey) => element.getAttribute(attributeKey), attributeKey); + await elementHandle.dispose(); + throw new Error(`Expect attribute ${attributeKey} to be ${attributeValue}, got ${actualAttributeValue}`); } + await elementHandle.dispose(); }; /** @@ -818,3 +848,19 @@ module.exports.expectLink = async (element, selector, { href, innerText }) => { * @return {boolean} true if format is correct, false otherwise */ module.exports.validateDate = (date, format = 'DD/MM/YYYY hh:mm:ss') => !isNaN(dateAndTime.parse(date, format)); + +/** + * Return the selector for all the inputs composing a period inputs + * + * @param {string} popoverSelector the selector of the period inputs parent + * @return {{fromDateSelector: string, fromTimeSelector: string, toDateSelector: string, toTimeSelector: string}} the selectors + */ +module.exports.getPeriodInputsSelectors = (popoverSelector) => { + const commonInputsAncestor = `${popoverSelector} > div > div > div > div`; + return { + fromDateSelector: `${commonInputsAncestor} > div:nth-child(1) input:nth-child(1)`, + fromTimeSelector: `${commonInputsAncestor} > div:nth-child(1) input:nth-child(2)`, + toDateSelector: `${commonInputsAncestor} > div:nth-child(2) input:nth-child(1)`, + toTimeSelector: `${commonInputsAncestor} > div:nth-child(2) input:nth-child(2)`, + }; +}; diff --git a/test/public/flps/index.js b/test/public/flps/index.js index 1e9c07de13..d41f96315f 100644 --- a/test/public/flps/index.js +++ b/test/public/flps/index.js @@ -12,11 +12,9 @@ */ const OverviewSuite = require('./overview.test'); -// eslint-disable-next-line capitalized-comments const DetailSuite = require('./detail.test'); module.exports = () => { describe('Overview Page', OverviewSuite); - // eslint-disable-next-line capitalized-comments describe('Detail Page', DetailSuite); }; diff --git a/test/public/flps/overview.test.js b/test/public/flps/overview.test.js index 1762e4e4b2..4a741e2ff3 100644 --- a/test/public/flps/overview.test.js +++ b/test/public/flps/overview.test.js @@ -21,6 +21,7 @@ const { waitForTableLength, goToPage, getInnerText, + fillInput, } = require('../defaults.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -33,6 +34,9 @@ module.exports = () => { let table; let firstRowId; + const amountSelectorSelector = '#amountSelector'; + const amountSelectorButtonSelector = `${amountSelectorSelector} button`; + before(async () => { [page, browser] = await defaultBefore(page, browser); await page.setViewport({ @@ -108,8 +112,6 @@ module.exports = () => { await page.waitForSelector('table tbody tr:nth-child(2)'); expect(await page.$(`table tbody tr:nth-child(${INFINITE_SCROLL_CHUNK})`)).to.be.null; - const amountSelectorButtonSelector = '#amountSelector button'; - // Expect the dropdown options to be visible when it is selected await pressElement(page, amountSelectorButtonSelector); @@ -133,23 +135,19 @@ module.exports = () => { it('can set how many flps are available per page', async () => { // Expect the amount selector to currently be set to Infinite (after the previous test) - const amountSelectorId = '#amountSelector'; - await expectInnerText(page, `${amountSelectorId} button`, 'Rows per page: Infinite '); + await expectInnerText(page, `${amountSelectorSelector} button`, 'Rows per page: Infinite '); - await pressElement(page, `${amountSelectorId} button`); - await page.waitForSelector(`${amountSelectorId} .dropup-menu`); + await pressElement(page, `${amountSelectorSelector} button`); + await page.waitForSelector(`${amountSelectorSelector} .dropup-menu`); // Expect the amount of visible flps to reduce when the first option (5) is selected - await pressElement(page, `${amountSelectorId} .dropup-menu .menu-item`); + await pressElement(page, `${amountSelectorSelector} .dropup-menu .menu-item`); await waitForTableLength(page, 5); }); it('dynamically switches between visible pages in the page selector', async () => { - // Override the amount of flps visible per page manually - await page.evaluate(() => { - // eslint-disable-next-line no-undef - model.flps.pagination.itemsPerPage = 1; - }); + await pressElement(page, amountSelectorButtonSelector); + await fillInput(page, `${amountSelectorSelector} input`, '1', ['input', 'change']); await waitForTableLength(page, 1); // Expect the page five button to now be visible, but no more than that @@ -164,22 +162,13 @@ module.exports = () => { }); it('notifies if table loading returned an error', async () => { - /* - * As an example, override the amount of flps visible per page manually - * We know the limit is 100 as specified by the Dto - */ - await page.evaluate(() => { - // eslint-disable-next-line no-undef - model.flps.pagination.itemsPerPage = 200; - }); + await pressElement(page, amountSelectorButtonSelector); + await fillInput(page, `${amountSelectorSelector} input`, '200', ['input', 'change']); // We expect there to be a fitting error message await expectInnerText(page, '.alert-danger', 'Invalid Attribute: "query.page.limit" must be less than or equal to 100'); - // Revert changes for next test - await page.evaluate(() => { - // eslint-disable-next-line no-undef - model.flps.pagination.itemsPerPage = 10; - }); + await pressElement(page, amountSelectorButtonSelector); + await fillInput(page, `${amountSelectorSelector} input`, '10', ['input', 'change']); }); }; diff --git a/test/public/logs/create.test.js b/test/public/logs/create.test.js index 5218bcc9d8..3147896907 100644 --- a/test/public/logs/create.test.js +++ b/test/public/logs/create.test.js @@ -46,6 +46,7 @@ module.exports = () => { let browser; let url; const assetsDir = [__dirname, '../..', 'assets']; + const downloadDir = [__dirname, '../../../..', 'database/storage']; before(async () => { [page, browser, url] = await defaultBefore(); @@ -333,6 +334,13 @@ module.exports = () => { expect(lastLog.attachments).to.lengthOf(2); expect(lastLog.attachments[0].originalName).to.equal(file1); expect(lastLog.attachments[1].originalName).to.equal(file2); + + try { + fs.unlinkSync(path.resolve(...downloadDir, file1)); + fs.unlinkSync(path.resolve(...downloadDir, file2)); + } catch (_) { + // Do not care if file do not exist, this is just cleaning + } }).timeout(12000); it('can clear the file attachment input if at least one is submitted', async () => { @@ -345,7 +353,8 @@ module.exports = () => { // Add a single file attachment to the input field const attachmentsInput = await page.$('#attachments'); - attachmentsInput.uploadFile(path.resolve(...assetsDir, '1200px-CERN_logo.png')); + const fileName = '1200px-CERN_logo.png'; + attachmentsInput.uploadFile(path.resolve(...assetsDir, fileName)); await waitForTimeout(500); // We expect the clear button to appear @@ -360,6 +369,12 @@ module.exports = () => { await waitForTimeout(100); const newUploadedAttachments = await page.evaluate((element) => element.value, attachmentsInput); expect(newUploadedAttachments).to.equal(''); + + try { + fs.unlinkSync(path.resolve(...downloadDir, fileName)); + } catch (_) { + // Do not care if file do not exist, this is just cleaning + } }); it('can create a log with a run number', async () => { diff --git a/test/public/runs/detail.test.js b/test/public/runs/detail.test.js index cbadd96da5..6e9fc641dc 100644 --- a/test/public/runs/detail.test.js +++ b/test/public/runs/detail.test.js @@ -56,14 +56,13 @@ const banIconPath = const goToRunDetails = async (page, runNumber) => { await waitForNavigation(page, () => pressElement(page, '#run-overview')); await fillInput(page, '#runNumber', `${runNumber},${runNumber}`); - return waitForNavigation(page, () => pressElement(page, `#row${runNumber}-runNumber-text a`)); + return waitForNavigation(page, () => pressElement(page, `a[href="?page=run-detail&runNumber=${runNumber}"]`)); }; module.exports = () => { let page; let browser; let url; - let createdRunId; before(async () => { [page, browser, url] = await defaultBefore(page, browser); @@ -73,8 +72,7 @@ module.exports = () => { deviceScaleFactor: 1, }); await resetDatabaseContent(); - const { id } = await runService.create({ runNumber: 1010, timeTrgStart: new Date(), environmentId: 'CmCvjNbg' }); - createdRunId = id; + await runService.create({ runNumber: 1010, timeTrgStart: new Date(), environmentId: 'CmCvjNbg' }); }); after(async () => { [page, browser] = await defaultAfter(page, browser); @@ -97,31 +95,31 @@ module.exports = () => { }); it('successfully changed run tags in EDIT mode', async () => { - await reloadPage(page); await pressElement(page, '#edit-run'); - await pressElement(page, '#tags-selection #tagCheckbox1'); + await pressElement(page, '#tags .popover-trigger'); + await pressElement(page, '#tags-dropdown-option-CPV'); await pressElement(page, '#save-run'); await pressElement(page, '#edit-run'); - await page.waitForSelector('#tags-selection #tagCheckbox1'); - expect(await page.$eval('#tags-selection #tagCheckbox1', (elem) => elem.checked)).to.be.true; + await page.waitForSelector('#tags-dropdown-option-CPV:checked'); + await pressElement(page, '#cancel-run'); + await page.waitForSelector('#edit-run'); }); it('should display detectors names', async () => { - await reloadPage(page); - const detectorNameSelector = '#Run-detectors .detector-name'; + const detectorNameSelector = '#detectors .detector-name'; const detectorNames = await page.$$eval(detectorNameSelector, (detectors) => detectors.map((detector) => detector.innerText)); const expectedDetectorNames = ['ACO', 'CPV', 'CTP', 'EMC', 'FDD', 'FIT', 'FT0', 'FV0', 'HMP', 'ITS'] .concat(['MCH', 'MFT', 'MID', 'PHS', 'TOF', 'TPC', 'TRD', 'TST', 'ZDC']); expect(detectorNames).to.deep.equal(expectedDetectorNames); - const presentDetectorNameSelector = '#Run-detectors :is(.success, .danger) .detector-name'; + const presentDetectorNameSelector = '#detectors :is(.success, .danger) .detector-name'; const presentDetectorName = await page.$eval(presentDetectorNameSelector, (detector) => detector.innerText); expect(presentDetectorName).to.equal('CPV'); }); it('should display detectors qualities and colors', async () => { - const detectorBadgeClassesSelector = '#Run-detectors .detector-badge'; + const detectorBadgeClassesSelector = '#detectors .detector-badge'; const detectorBadgeClasses = await page.$$eval(detectorBadgeClassesSelector, (badges) => badges.map((badge) => badge.className)); const detectorBadgesPresent = detectorBadgeClasses.filter((elem) => !elem.includes('gray')); @@ -142,7 +140,7 @@ module.exports = () => { }); it('should successfully display detectors icons', async () => { - const svgPaths = await page.$$eval('#Run-detectors .detector-quality-icon svg path', (elements) => + const svgPaths = await page.$$eval('#detectors .detector-quality-icon svg path', (elements) => elements.map((elem) => elem.getAttribute('d'))); svgPaths.every((path, index) => { @@ -156,9 +154,9 @@ module.exports = () => { it('successfully update detectors qualities in EDIT mode', async () => { await pressElement(page, '#edit-run'); - await pressElement(page, '#Run-detectors .dropdown-trigger'); + await pressElement(page, '#detectors .dropdown-trigger'); - const popoverSelector = await getPopoverSelector(await page.$('#Run-detectors .popover-trigger')); + const popoverSelector = await getPopoverSelector(await page.$('#detectors .popover-trigger')); await page.waitForSelector(`${popoverSelector} .dropdown`); const goodQualityRadioSelector = '#detector-quality-1-good'; @@ -166,20 +164,20 @@ module.exports = () => { expect(await page.$eval(goodQualityRadioSelector, (element) => element.checked)).to.be.true; expect(await page.$eval(badQualityRadioSelector, (element) => element.checked)).to.be.false; await pressElement(page, badQualityRadioSelector); - await fillInput(page, '#Run-detectors textarea', 'Justification'); + await fillInput(page, '#detectors textarea', 'Justification'); await pressElement(page, '#save-run'); - const detectorBadgeSelector = '#Run-detectors .detector-badge:nth-child(2)'; + const detectorBadgeSelector = '#detectors .detector-badge:nth-child(2)'; await page.waitForSelector(detectorBadgeSelector); const detectorBadgeClass = await page.$eval(detectorBadgeSelector, (element) => element.className); expect(detectorBadgeClass).to.contain('b-danger'); expect(detectorBadgeClass).to.contain('danger'); expect(await page.$eval(detectorBadgeSelector, (element) => element.innerText)).to.equal('CPV'); - expect(await page.$eval('#Run-detectors .detector-badge:nth-child(2) .detector-quality-icon svg path', (element) => + expect(await page.$eval('#detectors .detector-badge:nth-child(2) .detector-quality-icon svg path', (element) => element.getAttribute('d'))).to.equal(xIconPath); await pressElement(page, '#edit-run'); - await pressElement(page, '#Run-detectors .dropdown-trigger'); + await pressElement(page, '#detectors .dropdown-trigger'); await page.waitForSelector('.dropdown'); expect(await page.$eval(goodQualityRadioSelector, (element) => element.checked)).to.be.false; @@ -190,23 +188,23 @@ module.exports = () => { it('should successfully update end of run reasons', async () => { await pressElement(page, '#edit-run'); - await page.waitForSelector('#Run-eorReasons select'); - await page.select('#Run-eorReasons select', 'DETECTORS'); + await page.waitForSelector('#eor-reasons select'); + await page.select('#eor-reasons select', 'DETECTORS'); - await page.waitForSelector('#Run-eorReasons select:nth-child(2) option:nth-of-type(2)'); - await page.select('#Run-eorReasons select:nth-child(2)', 'CPV'); - await page.type('#Run-eorReasons input', 'A new EOR reason'); + await page.waitForSelector('#eor-reasons select:nth-child(2) option:nth-of-type(2)'); + await page.select('#eor-reasons select:nth-child(2)', 'CPV'); + await page.type('#eor-reasons input', 'A new EOR reason'); await pressElement(page, '#add-eor-reason', true); // Flaky test, these options seem to fix it for now - await page.waitForFunction(() => document.querySelectorAll('#Run-eorReasons .remove-eor-reason').length === 3, { polling: 'mutation' }); + await page.waitForFunction(() => document.querySelectorAll('#eor-reasons .remove-eor-reason').length === 3, { polling: 'mutation' }); // Remove the first EOR reason await pressElement(page, '.remove-eor-reason'); - await page.waitForFunction(() => document.querySelectorAll('#Run-eorReasons .remove-eor-reason').length === 2, { polling: 'mutation' }); + await page.waitForFunction(() => document.querySelectorAll('#eor-reasons .remove-eor-reason').length === 2, { polling: 'mutation' }); await pressElement(page, '#save-run'); await page.waitForSelector('#edit-run'); - const eorReasons = await page.$$('#Run-eorReasons .eor-reason'); + const eorReasons = await page.$$('#eor-reasons .eor-reason'); expect(eorReasons).to.lengthOf(2); expect(await eorReasons[0].evaluate((element) => element.innerText)) @@ -219,12 +217,12 @@ module.exports = () => { it('should successfully revert the update end of run reasons', async () => { await pressElement(page, '#edit-run'); - await page.waitForSelector('#Run-eorReasons select'); - await page.select('#Run-eorReasons select', 'OTHER'); + await page.waitForSelector('#eor-reasons select'); + await page.select('#eor-reasons select', 'OTHER'); - await page.waitForSelector('#Run-eorReasons select:nth-child(2)'); - await page.select('#Run-eorReasons select:nth-child(2)', 'Some-other'); - await page.type('#Run-eorReasons input', 'A new new EOR reason'); + await page.waitForSelector('#eor-reasons select:nth-child(2)'); + await page.select('#eor-reasons select:nth-child(2)', 'Some-other'); + await page.type('#eor-reasons input', 'A new new EOR reason'); await pressElement(page, '#add-eor-reason'); // Remove the first EOR reason @@ -232,7 +230,7 @@ module.exports = () => { await pressElement(page, '#cancel-run'); await page.waitForSelector('#save-run', { hidden: true }); - const eorReasons = await page.$$('#Run-eorReasons .eor-reason'); + const eorReasons = await page.$$('#eor-reasons .eor-reason'); expect(eorReasons).to.lengthOf(2); expect(await eorReasons[0].evaluate((element) => element.innerText)) @@ -245,41 +243,36 @@ module.exports = () => { it('should successfully update inelasticInteractionRate values of PbPb run', async () => { await goToRunDetails(page, 54); await pressElement(page, '#edit-run'); - await fillInput(page, '#Run-inelasticInteractionRateAvg input', 100.1); - await fillInput(page, '#Run-inelasticInteractionRateAtStart input', 101.1); - await fillInput(page, '#Run-inelasticInteractionRateAtMid input', 102.1); - await fillInput(page, '#Run-inelasticInteractionRateAtEnd input', 103.1); + await fillInput(page, '#inelastic-interaction-rate-avg input', 100.1); + await fillInput(page, '#inelastic-interaction-rate-at-start input', 101.1); + await fillInput(page, '#inelastic-interaction-rate-at-mid input', 102.1); + await fillInput(page, '#inelastic-interaction-rate-at-end input', 103.1); await pressElement(page, '#save-run'); // Wait for edition mode to be gone await page.waitForSelector('#edit-run'); - await expectInnerText(page, '#Run-inelasticInteractionRateAvg', 'INELavg:\n100.1\nHz'); - await expectInnerText(page, '#Run-inelasticInteractionRateAtStart', 'INELstart:\n101.1\nHz'); - await expectInnerText(page, '#Run-inelasticInteractionRateAtMid', 'INELmid:\n102.1\nHz'); - await expectInnerText(page, '#Run-inelasticInteractionRateAtEnd', 'INELend:\n103.1\nHz'); + await expectInnerText(page, '#inelastic-interaction-rate-avg', '100.1\nHz'); + await expectInnerText(page, '#inelastic-interaction-rate-at-start', '101.1\nHz'); + await expectInnerText(page, '#inelastic-interaction-rate-at-mid', '102.1\nHz'); + await expectInnerText(page, '#inelastic-interaction-rate-at-end', '103.1\nHz'); }); - it('should successfully update inelasticInteractionRateAvg of pp run', async () => { + it('should successfully update inelastic-interaction-rate-avg of pp run', async () => { await goToRunDetails(page, 55); await pressElement(page, '#edit-run'); - await fillInput(page, '#Run-inelasticInteractionRateAvg input', 100000); + await fillInput(page, '#inelastic-interaction-rate-avg input', 100000); await pressElement(page, '#save-run'); // Wait for edition mode to be gone await page.waitForSelector('#edit-run'); - await expectInnerText(page, '#Run-inelasticInteractionRateAvg', 'INELavg:\n100,000\nHz'); - await expectInnerText(page, '#Run-muInelasticInteractionRate', '\u03BC(INEL):\n0.009'); - }); - - it('should show lhc data in edit mode', async () => { - await goToRunDetails(page, 1); - await pressElement(page, '#edit-run'); - await expectInnerText(page, '#lhc-fill-fillNumber>strong', 'Fill number:'); + await expectInnerText(page, '#inelastic-interaction-rate-avg', '100,000\nHz'); + await expectInnerText(page, '#mu-inelastic-interaction-rate', '0.009'); }); it('can navigate to the flp panel', async () => { + await goToRunDetails(page, 1); await waitForNavigation(page, () => pressElement(page, '#flps-tab')); await expectUrlParams(page, { page: 'run-detail', @@ -334,9 +327,7 @@ module.exports = () => { }); it('should show lhc data in normal mode', async () => { - const element = await page.$('#lhc-fill-fillNumber>strong'); - const value = await element.evaluate((el) => el.textContent); - expect(value).to.equal('Fill number:'); + await expectInnerText(page, '#fill-number', 'Fill 5'); }); it('successfully prevent from editing run quality of not ended runs', async () => { @@ -352,13 +343,13 @@ module.exports = () => { await pressElement(page, '#edit-run'); await page.waitForSelector('#cancel-run'); - expect(await page.$('#Run-detectors .dropdown-trigger')).to.be.null; + expect(await page.$('#detectors .dropdown-trigger')).to.be.null; }); it('should successfully navigate to the LHC fill details page', async () => { await goToRunDetails(page, 108); - await waitForNavigation(page, () => pressElement(page, '#lhc-fill-fillNumber a')); + await waitForNavigation(page, () => pressElement(page, '#fill-number a')); expectUrlParams(page, { page: 'lhc-fill-details', fillNumber: 1 }); }); @@ -429,39 +420,37 @@ module.exports = () => { it('should display OFF in the nEPNs field when EPNs is null', async () => { await goToRunDetails(page, 3); - await page.waitForSelector('#Run-nEpns'); - await expectInnerText(page, '#Run-nEpns', 'Number of EPNs:\nOFF'); + await expectInnerText(page, '#n-epns', '# EPNs\nOFF'); }); it('should not display OFF in the nEPNs field when EPNs is not null', async () => { await goToRunDetails(page, 106); - await page.waitForSelector('#Run-nEpns'); - await expectInnerText(page, '#Run-nEpns', 'Number of EPNs:\n12'); + await expectInnerText(page, '#n-epns', '# EPNs\n12'); }); it('should not display calibration status on non-calibration runs', async () => { - await page.waitForSelector('#Run-definition'); - expect(await page.$('#Run-definition + #Run-runType')).to.not.be.null; + await page.waitForSelector('#definition'); + await page.waitForSelector('#definition #calibration-status', { hidden: true }); }); it('should display calibration status on calibration runs', async () => { await goToRunDetails(page, 40); - await page.waitForSelector('#Run-calibrationStatus'); - await expectInnerText(page, '#Run-calibrationStatus', `Calibration status:\n${RunCalibrationStatus.NO_STATUS}`); + await page.waitForSelector('#calibration-status'); + await expectInnerText(page, '#calibration-status', RunCalibrationStatus.NO_STATUS); }); it('should allow to update calibration status on calibration runs', async () => { const runNumber = 40; await goToPage(page, 'run-detail', { queryParameters: { runNumber: runNumber } }); - await expectInnerText(page, '#Run-calibrationStatus', `Calibration status:\n${RunCalibrationStatus.NO_STATUS}`); + await expectInnerText(page, '#calibration-status', RunCalibrationStatus.NO_STATUS); await pressElement(page, '#edit-run'); - await page.waitForSelector('#Run-calibrationStatus select'); - await page.select('#Run-calibrationStatus select', RunCalibrationStatus.SUCCESS); + await page.waitForSelector('#calibration-status select'); + await page.select('#calibration-status select', RunCalibrationStatus.SUCCESS); await pressElement(page, '#save-run'); // Wait for page to be reloaded await page.waitForSelector('#edit-run'); - await expectInnerText(page, '#Run-calibrationStatus', `Calibration status:\n${RunCalibrationStatus.SUCCESS}`); + await expectInnerText(page, '#calibration-status', RunCalibrationStatus.SUCCESS); }); it('should successfully expose a button to create a new log related to the displayed environment', async () => { @@ -479,54 +468,43 @@ module.exports = () => { await waitForNavigation(page, () => pressElement(page, '#run-overview')); await waitForNavigation(page, () => pressElement(page, '#row107-runNumber-text > div > a')); - await expectInnerText(page, '#NoLHCDataNotStable', 'No LHC Fill information, beam mode was: UNSTABLE BEAMS'); + await expectInnerText(page, '#non-stable-beam-message', 'No LHC Fill information, beam mode was: UNSTABLE BEAMS'); }); - it('should display the LHC fill number when beam is stable', async () => { - await waitForNavigation(page, () => pressElement(page, '#run-overview')); - await waitForNavigation(page, () => pressElement(page, '#row108-runNumber-text > div > a')); - - await expectInnerText(page, '#lhc-fill-fillNumber', 'Fill number:\n1'); + it('should successfully display a link to related enviroment', async () => { + page.waitForSelector('a href="http://localhost:4000/?page=env-details&environmentId=CmCvjNbg"'); }); it('should successfully display links to infologger and QCG', async () => { await waitForNavigation(page, () => pressElement(page, 'a#run-overview')); await waitForNavigation(page, () => pressElement(page, '#row108 a')); - await page.waitForSelector('.external-links'); - await expectLink(page, '.external-links a', { + await expectLink(page, 'a.external-link', { innerText: 'FLP', href: 'http://localhost:8081/?q={%22run%22:{%22match%22:%22108%22},%22severity%22:{%22in%22:%22W%20E%20F%22}}', }); - await expectLink(page, '.external-links a:nth-of-type(2)', { + await expectLink(page, 'a.external-link:nth-of-type(2)', { innerText: 'QCG', href: 'http://localhost:8082/?page=layoutShow&runNumber=108&definition=PHYSICS&pdpBeamType=pp&runType=PHYSICS', }); }); it('should display links to environment in ECS if run is running', async () => { - // Test for not running run - await waitForNavigation(page, () => pressElement(page, 'a#run-overview')); - await waitForNavigation(page, () => pressElement(page, '#row104 a')); + await goToRunDetails(page, 104); - await page.waitForSelector('.external-links a:nth-of-type(3)', { hidden: true, timeout: 250 }); + await page.waitForSelector('a.external-link:nth-of-type(3)', { hidden: true, timeout: 250 }); // Create running run - await waitForNavigation(page, () => pressElement(page, 'a#run-overview')); - await waitForNavigation(page, () => pressElement(page, `#row${createdRunId} a`)); + await goToRunDetails(page, '1010'); await expectUrlParams(page, { page: 'run-detail', runNumber: '1010' }); await page.waitForSelector('.alert.alert-danger', { hidden: true, timeout: 300 }); await expectInnerText(page, '#runDurationValue', 'RUNNING'); - await expectLink(page, '.external-links a:nth-of-type(3)', { + await expectLink(page, 'a.external-link:nth-of-type(4)', { href: 'http://localhost:8080/?page=environment&id=CmCvjNbg', innerText: 'ECS', }); - await expectLink(page, '#Run-environmentId a', { - href: 'http://localhost:4000/?page=env-details&environmentId=CmCvjNbg', - innerText: 'CmCvjNbg', - }); }); it('should display correct tag styling after updating in tag overview', async () => { diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 96ccef4408..4f574cbbcf 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -22,25 +22,26 @@ const { getFirstRow, goToPage, checkColumnBalloon, - expectLink, - waitForDownload, fillInput, getPopoverContent, getInnerText, - waitForTimeout, getPopoverSelector, + getPeriodInputsSelectors, waitForTableLength, - waitForTableTotalRowsCountToEqual, - waitForEmptyTable, waitForNavigation, + waitForTableTotalRowsCountToEqual, expectInputValue, expectColumnValues, + waitForEmptyTable, + waitForDownload, + expectLink, expectUrlParams, + expectAttributeValue, } = require('../defaults.js'); const { RUN_QUALITIES, RunQualities } = require('../../../lib/domain/enums/RunQualities.js'); -const { runService } = require('../../../lib/server/services/run/RunService.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); const { RunDefinition } = require('../../../lib/domain/enums/RunDefinition.js'); +const { runService } = require('../../../lib/server/services/run/RunService.js'); const { expect } = chai; @@ -53,18 +54,6 @@ module.exports = () => { let table; let firstRowId; const runNumberInputSelector = '.runNumber-filter input'; - const timeFilterSelectors = { - startFrom: '#o2startFilterFromTime', - startTo: '#o2startFilterToTime', - endFrom: '#o2endFilterFromTime', - endTo: '#o2endFilterToTime', - }; - const dateFilterSelectors = { - startFrom: '#o2startFilterFrom', - startTo: '#o2startFilterTo', - endFrom: '#o2endFilterFrom', - endTo: '#o2endFilterTo', - }; before(async () => { [page, browser] = await defaultBefore(page, browser); @@ -282,6 +271,7 @@ module.exports = () => { // Run 106 has detectors and tags that overflow await page.type(runNumberInputSelector, '106'); + await fillInput(page, runNumberInputSelector, '106'); await waitForTableLength(page, 1); await checkColumnBalloon(page, 1, 2); @@ -334,7 +324,7 @@ module.exports = () => { it('should successfully filter on tags', async () => { await waitForTableLength(page, 8); - // Open filter toggle + // Open filter toggle and wait for the dropdown to be visible await pressElement(page, '.tags-filter .dropdown-trigger'); await pressElement(page, '#tag-dropdown-option-FOOD', true); await pressElement(page, '#tag-dropdown-option-RUN', true); @@ -437,110 +427,39 @@ module.exports = () => { ); }); - it('should update to current date when empty and time is set', async () => { + it('Should correctly set the the min/max of time range picker inputs', async () => { await goToPage(page, 'run-overview'); // Open the filters await pressElement(page, '#openFilterToggle'); - await page.waitForSelector('#o2startFilterFromTime'); - let today = new Date(); - today.setMinutes(today.getMinutes() - today.getTimezoneOffset()); - [today] = today.toISOString().split('T'); - const time = '00:01'; - - for (const selector of Object.values(timeFilterSelectors)) { - await page.type(selector, time); - await waitForTimeout(300); - } - for (const selector of Object.values(dateFilterSelectors)) { - const value = await page.$eval(selector, (element) => element.value); - expect(String(value)).to.equal(today); - } - const date = new Date(); - const now = `${date.getHours()}:${(date.getMinutes() < 10 ? '0' : '') + date.getMinutes()}`; - const firstTill = await page.$eval(timeFilterSelectors.startFrom, (element) => element.getAttribute('max')); - const secondTill = await page.$eval(timeFilterSelectors.endFrom, (element) => element.getAttribute('max')); - expect(String(firstTill)).to.equal(now); - expect(String(secondTill)).to.equal(now); - }); - it('Validates date will not be set again', async () => { - await goToPage(page, 'run-overview'); - const dateString = '03-21-2021'; - const validValue = '2021-03-21'; - // Open the filters - await pressElement(page, '#openFilterToggle'); - await page.waitForSelector('#eorDescription'); - - // Set date - for (const key in dateFilterSelectors) { - await page.focus(dateFilterSelectors[key]); - await page.keyboard.type(dateString); - await waitForTimeout(500); - await page.focus(timeFilterSelectors[key]); - await page.keyboard.type('00-01-AM'); - await waitForTimeout(500); - const value = await page.$eval(dateFilterSelectors[key], (element) => element.value); - expect(value).to.equal(validValue); - } - }); + await pressElement(page, '.timeO2Start-filter .popover-trigger'); - it('The max/min should be the right value when date is set to same day', async () => { - await goToPage(page, 'run-overview'); + const o2StartPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); + const periodInputsSelectors = getPeriodInputsSelectors(o2StartPopoverSelector); - const dateString = '03-02-2021'; - // Open the filters - await pressElement(page, '#openFilterToggle'); - await page.waitForSelector('#eorDescription'); + await fillInput(page, periodInputsSelectors.fromTimeSelector, '11:11', ['change']); + await fillInput(page, periodInputsSelectors.toTimeSelector, '14:00', ['change']); - // Set date to an open day - for (const selector of Object.values(dateFilterSelectors)) { - await page.type(selector, dateString); - await waitForTimeout(300); - } - await page.type(timeFilterSelectors.startFrom, '11:11'); - await page.type(timeFilterSelectors.startTo, '14:00'); - await page.type(timeFilterSelectors.endFrom, '11:11'); - await page.type(timeFilterSelectors.endTo, '14:00'); - await waitForTimeout(500); - - // Validate if the max value is the same as the till values - const startMax = await page.$eval(timeFilterSelectors.startFrom, (element) => element.getAttribute('max')); - const endMax = await page.$eval(timeFilterSelectors.endFrom, (element) => element.getAttribute('max')); - expect(String(startMax)).to.equal(await page.$eval(timeFilterSelectors.startTo, (element) => element.value)); - expect(String(endMax)).to.equal(await page.$eval(timeFilterSelectors.endTo, (element) => element.value)); - - // Validate if the min value is the same as the from values - const startMin = await page.$eval(timeFilterSelectors.startTo, (element) => element.getAttribute('min')); - const endMin = await page.$eval(timeFilterSelectors.endTo, (element) => element.getAttribute('min')); - expect(String(startMin)).to.equal(await page.$eval(timeFilterSelectors.startFrom, (element) => element.value)); - expect(String(endMin)).to.equal(await page.$eval(timeFilterSelectors.endFrom, (element) => element.value)); - }); + // American style input + await fillInput(page, periodInputsSelectors.fromDateSelector, '2021-02-03', ['change']); + await fillInput(page, periodInputsSelectors.toDateSelector, '2021-02-03', ['change']); - it('The max should be the maximum value when having different dates', async () => { - await goToPage(page, 'run-overview'); + // Wait for page to be refreshed + await expectAttributeValue(page, periodInputsSelectors.toTimeSelector, 'min', '11:12'); + await expectAttributeValue(page, periodInputsSelectors.toDateSelector, 'min', '2021-02-03'); - const dateString = '03-20-2021'; - const maxTime = '23:59'; - const minTime = '00:00'; - // Open the filters - await pressElement(page, '#openFilterToggle'); - await page.waitForSelector('#eorDescription'); - // Set date to an open day - for (const selector of Object.values(dateFilterSelectors)) { - await page.type(selector, dateString); - await waitForTimeout(500); - } - const startMax = await page.$eval(timeFilterSelectors.startFrom, (element) => element.getAttribute('max')); - const endMax = await page.$eval(timeFilterSelectors.endFrom, (element) => element.getAttribute('max')); - expect(String(startMax)).to.equal(maxTime); - expect(String(endMax)).to.equal(maxTime); - - // Validate if the min value is the same as the from values - const startMin = await page.$eval(timeFilterSelectors.startTo, (element) => element.getAttribute('min')); - const endMin = await page.$eval(timeFilterSelectors.endTo, (element) => element.getAttribute('min')); - expect(String(startMin)).to.equal(minTime); - expect(String(endMin)).to.equal(minTime); + await expectAttributeValue(page, periodInputsSelectors.fromTimeSelector, 'max', '13:59'); + await expectAttributeValue(page, periodInputsSelectors.fromDateSelector, 'max', '2021-02-03'); + + // Setting different dates, still american style input + await fillInput(page, periodInputsSelectors.toDateSelector, '2021-02-05', ['change']); + + await expectAttributeValue(page, periodInputsSelectors.toTimeSelector, 'min', ''); + await expectAttributeValue(page, periodInputsSelectors.toDateSelector, 'min', '2021-02-03'); + + await expectAttributeValue(page, periodInputsSelectors.fromTimeSelector, 'max', ''); + await expectAttributeValue(page, periodInputsSelectors.fromDateSelector, 'max', '2021-02-05'); }); it('should successfully filter on duration', async () => { @@ -596,6 +515,17 @@ module.exports = () => { expect(runDurationList.every((runDuration) => runDuration === 'UNKNOWN')).to.be.true; }); + it('should successfully apply alice currents filters', async () => { + await pressElement(page, '#reset-filters'); + + 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 pressElement(page, '#reset-filters'); + }); + it('Should successfully filter runs by their run quality', async () => { await goToPage(page, 'run-overview'); const filterInputSelectorPrefix = '#runQualityCheckbox'; @@ -1116,19 +1046,19 @@ module.exports = () => { await waitForNavigation(page, () => pressElement(page, 'a#home')); await waitForNavigation(page, () => pressElement(page, 'a#run-overview')); - // Not running run + // Not running run, wait for popover to be visible await pressElement(page, '#row104-runNumber-text .popover-trigger'); let popoverSelector = await getPopoverSelector(await page.waitForSelector('#row104-runNumber-text .popover-trigger')); await page.waitForSelector(popoverSelector); await expectLink(page, `${popoverSelector} a:nth-of-type(1)`, { href: 'http://localhost:8081/?q={%22partition%22:{%22match%22:%22TDI59So3d%22},' - + '%22run%22:{%22match%22:%22104%22},%22severity%22:{%22in%22:%22W%20E%20F%22}}', + + '%22run%22:{%22match%22:%22104%22},%22severity%22:{%22in%22:%22W%20E%20F%22}}', innerText: 'Infologger FLP', }); await expectLink(page, `${popoverSelector} a:nth-of-type(2)`, { href: 'http://localhost:8082/' + - '?page=layoutShow&runNumber=104&definition=COMMISSIONING&detector=CPV&pdpBeamType=cosmic&runType=COSMICS', + '?page=layoutShow&runNumber=104&definition=COMMISSIONING&detector=CPV&pdpBeamType=cosmic&runType=COSMICS', innerText: 'QCG', }); diff --git a/test/public/runs/runsPerDataPass.overview.test.js b/test/public/runs/runsPerDataPass.overview.test.js index 990ca1d3bf..1906833373 100644 --- a/test/public/runs/runsPerDataPass.overview.test.js +++ b/test/public/runs/runsPerDataPass.overview.test.js @@ -347,28 +347,8 @@ module.exports = () => { await expectColumnValues(page, 'runNumber', ['108', '107', '106']); }); - it('should successfully apply timeStart filter', async () => { - await navigateToRunsPerDataPass(page, { lhcPeriodId: 2, dataPassId: 2 }, { epectedRowsCount: 3 }); - await pressElement(page, '#openFilterToggle'); - - await fillInput(page, '.timeO2Start-filter input[type=date]', '2021-01-01'); - await expectColumnValues(page, 'runNumber', ['1']); - - await pressElement(page, '#reset-filters'); - await expectColumnValues(page, 'runNumber', ['55', '2', '1']); - }); - - it('should successfully apply timeEnd filter', async () => { - await pressElement(page, '#openFilterToggle'); - - await fillInput(page, '.timeO2End-filter input[type=date]', '2021-01-01'); - await expectColumnValues(page, 'runNumber', ['1']); - - await pressElement(page, '#reset-filters', true); - await expectColumnValues(page, 'runNumber', ['55', '2', '1']); - }); - it('should successfully apply duration filter', async () => { + await navigateToRunsPerDataPass(page, { lhcPeriodId: 2, dataPassId: 2 }, { epectedRowsCount: 3 }); await pressElement(page, '#openFilterToggle'); await page.select('.runDuration-filter select', '>=');