diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e941930f0..d0e586409f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ 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. +## [0.86.0](https://github.com/AliceO2Group/Bookkeeping/releases/tag/%40aliceo2%2Fbookkeeping%400.86.0) +* Notable changes for users: + * Placed RCT entry point directly in navigation bar + * Changed QC flag delete button access to admins only + * Added runs counts for simulation pass overview + * Added data passes counts for simulation pass overview + * ALICE efficiency computation now uses mean weighted by stable beam duration + * Removed missing trigger start/stop warning if trigger is OFF +* Notable change for developers: + * Run optional properties are now optional in proto file + +## [0.85.0](https://github.com/AliceO2Group/Bookkeeping/releases/tag/%40aliceo2%2Fbookkeeping%400.85.0) +* Notable changes for users: + * Added possibility to filter out runs that contains any of the given tags + * Added input inelastic interaction rate values in run details page and RCT runs overviews +* Notable change for developers: + * Added possibility to set visibility of detail component configuration based on the current item + ## [0.84.0](https://github.com/AliceO2Group/Bookkeeping/releases/tag/%40aliceo2%2Fbookkeeping%400.84.0) * Notable changes for users: * Removed spurious detectors from Runs Per Data Pass and Runs Per Simulation Pass pages diff --git a/database/CHANGELOG.md b/database/CHANGELOG.md index e7f9683514..1c5942407c 100644 --- a/database/CHANGELOG.md +++ b/database/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.85.0] +* Changes made to the database + * Added four columns to run table storing information about inelastic interaction rate + ## [0.83.0] * Changes made to the database * Added sequelize migration file for QC Flag tables creation diff --git a/lib/database/adapters/DataPassQcFlagAdapter.js b/lib/database/adapters/DataPassQcFlagAdapter.js new file mode 100644 index 0000000000..f8269ec56c --- /dev/null +++ b/lib/database/adapters/DataPassQcFlagAdapter.js @@ -0,0 +1,72 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const { AdapterError } = require('../../server/errors/AdapterError.js'); + +/** + * Adapter for data pass QC flag + */ +class DataPassQcFlagAdapter { + /** + * Constructor + */ + constructor() { + this.toEntity = this.toEntity.bind(this); + + this.dataPassAdapter = null; + this.qcFlagTypeAdapter = null; + this.qcFlagVerificationAdapter = null; + this.userAdapter = null; + } + + /** + * Converts the given database object to an entity object. + * + * @param {SequelizeDataPassQcFlag} databaseObject Object to convert. + * @returns {DataPassQcFlag} Converted entity object. + */ + toEntity(databaseObject) { + const { + dataPassId, + qualityControlFlagId, + qcFlag, + createdAt, + updatedAt, + dataPass, + } = databaseObject; + + if (qcFlag === null) { + throw new AdapterError('Related QC flag missing in DataPassQcFlag.'); + } + + return { + dataPassId, + qcFlagId: qualityControlFlagId, + from: qcFlag.from, + to: qcFlag.to, + comment: qcFlag.comment, + createdById: qcFlag.createdById, + flagTypeId: qcFlag.flagTypeId, + runNumber: qcFlag.runNumber, + dplDetectorId: qcFlag.dplDetectorId, + createdAt, + updatedAt, + dataPass: dataPass ? this.dataPassAdapter.toEntity(dataPass) : null, + createdBy: qcFlag.createdBy ? this.userAdapter.toEntity(qcFlag.createdBy) : null, + flagType: qcFlag.flagType ? this.qcFlagTypeAdapter.toEntity(qcFlag.flagType) : null, + verifications: qcFlag.verifications ? this.qcFlagVerificationAdapter.toEntity(qcFlag.verifications) : null, + }; + } +} + +exports.DataPassQcFlagAdapter = DataPassQcFlagAdapter; diff --git a/lib/database/adapters/LhcFillStatisticsAdapter.js b/lib/database/adapters/LhcFillStatisticsAdapter.js index 289a6b04cd..bb016a7e5a 100644 --- a/lib/database/adapters/LhcFillStatisticsAdapter.js +++ b/lib/database/adapters/LhcFillStatisticsAdapter.js @@ -24,6 +24,7 @@ class LhcFillStatisticsAdapter { toEntity(databaseObject) { const { fillNumber, + stableBeamsDuration, runsCoverage, efficiency, timeLossAtStart, @@ -38,6 +39,8 @@ class LhcFillStatisticsAdapter { return { fillNumber, // Convert to ms + stableBeamsDuration: stableBeamsDuration * 1000, + // Convert to ms runsCoverage: runsCoverage * 1000, efficiency: parseFloat(efficiency), // Convert to ms diff --git a/lib/database/adapters/RunAdapter.js b/lib/database/adapters/RunAdapter.js index 968a9b5ca1..17948a8821 100644 --- a/lib/database/adapters/RunAdapter.js +++ b/lib/database/adapters/RunAdapter.js @@ -11,6 +11,22 @@ * or submit itself to any jurisdiction. */ +const LHC_REVOLUTION_FREQUENCY_HZ = 11245; // The LHC revolution frequency: 11.245kHz (~ c/26.7km). + +/** + * Extract number of colliding LHC bunch crossing (for ALICE) from LHC fill schema + * @param {string} fillingSchema filling schema + * @return {number} number of colliding LHC bunch crossing + */ +const extractNumberOfCollidingLhcBunchCrossings = (fillingSchema) => { + const collidingLhcBunchCrossingsInFillSchemaRegex = /[A-Za-z0-9]+_[A-Za-z0-9]+_[0-9]+_([0-9]+)_.*/; + const [, matchedValueForAlice] = fillingSchema?.match(collidingLhcBunchCrossingsInFillSchemaRegex) || []; + if (!matchedValueForAlice) { + return null; + } + return parseInt(matchedValueForAlice, 10); +}; + /** * RunAdapter */ @@ -127,7 +143,12 @@ class RunAdapter { logs, definition, calibrationStatus, + inelasticInteractionRateAvg, + inelasticInteractionRateAtStart, + inelasticInteractionRateAtMid, + inelasticInteractionRateAtEnd, } = databaseObject; + const entityObject = { id, runNumber, @@ -209,6 +230,19 @@ class RunAdapter { entityObject.flpRoles = flpRoles ? flpRoles.map(this.flpRoleAdapter.toEntity) : []; entityObject.lhcPeriod = lhcPeriod ? this.lhcPeriodAdapter.toEntity(lhcPeriod) : lhcPeriod; + entityObject.inelasticInteractionRateAvg = inelasticInteractionRateAvg; + entityObject.inelasticInteractionRateAtStart = inelasticInteractionRateAtStart; + entityObject.inelasticInteractionRateAtMid = inelasticInteractionRateAtMid; + entityObject.inelasticInteractionRateAtEnd = inelasticInteractionRateAtEnd; + if (lhcFill && inelasticInteractionRateAvg !== null) { + const numberOfCollidingLhcBunchCrossings = extractNumberOfCollidingLhcBunchCrossings(lhcFill.fillingSchemeName); + entityObject.muInelasticInteractionRate = numberOfCollidingLhcBunchCrossings + ? inelasticInteractionRateAvg / (numberOfCollidingLhcBunchCrossings * LHC_REVOLUTION_FREQUENCY_HZ) + : null; + } else { + entityObject.muInelasticInteractionRate = null; + } + return entityObject; } @@ -275,6 +309,11 @@ class RunAdapter { logs: entityObject.logs?.map(this.logAdapter.toDatabase), tags: entityObject.tags?.map(this.tagAdapter.toDatabase), lhcFill: entityObject.lhcFill ? this.lhcFillAdapter.toDatabase(entityObject.lhcFill) : entityObject.lhcFill, + + inelasticInteractionRateAvg: entityObject.inelasticInteractionRateAvg, + inelasticInteractionRateAtStart: entityObject.inelasticInteractionRateAtStart, + inelasticInteractionRateAtMid: entityObject.inelasticInteractionRateAtMid, + inelasticInteractionRateAtEnd: entityObject.inelasticInteractionRateAtEnd, }; } diff --git a/lib/database/adapters/SimulationPassQcFlagAdapter.js b/lib/database/adapters/SimulationPassQcFlagAdapter.js new file mode 100644 index 0000000000..71c202e648 --- /dev/null +++ b/lib/database/adapters/SimulationPassQcFlagAdapter.js @@ -0,0 +1,72 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const { AdapterError } = require('../../server/errors/AdapterError.js'); + +/** + * Adapter for simulation pass QC flag + */ +class SimulationPassQcFlagAdapter { + /** + * Constructor + */ + constructor() { + this.toEntity = this.toEntity.bind(this); + + this.simulationPassAdapter = null; + this.qcFlagTypeAdapter = null; + this.qcFlagVerificationAdapter = null; + this.userAdapter = null; + } + + /** + * Converts the given database object to an entity object. + * + * @param {SequelizeSimulationPassQcFlag} databaseObject Object to convert. + * @returns {SimulationPassQcFlag} Converted entity object. + */ + toEntity(databaseObject) { + const { + simulationPassId, + qualityControlFlagId, + qcFlag, + createdAt, + updatedAt, + simulationPass, + } = databaseObject; + + if (qcFlag === null) { + throw new AdapterError('Related QC flag missing in SimulationPassQcFlag.'); + } + + return { + simulationPassId, + qcFlagId: qualityControlFlagId, + from: qcFlag.from, + to: qcFlag.to, + comment: qcFlag.comment, + createdById: qcFlag.createdById, + flagTypeId: qcFlag.flagTypeId, + runNumber: qcFlag.runNumber, + dplDetectorId: qcFlag.dplDetectorId, + createdAt, + updatedAt, + simulationPass: simulationPass ? this.simulationPassAdapter.toEntity(simulationPass) : null, + createdBy: qcFlag.createdBy ? this.userAdapter.toEntity(qcFlag.createdBy) : null, + flagType: qcFlag.flagType ? this.qcFlagTypeAdapter.toEntity(qcFlag.flagType) : null, + verifications: qcFlag.verifications ? this.qcFlagVerificationAdapter.toEntity(qcFlag.verifications) : null, + }; + } +} + +exports.SimulationPassQcFlagAdapter = SimulationPassQcFlagAdapter; diff --git a/lib/database/adapters/index.js b/lib/database/adapters/index.js index f1c25dab84..14736f850b 100644 --- a/lib/database/adapters/index.js +++ b/lib/database/adapters/index.js @@ -12,6 +12,7 @@ */ const AttachmentAdapter = require('./AttachmentAdapter'); +const { DataPassQcFlagAdapter } = require('./DataPassQcFlagAdapter.js'); const DetectorAdapter = require('./DetectorAdapter'); const { DplDetectorAdapter } = require('./dpl/DplDetectorAdapter.js'); const { DplProcessExecutionAdapter } = require('./dpl/DplProcessExecutionAdapter.js'); @@ -41,8 +42,10 @@ const LhcPeriodAdapter = require('./LhcPeriodAdapter'); const LhcPeriodStatisticsAdapter = require('./LhcPeriodStatisticsAdapter'); const DataPassAdapter = require('./DataPassAdapter'); const SimulationPassAdapter = require('./SimulationPassAdapter.js'); +const { SimulationPassQcFlagAdapter } = require('./SimulationPassQcFlagAdapter'); const attachmentAdapter = new AttachmentAdapter(); +const dataPassQcFlagAdapter = new DataPassQcFlagAdapter(); const dataPassAdapter = new DataPassAdapter(); const detectorAdapter = new DetectorAdapter(); const dplDetectorAdapter = new DplDetectorAdapter(); @@ -69,11 +72,17 @@ const runAdapter = new RunAdapter(); const runDetectorsAdapter = new RunDetectorsAdapter(); const runTypeAdapter = new RunTypeAdapter(); const simulationPassAdapter = new SimulationPassAdapter(); +const simulationPassQcFlagAdapter = new SimulationPassQcFlagAdapter(); const subsystemAdapter = new SubsystemAdapter(); const tagAdapter = new TagAdapter(); const userAdapter = new UserAdapter(); // Fill dependencies +dataPassQcFlagAdapter.dataPassAdapter = dataPassAdapter; +dataPassQcFlagAdapter.qcFlagTypeAdapter = qcFlagTypeAdapter; +dataPassQcFlagAdapter.userAdapter = userAdapter; +dataPassQcFlagAdapter.qcFlagVerificationAdapter = qcFlagVerificationAdapter; + dplDetectorAdapter.dplProcessExecutionAdapter = dplProcessExecutionAdapter; dplProcessAdapter.dplProcessExecutionAdapter = dplProcessExecutionAdapter; @@ -123,9 +132,15 @@ runAdapter.logAdapter = logAdapter; runAdapter.runTypeAdapter = runTypeAdapter; runAdapter.tagAdapter = tagAdapter; +simulationPassQcFlagAdapter.simulationPassAdapter = simulationPassAdapter; +simulationPassQcFlagAdapter.userAdapter = userAdapter; +simulationPassQcFlagAdapter.qcFlagTypeAdapter = qcFlagTypeAdapter; +simulationPassQcFlagAdapter.qcFlagVerificationAdapter = qcFlagVerificationAdapter; + module.exports = { attachmentAdapter, dataPassAdapter, + dataPassQcFlagAdapter, detectorAdapter, dplDetectorAdapter, dplProcessAdapter, @@ -151,6 +166,7 @@ module.exports = { runDetectorsAdapter, runTypeAdapter, simulationPassAdapter, + simulationPassQcFlagAdapter, subsystemAdapter, tagAdapter, userAdapter, diff --git a/lib/database/migrations/20230901090207-include-fill-without-runs-in-statistics.js b/lib/database/migrations/20230901090207-include-fill-without-runs-in-statistics.js index eec7c9f915..fb4d537b5e 100644 --- a/lib/database/migrations/20230901090207-include-fill-without-runs-in-statistics.js +++ b/lib/database/migrations/20230901090207-include-fill-without-runs-in-statistics.js @@ -1,3 +1,16 @@ +/** + * @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. + */ + 'use strict'; const ROLLBACK_FILL_STATISTICS_TO_PREVIOUS = ` diff --git a/lib/database/migrations/20240417163030-add-Mu-and-Inel-columns-to-runs.js b/lib/database/migrations/20240417163030-add-Mu-and-Inel-columns-to-runs.js new file mode 100644 index 0000000000..adfc9505bf --- /dev/null +++ b/lib/database/migrations/20240417163030-add-Mu-and-Inel-columns-to-runs.js @@ -0,0 +1,44 @@ +/** + * @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. + */ + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface, Sequelize) => queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.addColumn('runs', 'inelastic_interaction_rate_avg', { + type: Sequelize.FLOAT, + allowNull: true, + }, { transaction }); + + await queryInterface.addColumn('runs', 'inelastic_interaction_rate_at_start', { + type: Sequelize.FLOAT, + allowNull: true, + }, { transaction }); + + await queryInterface.addColumn('runs', 'inelastic_interaction_rate_at_mid', { + type: Sequelize.FLOAT, + allowNull: true, + }, { transaction }); + + await queryInterface.addColumn('runs', 'inelastic_interaction_rate_at_end', { + type: Sequelize.FLOAT, + allowNull: true, + }, { transaction }); + }), + + down: async (queryInterface) => queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.removeColumn('runs', 'inelastic_interaction_rate_avg', { transaction }); + await queryInterface.removeColumn('runs', 'inelastic_interaction_rate_at_start', { transaction }); + await queryInterface.removeColumn('runs', 'inelastic_interaction_rate_at_mid', { transaction }); + await queryInterface.removeColumn('runs', 'inelastic_interaction_rate_at_end', { transaction }); + }), +}; diff --git a/lib/database/migrations/20240423072839-add-sb-duration-to-fill-statistics-view.js b/lib/database/migrations/20240423072839-add-sb-duration-to-fill-statistics-view.js new file mode 100644 index 0000000000..9af48f74c5 --- /dev/null +++ b/lib/database/migrations/20240423072839-add-sb-duration-to-fill-statistics-view.js @@ -0,0 +1,83 @@ +/** + * @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. + */ + +'use strict'; + +const UPDATE_FILL_STATISTICS_VIEW = ` +CREATE OR REPLACE VIEW fill_statistics AS +SELECT lf.fill_number, + -- Use max to comply with gruop by, but all the values are the same in the group + COALESCE(MAX(sbr.sb_duration), 0) stable_beams_duration, + SUM(sbr.sb_run_duration) runs_coverage, + SUM(COALESCE(sbr.sb_run_duration / sbr.sb_duration, 0)) efficiency, + COALESCE( + TO_SECONDS(MIN(sbr.sb_run_start)) - TO_SECONDS(MAX(sbr.sb_start)), + 0 + ) time_loss_at_start, + COALESCE( + (TO_SECONDS(MIN(sbr.sb_run_start)) - TO_SECONDS(MAX(sbr.sb_start))) / MAX(sbr.sb_duration), + 0 + ) efficiency_loss_at_start, + COALESCE(TO_SECONDS(MIN(sbr.sb_end)) - TO_SECONDS(MAX(sbr.sb_run_end)), 0) time_loss_at_end, + COALESCE( + (TO_SECONDS(MIN(sbr.sb_end)) - TO_SECONDS(MAX(sbr.sb_run_end))) / MAX(sbr.sb_duration), + 0 + ) efficiency_loss_at_end, + COALESCE(AVG(sbr.sb_run_duration), 0) mean_run_duration, + COALESCE(SUM(r.ctf_file_size), 0) total_ctf_file_size, + COALESCE(SUM(r.tf_file_size), 0) total_tf_file_size +FROM lhc_fills lf + LEFT JOIN runs r ON r.fill_number = lf.fill_number AND r.definition = 'PHYSICS' AND r.run_quality = 'good' + LEFT JOIN stable_beam_runs sbr ON sbr.run_number = r.run_number +WHERE lf.stable_beams_start IS NOT NULL +GROUP BY lf.fill_number; +`; + +const ROLLBACK_FILL_STATISTICS_TO_PREVIOUS = ` +CREATE OR REPLACE VIEW fill_statistics AS +SELECT lf.fill_number, + SUM(sbr.sb_run_duration) runs_coverage, + SUM(COALESCE(sbr.sb_run_duration / sbr.sb_duration, 0)) efficiency, + COALESCE( + TO_SECONDS(MIN(sbr.sb_run_start)) - TO_SECONDS(MAX(sbr.sb_start)), + 0 + ) time_loss_at_start, + COALESCE( + (TO_SECONDS(MIN(sbr.sb_run_start)) - TO_SECONDS(MAX(sbr.sb_start))) / MAX(sbr.sb_duration), + 0 + ) efficiency_loss_at_start, + COALESCE(TO_SECONDS(MIN(sbr.sb_end)) - TO_SECONDS(MAX(sbr.sb_run_end)), 0) time_loss_at_end, + COALESCE( + (TO_SECONDS(MIN(sbr.sb_end)) - TO_SECONDS(MAX(sbr.sb_run_end))) / MAX(sbr.sb_duration), + 0 + ) efficiency_loss_at_end, + COALESCE(AVG(sbr.sb_run_duration), 0) mean_run_duration, + COALESCE(SUM(r.ctf_file_size), 0) total_ctf_file_size, + COALESCE(SUM(r.tf_file_size), 0) total_tf_file_size +FROM lhc_fills lf + LEFT JOIN runs r ON r.fill_number = lf.fill_number AND r.definition = 'PHYSICS' AND r.run_quality = 'good' + LEFT JOIN stable_beam_runs sbr ON sbr.run_number = r.run_number +WHERE lf.stable_beams_start IS NOT NULL +GROUP BY lf.fill_number; +`; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface) => queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.sequelize.query(UPDATE_FILL_STATISTICS_VIEW, { transaction }); + }), + + down: async (queryInterface) => queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.sequelize.query(ROLLBACK_FILL_STATISTICS_TO_PREVIOUS, { transaction }); + }), +}; diff --git a/lib/database/models/dataPassQcFlag.js b/lib/database/models/dataPassQcFlag.js new file mode 100644 index 0000000000..051eedc806 --- /dev/null +++ b/lib/database/models/dataPassQcFlag.js @@ -0,0 +1,38 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const Sequelize = require('sequelize'); + +module.exports = (sequelize) => { + const QcFlag = sequelize.define( + 'DataPassQcFlag', + { + dataPassId: { + type: Sequelize.INTEGER, + primaryKey: true, + }, + qualityControlFlagId: { + type: Sequelize.INTEGER, + primaryKey: true, + }, + }, + { tableName: 'data_pass_quality_control_flag' }, + ); + + QcFlag.associate = (models) => { + QcFlag.belongsTo(models.QcFlag, { as: 'qcFlag', foreignKey: 'qualityControlFlagId', targetKey: 'id' }); + QcFlag.belongsTo(models.DataPass, { as: 'dataPass' }); + }; + + return QcFlag; +}; diff --git a/lib/database/models/index.js b/lib/database/models/index.js index 60030a3feb..f3707f71e4 100644 --- a/lib/database/models/index.js +++ b/lib/database/models/index.js @@ -12,6 +12,8 @@ */ const Attachment = require('./attachment'); +const DataPass = require('./dataPass.js'); +const DataPassQcFlag = require('./dataPassQcFlag.js'); const Detector = require('./detector'); const DplDetector = require('./dpl/dpldetector.js'); const DplProcess = require('./dpl/dplprocess.js'); @@ -25,26 +27,28 @@ const FlpRole = require('./flprole'); const Host = require('./host.js'); const LhcFill = require('./lhcFill'); const LhcFillStatistics = require('./lhcFillStatistics.js'); +const LhcPeriod = require('./lhcPeriod'); +const LhcPeriodStatistics = require('./lhcPeriodsStatistics'); const Log = require('./log'); +const QcFlag = require('./qcFlag.js'); +const QcFlagType = require('./qcFlagType.js'); +const QcFlagVerification = require('./qcFlagVerification.js'); const ReasonType = require('./reasontype'); const Run = require('./run'); const RunDetectors = require('./rundetectors.js'); const RunType = require('./runType'); +const SimulationPass = require('./simulationPass.js'); +const SimulationPassQcFlag = require('./simulationPassQcFlag.js'); const StableBeamRun = require('./stableBeamsRun.js'); const Subsystem = require('./subsystem'); const Tag = require('./tag'); const User = require('./user'); -const LhcPeriod = require('./lhcPeriod'); -const LhcPeriodStatistics = require('./lhcPeriodsStatistics'); -const DataPass = require('./dataPass.js'); -const QcFlagType = require('./qcFlagType.js'); -const QcFlag = require('./qcFlag.js'); -const QcFlagVerification = require('./qcFlagVerification.js'); -const SimulationPass = require('./simulationPass.js'); module.exports = (sequelize) => { const models = { Attachment: Attachment(sequelize), + DataPass: DataPass(sequelize), + DataPassQcFlag: DataPassQcFlag(sequelize), Detector: Detector(sequelize), DplDetector: DplDetector(sequelize), DplProcess: DplProcess(sequelize), @@ -58,22 +62,22 @@ module.exports = (sequelize) => { Host: Host(sequelize), LhcFill: LhcFill(sequelize), LhcFillStatistics: LhcFillStatistics(sequelize), + LhcPeriod: LhcPeriod(sequelize), + LhcPeriodStatistics: LhcPeriodStatistics(sequelize), Log: Log(sequelize), + QcFlag: QcFlag(sequelize), + QcFlagType: QcFlagType(sequelize), + QcFlagVerification: QcFlagVerification(sequelize), ReasonType: ReasonType(sequelize), Run: Run(sequelize), RunDetectors: RunDetectors(sequelize), RunType: RunType(sequelize), + SimulationPass: SimulationPass(sequelize), + SimulationPassQcFlag: SimulationPassQcFlag(sequelize), StableBeamRun: StableBeamRun(sequelize), Subsystem: Subsystem(sequelize), Tag: Tag(sequelize), User: User(sequelize), - LhcPeriod: LhcPeriod(sequelize), - LhcPeriodStatistics: LhcPeriodStatistics(sequelize), - DataPass: DataPass(sequelize), - QcFlagType: QcFlagType(sequelize), - QcFlag: QcFlag(sequelize), - QcFlagVerification: QcFlagVerification(sequelize), - SimulationPass: SimulationPass(sequelize), }; Object.entries(models).forEach(([_key, model]) => { diff --git a/lib/database/models/lhcFillStatistics.js b/lib/database/models/lhcFillStatistics.js index 1f2541c519..672cf46849 100644 --- a/lib/database/models/lhcFillStatistics.js +++ b/lib/database/models/lhcFillStatistics.js @@ -20,6 +20,10 @@ module.exports = (sequelize) => { type: Sequelize.NUMBER, primaryKey: true, }, + stableBeamsDuration: { + allowNull: false, + type: Sequelize.NUMBER, + }, runsCoverage: { allowNull: false, type: Sequelize.NUMBER, diff --git a/lib/database/models/run.js b/lib/database/models/run.js index aaded7ed33..3173bd32fa 100644 --- a/lib/database/models/run.js +++ b/lib/database/models/run.js @@ -215,6 +215,26 @@ module.exports = (sequelize) => { type: Sequelize.INTEGER, default: null, }, + inelasticInteractionRateAvg: { + type: Sequelize.FLOAT, + allowNull: true, + default: null, + }, + inelasticInteractionRateAtStart: { + type: Sequelize.FLOAT, + allowNull: true, + default: null, + }, + inelasticInteractionRateAtMid: { + type: Sequelize.FLOAT, + allowNull: true, + default: null, + }, + inelasticInteractionRateAtEnd: { + type: Sequelize.FLOAT, + allowNull: true, + default: null, + }, }); Run.associate = (models) => { diff --git a/lib/database/models/simulationPassQcFlag.js b/lib/database/models/simulationPassQcFlag.js new file mode 100644 index 0000000000..b366ece369 --- /dev/null +++ b/lib/database/models/simulationPassQcFlag.js @@ -0,0 +1,38 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const Sequelize = require('sequelize'); + +module.exports = (sequelize) => { + const QcFlag = sequelize.define( + 'SimulationPassQcFlag', + { + simulationPassId: { + type: Sequelize.INTEGER, + primaryKey: true, + }, + qualityControlFlagId: { + type: Sequelize.INTEGER, + primaryKey: true, + }, + }, + { tableName: 'simulation_pass_quality_control_flag' }, + ); + + QcFlag.associate = (models) => { + QcFlag.belongsTo(models.QcFlag, { as: 'qcFlag', foreignKey: 'qualityControlFlagId', targetKey: 'id' }); + QcFlag.belongsTo(models.SimulationPass, { as: 'simulationPass' }); + }; + + return QcFlag; +}; diff --git a/lib/database/models/typedefs/SequelizeDataPassQcFlag.js b/lib/database/models/typedefs/SequelizeDataPassQcFlag.js new file mode 100644 index 0000000000..f9e4d70dcd --- /dev/null +++ b/lib/database/models/typedefs/SequelizeDataPassQcFlag.js @@ -0,0 +1,22 @@ +/** + * @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. + */ + +/** + * @typedef SequelizeDataPassQcFlag + * @property {number} dataPassId + * @property {number} qualityControlFlagId + * @property {number} createdAt + * @property {number} updatedAt + * @property {SequelizeQcFlag|null} qcFlag + * @property {SequelizeDataPass|null} dataPass + */ diff --git a/lib/database/models/typedefs/SequelizeLhcFillStatistics.js b/lib/database/models/typedefs/SequelizeLhcFillStatistics.js index e2fba25acc..17f6698b5d 100644 --- a/lib/database/models/typedefs/SequelizeLhcFillStatistics.js +++ b/lib/database/models/typedefs/SequelizeLhcFillStatistics.js @@ -15,6 +15,7 @@ * @typedef SequelizeLhcFillStatistics * * @property {number} fillNumber the fill number to which the statistics applies to + * @property {number} stableBeamsDuration the duration of the fill stable beams * @property {number} runsCoverage total duration covered by at least one run (in seconds) * @property {string} efficiency efficiency of the fill * @property {number} timeLossAtStart duration between the start of the fill and the start of the first run (in seconds) diff --git a/lib/database/models/typedefs/SequelizeQcFlag.js b/lib/database/models/typedefs/SequelizeQcFlag.js index b369261208..66544ac9dc 100644 --- a/lib/database/models/typedefs/SequelizeQcFlag.js +++ b/lib/database/models/typedefs/SequelizeQcFlag.js @@ -19,16 +19,12 @@ * @property {number} to * @property {string} comment * @property {number} createdById - * @property {SequelizeUser} createdBy * @property {number} flagTypeId - * @property {SequelizeQcType} flagType * @property {number} runNumber * @property {number} dplDetectorId - * @param {SequelizeDplDetector} dplDetector - * - * @param {SequelizeQcFlagVerification[]} verifications - * * @property {number} createdAt * @property {number} updatedAt + * @property {SequelizeQcFlagType|null} flagType + * @property {SequelizeUser|null} createdBy * */ diff --git a/lib/database/models/typedefs/SequelizeRun.js b/lib/database/models/typedefs/SequelizeRun.js index d0fdeba296..550531a532 100644 --- a/lib/database/models/typedefs/SequelizeRun.js +++ b/lib/database/models/typedefs/SequelizeRun.js @@ -65,6 +65,10 @@ * @property {string|number|null} tfFileSize * @property {string|null} otherFileCount * @property {string|number|null} otherFileSize + * @property {number|null} inelasticInteractionRateAvg + * @property {number|null} inelasticInteractionRateAtStart + * @property {number|null} inelasticInteractionRateAtMid + * @property {number|null} inelasticInteractionRateAtEnd * @property {string} createdAt * @property {string} updatedAt * @property {number|null} runDuration @@ -79,4 +83,5 @@ * @property {SequelizeLhcFill|null} lhcFill * @property {SequelizeRunType|null} runType * @property {SequelizeLog[]|null} logs + * */ diff --git a/lib/database/models/typedefs/SequelizeSimulationPassQcFlag.js b/lib/database/models/typedefs/SequelizeSimulationPassQcFlag.js new file mode 100644 index 0000000000..7170a9d5d6 --- /dev/null +++ b/lib/database/models/typedefs/SequelizeSimulationPassQcFlag.js @@ -0,0 +1,22 @@ +/** + * @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. + */ + +/** + * @typedef SequelizeSimulationPassQcFlag + * @property {number} simulationPassId + * @property {number} qcFlagId + * @property {number} createdAt + * @property {number} updatedAt + * @property {SequelizeQcFlag|null} qcFlag + * @property {SequelizeSimulationPass|null} simulationPass + */ diff --git a/lib/database/repositories/DataPassQcFlagRepository.js b/lib/database/repositories/DataPassQcFlagRepository.js new file mode 100644 index 0000000000..d6a38c0a7b --- /dev/null +++ b/lib/database/repositories/DataPassQcFlagRepository.js @@ -0,0 +1,29 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const { models: { DataPassQcFlag } } = require('..'); +const Repository = require('./Repository'); + +/** + * Sequelize implementation of the QcFlagTypeRepository. + */ +class DataPassQcFlagRepository extends Repository { + /** + * Creates a new `QcFlagTypeRepository` instance. + */ + constructor() { + super(DataPassQcFlag); + } +} + +module.exports = new DataPassQcFlagRepository(); diff --git a/lib/database/repositories/SimulationPassQcFlagRepository.js b/lib/database/repositories/SimulationPassQcFlagRepository.js new file mode 100644 index 0000000000..f64a98bbc9 --- /dev/null +++ b/lib/database/repositories/SimulationPassQcFlagRepository.js @@ -0,0 +1,29 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const { models: { SimulationPassQcFlag } } = require('..'); +const Repository = require('./Repository'); + +/** + * Sequelize implementation of the QcFlagTypeRepository. + */ +class SimulationPassQcFlagRepository extends Repository { + /** + * Creates a new `QcFlagTypeRepository` instance. + */ + constructor() { + super(SimulationPassQcFlag); + } +} + +module.exports = new SimulationPassQcFlagRepository(); diff --git a/lib/database/repositories/index.js b/lib/database/repositories/index.js index b72d2bc7c8..964b369456 100644 --- a/lib/database/repositories/index.js +++ b/lib/database/repositories/index.js @@ -12,6 +12,8 @@ */ const AttachmentRepository = require('./AttachmentRepository'); +const DataPassRepository = require('./DataPassRepository.js'); +const DataPassQcFlagRepository = require('./DataPassQcFlagRepository.js'); const DetectorRepository = require('./DetectorRepository'); const DplDetectorRepository = require('./dpl/DplDetectorRepository.js'); const DplProcessExecutionRepository = require('./dpl/DplProcessExecutionRepository.js'); @@ -24,30 +26,32 @@ const FlpRoleRepository = require('./FlpRoleRepository'); const HostRepository = require('./HostRepository.js'); const LhcFillRepository = require('./LhcFillRepository'); const LhcFillStatisticsRepository = require('./LhcFillStatisticsRepository.js'); +const LhcPeriodRepository = require('./LhcPeriodRepository'); +const LhcPeriodStatisticsRepository = require('./LhcPeriodStatisticsRepository'); const LogEnvironmentsRepository = require('./LogEnvironmentsRepository'); const LogLhcFillsRepository = require('./LogLhcFillsRepository'); const LogRepository = require('./LogRepository'); const LogRunsRepository = require('./LogRunsRepository'); const LogTagsRepository = require('./LogTagsRepository'); +const QcFlagRepository = require('./QcFlagRepository.js'); +const QcFlagTypeRepository = require('./QcFlagTypeRepository.js'); +const QcFlagVerificationRepository = require('./QcFlagVerificationRepository.js'); const ReasonTypeRepository = require('./ReasonTypeRepository'); const RunDetectorsRepository = require('./RunDetectorsRepository'); const RunRepository = require('./RunRepository'); const RunTagsRepository = require('./RunTagsRepository'); const RunTypeRepository = require('./RunTypeRepository'); +const SimulationPassRepository = require('./SimulationPassRepository.js'); +const SimulationPassQcFlagRepository = require('./SimulationPassQcFlagRepository.js'); const StableBeamRunsRepository = require('./StableBeamRunsRepository.js'); const SubsystemRepository = require('./SubsystemRepository'); const TagRepository = require('./TagRepository'); const UserRepository = require('./UserRepository'); -const LhcPeriodRepository = require('./LhcPeriodRepository'); -const LhcPeriodStatisticsRepository = require('./LhcPeriodStatisticsRepository'); -const DataPassRepository = require('./DataPassRepository.js'); -const SimulationPassRepository = require('./SimulationPassRepository.js'); -const QcFlagTypeRepository = require('./QcFlagTypeRepository.js'); -const QcFlagRepository = require('./QcFlagRepository.js'); -const QcFlagVerificationRepository = require('./QcFlagVerificationRepository.js'); module.exports = { AttachmentRepository, + DataPassRepository, + DataPassQcFlagRepository, DetectorRepository, DplDetectorRepository, DplProcessExecutionRepository, @@ -60,25 +64,25 @@ module.exports = { HostRepository, LhcFillRepository, LhcFillStatisticsRepository, + LhcPeriodRepository, + LhcPeriodStatisticsRepository, LogEnvironmentsRepository, LogLhcFillsRepository, LogRepository, LogRunsRepository, LogTagsRepository, + QcFlagRepository, + QcFlagTypeRepository, + QcFlagVerificationRepository, ReasonTypeRepository, RunDetectorsRepository, RunRepository, RunTagsRepository, RunTypeRepository, + SimulationPassRepository, + SimulationPassQcFlagRepository, StableBeamRunsRepository, SubsystemRepository, TagRepository, UserRepository, - LhcPeriodRepository, - LhcPeriodStatisticsRepository, - DataPassRepository, - SimulationPassRepository, - QcFlagTypeRepository, - QcFlagRepository, - QcFlagVerificationRepository, }; diff --git a/lib/database/seeders/20200713103855-runs.js b/lib/database/seeders/20200713103855-runs.js index 066376b227..ab1df22841 100644 --- a/lib/database/seeders/20200713103855-runs.js +++ b/lib/database/seeders/20200713103855-runs.js @@ -2667,8 +2667,6 @@ module.exports = { run_number: 107, time_o2_start: '2019-08-08 13:00:00', time_o2_end: '2019-08-09 14:00:00', - time_trg_start: '2019-08-08 13:00:00', - time_trg_end: '2019-08-09 14:00:00', run_type_id: 12, run_quality: 'good', n_detectors: 15, @@ -2682,6 +2680,7 @@ module.exports = { fill_number: 1, epn_topology: 'a quite long topology, which will for sure require a balloon to be displayed properly', concatenated_detectors: 'ACO, CPV, CTP, EMC, FIT, HMP, ITS, MCH, MFT, MID, PHS, TOF, TPC, TRD, ZDC', + trigger_value: 'OFF', lhc_period_id: 2, lhc_beam_mode: 'UNSTABLE BEAMS', odc_topology_full_name: 'hash', diff --git a/lib/database/seeders/20220503120937-lhc-fills.js b/lib/database/seeders/20220503120937-lhc-fills.js index 9b87ac54b1..1037fc9832 100644 --- a/lib/database/seeders/20220503120937-lhc-fills.js +++ b/lib/database/seeders/20220503120937-lhc-fills.js @@ -65,7 +65,7 @@ module.exports = { stable_beams_start: '2019-08-08 11:00:00', stable_beams_end: '2019-08-08 23:00:00', stable_beams_duration: 60 * 60 * 12, - filling_scheme_name: 'schemename', + filling_scheme_name: 'Single_12b_8_1024_8_2018', created_at: new Date('2019-08-09 21:00:00'), updated_at: new Date('2019-08-09 21:00:00'), }, diff --git a/lib/domain/dtos/UpdateRunDto.js b/lib/domain/dtos/UpdateRunDto.js index a23a363e85..eb01fac3de 100644 --- a/lib/domain/dtos/UpdateRunDto.js +++ b/lib/domain/dtos/UpdateRunDto.js @@ -39,6 +39,11 @@ const BodyDto = Joi.object({ detectorsQualitiesChangeReason: Joi.string().optional(), calibrationStatus: Joi.string().valid(...RUN_CALIBRATION_STATUS).optional(), calibrationStatusChangeReason: Joi.string().optional(), + + inelasticInteractionRateAvg: Joi.number().optional().allow(null), + inelasticInteractionRateAtStart: Joi.number().optional().allow(null), + inelasticInteractionRateAtMid: Joi.number().optional().allow(null), + inelasticInteractionRateAtEnd: Joi.number().optional().allow(null), }); const QueryDto = Joi.object({ diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index bae66b3d2b..f7c6518273 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -13,7 +13,6 @@ const Joi = require('joi'); const { CustomJoi } = require('../CustomJoi.js'); const { RUN_DEFINITIONS } = require('../../../server/services/run/getRunDefinition.js'); -const { TagsFilterDto } = require('./TagsFilterDto.js'); const { FromToFilterDto } = require('./FromToFilterDto.js'); const { RUN_QUALITIES } = require('../../enums/RunQualities.js'); const { NumericalComparisonDto } = require('./NumericalComparisonDto.js'); @@ -35,7 +34,10 @@ exports.RunFilterDto = Joi.object({ calibrationStatuses: Joi.array().items(...RUN_CALIBRATION_STATUS), definitions: CustomJoi.stringArray().items(Joi.string().uppercase().trim().valid(...RUN_DEFINITIONS)), eorReason: EorReasonFilterDto, - tags: TagsFilterDto, + tags: Joi.object({ + values: CustomJoi.stringArray().items(Joi.string()).single().required(), + operation: Joi.string().valid('and', 'or', 'none-of').required(), + }), fillNumbers: Joi.string().trim(), o2start: FromToFilterDto, o2end: FromToFilterDto, diff --git a/lib/domain/entities/DataPassQcFlag.js b/lib/domain/entities/DataPassQcFlag.js new file mode 100644 index 0000000000..edda926544 --- /dev/null +++ b/lib/domain/entities/DataPassQcFlag.js @@ -0,0 +1,31 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * @typedef DataPassQcFlag + * @property {number} dataPassId + * @property {number} qcFlagId + * @property {number} from + * @property {number} to + * @property {string} comment + * @property {number} createdById + * @property {number} flagTypeId + * @property {number} runNumber + * @property {number} dplDetectorId + * @property {number} createdAt + * @property {number} updatedAt + * @property {DataPass|null} dataPass + * @property {User|null} createdBy + * @property {QcFlagType|null} flagType + * @property {QcFlagVerification[]} verifications + */ diff --git a/lib/domain/entities/qcFlag.js b/lib/domain/entities/QcFlag.js similarity index 53% rename from lib/domain/entities/qcFlag.js rename to lib/domain/entities/QcFlag.js index f8e0d9311e..075ed7dc02 100644 --- a/lib/domain/entities/qcFlag.js +++ b/lib/domain/entities/QcFlag.js @@ -14,22 +14,18 @@ /** * @typedef QcFlag * - * @param {number} id - * @param {number} from - * @param {number} to - * @param {string} comment - * @param {number} createdById - * @param {User} createdBy - * @param {number} flagTypeId - * @param {QcFlagType} flagType - * @param {number} runNumber - * @param {number} dataPassId - * @param {number} dplDetectorId - * @param {MinifiedDplDetector} dplDetector - * - * @param {QcFlagVerification[]} verifications - * - * @param {number} createdAt - * @param {number} updatedAt - * + * @property {number} id + * @property {number} from + * @property {number} to + * @property {string} comment + * @property {number} createdById + * @property {number} flagTypeId + * @property {number} runNumber + * @property {number} dataPassId + * @property {number} dplDetectorId + * @property {number} createdAt + * @property {number} updatedAt + * @property {User|null} createdBy + * @property {QcFlagType|null} flagType + * @property {QcFlagVerification[]} verifications */ diff --git a/lib/domain/entities/qcFlagType.js b/lib/domain/entities/QcFlagType.js similarity index 94% rename from lib/domain/entities/qcFlagType.js rename to lib/domain/entities/QcFlagType.js index d5bf80107b..7746bbd33e 100644 --- a/lib/domain/entities/qcFlagType.js +++ b/lib/domain/entities/QcFlagType.js @@ -19,14 +19,13 @@ * @property {string} method * @property {boolean} bad * @property {string} color as hex - * * @property {boolean} archived * @property {number} archivedAt - * - * @property {number} createdAt * @property {number} createdById - * @property {User} createdBy - * @property {number} updatedAt * @property {number} lastUpdatedById + * @property {number} createdAt + * @property {number} updatedAt + * @property {User} createdBy + * @property {QcFlagVerification[]} verifications * @property {User} lastUpdatedBy */ diff --git a/lib/domain/entities/Run.js b/lib/domain/entities/Run.js index e4ff107e63..bcbec6c4f8 100644 --- a/lib/domain/entities/Run.js +++ b/lib/domain/entities/Run.js @@ -63,6 +63,11 @@ * @property {string|null} tfFileSize * @property {string|null} otherFileCount * @property {string|null} otherFileSize + * @property {number|null} muInelasticInteractionRate + * @property {number|null} inelasticInteractionRateAvg + * @property {number|null} inelasticInteractionRateAtStart + * @property {number|null} inelasticInteractionRateAtMid + * @property {number|null} inelasticInteractionRateAtEnd * @property {Date} updatedAt * @property {Date} createdAt * @property {number|null} runDuration diff --git a/lib/domain/entities/SimulationPassQcFlag.js b/lib/domain/entities/SimulationPassQcFlag.js new file mode 100644 index 0000000000..33e8d73201 --- /dev/null +++ b/lib/domain/entities/SimulationPassQcFlag.js @@ -0,0 +1,31 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * @typedef SimulationPassQcFlag + * @property {number} simulationPassId + * @property {number} qcFlagId + * @property {number} from + * @property {number} to + * @property {string} comment + * @property {number} createdById + * @property {number} flagTypeId + * @property {number} runNumber + * @property {number} dplDetectorId + * @property {number} createdAt + * @property {number} updatedAt + * @property {SimulationPass|null} simulationPass + * @property {User|null} createdBy + * @property {QcFlagType|null} flagType + * @property {QcFlagVerification[]} verifications + */ diff --git a/lib/domain/entities/statistics/LhcFillStatistics.js b/lib/domain/entities/statistics/LhcFillStatistics.js index 827c9eeeee..0e575a919e 100644 --- a/lib/domain/entities/statistics/LhcFillStatistics.js +++ b/lib/domain/entities/statistics/LhcFillStatistics.js @@ -15,6 +15,7 @@ * @typedef LhcFillStatistics * * @property {number} fillNumber the fill number to which the statistics applies to + * @property {number} stableBeamsDuration the total stable beam duration of the fill * @property {number} runsCoverage total duration covered by at least one run (in ms) * @property {number} efficiency efficiency of the fill * @property {number} timeLossAtStart duration between the start of the fill and the start of the first run (in ms) diff --git a/lib/public/app.css b/lib/public/app.css index d47b8b13f2..dcc771380a 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -71,6 +71,7 @@ html, body { .w-85 { width: 85%; } .w-90 { width: 90%; } .w-95 { width: 95%; } +.mw-l { max-width: var(--ui-component-large); } .mw-100 { max-width: 100%; } .mh-100 { max-height: 100%; } .w-30rem { width: 30rem; } diff --git a/lib/public/components/Detail/detailsList.js b/lib/public/components/Detail/detailsList.js index 54d7639927..34667d3777 100644 --- a/lib/public/components/Detail/detailsList.js +++ b/lib/public/components/Detail/detailsList.js @@ -52,7 +52,7 @@ export const detailsList = (fields, subject, configuration) => { Object.entries(fields).map(([key, { name, format, visible }]) => { const fieldValue = subject[key]; - if (key.startsWith('_') || (typeof visible === 'function' ? visible(fieldValue) : visible) === false) { + if (key.startsWith('_') || (typeof visible === 'function' ? visible(fieldValue, subject) : visible) === false) { // The key is configuration, not a real property return null; } @@ -66,7 +66,7 @@ export const detailsList = (fields, subject, configuration) => { return h( `.flex-row.justify-between.g3#${selector}-${key}`, - h('strong', `${name}:`), + h('strong', [name, ':']), content, ); }), diff --git a/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js b/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js index 5b7253b55f..30affd6398 100644 --- a/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js @@ -11,7 +11,7 @@ * or submit itself to any jurisdiction. */ import { Observable } from '/js/src/index.js'; -import { CombinationOperatorChoiceModel, NoneCombinationOperator } from '../common/CombinationOperatorChoiceModel.js'; +import { CombinationOperator, CombinationOperatorChoiceModel } from '../common/CombinationOperatorChoiceModel.js'; import { DetectorSelectionDropdownModel } from '../../detector/DetectorSelectionDropdownModel.js'; /** @@ -26,7 +26,11 @@ export class DetectorsFilterModel extends Observable { this._dropdownModel = new DetectorSelectionDropdownModel(); this._dropdownModel.bubbleTo(this); - this._combinationOperatorModel = new CombinationOperatorChoiceModel(true); + this._combinationOperatorModel = new CombinationOperatorChoiceModel([ + CombinationOperator.AND, + CombinationOperator.OR, + CombinationOperator.NONE, + ]); this._combinationOperatorModel.bubbleTo(this); } @@ -45,7 +49,7 @@ export class DetectorsFilterModel extends Observable { * @return {boolean} true if the current combination operator is none */ isNone() { - return this.combinationOperator === NoneCombinationOperator.value; + return this.combinationOperator === CombinationOperator.NONE.value; } /** diff --git a/lib/public/components/Filters/RunsFilter/triggerValue.js b/lib/public/components/Filters/RunsFilter/triggerValueFilter.js similarity index 83% rename from lib/public/components/Filters/RunsFilter/triggerValue.js rename to lib/public/components/Filters/RunsFilter/triggerValueFilter.js index fd0217a7aa..5addab02fe 100644 --- a/lib/public/components/Filters/RunsFilter/triggerValue.js +++ b/lib/public/components/Filters/RunsFilter/triggerValueFilter.js @@ -1,13 +1,12 @@ import { checkboxFilter } from '../common/filters/checkboxFilter.js'; - -const TRIGGER_VALUES = ['OFF', 'LTU', 'CTP']; +import { TRIGGER_VALUES } from '../../../domain/enums/TriggerValue.js'; /** * Returns a panel to be used by user to filter runs by trigger value * @param {RunsOverviewModel} runModel The global model object * @return {vnode} Multiple checkboxes for a user to select the values to be filtered. */ -const triggerValue = (runModel) => checkboxFilter( +export const triggerValueFilter = (runModel) => checkboxFilter( 'triggerValue', TRIGGER_VALUES, (value) => runModel.triggerValuesFilters.has(value), @@ -20,5 +19,3 @@ const triggerValue = (runModel) => checkboxFilter( runModel.triggerValuesFilters = Array.from(runModel.triggerValuesFilters); }, ); - -export default triggerValue; diff --git a/lib/public/components/Filters/common/CombinationOperatorChoiceModel.js b/lib/public/components/Filters/common/CombinationOperatorChoiceModel.js index 213cf30e13..e7489b012e 100644 --- a/lib/public/components/Filters/common/CombinationOperatorChoiceModel.js +++ b/lib/public/components/Filters/common/CombinationOperatorChoiceModel.js @@ -20,23 +20,11 @@ import { SelectionModel } from '../../common/selection/SelectionModel.js'; export const CombinationOperator = { OR: { label: 'OR', value: 'or' }, AND: { label: 'AND', value: 'and' }, + NONE: { label: 'NONE', value: 'none' }, + NONE_OF: { label: 'NONE-OF', value: 'none-of' }, }; -export const NoneCombinationOperator = { label: 'NONE', value: 'none' }; - -/** - * Returns the list of available combination operator - * - * @param {boolean} allowNone if true, a "NONE" combination operator will be added - * @return {SelectionOption[]} the available options - */ -const getAvailableCombinations = (allowNone) => { - const ret = [...Object.values(CombinationOperator)]; - if (allowNone) { - ret.push(NoneCombinationOperator); - } - return ret; -}; +const DEFAULT_OPERATORS = [CombinationOperator.AND, CombinationOperator.OR]; /** * Model storing the state of a combination operator choice @@ -45,12 +33,12 @@ export class CombinationOperatorChoiceModel extends SelectionModel { /** * Constructor * - * @param {boolean} allowNone if true, a "NONE" option will be added to the available combination options + * @param {SelectionOption[]} [operators] the list of possible operators */ - constructor(allowNone = false) { + constructor(operators) { super({ - availableOptions: getAvailableCombinations(allowNone), - defaultSelection: [CombinationOperator.AND], + availableOptions: operators?.length ? operators : DEFAULT_OPERATORS, + defaultSelection: operators?.length ? operators.slice(0, 1) : [CombinationOperator.AND], multiple: false, allowEmpty: false, }); diff --git a/lib/public/components/Filters/common/TagFilterModel.js b/lib/public/components/Filters/common/TagFilterModel.js index 3317357087..cd929076c3 100644 --- a/lib/public/components/Filters/common/TagFilterModel.js +++ b/lib/public/components/Filters/common/TagFilterModel.js @@ -21,14 +21,15 @@ export class TagFilterModel extends Observable { /** * Constructor * + * @param {SelectionOption} [operators] optionally the list of available operators for the filter * @constructor */ - constructor() { + constructor(operators) { super(); this._selectionModel = new TagSelectionDropdownModel({ includeArchived: true }); this._selectionModel.bubbleTo(this); - this._combinationOperatorModel = new CombinationOperatorChoiceModel(); + this._combinationOperatorModel = new CombinationOperatorChoiceModel(operators); this._combinationOperatorModel.bubbleTo(this); } diff --git a/lib/public/components/NavBar/index.js b/lib/public/components/NavBar/index.js index 030325a39d..0ffa914a39 100644 --- a/lib/public/components/NavBar/index.js +++ b/lib/public/components/NavBar/index.js @@ -144,15 +144,9 @@ function navBar(model) { pageTab('home', 'Home'), pageTab('log-overview', 'Log Entries'), pageTab('env-overview', 'Environments'), - dropdownSubMenu('LHC', model.isDropdownMenuOpened('LHC'), () => model.toggleDropdownMenu('LHC'), [ - pageMenuLink('LHC Fills', 'lhc-fill-overview'), - pageMenuLink('LHC Periods', 'lhc-period-overview'), - ], { - menuAttributes: { style: 'width: max-content;' }, - isSelected: ['lhc-fill-overview', 'lhc-period-overview'].includes(currentPage), - }), - + pageTab('lhc-fill-overview', 'LHC Fills'), pageTab('run-overview', 'Runs'), + pageTab('lhc-period-overview', 'RCT'), dropdownSubMenu('Overview', model.isDropdownMenuOpened('overview'), () => model.toggleDropdownMenu('overview'), [ pageMenuLink('Tag Overview', 'tag-overview'), pageMenuLink('Subsystem Overview', 'subsystem-overview'), @@ -161,7 +155,6 @@ function navBar(model) { menuAttributes: { style: 'width: max-content;' }, isSelected: ['tag-overview', 'subsystem-overview'].includes(currentPage), }), - pageTab('about-overview', 'About'), ]), diff --git a/lib/public/components/common/table/table.js b/lib/public/components/common/table/table.js index 64d5e7fd69..aaddf87048 100644 --- a/lib/public/components/common/table/table.js +++ b/lib/public/components/common/table/table.js @@ -142,7 +142,11 @@ export const table = ( }, [ headers(displayedColumns, models), - remoteDataTableBody(remoteData, (payload) => rows(payload, idKey, displayedColumns, rowsConfiguration), columnsConfiguration.length), + remoteDataTableBody( + remoteData, + (payload) => rows(payload, idKey, displayedColumns, rowsConfiguration), + displayedColumns.length, + ), ], ); }; diff --git a/lib/public/domain/enums/TriggerValue.js b/lib/public/domain/enums/TriggerValue.js new file mode 100644 index 0000000000..c4749b2808 --- /dev/null +++ b/lib/public/domain/enums/TriggerValue.js @@ -0,0 +1,20 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +export const TriggerValue = Object.freeze({ + Off: 'OFF', + CTP: 'CTP', + LTU: 'LTU', +}); + +export const TRIGGER_VALUES = Object.values(TriggerValue); diff --git a/lib/public/utilities/formatting/formatFloat.js b/lib/public/utilities/formatting/formatFloat.js new file mode 100644 index 0000000000..d8cecf5ef9 --- /dev/null +++ b/lib/public/utilities/formatting/formatFloat.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. + */ + +/** + * Format float number + * @param {number} value number to be formatted + * @param {number} [options.precision = 3] precision of displayed value + * @return {string} formatted number + */ +export const formatFloat = (value, { precision = 3 } = {}) => + value !== null && value !== undefined ? parseFloat(value).toLocaleString(undefined, { maximumFractionDigits: precision }) : '-'; diff --git a/lib/public/views/QcFlagTypes/Create/QcFlagTypeCreationPage.js b/lib/public/views/QcFlagTypes/Create/QcFlagTypeCreationPage.js index 3a743876a3..e802c3eaea 100644 --- a/lib/public/views/QcFlagTypes/Create/QcFlagTypeCreationPage.js +++ b/lib/public/views/QcFlagTypes/Create/QcFlagTypeCreationPage.js @@ -26,7 +26,7 @@ import { LabelPanelHeaderComponent } from '../../../components/common/panel/Labe const qcFlagTypeCreationComponent = (qcFlagTypeCreationModel) => { const { name, method, color, bad } = qcFlagTypeCreationModel.formData; - const panel = [ + return h('.flex-column.g3.mw-l', [ h(PanelComponent, [ h(LabelPanelHeaderComponent, { for: 'name' }, 'Name'), h('input#name.form-control.mb2', { @@ -75,12 +75,9 @@ const qcFlagTypeCreationComponent = (qcFlagTypeCreationModel) => { h(PanelComponent, [ h(LabelPanelHeaderComponent, { for: 'color' }, 'Color'), - h('label.f5.mb2', 'Please fill in a hexadecimal color after the #'), colorInputComponent(color, (color) => qcFlagTypeCreationModel.patchFormData({ color })), ]), - ]; - - return h('.w-25', panel); + ]); }; /** diff --git a/lib/public/views/QcFlags/ForDataPass/QcFlagsForDataPassOverviewModel.js b/lib/public/views/QcFlags/ForDataPass/QcFlagsForDataPassOverviewModel.js index eb1f2d00b9..6ff7e56bd8 100644 --- a/lib/public/views/QcFlags/ForDataPass/QcFlagsForDataPassOverviewModel.js +++ b/lib/public/views/QcFlags/ForDataPass/QcFlagsForDataPassOverviewModel.js @@ -37,12 +37,11 @@ export class QcFlagsForDataPassOverviewModel extends QcFlagsOverviewModel { * @inheritdoc */ getRootEndpoint() { - const params = { - filter: { - dataPassIds: [this._dataPassId], - }, - }; - return buildUrl(super.getRootEndpoint(), params); + return buildUrl('/api/qcFlags/perDataPass', { + dataPassId: this._dataPassId, + runNumber: this._runNumber, + dplDetectorId: this._dplDetectorId, + }); } // eslint-disable-next-line valid-jsdoc @@ -63,12 +62,6 @@ export class QcFlagsForDataPassOverviewModel extends QcFlagsOverviewModel { try { const { data: [dataPass] } = await getRemoteData(`/api/dataPasses/?filter[ids][]=${this._dataPassId}`); this._dataPass$.setCurrent(RemoteData.success(dataPass)); - - /** - * Workadround to have dataPassId available in activeColumns - */ - this._observableItems - .setCurrent(this.items.apply({ Success: (qcFlags) => qcFlags.map((qcFlag) => ({ ...qcFlag, dataPassId: dataPass.id })) })); } catch (error) { this._dataPass$.setCurrent(RemoteData.failure(error)); } diff --git a/lib/public/views/QcFlags/ForDataPass/QcFlagsForDataPassOverviewPage.js b/lib/public/views/QcFlags/ForDataPass/QcFlagsForDataPassOverviewPage.js index 07005e7962..ae8d92625f 100644 --- a/lib/public/views/QcFlags/ForDataPass/QcFlagsForDataPassOverviewPage.js +++ b/lib/public/views/QcFlags/ForDataPass/QcFlagsForDataPassOverviewPage.js @@ -33,7 +33,6 @@ const PAGE_USED_HEIGHT = 215; */ export const QcFlagsForDataPassOverviewPage = ({ qcFlags: { forDataPassOverviewModel: qcFlagsForDataPassOverviewModel } }) => { const { - run: remoteRun, dplDetector: remoteDplDetector, dataPass: remoteDataPass, runNumber, @@ -47,14 +46,14 @@ export const QcFlagsForDataPassOverviewPage = ({ qcFlags: { forDataPassOverviewM )); const activeColumns = { - id: { + qcFlagId: { name: 'Id', visible: true, - format: (id, { dataPassId, dplDetectorId, runNumber }) => + format: (qcFlagId, { dataPassId, dplDetectorId, runNumber }) => frontLink( - h('.btn.btn-primary.white', id), + h('.btn.btn-primary.white', qcFlagId), 'qc-flag-details-for-data-pass', - { id, dataPassId, runNumber, dplDetectorId }, + { id: qcFlagId, dataPassId, runNumber, dplDetectorId }, ), classes: 'w-5', }, @@ -68,51 +67,47 @@ export const QcFlagsForDataPassOverviewPage = ({ qcFlags: { forDataPassOverviewM } = qcFlagsForDataPassOverviewModel; const commonTitle = h('h2', 'QC'); - return h('', { - onremove: () => qcFlagsForDataPassOverviewModel.reset(), - }, [ - h('.flex-row.justify-between.items-center', [ - h('.flex-row.g1.items-center', [ - remoteDataPass.match({ - Success: (dataPass) => remoteRun.match({ - Success: (run) => remoteDplDetector.match({ - Success: (dplDetector) => breadcrumbs([ - commonTitle, - h('h2', frontLink(dataPass.name, 'runs-per-data-pass', { dataPassId: dataPass.id })), - h('h2', frontLink(run.runNumber, 'run-detail', { runNumber: run.runNumber })), - h('h2', dplDetector.name), - ]), - Failure: () => [commonTitle, tooltip(h('.f3', iconWarning()), 'Not able to load detector info')], - Loading: () => [commonTitle, h('', spinner({ size: 2, absolute: false }))], - NotAsked: () => [commonTitle, tooltip(h('.f3', iconWarning()), 'No detector data was asked for')], - }), - Failure: () => [commonTitle, tooltip(h('.f3', iconWarning()), 'Not able to load run info')], - Loading: () => [commonTitle, h('', spinner({ size: 2, absolute: false }))], - NotAsked: () => [commonTitle, tooltip(h('.f3', iconWarning()), 'No run data was asked for')], + return h( + '', + { onremove: () => qcFlagsForDataPassOverviewModel.reset() }, + [ + h('.flex-row.justify-between.items-center', [ + h('.flex-row.g1.items-center', breadcrumbs([ + commonTitle, + remoteDataPass.match({ + Success: (dataPass) => h('h2', frontLink(dataPass.name, 'runs-per-data-pass', { dataPassId })), + Failure: () => tooltip(h('.f3', iconWarning()), 'Not able to load data pass info'), + Loading: () => h('', spinner({ size: 2, absolute: false })), + NotAsked: () => tooltip(h('.f3', iconWarning()), 'No data pass data was asked for'), }), - Failure: () => [commonTitle, tooltip(h('.f3', iconWarning()), 'Not able to load data pass info')], - Loading: () => [commonTitle, h('', spinner({ size: 2, absolute: false }))], - NotAsked: () => [commonTitle, tooltip(h('.f3', iconWarning()), 'No data pass data was asked for')], - }), + h('h2', frontLink(runNumber, 'run-detail', { runNumber })), + remoteDplDetector.match({ + Success: (dplDetector) => h('h2', dplDetector.name), + Failure: () => tooltip(h('.f3', iconWarning()), 'Not able to load detector info'), + Loading: () => h('', spinner({ size: 2, absolute: false })), + NotAsked: () => tooltip(h('.f3', iconWarning()), 'No detector data was asked for'), + }), + ])), + frontLink( + h('.flex-row.items-center.g1', [h('small', iconPlus()), 'QC']), + 'qc-flag-creation-for-data-pass', + { runNumber, dataPassId, dplDetectorId }, + { + class: 'btn btn-primary', + title: 'Create a QC flag', + }, + ), + ]), + h('.w-100.flex-column', [ + table( + qcFlags, + activeColumns, + { classes: '.table-sm' }, + null, + { sort: sortModel }, + ), + paginationComponent(paginationModel), ]), - frontLink(h('.flex-row.items-center.g1', [h('small', iconPlus()), 'QC']), 'qc-flag-creation-for-data-pass', { - runNumber, - dataPassId, - dplDetectorId, - }, { - class: 'btn btn-primary', - title: 'Create a QC flag', - }), - ]), - h('.w-100.flex-column', [ - table( - qcFlags, - activeColumns, - { classes: '.table-sm' }, - null, - { sort: sortModel }, - ), - paginationComponent(paginationModel), - ]), - ]); + ], + ); }; diff --git a/lib/public/views/QcFlags/ForSimulationPass/QcFlagsForSimulationPassOverviewModel.js b/lib/public/views/QcFlags/ForSimulationPass/QcFlagsForSimulationPassOverviewModel.js index 2c18218d93..08815067cc 100644 --- a/lib/public/views/QcFlags/ForSimulationPass/QcFlagsForSimulationPassOverviewModel.js +++ b/lib/public/views/QcFlags/ForSimulationPass/QcFlagsForSimulationPassOverviewModel.js @@ -37,13 +37,11 @@ export class QcFlagsForSimulationPassOverviewModel extends QcFlagsOverviewModel * @inheritdoc */ getRootEndpoint() { - const params = { - filter: { - simulationPassIds: [this._simulationPassId], - }, - }; - - return buildUrl(super.getRootEndpoint(), params); + return buildUrl('/api/qcFlags/perSimulationPass', { + simulationPassId: this._simulationPassId, + runNumber: this._runNumber, + dplDetectorId: this._dplDetectorId, + }); } // eslint-disable-next-line valid-jsdoc @@ -65,14 +63,6 @@ export class QcFlagsForSimulationPassOverviewModel extends QcFlagsOverviewModel try { const { data: simulationPass } = await getRemoteData(`/api/simulationPasses/${this._simulationPassId}`); this._simulationPass$.setCurrent(RemoteData.success(simulationPass)); - - /** - * Workadround to have simulationPassId available in activeColumns - */ - this._observableItems - .setCurrent(this.items.apply({ - Success: (qcFlags) => qcFlags.map((qcFlag) => ({ ...qcFlag, simulationPassId: simulationPass.id })), - })); } catch (error) { this._simulationPass$.setCurrent(RemoteData.failure(error)); } @@ -80,7 +70,7 @@ export class QcFlagsForSimulationPassOverviewModel extends QcFlagsOverviewModel /** * Set id of simulation pass which for QC flags should be fetched - * @param {number} simulationPassId siumulation pass id + * @param {number} simulationPassId simulation pass id */ set simulationPassId(simulationPassId) { this._simulationPassId = simulationPassId; diff --git a/lib/public/views/QcFlags/ForSimulationPass/QcFlagsForSimulationPassOverviewPage.js b/lib/public/views/QcFlags/ForSimulationPass/QcFlagsForSimulationPassOverviewPage.js index 30c594af0e..dbd9f0298d 100644 --- a/lib/public/views/QcFlags/ForSimulationPass/QcFlagsForSimulationPassOverviewPage.js +++ b/lib/public/views/QcFlags/ForSimulationPass/QcFlagsForSimulationPassOverviewPage.js @@ -36,7 +36,6 @@ export const QcFlagsForSimulationPassOverviewPage = ({ qcFlags: { forSimulationPassOverviewModel: qcFlagsForSimulationPassOverviewModel }, }) => { const { - run: remoteRun, dplDetector: remoteDplDetector, simulationPass: remoteSimulationPass, runNumber, @@ -50,14 +49,14 @@ export const QcFlagsForSimulationPassOverviewPage = ({ )); const activeColumns = { - id: { + qcFlagId: { name: 'Id', visible: true, - format: (id, { simulationPassId, dplDetectorId, runNumber }) => + format: (qcFlagId, { simulationPassId, dplDetectorId, runNumber }) => frontLink( - h('.btn.btn-primary.white', id), + h('.btn.btn-primary.white', qcFlagId), 'qc-flag-details-for-simulation-pass', - { id, simulationPassId, runNumber, dplDetectorId }, + { id: qcFlagId, simulationPassId, runNumber, dplDetectorId }, ), classes: 'w-5', }, @@ -87,12 +86,7 @@ export const QcFlagsForSimulationPassOverviewPage = ({ Loading: () => h('', spinner({ size: 2, absolute: false })), NotAsked: () => tooltip(h('.f3', iconWarning()), 'No simulation pass data was asked for'), }), - remoteRun.match({ - Success: (run) => h('h2', frontLink(run.runNumber, 'run-detail', { runNumber: run.runNumber })), - Failure: () => tooltip(h('.f3', iconWarning()), 'Not able to load run info'), - Loading: () => h('', spinner({ size: 2, absolute: false })), - NotAsked: () => tooltip(h('.f3', iconWarning()), 'No run data was asked for'), - }), + h('h2', frontLink(runNumber, 'run-detail', { runNumber })), remoteDplDetector.match({ Success: (dplDetector) => h('h2', dplDetector.name), Failure: () => tooltip(h('.f3', iconWarning()), 'Not able to load detector info'), diff --git a/lib/public/views/QcFlags/Overview/QcFlagsOverviewModel.js b/lib/public/views/QcFlags/Overview/QcFlagsOverviewModel.js index d3de971a4b..7de6f34897 100644 --- a/lib/public/views/QcFlags/Overview/QcFlagsOverviewModel.js +++ b/lib/public/views/QcFlags/Overview/QcFlagsOverviewModel.js @@ -11,10 +11,8 @@ * or submit itself to any jurisdiction. */ -import { buildUrl } from '../../../utilities/fetch/buildUrl.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { RemoteData } from '/js/src/index.js'; -import { getRemoteData } from '../../../utilities/fetch/getRemoteData.js'; import { ObservableData } from '../../../utilities/ObservableData.js'; import { dplDetectorsProvider } from '../../../services/detectors/dplDetectorsProvider.js'; @@ -30,34 +28,17 @@ export class QcFlagsOverviewModel extends OverviewPageModel { */ constructor() { super(); - this._run$ = new ObservableData(RemoteData.notAsked()); - this._run$.bubbleTo(this); dplDetectorsProvider.physical$.observe(() => this._getDetector()); this._dplDetector$ = new ObservableData(RemoteData.notAsked()); this._dplDetector$.bubbleTo(this); } - // eslint-disable-next-line valid-jsdoc - /** - * @inheritdoc - */ - getRootEndpoint() { - const params = { - filter: { - runNumbers: [this._runNumber], - dplDetectorIds: [this._dplDetectorId], - }, - }; - return buildUrl('/api/qcFlags', params); - } - // eslint-disable-next-line valid-jsdoc /** * @inheritdoc */ async load() { this._getDetector(); - this._fetchRun(); super.load(); } @@ -80,25 +61,11 @@ export class QcFlagsOverviewModel extends OverviewPageModel { } /** - * Fetch run data - * @return {Promise} promise - */ - async _fetchRun() { - this._run$.setCurrent(RemoteData.loading()); - try { - const { data: run } = await getRemoteData(`/api/runs/${this._runNumber}`); - this._run$.setCurrent(RemoteData.success(run)); - } catch (error) { - this._run$.setCurrent(RemoteData.failure(error)); - } - } - - /** - * Set id of DPL detector which for QC flags should be fetched - * @param {number} dplDetectorId detector id + * Run number getter + * @return {number} current run number */ - set dplDetectorId(dplDetectorId) { - this._dplDetectorId = dplDetectorId; + get runNumber() { + return this._runNumber; } /** @@ -109,14 +76,6 @@ export class QcFlagsOverviewModel extends OverviewPageModel { this._runNumber = runNumber; } - /** - * Run getter - * @return {RemoteData} current run - */ - get run() { - return this._run$.getCurrent(); - } - /** * Detector getter * @return {RemoteData} current detector @@ -125,14 +84,6 @@ export class QcFlagsOverviewModel extends OverviewPageModel { return this._dplDetector$.getCurrent(); } - /** - * Run number getter - * @return {number} current run number - */ - get runNumber() { - return this._runNumber; - } - /** * DplDetector id getter * @return {number} current dpl detector id @@ -140,4 +91,12 @@ export class QcFlagsOverviewModel extends OverviewPageModel { get dplDetectorId() { return this._dplDetectorId; } + + /** + * Set id of DPL detector which for QC flags should be fetched + * @param {number} dplDetectorId detector id + */ + set dplDetectorId(dplDetectorId) { + this._dplDetectorId = dplDetectorId; + } } diff --git a/lib/public/views/QcFlags/details/forDataPass/QcFlagDetailsForDataPassPage.js b/lib/public/views/QcFlags/details/forDataPass/QcFlagDetailsForDataPassPage.js index 5c49c84c5a..ccaa622775 100644 --- a/lib/public/views/QcFlags/details/forDataPass/QcFlagDetailsForDataPassPage.js +++ b/lib/public/views/QcFlags/details/forDataPass/QcFlagDetailsForDataPassPage.js @@ -12,6 +12,7 @@ * or submit itself to any jurisdiction. */ +import { BkpRoles } from '../../../../domain/enums/BkpRoles.js'; import { detailsList } from '../../../../components/Detail/detailsList.js'; import errorAlert from '../../../../components/common/errorAlert.js'; import { deleteButton } from '../../../../components/common/form/deleteButton.js'; @@ -22,7 +23,7 @@ import { PanelComponent } from '../../../../components/common/panel/PanelCompone import { tooltip } from '../../../../components/common/popover/tooltip.js'; import spinner from '../../../../components/common/spinner.js'; import { qcFlagDetailsConfiguration } from '../qcFlagDetailsConfiguration.js'; -import { h, iconWarning } from '/js/src/index.js'; +import { h, iconWarning, sessionService } from '/js/src/index.js'; /** * Render QC flag details for data pass page @@ -40,7 +41,7 @@ export const QcFlagDetailsForDataPassPage = ({ qcFlags: { detailsForDataPassMode return h('', [ h('.flex-row.justify-between.items-center', [ h('h2', 'QC Flag Details'), - h('.pv3', deleteResult.match({ + sessionService.hasAccess(BkpRoles.ADMIN) && h('.pv3', deleteResult.match({ Loading: () => deleteButton(null, 'Processing...'), Success: () => deleteButton(null, 'Processed!'), Failure: () => deleteButton(() => detailsForDataPassModel.delete()), diff --git a/lib/public/views/QcFlags/details/forSimulationPass/QcFlagDetailsForSimulationPassPage.js b/lib/public/views/QcFlags/details/forSimulationPass/QcFlagDetailsForSimulationPassPage.js index 302d41e005..4bc121b3a1 100644 --- a/lib/public/views/QcFlags/details/forSimulationPass/QcFlagDetailsForSimulationPassPage.js +++ b/lib/public/views/QcFlags/details/forSimulationPass/QcFlagDetailsForSimulationPassPage.js @@ -21,8 +21,9 @@ import { LabelPanelHeaderComponent } from '../../../../components/common/panel/L import { PanelComponent } from '../../../../components/common/panel/PanelComponent.js'; import { tooltip } from '../../../../components/common/popover/tooltip.js'; import spinner from '../../../../components/common/spinner.js'; +import { BkpRoles } from '../../../../domain/enums/BkpRoles.js'; import { qcFlagDetailsConfiguration } from '../qcFlagDetailsConfiguration.js'; -import { h, iconWarning } from '/js/src/index.js'; +import { h, iconWarning, sessionService } from '/js/src/index.js'; /** * Render QC flag details for simulation pass page @@ -40,7 +41,7 @@ export const QcFlagDetailsForSimulationPassPage = ({ qcFlags: { detailsForSimula return h('', [ h('.flex-row.justify-between.items-center', [ h('h2', 'QC Flag Details'), - h('.pv3', deleteResult.match({ + sessionService.hasAccess(BkpRoles.ADMIN) && h('.pv3', deleteResult.match({ Loading: () => deleteButton(null, 'Processing...'), Success: () => deleteButton(null, 'Processed!'), Failure: () => deleteButton(() => detailsForSimulationPassModel.delete()), diff --git a/lib/public/views/Runs/ActiveColumns/inelasticInteractionRateActiveColumnsForPbPb.js b/lib/public/views/Runs/ActiveColumns/inelasticInteractionRateActiveColumnsForPbPb.js new file mode 100644 index 0000000000..6d7131daed --- /dev/null +++ b/lib/public/views/Runs/ActiveColumns/inelasticInteractionRateActiveColumnsForPbPb.js @@ -0,0 +1,65 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { h } from '/js/src/index.js'; +import { formatFloat } from '../../../utilities/formatting/formatFloat.js'; + +/** + * Active columns configuration for inelastic interaction rate values for PbPb runs + */ +export const inelasticInteractionRateActiveColumnsForPbPb = { + inelasticInteractionRateAvg: { + name: h('.flex-wrap', [h('', ['INEL', h('sub', 'avg')]), '(Hz)']), + format: formatFloat, + visible: true, + classes: 'f6', + profiles: [ + 'runsPerLhcPeriod', + 'runsPerDataPass', + 'runsPerSimulationPass', + ], + }, + inelasticInteractionRateAtStart: { + name: h('.flex-wrap', [h('', ['INEL', h('sub', 'start')]), '(Hz)']), + format: formatFloat, + visible: true, + classes: 'f6', + profiles: [ + 'runsPerLhcPeriod', + 'runsPerDataPass', + 'runsPerSimulationPass', + ], + }, + inelasticInteractionRateAtMid: { + name: h('.flex-wrap', [h('', ['INEL', h('sub', 'mid')]), '(Hz)']), + format: formatFloat, + visible: true, + classes: 'f6', + profiles: [ + 'runsPerLhcPeriod', + 'runsPerDataPass', + 'runsPerSimulationPass', + ], + }, + inelasticInteractionRateAtEnd: { + name: h('.flex-wrap', [h('', ['INEL', h('sub', 'end')]), '(Hz)']), + format: formatFloat, + visible: true, + classes: 'f6', + profiles: [ + 'runsPerLhcPeriod', + 'runsPerDataPass', + 'runsPerSimulationPass', + ], + }, +}; diff --git a/lib/public/views/Runs/ActiveColumns/inelasticInteractionRateActiveColumnsForProtonProton.js b/lib/public/views/Runs/ActiveColumns/inelasticInteractionRateActiveColumnsForProtonProton.js new file mode 100644 index 0000000000..9091812748 --- /dev/null +++ b/lib/public/views/Runs/ActiveColumns/inelasticInteractionRateActiveColumnsForProtonProton.js @@ -0,0 +1,45 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { h } from '/js/src/index.js'; +import { formatFloat } from '../../../utilities/formatting/formatFloat.js'; + +const GREEK_LOWER_MU_CHAR = '\u03BC'; + +/** + * Active columns configuration for inelastic interaction rate values for proton-proton runs + */ +export const inelasticInteractionRateActiveColumnsForProtonProton = { + muInelasticInteractionRate: { + name: `${GREEK_LOWER_MU_CHAR}(INEL)`, + format: formatFloat, + visible: true, + classes: 'f6', + profiles: [ + 'runsPerLhcPeriod', + 'runsPerDataPass', + 'runsPerSimulationPass', + ], + }, + inelasticInteractionRateAvg: { + name: h('.flex-wrap', [h('', ['INEL', h('sub', 'avg')]), '(Hz)']), + format: formatFloat, + visible: true, + classes: 'f6', + profiles: [ + 'runsPerLhcPeriod', + 'runsPerDataPass', + 'runsPerSimulationPass', + ], + }, +}; diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index ffa9fc89e5..aba953c908 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -31,7 +31,7 @@ import { displayRunDuration } from '../format/displayRunDuration.js'; import fillNumbersFilter from '../../../components/Filters/RunsFilter/fillNumbers.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; import nEpnsFilter from '../../../components/Filters/RunsFilter/nEpns.js'; -import triggerValueFilter from '../../../components/Filters/RunsFilter/triggerValue.js'; +import { triggerValueFilter } from '../../../components/Filters/RunsFilter/triggerValueFilter.js'; import lhcPeriodsFilter from '../../../components/Filters/RunsFilter/lhcPeriod.js'; import { formatRunType } from '../../../utilities/formatting/formatRunType.js'; import definitionFilter from '../../../components/Filters/RunsFilter/definitionFilter.js'; diff --git a/lib/public/views/Runs/Details/RunPatch.js b/lib/public/views/Runs/Details/RunPatch.js index d6c694d9d7..3b000b8223 100644 --- a/lib/public/views/Runs/Details/RunPatch.js +++ b/lib/public/views/Runs/Details/RunPatch.js @@ -91,6 +91,19 @@ export class RunPatch extends Observable { ret.calibrationStatusChangeReason = this._calibrationStatusChangeReason; } + if (this.formData.inelasticInteractionRateAvg !== this._run.inelasticInteractionRateAvg) { + ret.inelasticInteractionRateAvg = this.formData.inelasticInteractionRateAvg; + } + if (this.formData.inelasticInteractionRateAtStart !== this._run.inelasticInteractionRateAtStart) { + ret.inelasticInteractionRateAtStart = this.formData.inelasticInteractionRateAtStart; + } + if (this.formData.inelasticInteractionRateAtMid !== this._run.inelasticInteractionRateAtMid) { + ret.inelasticInteractionRateAtMid = this.formData.inelasticInteractionRateAtMid; + } + if (this.formData.inelasticInteractionRateAtEnd !== this._run.inelasticInteractionRateAtEnd) { + ret.inelasticInteractionRateAtEnd = this.formData.inelasticInteractionRateAtEnd; + } + return ret; } @@ -100,12 +113,29 @@ export class RunPatch extends Observable { * @return {void} */ reset() { - const { calibrationStatus, eorReasons = [], tags = [], detectorsQualities = [], runQuality } = this._run || {}; + const { + calibrationStatus, + eorReasons = [], + tags = [], + detectorsQualities = [], + runQuality, + inelasticInteractionRateAvg, + inelasticInteractionRateAtStart, + inelasticInteractionRateAtMid, + inelasticInteractionRateAtEnd, + } = this._run || {}; this._runQuality = runQuality; this._eorReasons = eorReasons.map(({ id, description, reasonTypeId }) => ({ id, description, reasonTypeId })); this._tags = tags.map(({ text }) => text); + this.formData = { + inelasticInteractionRateAvg, + inelasticInteractionRateAtStart, + inelasticInteractionRateAtMid, + inelasticInteractionRateAtEnd, + }; + /** * Stores, for each detector, its current quality * @type {Map} @@ -124,6 +154,19 @@ export class RunPatch extends Observable { this._detectorsQualitiesPatches = new Map(); this._calibrationStatus = calibrationStatus; + + this.notify(); + } + + /** + * Apply a patch on current form data + * + * @param {Partial} patch the patch to apply + * @return {void} + */ + patchFormData(patch) { + this.formData = { ...this.formData, ...patch }; + this.notify(); } /** diff --git a/lib/public/views/Runs/Details/runDetailsConfiguration.js b/lib/public/views/Runs/Details/runDetailsConfiguration.js index cf26679ce5..228a7e7d6f 100644 --- a/lib/public/views/Runs/Details/runDetailsConfiguration.js +++ b/lib/public/views/Runs/Details/runDetailsConfiguration.js @@ -24,6 +24,11 @@ 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'; + +const GREEK_LOWER_MU_ENCODING = '\u03BC'; /** * Returns the configuration to display a given run @@ -126,6 +131,55 @@ export const runDetailsConfiguration = (runDetailsModel) => ({ name: 'PDP Beam Type', visible: true, }, + muInelasticInteractionRate: { + name: `${GREEK_LOWER_MU_ENCODING}(INEL)`, + visible: (_, { pdpBeamType, definition }) => pdpBeamType === 'pp' && 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 === 'PbPb' && 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 === 'PbPb' && 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 === 'PbPb' && 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, diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index e1f4cf6951..e950757d46 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -21,6 +21,7 @@ import { EorReasonFilterModel } from '../../../components/Filters/RunsFilter/Eor 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'; /** * Model representing handlers for runs page @@ -35,7 +36,11 @@ export class RunsOverviewModel extends OverviewPageModel { constructor(model) { super(); - this._listingTagsFilterModel = new TagFilterModel(); + this._listingTagsFilterModel = new TagFilterModel([ + CombinationOperator.AND, + CombinationOperator.OR, + CombinationOperator.NONE_OF, + ]); this._listingTagsFilterModel.observe(() => this._applyFilters(true)); this._listingTagsFilterModel.visualChange$.bubbleTo(this); @@ -96,7 +101,7 @@ export class RunsOverviewModel extends OverviewPageModel { return [key, formatExport(value, selectedRun)]; }); return Object.fromEntries(formattedEntries); - }), + }); this.getSelectedExportType() === 'CSV' ? createCSVExport(runs, `${fileName}.csv`, 'text/csv;charset=utf-8;') : createJSONExport(runs, `${fileName}.json`, 'application/json'); diff --git a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js index ad5ade856c..1ac5b1a30d 100644 --- a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js +++ b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js @@ -21,6 +21,8 @@ import spinner from '../../../components/common/spinner.js'; import { tooltip } from '../../../components/common/popover/tooltip.js'; import { breadcrumbs } from '../../../components/common/navigation/breadcrumbs.js'; import { createRunDetectorsAsyncQcActiveColumns } from '../ActiveColumns/runDetectorsAsyncQcActiveColumns.js'; +import { inelasticInteractionRateActiveColumnsForPbPb } from '../ActiveColumns/inelasticInteractionRateActiveColumnsForPbPb.js'; +import { inelasticInteractionRateActiveColumnsForProtonProton } from '../ActiveColumns/inelasticInteractionRateActiveColumnsForProtonProton.js'; const TABLEROW_HEIGHT = 59; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -38,10 +40,22 @@ export const RunsPerDataPassOverviewPage = ({ runs: { perDataPassOverviewModel } PAGE_USED_HEIGHT, )); - const { items: runs, detectors: remoteDetectors, dataPass: remoteDataPass } = perDataPassOverviewModel; + const { items: remoteRuns, detectors: remoteDetectors, dataPass: remoteDataPass } = perDataPassOverviewModel; const activeColumns = { ...runsActiveColumns, + ...remoteRuns.match({ + Success: (runs) => runs.some((run) => run.pdpBeamType === 'PbPb') + ? inelasticInteractionRateActiveColumnsForPbPb + : {}, + Other: () => {}, + }), + ...remoteRuns.match({ + Success: (runs) => runs.some((run) => run.pdpBeamType === 'pp') + ? inelasticInteractionRateActiveColumnsForProtonProton + : {}, + Other: () => {}, + }), ...createRunDetectorsAsyncQcActiveColumns(remoteDetectors.match({ Success: (payload) => payload, Other: () => [], @@ -70,7 +84,7 @@ export const RunsPerDataPassOverviewPage = ({ runs: { perDataPassOverviewModel } exportRunsTriggerAndModal(perDataPassOverviewModel, modalModel), ]), h('.flex-column.w-100', [ - table(runs, activeColumns, null, { profile: 'runsPerDataPass' }), + table(remoteRuns, activeColumns, null, { profile: 'runsPerDataPass' }), paginationComponent(perDataPassOverviewModel.pagination), ]), ]); diff --git a/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js b/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js index dabae1ae87..53a5ed228f 100644 --- a/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js +++ b/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js @@ -18,6 +18,8 @@ import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplay import { exportRunsTriggerAndModal } from '../Overview/exportRunsTriggerAndModal.js'; import { runsActiveColumns } from '../ActiveColumns/runsActiveColumns.js'; import { createRunDetectorsSyncQcActiveColumns } from '../ActiveColumns/runDetectorsSyncQcActiveColumns.js'; +import { inelasticInteractionRateActiveColumnsForProtonProton } from '../ActiveColumns/inelasticInteractionRateActiveColumnsForProtonProton.js'; +import { inelasticInteractionRateActiveColumnsForPbPb } from '../ActiveColumns/inelasticInteractionRateActiveColumnsForPbPb.js'; const TABLEROW_HEIGHT = 59; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -35,10 +37,22 @@ export const RunsPerLhcPeriodOverviewPage = ({ runs: { perLhcPeriodOverviewModel PAGE_USED_HEIGHT, )); - const { items: runs, detectors: remoteDetectors, lhcPeriodName } = perLhcPeriodOverviewModel; + const { items: remoteRuns, detectors: remoteDetectors, lhcPeriodName } = perLhcPeriodOverviewModel; const activeColumns = { ...runsActiveColumns, + ...remoteRuns.match({ + Success: (runs) => runs.some((run) => run.pdpBeamType === 'PbPb') + ? inelasticInteractionRateActiveColumnsForPbPb + : {}, + Other: () => {}, + }), + ...remoteRuns.match({ + Success: (runs) => runs.some((run) => run.pdpBeamType === 'pp') + ? inelasticInteractionRateActiveColumnsForProtonProton + : {}, + Other: () => {}, + }), ...createRunDetectorsSyncQcActiveColumns(remoteDetectors.match({ Success: (payload) => payload, Other: () => [], @@ -51,7 +65,7 @@ export const RunsPerLhcPeriodOverviewPage = ({ runs: { perLhcPeriodOverviewModel exportRunsTriggerAndModal(perLhcPeriodOverviewModel, modalModel), ]), h('.flex-column.w-100', [ - table(remoteDetectors.isSuccess() ? runs : remoteDetectors, activeColumns, null, { profile: 'runsPerLhcPeriod' }), + table(remoteDetectors.isSuccess() ? remoteRuns : remoteDetectors, activeColumns, null, { profile: 'runsPerLhcPeriod' }), paginationComponent(perLhcPeriodOverviewModel.pagination), ]), ]); diff --git a/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewPage.js b/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewPage.js index bc7aeaaad8..a11032c810 100644 --- a/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewPage.js +++ b/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewPage.js @@ -21,6 +21,8 @@ import { breadcrumbs } from '../../../components/common/navigation/breadcrumbs.j import { tooltip } from '../../../components/common/popover/tooltip.js'; import spinner from '../../../components/common/spinner.js'; import { createRunDetectorsAsyncQcActiveColumns } from '../ActiveColumns/runDetectorsAsyncQcActiveColumns.js'; +import { inelasticInteractionRateActiveColumnsForPbPb } from '../ActiveColumns/inelasticInteractionRateActiveColumnsForPbPb.js'; +import { inelasticInteractionRateActiveColumnsForProtonProton } from '../ActiveColumns/inelasticInteractionRateActiveColumnsForProtonProton.js'; const TABLEROW_HEIGHT = 59; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -39,10 +41,22 @@ export const RunsPerSimulationPassOverviewPage = ({ PAGE_USED_HEIGHT, )); - const { items: runs, detectors: remoteDetectors, simulationPass: remoteSimulationPass } = runsPerSimulationPassOverviewModel; + const { items: remoteRuns, detectors: remoteDetectors, simulationPass: remoteSimulationPass } = runsPerSimulationPassOverviewModel; const activeColumns = { ...runsActiveColumns, + ...remoteRuns.match({ + Success: (runs) => runs.some((run) => run.pdpBeamType === 'PbPb') + ? inelasticInteractionRateActiveColumnsForPbPb + : {}, + Other: () => {}, + }), + ...remoteRuns.match({ + Success: (runs) => runs.some((run) => run.pdpBeamType === 'pp') + ? inelasticInteractionRateActiveColumnsForProtonProton + : {}, + Other: () => {}, + }), ...createRunDetectorsAsyncQcActiveColumns(remoteDetectors.match({ Success: (detectors) => detectors, Other: () => [], @@ -70,7 +84,7 @@ export const RunsPerSimulationPassOverviewPage = ({ exportRunsTriggerAndModal(runsPerSimulationPassOverviewModel, modalModel), ]), h('.flex-column.w-100', [ - table(runs, activeColumns, null, { profile: 'runsPerSimulationPass' }), + table(remoteRuns, activeColumns, null, { profile: 'runsPerSimulationPass' }), paginationComponent(runsPerSimulationPassOverviewModel.pagination), ]), ]); diff --git a/lib/public/views/Runs/format/displayRunDuration.js b/lib/public/views/Runs/format/displayRunDuration.js index 36fa8c2724..f330f4b523 100644 --- a/lib/public/views/Runs/format/displayRunDuration.js +++ b/lib/public/views/Runs/format/displayRunDuration.js @@ -15,16 +15,16 @@ import { h, iconWarning } from '/js/src/index.js'; import { MAX_RUN_DURATION } from '../../../services/run/constants.mjs'; import { formatRunDuration } from '../../../utilities/formatting/formatRunDuration.mjs'; import { tooltip } from '../../../components/common/popover/tooltip.js'; +import { TriggerValue } from '../../../domain/enums/TriggerValue.js'; /** * Format the duration of a given run * - * @param {Object} run for which duration must be displayed - * + * @param {Run} run for which duration must be displayed * @return {string|vnode} the formatted duration */ export const displayRunDuration = (run) => { - const { runDuration, timeTrgStart, timeTrgEnd, timeO2Start, timeO2End } = run; + const { runDuration, timeTrgStart, timeTrgEnd, timeO2Start, timeO2End, triggerValue } = run; const formattedRunDuration = formatRunDuration(run); @@ -35,15 +35,18 @@ export const displayRunDuration = (run) => { const missingTimeTrgStart = timeTrgStart === null || timeTrgStart === undefined; const missingTimeTrgEnd = timeTrgEnd === null || timeTrgEnd === undefined; + // Run has ended if (timeTrgEnd || timeO2End) { let warningPopover = null; - if (missingTimeTrgStart && missingTimeTrgEnd) { - warningPopover = 'Duration based on o2 start AND stop because of missing trigger information'; - } else if (missingTimeTrgStart) { - warningPopover = 'Duration based on o2 start because of missing trigger start information'; - } else if (missingTimeTrgEnd) { - warningPopover = 'Duration based on o2 stop because of missing trigger stop information'; + if (triggerValue !== TriggerValue.Off) { + if (missingTimeTrgStart && missingTimeTrgEnd) { + warningPopover = 'Duration based on o2 start AND stop because of missing trigger information'; + } else if (missingTimeTrgStart) { + warningPopover = 'Duration based on o2 start because of missing trigger start information'; + } else if (missingTimeTrgEnd) { + warningPopover = 'Duration based on o2 stop because of missing trigger stop information'; + } } return h('.flex-row', h( @@ -52,6 +55,8 @@ export const displayRunDuration = (run) => { )); } + // Run is either running or lost + const timeStart = missingTimeTrgStart ? timeO2Start : timeTrgStart; let classes = 'success'; diff --git a/lib/public/views/Runs/format/formatEditableNumber.js b/lib/public/views/Runs/format/formatEditableNumber.js new file mode 100644 index 0000000000..d69b7ec4ae --- /dev/null +++ b/lib/public/views/Runs/format/formatEditableNumber.js @@ -0,0 +1,50 @@ +/** + * @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 { formatFloat } from '../../../utilities/formatting/formatFloat.js'; +import { h } from '/js/src/index.js'; + +/** + * Format a editable numeric property + * + * @param {boolean} isEditionEnabled if true the input is displayed, current values otherwise + * @param {number} current the current value + * @param {function(InputEvent, void)} oninput callback + * @param {RunDetailsModel} runDetailsModel details model + * @param {number} [options.inputDecimals = 3] precision of input value + * @param {number} [options.displayDecimals = 3] precision of displayed value + * @return {Component} display or input + */ +export const formatEditableNumber = ( + isEditionEnabled, + current, + oninput, + { inputDecimals = 3, displayDecimals = 3, unit = null } = {}, +) => { + if (isEditionEnabled) { + return h('.flex-row.g1', [ + h('input.form-control', { + type: 'number', + step: Math.pow(10, -inputDecimals), + value: current, + oninput, + }), + unit ? h('', unit) : null, + ]); + } else { + return current !== null && current !== undefined ? h('.flex-row.g1', [ + formatFloat(current, { precision: displayDecimals }), + unit ? h('', unit) : null, + ]) : '-'; + } +}; diff --git a/lib/public/views/Runs/format/formatRunEnd.js b/lib/public/views/Runs/format/formatRunEnd.js index 8a96d7cd99..8fa044570a 100644 --- a/lib/public/views/Runs/format/formatRunEnd.js +++ b/lib/public/views/Runs/format/formatRunEnd.js @@ -15,6 +15,7 @@ import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.j import { iconWarning, h } from '/js/src/index.js'; import { getLocaleDateAndTime } from '../../../utilities/dateUtils.js'; import { tooltip } from '../../../components/common/popover/tooltip.js'; +import { TriggerValue } from '../../../domain/enums/TriggerValue.js'; const MISSING_TRIGGER_STOP_WARNING = 'O2 stop is displayed because trigger stop is missing'; @@ -26,20 +27,27 @@ const MISSING_TRIGGER_STOP_WARNING = 'O2 stop is displayed because trigger stop * @return {Component} the formatted end date */ export const formatRunEnd = (run, inline) => { - if (run.timeTrgEnd) { - return formatTimestamp(run.timeTrgEnd, inline); + const { timeTrgEnd, timeO2End, triggerValue } = run; + + if (timeTrgEnd || triggerValue === TriggerValue.Off) { + return formatTimestamp(timeTrgEnd || timeO2End, inline); } - if (run.timeO2End) { - const { date, time } = getLocaleDateAndTime(run.timeO2End); - const content = inline - ? h('span', [ + + if (timeO2End) { + if (inline) { + return h('span', [ h('.flex-row.items-center.g2', [ - formatTimestamp(run.timeO2End, inline), + formatTimestamp(timeO2End, inline), tooltip(iconWarning(), MISSING_TRIGGER_STOP_WARNING), ]), - ]) - : h('', h('', date), h('.flex-row.g2.items-center', [h('', time), tooltip(iconWarning(), MISSING_TRIGGER_STOP_WARNING)])); - return content; + ]); + } else { + const { date, time } = getLocaleDateAndTime(timeO2End); + return h('', [ + h('', date), + h('.flex-row.g2.items-center', [h('', time), tooltip(iconWarning(), MISSING_TRIGGER_STOP_WARNING)]), + ]); + } } return '-'; }; diff --git a/lib/public/views/Runs/format/formatRunStart.js b/lib/public/views/Runs/format/formatRunStart.js index 70fe020c8f..8b826dfe8c 100644 --- a/lib/public/views/Runs/format/formatRunStart.js +++ b/lib/public/views/Runs/format/formatRunStart.js @@ -15,8 +15,9 @@ import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.j import { iconWarning, h } from '/js/src/index.js'; import { getLocaleDateAndTime } from '../../../utilities/dateUtils.js'; import { tooltip } from '../../../components/common/popover/tooltip.js'; +import { TriggerValue } from '../../../domain/enums/TriggerValue.js'; -const MISSING_TRIGGER_STOP_WARNING = 'O2 stop is displayed because trigger stop is missing'; +const MISSING_TRIGGER_START_WARNING = 'O2 start is displayed because trigger stop is missing'; /** * Format a given run's start date @@ -26,20 +27,27 @@ const MISSING_TRIGGER_STOP_WARNING = 'O2 stop is displayed because trigger stop * @return {Component} the formatted start date */ export const formatRunStart = (run, inline) => { - if (run.timeTrgStart) { - return formatTimestamp(run.timeTrgStart, inline); + const { timeO2Start, timeTrgStart, triggerValue } = run; + + if (timeTrgStart || triggerValue === TriggerValue.Off) { + return formatTimestamp(timeTrgStart || timeO2Start, inline); } - if (run.timeO2Start) { - const { date, time } = getLocaleDateAndTime(run.timeO2Start); - const content = inline - ? h('span', [ + + if (timeO2Start) { + if (inline) { + return h('span', [ h('.flex-row.items-center.g2', [ - formatTimestamp(run.timeO2Start, inline), - tooltip(iconWarning(), MISSING_TRIGGER_STOP_WARNING), + formatTimestamp(timeO2Start, inline), + tooltip(iconWarning(), MISSING_TRIGGER_START_WARNING), ]), - ]) - : h('', h('', date), h('.flex-row.g2.items-center', [h('', time), tooltip(iconWarning(), MISSING_TRIGGER_STOP_WARNING)])); - return content; + ]); + } else { + const { date, time } = getLocaleDateAndTime(timeO2Start); + return h('', [ + h('', date), + h('.flex-row.g2.items-center', [h('', time), tooltip(iconWarning(), MISSING_TRIGGER_START_WARNING)]), + ]); + } } return '-'; }; diff --git a/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js b/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js index 8bf6eba530..fcdb1a94f3 100644 --- a/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js +++ b/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js @@ -17,6 +17,7 @@ import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { externalLinks } from '../../../components/common/navigation/externalLinks.js'; import { formatItemsCount } from '../../../utilities/formatting/formatItemsCount.js'; import { formatSizeInBytes } from '../../../utilities/formatting/formatSizeInBytes.js'; +import { badge } from '../../../components/common/badge.js'; /** * List of active columns for a generic simulation passes table @@ -36,14 +37,28 @@ export const simulationPassesActiveColumns = { associatedRuns: { name: 'Runs', visible: true, - format: (_, { id }) => frontLink('Runs', 'runs-per-simulation-pass', { simulationPassId: id }), + format: (_, { id, runsCount }) => + runsCount === 0 + ? 'No runs' + : frontLink( + badge(runsCount), + 'runs-per-simulation-pass', + { simulationPassId: id }, + ), classes: 'w-10 f6', }, associatedDataPasses: { name: 'Anchorage', visible: true, - format: (_, { id }) => frontLink('Anchorage', 'data-passes-per-simulation-pass-overview', { simulationPassId: id }), + format: (_, { id, dataPassesCount }) => + dataPassesCount === 0 + ? 'No anchorage' + : frontLink( + badge(dataPassesCount), + 'data-passes-per-simulation-pass-overview', + { simulationPassId: id }, + ), classes: 'w-10 f6', }, diff --git a/lib/public/views/Statistics/charts/efficiencyChartComponent.js b/lib/public/views/Statistics/charts/efficiencyChartComponent.js index 68750d5b9c..6ff0938388 100644 --- a/lib/public/views/Statistics/charts/efficiencyChartComponent.js +++ b/lib/public/views/Statistics/charts/efficiencyChartComponent.js @@ -30,16 +30,20 @@ import { formatPercentage } from '../../../utilities/formatting/formatPercentage */ export const efficiencyChartComponent = (statistics, onPointHover, periodLabel) => { const points = []; - let efficiencySum = 0; - for (const { fillNumber, efficiency, efficiencyLossAtStart } of statistics) { + let totalRunsCoverage = 0; + let totalStableBeamDuration = 0; + for (const { fillNumber, stableBeamsDuration, runsCoverage, efficiency, efficiencyLossAtStart } of statistics) { points.push({ x: fillNumber, y: [efficiency + efficiencyLossAtStart, efficiency], }); - efficiencySum += efficiency; + + totalRunsCoverage += runsCoverage; + totalStableBeamDuration += stableBeamsDuration; } - const meanEfficiency = efficiencySum / points.length; + const meanEfficiency = totalRunsCoverage / totalStableBeamDuration; + if (points.length) { points[0].y.push(meanEfficiency); points[points.length - 1].y.push(meanEfficiency); diff --git a/lib/server/controllers/qcFlag.controller.js b/lib/server/controllers/qcFlag.controller.js index affea30d71..f409511d56 100644 --- a/lib/server/controllers/qcFlag.controller.js +++ b/lib/server/controllers/qcFlag.controller.js @@ -42,21 +42,50 @@ const getQcFlagByIdHandler = async (req, res) => { // eslint-disable-next-line valid-jsdoc /** - * List All QcFlags + * List All QcFlags for a given data pass */ -const listQcFlagsHandler = async (req, res) => { +const listQcFlagsPerDataPassHandler = async (req, res) => { const validatedDTO = await dtoValidator( DtoFactory.queryOnly({ - filter: { - ids: Joi.array().items(Joi.number()), - dataPassIds: Joi.array().items(Joi.number()), - simulationPassIds: Joi.array().items(Joi.number()), - runNumbers: Joi.array().items(Joi.number()), - dplDetectorIds: Joi.array().items(Joi.number()), - createdBy: Joi.array().items(Joi.string()), - }, + dataPassId: Joi.number(), + runNumber: Joi.number(), + dplDetectorId: Joi.number(), + page: PaginationDto, + }), + req, + res, + ); + if (validatedDTO) { + try { + const { + dataPassId, + runNumber, + dplDetectorId, + page: { limit = ApiConfig.pagination.limit, offset } = {}, + } = validatedDTO.query; + + const { count, rows: items } = await qcFlagService.getAllPerDataPassAndRunAndDetector( + { dataPassId, runNumber, dplDetectorId }, + { limit, offset }, + ); + res.json(countedItemsToHttpView({ count, items }, limit)); + } catch (error) { + updateExpressResponseFromNativeError(res, error); + } + } +}; + +// eslint-disable-next-line valid-jsdoc +/** + * List All QcFlags for a given data pass + */ +const listQcFlagsPerSimulationPassHandler = async (req, res) => { + const validatedDTO = await dtoValidator( + DtoFactory.queryOnly({ + simulationPassId: Joi.number(), + runNumber: Joi.number(), + dplDetectorId: Joi.number(), page: PaginationDto, - sort: DtoFactory.order(['id', 'from', 'to', 'createdBy', 'flagType', 'createdAt', 'updatedAt']), }), req, res, @@ -64,17 +93,16 @@ const listQcFlagsHandler = async (req, res) => { if (validatedDTO) { try { const { - filter, + simulationPassId, + runNumber, + dplDetectorId, page: { limit = ApiConfig.pagination.limit, offset } = {}, - sort = { updatedAt: 'DESC' }, } = validatedDTO.query; - const { count, rows: items } = await qcFlagService.getAll({ - filter, - limit, - offset, - sort, - }); + const { count, rows: items } = await qcFlagService.getAllPerSimulationPassAndRunAndDetector( + { simulationPassId, runNumber, dplDetectorId }, + { limit, offset }, + ); res.json(countedItemsToHttpView({ count, items }, limit)); } catch (error) { updateExpressResponseFromNativeError(res, error); @@ -147,12 +175,7 @@ const deleteQcFlagByIdHandler = async (req, res) => { ); if (validatedDTO) { try { - const userWithRoles = { - externalUserId: validatedDTO.session.externalId, - roles: validatedDTO.session?.access ?? [], - }; - - const qcFlag = await qcFlagService.delete(validatedDTO.params.id, { userWithRoles }); + const qcFlag = await qcFlagService.delete(validatedDTO.params.id); res.json({ data: qcFlag }); } catch (error) { updateExpressResponseFromNativeError(res, error); @@ -198,7 +221,8 @@ const verifyQcFlagHandler = async (req, res) => { exports.QcFlagController = { getQcFlagByIdHandler, - listQcFlagsHandler, + listQcFlagsPerDataPassHandler, + listQcFlagsPerSimulationPassHandler, createQcFlagHandler, deleteQcFlagByIdHandler, verifyQcFlagHandler, diff --git a/lib/server/errors/AdapterError.js b/lib/server/errors/AdapterError.js new file mode 100644 index 0000000000..eaf497e594 --- /dev/null +++ b/lib/server/errors/AdapterError.js @@ -0,0 +1,20 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * Specific error being thrown when an adapter conversion fails + */ +class AdapterError extends Error { +} + +exports.AdapterError = AdapterError; diff --git a/lib/server/routers/qcFlag.router.js b/lib/server/routers/qcFlag.router.js index 89707c946d..d33eaf15e9 100644 --- a/lib/server/routers/qcFlag.router.js +++ b/lib/server/routers/qcFlag.router.js @@ -11,11 +11,23 @@ * or submit itself to any jurisdiction. */ +const { BkpRoles } = require('../../domain/enums/BkpRoles.js'); const { QcFlagController } = require('../controllers/qcFlag.controller.js'); +const { rbacMiddleware } = require('../middleware/rbac.middleware.js'); exports.qcFlagsRouter = { path: '/qcFlags', children: [ + { + path: 'perDataPass', + method: 'get', + controller: QcFlagController.listQcFlagsPerDataPassHandler, + }, + { + path: 'perSimulationPass', + method: 'get', + controller: QcFlagController.listQcFlagsPerSimulationPassHandler, + }, { path: ':id', children: [ @@ -25,7 +37,7 @@ exports.qcFlagsRouter = { }, { method: 'delete', - controller: QcFlagController.deleteQcFlagByIdHandler, + controller: [rbacMiddleware(BkpRoles.ADMIN), QcFlagController.deleteQcFlagByIdHandler], }, { method: 'post', @@ -34,10 +46,6 @@ exports.qcFlagsRouter = { }, ], }, - { - method: 'get', - controller: QcFlagController.listQcFlagsHandler, - }, { method: 'post', controller: QcFlagController.createQcFlagHandler, diff --git a/lib/server/services/qualityControlFlag/QcFlagService.js b/lib/server/services/qualityControlFlag/QcFlagService.js index 45c5abdddd..e3698f54d8 100644 --- a/lib/server/services/qualityControlFlag/QcFlagService.js +++ b/lib/server/services/qualityControlFlag/QcFlagService.js @@ -11,19 +11,22 @@ * or submit itself to any jurisdiction. */ -const { repositories: { - QcFlagRepository, - DplDetectorRepository, - RunRepository, - QcFlagVerificationRepository, -} } = require('../../../database/index.js'); +const { + repositories: { + QcFlagRepository, + DataPassQcFlagRepository, + SimulationPassQcFlagRepository, + DplDetectorRepository, + RunRepository, + QcFlagVerificationRepository, + }, +} = require('../../../database/index.js'); const { dataSource } = require('../../../database/DataSource.js'); -const { qcFlagAdapter } = require('../../../database/adapters/index.js'); +const { qcFlagAdapter, dataPassQcFlagAdapter, simulationPassQcFlagAdapter } = require('../../../database/adapters/index.js'); const { BadParameterError } = require('../../errors/BadParameterError.js'); const { NotFoundError } = require('../../errors/NotFoundError.js'); const { getUserOrFail } = require('../user/getUserOrFail.js'); const { AccessDeniedError } = require('../../errors/AccessDeniedError.js'); -const { BkpRoles } = require('../../../domain/enums/BkpRoles.js'); const { ConflictError } = require('../../errors/ConflictError.js'); const NON_QC_DETECTORS = new Set(['TST']); @@ -106,9 +109,10 @@ class QcFlagService { /** * Validate QC flag DPL detector + * * @param {number} dplDetectorId DPL detector * @throws {BadParameterError} - * @return {void} + * @return {Promise} resolves with the found dpl detector */ async _validateQcFlagDplDetector(dplDetectorId) { const dplDetector = await DplDetectorRepository.findOne({ where: { id: dplDetectorId } }); @@ -253,7 +257,7 @@ class QcFlagService { ], }); if (!targetRun) { - // eslint-disable-next-line max-len + // eslint-disable-next-line max-len throw new BadParameterError(`There is not association between simulation pass with this id (${simulationPassId}), run with this number (${runNumber}) and detector with this name (${dplDetector.name})`); } @@ -281,11 +285,9 @@ class QcFlagService { /** * Delete single instance of QC flag * @param {number} id QC flag id - * @param {object} relations QC Flag entity relations - * @param {UserWithRoles} relations.userWithRoles user with roles * @return {Promise} promise */ - async delete(id, relations) { + async delete(id) { return dataSource.transaction(async () => { const qcFlag = await QcFlagRepository.findOne({ where: { id }, @@ -299,13 +301,6 @@ class QcFlagService { throw new ConflictError('Cannot delete QC flag which is verified'); } - const { userWithRoles: { userId, externalUserId, roles = [] } } = relations; - const user = await getUserOrFail({ userId, externalUserId }); - - if (qcFlag.createdById !== user.id && !roles.includes(BkpRoles.ADMIN)) { - throw new AccessDeniedError('You are not allowed to remove this QC flag'); - } - await qcFlag.removeDataPasses(qcFlag.dataPasses); await qcFlag.removeSimulationPasses(qcFlag.simulationPasses); return QcFlagRepository.removeOne({ where: { id } }); @@ -342,34 +337,37 @@ class QcFlagService { } /** - * Get all quality control flags instances - * @param {object} [options.filter] filtering defintion - * @param {number[]} [options.filter.ids] quality control flag ids to filter with - * @param {number[]} [options.filter.dataPassIds] data pass ids to filter with - * @param {number[]} [options.filter.simulationPassIds] simulation pass ids to filter with - * @param {number[]} [options.filter.runNumbers] run numbers to filter with - * @param {number[]} [options.filter.dplDetectorIds] dpl detector ids to filter with - * @param {string[]} [options.filter.createdBy] user names to filter with - * @param {number} [options.offset] paramter related to pagination - offset - * @param {number} [options.limit] paramter related to pagination - limit - * @param {Object[]} [options.sort] definition of sorting - * @return {Promise>} promise + * Return a paginated list of QC flags related to a given data pass, run and dpl detector + * + * @param {object} criteria the QC flag criteria + * @param {number} criteria.dataPassId the id of the data pass to which QC flag should relate + * @param {number} criteria.runNumber the run number of the run to which QC flag should relate + * @param {number} criteria.dplDetectorId the id of the DPL detector to which QC flag should release + * @param {object} [pagination] the pagination to apply + * @param {number} [pagination.offset] amount of items to skip + * @param {number} [pagination.limit] amount of items to fetch + * @return {Promise<{count, rows: DataPassQcFlag[]}>} paginated list of data pass QC flags */ - async getAll({ filter, sort, limit, offset } = {}) { - const queryBuilder = this.prepareQueryBuilder(); + async getAllPerDataPassAndRunAndDetector({ dataPassId, runNumber, dplDetectorId }, pagination) { + const { limit, offset } = pagination || {}; - if (sort) { - const { createdBy: createdByOrder, flagType: flagTypeOrder, ...otherSortProperties } = sort; - if (createdByOrder) { - queryBuilder.orderBy((sequelize) => sequelize.literal('`createdBy`.name'), createdByOrder); - } - if (flagTypeOrder) { - queryBuilder.orderBy((sequelize) => sequelize.literal('`flagType`.name'), flagTypeOrder); - } - for (const property in otherSortProperties) { - queryBuilder.orderBy(property, sort[property]); - } - } + const queryBuilder = dataSource.createQueryBuilder() + .where('dataPassId').is(dataPassId) + .include({ + association: 'qcFlag', + include: [ + { association: 'flagType' }, + { association: 'createdBy' }, + { association: 'verifications', include: [{ association: 'createdBy' }] }, + ], + where: { + runNumber, + dplDetectorId, + }, + required: true, + }) + .set('subQuery', false) + .orderBy('id', 'DESC', 'qcFlag'); if (limit) { queryBuilder.limit(limit); @@ -378,33 +376,67 @@ class QcFlagService { queryBuilder.offset(offset); } - if (filter) { - const { ids, dataPassIds, runNumbers, dplDetectorIds, createdBy, simulationPassIds } = filter; - if (ids) { - queryBuilder.where('id').oneOf(...ids); - } - if (dataPassIds) { - queryBuilder.whereAssociation('dataPasses', 'id').oneOf(...dataPassIds); - } - if (simulationPassIds) { - queryBuilder.whereAssociation('simulationPasses', 'id').oneOf(...simulationPassIds); - } - if (runNumbers) { - queryBuilder.whereAssociation('run', 'runNumber').oneOf(...runNumbers); - } - if (dplDetectorIds) { - queryBuilder.whereAssociation('dplDetector', 'id').oneOf(...dplDetectorIds); - } - if (createdBy) { - queryBuilder.whereAssociation('createdBy', 'name').oneOf(...createdBy); - } + // The findAndCountAll function is not working properly with required include and distinct (count only on data pass id) + const [rows, count] = await Promise.all([ + DataPassQcFlagRepository.findAll(queryBuilder), + DataPassQcFlagRepository.count(queryBuilder), + ]); + + return { + count, + rows: rows.map(dataPassQcFlagAdapter.toEntity), + }; + } + + /** + * Return a paginated list of QC flags related to a given simulation pass, run and dpl detector + * + * @param {object} criteria the QC flag criteria + * @param {number} criteria.simulationPassId the id of the simulation pass to which QC flag should relate + * @param {number} criteria.runNumber the run number of the run to which QC flag should relate + * @param {number} criteria.dplDetectorId the id of the DPL detector to which QC flag should release + * @param {object} [pagination] the pagination to apply + * @param {number} [pagination.offset] amount of items to skip + * @param {number} [pagination.limit] amount of items to fetch + * @return {Promise<{count, rows: SimulationPassQcFlag[]}>} paginated list of simulation pass QC flags + */ + async getAllPerSimulationPassAndRunAndDetector({ simulationPassId, runNumber, dplDetectorId }, pagination) { + const { limit, offset } = pagination || {}; + + const queryBuilder = dataSource.createQueryBuilder() + .where('simulationPassId').is(simulationPassId) + .include({ + association: 'qcFlag', + include: [ + { association: 'flagType' }, + { association: 'createdBy' }, + { association: 'verifications', include: [{ association: 'createdBy' }] }, + ], + where: { + runNumber, + dplDetectorId, + }, + required: true, + }) + .set('subQuery', false) + .orderBy('id', 'DESC', 'qcFlag'); + + if (limit) { + queryBuilder.limit(limit); + } + if (offset) { + queryBuilder.offset(offset); } - const { count, rows } = await QcFlagRepository.findAndCountAll(queryBuilder); + // The findAndCountAll function is not working properly with required include and distinct (count only on simulation pass id) + const [rows, count] = await Promise.all([ + SimulationPassQcFlagRepository.findAll(queryBuilder), + SimulationPassQcFlagRepository.count(queryBuilder), + ]); return { - count: count.length, - rows: rows.map(qcFlagAdapter.toEntity), + count: count, + rows: rows.map(simulationPassQcFlagAdapter.toEntity), }; } diff --git a/lib/usecases/run/GetAllRunsUseCase.js b/lib/usecases/run/GetAllRunsUseCase.js index 27c621013a..e83212ab72 100644 --- a/lib/usecases/run/GetAllRunsUseCase.js +++ b/lib/usecases/run/GetAllRunsUseCase.js @@ -262,7 +262,7 @@ class GetAllRunsUseCase { filteringQueryBuilder.where('runNumber').oneOf(...runNumbers); } - if (tags && tags.values.length > 0) { + if (tags?.values?.length) { if (tags.operation === 'and') { const runsWithExpectedTags = await RunRepository.findAll({ attributes: ['id'], @@ -275,13 +275,24 @@ class GetAllRunsUseCase { raw: true, }); filteringQueryBuilder.where('id').oneOf(...runsWithExpectedTags.map(({ id }) => id)); - } else { + } else if (tags.operation === 'or') { filteringQueryBuilder.include({ association: 'tags', where: { text: { [Op.in]: tags.values }, }, }); + } else { + const runsWithExpectedTags = await RunRepository.findAll({ + attributes: ['id'], + include: { + association: 'tags', + where: { text: { [Op.in]: tags.values } }, + }, + group: 'run_number', + raw: true, + }); + filteringQueryBuilder.where('id').not().oneOf(...runsWithExpectedTags.map(({ id }) => id)); } } } diff --git a/package-lock.json b/package-lock.json index c5ff9d0b8c..bc897c833b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aliceo2/bookkeeping", - "version": "0.84.0", + "version": "0.86.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@aliceo2/bookkeeping", - "version": "0.84.0", + "version": "0.86.0", "bundleDependencies": [ "@aliceo2/web-ui", "@grpc/grpc-js", @@ -6387,9 +6387,9 @@ "dev": true }, "node_modules/protobufjs": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", - "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", "hasInstallScript": true, "inBundle": true, "dependencies": { @@ -13171,9 +13171,9 @@ "dev": true }, "protobufjs": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.4.tgz", - "integrity": "sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", diff --git a/package.json b/package.json index 34a26f1fa0..f7ef4e024f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aliceo2/bookkeeping", - "version": "0.84.0", + "version": "0.86.0", "author": "ALICEO2", "scripts": { "coverage": "nyc npm test && npm run coverage:report", diff --git a/proto/common.proto b/proto/common.proto index f87b87f2ef..4a87d06dab 100644 --- a/proto/common.proto +++ b/proto/common.proto @@ -64,45 +64,45 @@ enum RunType { message Run { int32 runNumber = 1; - string environmentId = 2; - int32 bytesReadOut = 3; + optional string environmentId = 2; + optional int32 bytesReadOut = 3; // Unix timestamp when this entity was created int64 createdAt = 4; int32 Id = 5; - int32 nDetectors = 6; - int32 nEpns = 7; - int32 nFlps = 8; - int32 nSubtimeframes = 9; + optional int32 nDetectors = 6; + optional int32 nEpns = 7; + optional int32 nFlps = 8; + optional int32 nSubtimeframes = 9; // Repository hash - string pdpConfigOption = 10; + optional string pdpConfigOption = 10; // Library file location of the pdp topology description - string pdpTopologyDescriptionLibraryFile = 11; + optional string pdpTopologyDescriptionLibraryFile = 11; // Parameters given to the pdp workflow - string pdpWorkflowParameters = 12; + optional string pdpWorkflowParameters = 12; // Beam type of the pdp - string pdpBeamType = 13; + optional string pdpBeamType = 13; // Config uri of readout. - string readoutCfgUri = 14; + optional string readoutCfgUri = 14; RunQuality runQuality = 15; - RunType runType = 16; - string tfbDdMode = 17; - int64 timeO2End = 18; - int64 timeO2Start = 19; - int64 timeTrgEnd = 20; - int64 timeTrgStart = 21; + optional RunType runType = 16; + optional string tfbDdMode = 17; + optional int64 timeO2End = 18; + optional int64 timeO2Start = 19; + optional int64 timeTrgEnd = 20; + optional int64 timeTrgStart = 21; // Trigger value - string triggerValue = 22; + optional string triggerValue = 22; // The full name or file location of the odcTopology - string odcTopologyFullName = 23; - bool ddFlp = 24; - bool dcs = 25; - bool epn = 26; - string epnTopology = 27; + optional string odcTopologyFullName = 23; + optional bool ddFlp = 24; + optional bool dcs = 25; + optional bool epn = 26; + optional string epnTopology = 27; repeated Detector detectors = 28; // Unix timestamp when this entity was last updated int64 updatedAt = 30; // A string that marks the period of the lhc - string lhcPeriod = 31; + optional string lhcPeriod = 31; } message LHCFill { diff --git a/test/api/qcFlags.test.js b/test/api/qcFlags.test.js index 398ab464c8..8396644385 100644 --- a/test/api/qcFlags.test.js +++ b/test/api/qcFlags.test.js @@ -64,240 +64,62 @@ module.exports = () => { }); }); - describe('GET /api/qcFlags', () => { - it('should successfuly fetch all QC flags', async () => { - const response = await request(server).get('/api/qcFlags'); - expect(response.status).to.be.equal(200); - const { data, meta } = response.body; - expect(meta).to.be.eql({ page: { totalCount: 5, pageCount: 1 } }); - expect(data).to.be.an('array'); - expect(data).to.be.lengthOf(5); - const oneFetchedFlag = data.find(({ id }) => id === 4); - expect(oneFetchedFlag).to.be.eql({ - id: 4, - from: new Date('2022-03-22 04:46:40').getTime(), - to: new Date('2022-03-22 04:46:40').getTime(), - comment: 'Some qc comment 4', - createdAt: new Date('2024-02-13 11:57:19').getTime(), - updatedAt: new Date('2024-02-13 11:57:19').getTime(), - runNumber: 1, - dplDetectorId: 1, - createdById: 2, - createdBy: { id: 2, externalId: 456, name: 'Jan Jansen' }, - verifications: [ - { - id: 1, - comment: 'FLAG IS OK', - flagId: 4, - createdById: 1, - createdBy: { id: 1, externalId: 1, name: 'John Doe' }, - createdAt: new Date('2024-02-13 12:57:19').getTime(), - }, - ], - flagTypeId: 13, - flagType: { id: 13, name: 'Bad', method: 'Bad', bad: true, archived: false, color: null }, - }); - expect(data.map(({ id }) => id)).to.have.all.members([1, 2, 3, 4, 5]); - }); - - it('should successfuly filter on ids', async () => { - const response = await request(server).get('/api/qcFlags?filter[ids][]=1'); - expect(response.status).to.be.equal(200); - const { data, meta } = response.body; - expect(meta).to.be.eql({ page: { totalCount: 1, pageCount: 1 } }); - expect(data).to.be.an('array'); - expect(data).to.be.lengthOf(1); - expect(data[0].id).to.be.equal(1); - }); - it('should successfuly filter on dataPassIds, runNumbers, dplDetectorIds', async () => { + describe('GET /api/qcFlags/perDataPass and /api/qcFlags/perSimulationPass', () => { + it('should successfully fetch QC flags for data pass', async () => { const response = await request(server) - .get('/api/qcFlags?filter[dataPassIds][]=1&filter[runNumbers][]=106&filter[dplDetectorIds][]=1'); + .get('/api/qcFlags/perDataPass?dataPassId=1&runNumber=106&dplDetectorId=1'); expect(response.status).to.be.equal(200); const { data, meta } = response.body; expect(meta).to.be.eql({ page: { totalCount: 3, pageCount: 1 } }); expect(data).to.be.an('array'); expect(data).to.be.lengthOf(3); - expect(data.map(({ id }) => id)).to.have.all.members([1, 2, 3]); + expect(data.map(({ qcFlagId }) => qcFlagId)).to.have.all.members([1, 2, 3]); }); - it('should successfuly filter on simulationPassIds, runNumbers, dplDetectorIds', async () => { + it('should successfully fetch QC flags for simulation pass', async () => { const response = await request(server) - .get('/api/qcFlags?filter[simulationPassIds][]=1&filter[runNumbers][]=106&filter[dplDetectorIds][]=1'); + .get('/api/qcFlags/perSimulationPass?simulationPassId=1&runNumber=106&dplDetectorId=1'); expect(response.status).to.be.equal(200); const { data, meta } = response.body; expect(meta).to.be.eql({ page: { totalCount: 1, pageCount: 1 } }); expect(data).to.be.an('array'); expect(data).to.be.lengthOf(1); - expect(data[0].id).to.be.equal(5); - }); - - it('should retrive no records when filtering on non-existing ids', async () => { - const response = await request(server).get('/api/qcFlags?filter[ids][]=9999'); - expect(response.status).to.be.equal(200); - const { data } = response.body; - expect(data).to.be.an('array'); - expect(data).to.be.lengthOf(0); - }); - it('should successfuly filter on createdBy', async () => { - const response = await request(server).get('/api/qcFlags?filter[createdBy][]=John%20Doe'); - expect(response.status).to.be.equal(200); - const { data } = response.body; - expect(data).to.be.an('array'); - expect(data).to.be.lengthOf(3); - expect(data.map(({ id }) => id)).to.have.all.members([1, 2, 3]); - }); - it('should support sorting by id', async () => { - const response = await request(server).get('/api/qcFlags?sort[id]=ASC'); - const { data: flags, meta: { page: { totalCount: count } } } = response.body; - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ id }) => id); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort()); - }); - - // Sorting in ASC order - it('should support sorting by `from` property', async () => { - const response = await request(server).get('/api/qcFlags?sort[from]=ASC'); - const { data: flags, meta: { page: { totalCount: count } } } = response.body; - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ from }) => from); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort()); - }); - - it('should support sorting by `to` property', async () => { - const response = await request(server).get('/api/qcFlags?sort[to]=ASC'); - const { data: flags, meta: { page: { totalCount: count } } } = response.body; - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ to }) => to); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort()); - }); - - it('should support sorting by flag type name', async () => { - const response = await request(server).get('/api/qcFlags?sort[flagType]=ASC'); - const { data: flags, meta: { page: { totalCount: count } } } = response.body; - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ flagType: { name } }) => name); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort()); - }); - - it('should support sorting by createdBy name', async () => { - const response = await request(server).get('/api/qcFlags?sort[createdBy]=ASC'); - const { data: flags, meta: { page: { totalCount: count } } } = response.body; - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ createdBy: { name } }) => name); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort()); - }); - - it('should support sorting by createdAt', async () => { - const response = await request(server).get('/api/qcFlags?sort[createdAt]=ASC'); - const { data: flags, meta: { page: { totalCount: count } } } = response.body; - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ createdAt }) => createdAt); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort()); - }); - - it('should support sorting by updatedAt', async () => { - const response = await request(server).get('/api/qcFlags?sort[updatedAt]=ASC'); - const { data: flags, meta: { page: { totalCount: count } } } = response.body; - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ updatedAt }) => updatedAt); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort()); - }); - - // Sorting in DESC order - it('should support sorting by `from` property', async () => { - const response = await request(server).get('/api/qcFlags?sort[from]=DESC'); - const { data: flags, meta: { page: { totalCount: count } } } = response.body; - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ from }) => from); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort().reverse()); - }); - - it('should support sorting by `to` property', async () => { - const response = await request(server).get('/api/qcFlags?sort[to]=DESC'); - const { data: flags, meta: { page: { totalCount: count } } } = response.body; - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ to }) => to); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort().reverse()); - }); - - it('should support sorting by flag type name', async () => { - const response = await request(server).get('/api/qcFlags?sort[flagType]=DESC'); - const { data: flags, meta: { page: { totalCount: count } } } = response.body; - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ flagType: { name } }) => name); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort().reverse()); - }); - - it('should support sorting by createdBy name', async () => { - const response = await request(server).get('/api/qcFlags?sort[createdBy]=DESC'); - const { data: flags, meta: { page: { totalCount: count } } } = response.body; - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ createdBy: { name } }) => name); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort().reverse()); - }); - - it('should support sorting by createdAt', async () => { - const response = await request(server).get('/api/qcFlags?sort[createdAt]=DESC'); - const { data: flags, meta: { page: { totalCount: count } } } = response.body; - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ createdAt }) => createdAt); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort().reverse()); - }); - - it('should support sorting by updatedAt', async () => { - const response = await request(server).get('/api/qcFlags?sort[updatedAt]=DESC'); - const { data: flags, meta: { page: { totalCount: count } } } = response.body; - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ updatedAt }) => updatedAt); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort().reverse()); + expect(data[0].qcFlagId).to.be.equal(5); }); it('should support pagination', async () => { - const response = await request(server).get('/api/qcFlags?page[offset]=1&page[limit]=2&sort[id]=ASC'); + const response = await request(server) + .get('/api/qcFlags/perDataPass?dataPassId=1&runNumber=106&dplDetectorId=1&page[offset]=1&page[limit]=2'); expect(response.status).to.be.equal(200); const { data: qcFlags } = response.body; expect(qcFlags).to.be.an('array'); - expect(qcFlags.map(({ id }) => id)).to.have.ordered.deep.members([2, 3]); + expect(qcFlags.map(({ qcFlagId }) => qcFlagId)).to.have.ordered.deep.members([2, 1]); }); it('should return 400 when bad query paramter provided', async () => { - const response = await request(server).get('/api/qcFlags?a=1'); - expect(response.status).to.be.equal(400); - const { errors } = response.body; - const titleError = errors.find((err) => err.source.pointer === '/data/attributes/query/a'); - expect(titleError.detail).to.equal('"query.a" is not allowed'); + { + const response = await request(server).get('/api/qcFlags/perDataPass?a=1'); + expect(response.status).to.be.equal(400); + const { errors } = response.body; + const titleError = errors.find((err) => err.source.pointer === '/data/attributes/query/a'); + expect(titleError.detail).to.equal('"query.a" is not allowed'); + } + { + const response = await request(server).get('/api/qcFlags/perSimulationPass?a=1'); + expect(response.status).to.be.equal(400); + const { errors } = response.body; + const titleError = errors.find((err) => err.source.pointer === '/data/attributes/query/a'); + expect(titleError.detail).to.equal('"query.a" is not allowed'); + } }); it('should return 400 if the limit is below 1', async () => { - const response = await request(server).get('/api/qcFlags?page[limit]=0'); - expect(response.status).to.be.equal(400); - const { errors } = response.body; - const titleError = errors.find((err) => err.source.pointer === '/data/attributes/query/page/limit'); - expect(titleError.detail).to.equal('"query.page.limit" must be greater than or equal to 1'); + { + const response = await request(server) + .get('/api/qcFlags/perDataPass?dataPassId=1&runNumber=106&dplDetectorId=1&page[offset]=1&page[limit]=0'); + expect(response.status).to.be.equal(400); + const { errors } = response.body; + const titleError = errors.find((err) => err.source.pointer === '/data/attributes/query/page/limit'); + expect(titleError.detail).to.equal('"query.page.limit" must be greater than or equal to 1'); + } }); }); @@ -395,7 +217,7 @@ module.exports = () => { describe('DELETE /api/qcFlags/:id', () => { it('should fail to delete QC flag which is verified', async () => { const id = 4; - const response = await request(server).delete(`/api/qcFlags/${id}`); + const response = await request(server).delete(`/api/qcFlags/${id}?token=admin`); expect(response.status).to.be.equal(409); const { errors } = response.body; expect(errors).to.be.eql([ @@ -406,16 +228,15 @@ module.exports = () => { }, ]); }); - it('should fail to delete QC flag when being neither owner nor admin', async () => { + it('should fail to delete QC flag when not being admin', async () => { const id = 5; const response = await request(server).delete(`/api/qcFlags/${id}`); expect(response.status).to.be.equal(403); const { errors } = response.body; expect(errors).to.be.eql([ { - status: 403, - title: 'Access Denied', - detail: 'You are not allowed to remove this QC flag', + status: '403', + title: 'Access denied', }, ]); }); @@ -426,31 +247,6 @@ module.exports = () => { expect(response.status).to.be.equal(200); expect(response.body.data.id).to.be.equal(id); }); - it('should succesfuly delete QC flag as owner', async () => { - const qcFlagCreationParameters = { - from: new Date('2019-08-09 01:29:50').getTime(), - to: new Date('2019-08-09 05:40:00').getTime(), - comment: 'VERY INTERESTING REMARK', - flagTypeId: 2, - runNumber: 106, - dataPassId: 1, - dplDetectorId: 1, - }; - - const creationReponse = await request(server).post('/api/qcFlags').send(qcFlagCreationParameters); - expect(creationReponse.status).to.be.equal(201); - const { id } = creationReponse.body.data; - - let fetchedQcFlag = await QcFlagRepository.findOne({ where: { id } }); - expect(fetchedQcFlag.id).to.be.equal(id); - - const deletionResponse = await request(server).delete(`/api/qcFlags/${id}`); - expect(deletionResponse.status).to.be.equal(200); - expect(deletionResponse.body.data.id).to.be.equal(id); - - fetchedQcFlag = await QcFlagRepository.findOne({ where: { id } }); - expect(fetchedQcFlag).to.be.equal(null); - }); }); describe('POST /api/qcFlags/:id/verify', () => { diff --git a/test/api/runs.test.js b/test/api/runs.test.js index 043f146ffe..ea6a363803 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -199,6 +199,45 @@ module.exports = () => { expect(data).to.have.lengthOf(8); }); + it('should successfully filter on tags', async () => { + { + const response = await request(server).get('/api/runs?filter[tags][operation]=and&filter[tags][values]=FOOD,RUN'); + + expect(response.status).to.equal(200); + + const { data } = response.body; + expect(data).to.be.an('array'); + expect(data).to.have.lengthOf(1); + expect(data.map(({ runNumber }) => runNumber)).to.eql([106]); + } + + { + const response = await request(server).get('/api/runs?filter[tags][operation]=or&filter[tags][values]=FOOD,TEST-TAG-41'); + + expect(response.status).to.equal(200); + + const { data } = response.body; + expect(data).to.be.an('array'); + expect(data).to.have.lengthOf(2); + expect(data.map(({ runNumber }) => runNumber)).to.eql([106, 2]); + } + + { + const response = await request(server) + .get('/api/runs?filter[tags][operation]=none-of&filter[tags][values]=FOOD,TEST-TAG-41'); + + expect(response.status).to.equal(200); + + const { data: runs } = response.body; + expect(runs).to.be.an('array'); + expect(runs).to.have.lengthOf(100); + + for (const run of runs) { + expect(run.tags.every(({ text }) => text !== 'FOOD' && text !== 'TEST-TAG-41')).to.be.true; + } + } + }); + it('should successfully return 400 if the given definitions are not valid', async () => { const response = await request(server).get('/api/runs?filter[definitions]=bad,definition'); expect(response.status).to.equal(400); @@ -219,7 +258,7 @@ module.exports = () => { expect(data.every(({ definition }) => definition === RunDefinition.Physics)).to.be.true; }); - it ('should succefully filter on data pass id', async () => { + it('should succefully filter on data pass id', async () => { const response = await request(server).get('/api/runs?filter[dataPassIds][]=2&filter[dataPassIds][]=3'); expect(response.status).to.equal(200); @@ -228,7 +267,7 @@ module.exports = () => { expect(data.map(({ runNumber }) => runNumber)).to.have.all.members([1, 2, 55, 49, 54, 56, 105]); }); - it ('should succefully filter on simulation pass id', async () => { + it('should succefully filter on simulation pass id', async () => { const response = await request(server).get('/api/runs?filter[simulationPassIds][]=1'); expect(response.status).to.equal(200); @@ -351,7 +390,7 @@ module.exports = () => { const { data } = response.body; - expect(data.length).to.equal(20); + expect(data.length).to.equal(21); }); it('should filter runs on the odc topology value', async () => { @@ -1013,6 +1052,47 @@ module.exports = () => { expect(body.errors[0].detail) .to.equal(`Calibration status change require a reason when changing from/to ${RunCalibrationStatus.FAILED}`); }); + + it('should successfully update inelasticInteractionRateAtStart', async () => { + const response = await request(server).put('/api/runs/1').send({ + inelasticInteractionRateAtStart: 1.1, + }); + expect(response.status).to.be.equal(201); + + expect(response.body.data).to.be.an('object'); + expect(response.body.data.inelasticInteractionRateAtStart).to.equal(1.1); + }); + it('should successfully update inelasticInteractionRateAtMid', async () => { + const response = await request(server).put('/api/runs/1').send({ + inelasticInteractionRateAtMid: 1.1, + }); + expect(response.status).to.be.equal(201); + + expect(response.body.data).to.be.an('object'); + expect(response.body.data.inelasticInteractionRateAtMid).to.equal(1.1); + }); + it('should successfully update inelasticInteractionRateAtEnd', async () => { + const response = await request(server).put('/api/runs/1').send({ + inelasticInteractionRateAtEnd: 1.1, + }); + expect(response.status).to.be.equal(201); + + expect(response.body.data).to.be.an('object'); + expect(response.body.data.inelasticInteractionRateAtEnd).to.equal(1.1); + }); + it('should successfully update inelasticInteractionRateAvg', async () => { + const response = await request(server).put('/api/runs/49').send({ + inelasticInteractionRateAvg: 100000, + }); + expect(response.status).to.be.equal(201); + + expect(response.body.data).to.be.an('object'); + expect(response.body.data.inelasticInteractionRateAvg).to.equal(100000); + { + const response = await request(server).get('/api/runs/49'); + expect(response.body.data.muInelasticInteractionRate?.toFixed(5)).to.equal('0.00868'); + } + }); }); describe('PATCH api/runs query:runNumber', () => { diff --git a/test/lib/server/services/qualityControlFlag/QcFlagService.test.js b/test/lib/server/services/qualityControlFlag/QcFlagService.test.js index c5d803dfc3..9832f86e24 100644 --- a/test/lib/server/services/qualityControlFlag/QcFlagService.test.js +++ b/test/lib/server/services/qualityControlFlag/QcFlagService.test.js @@ -17,7 +17,6 @@ const assert = require('assert'); const { BadParameterError } = require('../../../../../lib/server/errors/BadParameterError.js'); const { qcFlagService } = require('../../../../../lib/server/services/qualityControlFlag/QcFlagService.js'); const { AccessDeniedError } = require('../../../../../lib/server/errors/AccessDeniedError.js'); -const { BkpRoles } = require('../../../../../lib/domain/enums/BkpRoles'); const { ConflictError } = require('../../../../../lib/server/errors/ConflictError'); const qcFlagWithId1 = { @@ -68,181 +67,43 @@ module.exports = () => { ); }); - it('should succesfuly fetch all flags', async () => { - const { rows: flags, count } = await qcFlagService.getAll(); - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedQcFlagWithId1 = flags.find(({ id }) => id === 1); - expect(fetchedQcFlagWithId1).to.be.eql(qcFlagWithId1); - expect(flags.map(({ id }) => id)).to.have.all.members([1, 2, 3, 4, 5]); - }); + it('should successfully fetch all flags for a given data pass', async () => { + { + const { rows: flags, count } = await qcFlagService.getAllPerDataPassAndRunAndDetector({ + dataPassId: 1, + runNumber: 106, + dplDetectorId: 1, + }); + expect(count).to.be.equal(3); + expect(flags).to.be.an('array'); + expect(flags).to.be.lengthOf(3); + expect(flags.map(({ qcFlagId }) => qcFlagId)).to.have.all.members([1, 2, 3]); + } - it('should succesfuly fetch all flags filtering with associations', async () => { - const { rows: flags, count } = await qcFlagService.getAll({ - filter: { - dataPassIds: [1], - runNumbers: [106], - dplDetectorIds: [1], - }, - }); - expect(count).to.be.equal(3); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(3); - expect(flags.map(({ id }) => id)).to.have.all.members([1, 2, 3]); + { + const { rows: flags, count } = await qcFlagService.getAllPerDataPassAndRunAndDetector({ + dataPassId: [2], + runNumber: [1], + dplDetectorId: [1], + }); + expect(count).to.be.equal(1); + expect(flags).to.be.an('array'); + expect(flags).to.be.lengthOf(1); + const [fetchedFlag] = flags; + expect(fetchedFlag.qcFlagId).to.equal(4); + } }); - it('should succesfuly fetch all flags filtering with associations - 2', async () => { - const { rows: flags, count } = await qcFlagService.getAll({ - filter: { - dataPassIds: [2], - runNumbers: [1], - dplDetectorIds: [1], - }, - }); - expect(count).to.be.equal(1); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(1); - const [fetchedFlag] = flags; - expect(fetchedFlag).to.be.eql({ - id: 4, - from: 1647924400000, - to: 1647924400000, - comment: 'Some qc comment 4', - runNumber: 1, + it('should successfully fetch all flags for a given simulation pass', async () => { + const { rows: flags, count } = await qcFlagService.getAllPerSimulationPassAndRunAndDetector({ + simulationPassId: 1, + runNumber: 106, dplDetectorId: 1, - createdById: 2, - createdBy: { id: 2, externalId: 456, name: 'Jan Jansen' }, - flagTypeId: 13, - flagType: { id: 13, name: 'Bad', method: 'Bad', bad: true, archived: false, color: null }, - verifications: [ - { - id: 1, - comment: 'FLAG IS OK', - flagId: 4, - createdById: 1, - createdBy: { id: 1, externalId: 1, name: 'John Doe' }, - createdAt: new Date('2024-02-13 12:57:19').getTime(), - }, - ], - createdAt: 1707825439000, - updatedAt: 1707825439000, - }); - }); - - it('should succesfuly fetch all flags filtering with associations (simulation pass)', async () => { - const { rows: flags, count } = await qcFlagService.getAll({ - filter: { - simulationPassIds: [1], - runNumbers: [106], - dplDetectorIds: [1], - }, }); expect(count).to.be.equal(1); expect(flags).to.be.an('array'); expect(flags).to.be.lengthOf(1); - expect(flags[0].id).to.be.eql(5); - }); - - it('should succesfuly fetch all flags filtering with createdBy', async () => { - const { rows: flags, count } = await qcFlagService.getAll({ - filter: { - createdBy: ['Jan Jansen'], - }, - }); - expect(count).to.be.equal(2); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(2); - expect(flags.map(({ createdBy: { name } }) => name)).to.have.all.members(['Jan Jansen', 'Jan Jansen']); - }); - - it('should succesfuly fetch all flags filtering with ids', async () => { - const { rows: flags, count } = await qcFlagService.getAll({ - filter: { - ids: [1, 4], - }, - }); - expect(count).to.be.equal(2); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(2); - expect(flags.map(({ id }) => id)).to.have.all.members([1, 4]); - }); - - it('should succesfuly sort by id', async () => { - const { rows: flags, count } = await qcFlagService.getAll({ - sort: { id: 'ASC' }, - }); - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedIds = flags.map(({ id }) => id); - expect(fetchedIds).to.have.all.ordered.members([...fetchedIds].sort()); - }); - - it('should succesfuly sort by `from` property', async () => { - const { rows: flags, count } = await qcFlagService.getAll({ - sort: { from: 'ASC' }, - }); - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ from }) => from); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort()); - }); - - it('should succesfuly sort by `to` property', async () => { - const { rows: flags, count } = await qcFlagService.getAll({ - sort: { to: 'ASC' }, - }); - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ to }) => to); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort()); - }); - - it('should succesfuly sort by flag type name', async () => { - const { rows: flags, count } = await qcFlagService.getAll({ - sort: { flagType: 'DESC' }, - }); - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ flagType: { name } }) => name); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort().reverse()); - }); - - it('should succesfuly sort by createdBy name', async () => { - const { rows: flags, count } = await qcFlagService.getAll({ - sort: { createdBy: 'DESC' }, - }); - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ createdBy: { name } }) => name); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort().reverse()); - }); - - it('should succesfuly sort by createdAt timestamp', async () => { - const { rows: flags, count } = await qcFlagService.getAll({ - sort: { createdAt: 'DESC' }, - }); - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ createdAt }) => createdAt); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort().reverse()); - }); - - it('should succesfuly sort by updatedAt timestamp', async () => { - const { rows: flags, count } = await qcFlagService.getAll({ - sort: { updatedAt: 'DESC' }, - }); - expect(count).to.be.equal(5); - expect(flags).to.be.an('array'); - expect(flags).to.be.lengthOf(5); - const fetchedSortedProperties = flags.map(({ updatedAt }) => updatedAt); - expect(fetchedSortedProperties).to.have.all.ordered.members([...fetchedSortedProperties].sort().reverse()); + expect(flags[0].qcFlagId).to.equal(5); }); }); @@ -271,9 +132,15 @@ module.exports = () => { }); it('should fail to create quality control flag because qc flag `from` timestamp is smaller than run.startTime', async () => { - const qcFlagCreationParameters = { - from: new Date('2019-08-08 11:36:40').getTime(), // Failing property + const period = { + from: new Date('2019-08-08 11:36:40').getTime(), to: new Date('2019-08-09 05:40:00').getTime(), + }; + const runStart = new Date('2019-08-08 13:00:00').getTime(); + const runEnd = new Date('2019-08-09 14:00:00').getTime(); + + const qcFlagCreationParameters = { + ...period, comment: 'VERY INTERESTING REMARK', }; @@ -290,7 +157,7 @@ module.exports = () => { await assert.rejects( () => qcFlagService.createForDataPass(qcFlagCreationParameters, relations), // eslint-disable-next-line max-len - new BadParameterError(`Given QC flag period (${new Date('2019-08-08 11:36:40').getTime()}, ${new Date('2019-08-09 05:40:00').getTime()}) is out of run (${new Date('2019-08-08 13:00:00').getTime()}, ${new Date('2019-08-09 14:00:00').getTime()}) period`), + new BadParameterError(`Given QC flag period (${period.from}, ${period.to}) is out of run (${runStart}, ${runEnd}) period`), ); }); @@ -337,7 +204,8 @@ module.exports = () => { await assert.rejects( () => qcFlagService.createForDataPass(qcFlagCreationParameters, relations), // eslint-disable-next-line max-len - new BadParameterError('There is not association between data pass with this id (9999), run with this number (106) and detector with this name (CPV)'), + new BadParameterError('There is not association between data pass with this id (9999),' + + ' run with this number (106) and detector with this name (CPV)'), ); }); @@ -445,9 +313,16 @@ module.exports = () => { }); it('should fail to create quality control flag because qc flag `from` timestamp is smaller than run.startTime', async () => { - const qcFlagCreationParameters = { - from: new Date('2019-08-08 11:36:40').getTime(), // Failing property + const period = { + from: new Date('2019-08-08 11:36:40').getTime(), to: new Date('2019-08-09 05:40:00').getTime(), + }; + + const runStart = new Date('2019-08-08 13:00:00').getTime(); + const runEnd = new Date('2019-08-09 14:00:00').getTime(); + + const qcFlagCreationParameters = { + ...period, comment: 'VERY INTERESTING REMARK', }; @@ -464,7 +339,7 @@ module.exports = () => { await assert.rejects( () => qcFlagService.createForSimulationPass(qcFlagCreationParameters, relations), // eslint-disable-next-line max-len - new BadParameterError(`Given QC flag period (${new Date('2019-08-08 11:36:40').getTime()}, ${new Date('2019-08-09 05:40:00').getTime()}) is out of run (${new Date('2019-08-08 13:00:00').getTime()}, ${new Date('2019-08-09 14:00:00').getTime()}) period`), + new BadParameterError(`Given QC flag period (${period.from}, ${period.to}) is out of run (${runStart}, ${runEnd}) period`), ); }); @@ -511,7 +386,8 @@ module.exports = () => { await assert.rejects( () => qcFlagService.createForSimulationPass(qcFlagCreationParameters, relations), // eslint-disable-next-line max-len - new BadParameterError('There is not association between simulation pass with this id (9999), run with this number (106) and detector with this name (CPV)'), + new BadParameterError('There is not association between simulation pass with this id (9999),' + + ' run with this number (106) and detector with this name (CPV)'), ); }); @@ -605,58 +481,16 @@ module.exports = () => { new ConflictError('Cannot delete QC flag which is verified'), ); }); - it('should fail to delete QC flag of dataPass when being neither owner nor admin', async () => { - const id = 1; - const relations = { - userWithRoles: { externalUserId: 456 }, - }; - await assert.rejects( - () => qcFlagService.delete(id, relations), - new AccessDeniedError('You are not allowed to remove this QC flag'), - ); - }); - it('should succesfuly delete QC flag of dataPass as admin', async () => { - const id = 2; - const relations = { - userWithRoles: { externalUserId: 456, roles: [BkpRoles.ADMIN] }, - }; - await qcFlagService.delete(id, relations); - const fetchedQcFlag = await qcFlagService.getById(id); - expect(fetchedQcFlag).to.be.equal(null); - }); - it('should succesfuly delete QC flag of dataPass as owner', async () => { + it('should succesfuly delete QC flag of dataPass', async () => { const id = 1; - const relations = { - userWithRoles: { externalUserId: 1 }, - }; - await qcFlagService.delete(id, relations); + await qcFlagService.delete(id); const fetchedQcFlag = await qcFlagService.getById(id); expect(fetchedQcFlag).to.be.equal(null); }); - it('should fail to delete QC flag of simulationPass when being neither owner nor admin', async () => { - const id = 5; - const relations = { - userWithRoles: { externalUserId: 1 }, - }; - await assert.rejects( - () => qcFlagService.delete(id, relations), - new AccessDeniedError('You are not allowed to remove this QC flag'), - ); - }); - it('should succesfuly delete QC flag of simulationPass as admin', async () => { - const id = 5; - const relations = { - userWithRoles: { externalUserId: 456, roles: [BkpRoles.ADMIN] }, - }; - - await qcFlagService.delete(id, relations); - const fetchedQcFlag = await qcFlagService.getById(id); - expect(fetchedQcFlag).to.be.equal(null); - }); - it('should succesfuly delete QC flag of simulationPass as owner', async () => { + it('should succesfuly delete QC flag of simulationPass ', async () => { const creationRelations = { user: { externalUserId: 1, @@ -668,11 +502,8 @@ module.exports = () => { }; const { id } = await qcFlagService.createForSimulationPass({}, creationRelations); - const deleteRelations = { - userWithRoles: { externalUserId: 1 }, - }; - await qcFlagService.delete(id, deleteRelations); + await qcFlagService.delete(id); const fetchedQcFlag = await qcFlagService.getById(id); expect(fetchedQcFlag).to.be.equal(null); }); diff --git a/test/lib/usecases/run/GetAllRunsUseCase.test.js b/test/lib/usecases/run/GetAllRunsUseCase.test.js index 7ee9b7a738..41cde3ec78 100644 --- a/test/lib/usecases/run/GetAllRunsUseCase.test.js +++ b/test/lib/usecases/run/GetAllRunsUseCase.test.js @@ -111,7 +111,7 @@ module.exports = () => { } }); - it('should successfully return an array only containing runs found with tags', async () => { + it('should successfully filter on tags', async () => { { getAllRunsDto.query = { filter: { @@ -133,8 +133,23 @@ module.exports = () => { const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto); expect(runs).to.lengthOf(2); for (const run of runs) { - const tagTexts = run.tags.map(({ text }) => text); - expect(tagTexts.includes('FOOD') || tagTexts.includes('TEST-TAG-41')).to.be.true; + expect(run.tags.some(({ text }) => text.includes('FOOD') || text.includes('TEST-TAG-41'))).to.be.true; + } + + { + getAllRunsDto.query = { + filter: { + tags: { operation: 'none-of', values: ['FOOD', 'TEST-TAG-41'] }, + }, + page: { + limit: 200, + }, + }; + const { runs } = await new GetAllRunsUseCase().execute(getAllRunsDto); + expect(runs).to.lengthOf(106); + for (const run of runs) { + expect(run.tags.every(({ text }) => text !== 'FOOD' && text !== 'TEST-TAG-41')).to.be.true; + } } } }); @@ -574,7 +589,7 @@ module.exports = () => { .execute(getAllRunsDto); expect(runs).to.be.an('array'); - expect(runs).to.have.lengthOf(20); + expect(runs).to.have.lengthOf(21); }); it('should successfully return an array, only containing runs found with lhc periods filter', async () => { getAllRunsDto.query = { diff --git a/test/public/defaults.js b/test/public/defaults.js index d04628a209..f4fd59bac4 100644 --- a/test/public/defaults.js +++ b/test/public/defaults.js @@ -305,8 +305,7 @@ module.exports.getInnerText = getInnerText; * @return {Promise} resolves once the text has been checked */ module.exports.expectInnerText = async (page, selector, innerText) => { - await page.waitForSelector(selector, { timeout: 200 }); - const actualInnerText = await getInnerText(await page.$(selector)); + const actualInnerText = await getInnerText(await page.waitForSelector(selector, { timeout: 200 })); expect(actualInnerText).to.equal(innerText); }; diff --git a/test/public/qcFlags/forDataPassCreation.test.js b/test/public/qcFlags/forDataPassCreation.test.js index c29b67a606..4932e6d27c 100644 --- a/test/public/qcFlags/forDataPassCreation.test.js +++ b/test/public/qcFlags/forDataPassCreation.test.js @@ -45,11 +45,13 @@ module.exports = () => { }); it('loads the page successfully', async () => { - const response = await goToPage(page, 'qc-flag-creation-for-data-pass', { queryParameters: { - dataPassId: 1, - runNumber: 106, - dplDetectorId: 1, - } }); + const response = await goToPage(page, 'qc-flag-creation-for-data-pass', { + queryParameters: { + dataPassId: 1, + runNumber: 106, + dplDetectorId: 1, + }, + }); // We expect the page to return the correct status code, making sure the server is running properly expect(response.status()).to.equal(200); @@ -65,33 +67,39 @@ module.exports = () => { }); it('can navigate to runs per data pass page from breadcrumbs link', async () => { - await goToPage(page, 'qc-flag-creation-for-data-pass', { queryParameters: { - dataPassId: 1, - runNumber: 106, - dplDetectorId: 1, - } }); + await goToPage(page, 'qc-flag-creation-for-data-pass', { + queryParameters: { + dataPassId: 1, + runNumber: 106, + dplDetectorId: 1, + }, + }); await pressElement(page, 'h2:nth-of-type(2)'); expect(await checkMismatchingUrlParam(page, { page: 'runs-per-data-pass', dataPassId: '1' })).to.be.eql({}); }); it('can navigate to run details page from breadcrumbs link', async () => { - await goToPage(page, 'qc-flag-creation-for-data-pass', { queryParameters: { - dataPassId: 1, - runNumber: 106, - dplDetectorId: 1, - } }); + await goToPage(page, 'qc-flag-creation-for-data-pass', { + queryParameters: { + dataPassId: 1, + runNumber: 106, + dplDetectorId: 1, + }, + }); await pressElement(page, 'h2:nth-of-type(3)'); expect(await checkMismatchingUrlParam(page, { page: 'run-detail', runNumber: '106' })).to.be.eql({}); }); it('should successfully create run-based QC flag', async () => { - await goToPage(page, 'qc-flag-creation-for-data-pass', { queryParameters: { - dataPassId: 1, - runNumber: 106, - dplDetectorId: 1, - } }); + await goToPage(page, 'qc-flag-creation-for-data-pass', { + queryParameters: { + dataPassId: 1, + runNumber: 106, + dplDetectorId: 1, + }, + }); await validateElement(page, 'button#submit[disabled]'); await expectInnerText(page, '.flex-row > .panel:nth-of-type(1) > div', '08/08/2019\n13:00:00'); @@ -116,11 +124,13 @@ module.exports = () => { }); it('should successfully create time-based QC flag', async () => { - await goToPage(page, 'qc-flag-creation-for-data-pass', { queryParameters: { - dataPassId: 1, - runNumber: 106, - dplDetectorId: 1, - } }); + await goToPage(page, 'qc-flag-creation-for-data-pass', { + queryParameters: { + dataPassId: 1, + runNumber: 106, + dplDetectorId: 1, + }, + }); await validateElement(page, 'button#submit[disabled]'); await expectInnerText(page, '.flex-row > .panel:nth-of-type(1) > div', '08/08/2019\n13:00:00'); @@ -151,11 +161,13 @@ module.exports = () => { }); it('should successfully create run-based QC flag in case of missing run start/stop', async () => { - await goToPage(page, 'qc-flag-creation-for-data-pass', { queryParameters: { - dataPassId: 3, - runNumber: 105, - dplDetectorId: 1, - } }); + await goToPage(page, 'qc-flag-creation-for-data-pass', { + queryParameters: { + dataPassId: 3, + runNumber: 105, + dplDetectorId: 1, + }, + }); await expectInnerText(page, '.panel:nth-child(3) em', 'Missing start/stop, the flag will be applied on the full run'); await validateElement(page, 'button#submit[disabled]'); diff --git a/test/public/qcFlags/index.js b/test/public/qcFlags/index.js index 2f00b28870..c13d80ee06 100644 --- a/test/public/qcFlags/index.js +++ b/test/public/qcFlags/index.js @@ -11,18 +11,18 @@ * or submit itself to any jurisdiction. */ -const QcFlagForDataPassOverviewSuite = require('./forDataPassOverview.test'); -const QcFlagForSimulationPassOverviewSuite = require('./forSimulationPassOverview.test'); -const QcFlagForDataPassCreationSuite = require('./forDataPassCreation.test'); -const QcFlagForSimulationPassCreationSuite = require('./forSimulationPassCreation.test'); -const QcFlagDetailsForDataPassPageSuite = require('./detailsForDataPass.test'); -const QcFlagDetailsForSimulationPassPageSuite = require('./detailsForSimulationPass.test'); +const ForDataPassOverviewSuite = require('./forDataPassOverview.test'); +const ForSimulationPassOverviewSuite = require('./forSimulationPassOverview.test'); +const ForDataPassCreationSuite = require('./forDataPassCreation.test'); +const ForSimulationPassCreationSuite = require('./forSimulationPassCreation.test'); +const DetailsForDataPassPageSuite = require('./detailsForDataPass.test'); +const DetailsForSimulationPassPageSuite = require('./detailsForSimulationPass.test'); module.exports = () => { - describe('For Data Pass Overview Page', QcFlagForDataPassOverviewSuite); - describe('For Simulation Pass Overview Page', QcFlagForSimulationPassOverviewSuite); - describe('For Data Pass Creation Page', QcFlagForDataPassCreationSuite); - describe('For Simulation Pass Creation Page', QcFlagForSimulationPassCreationSuite); - describe('Details For Data Pass Page', QcFlagDetailsForDataPassPageSuite); - describe('Details For Simulation Pass Page', QcFlagDetailsForSimulationPassPageSuite); + describe('For Data Pass Overview Page', ForDataPassOverviewSuite); + describe('For Simulation Pass Overview Page', ForSimulationPassOverviewSuite); + describe('For Data Pass Creation Page', ForDataPassCreationSuite); + describe('For Simulation Pass Creation Page', ForSimulationPassCreationSuite); + describe('Details For Data Pass Page', DetailsForDataPassPageSuite); + describe('Details For Simulation Pass Page', DetailsForSimulationPassPageSuite); }; diff --git a/test/public/runs/detail.test.js b/test/public/runs/detail.test.js index ac17e457d4..d147827516 100644 --- a/test/public/runs/detail.test.js +++ b/test/public/runs/detail.test.js @@ -211,8 +211,37 @@ module.exports = () => { .to.equal('DETECTORS - CPV - A new EOR reason'); }); + it('should successfully update inelasticInteractionRate values of PbPb run', async () => { + await goToPage(page, 'run-detail', { queryParameters: { runNumber: 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 page.click('#save-run'); + await page.waitForNetworkIdle(); + + 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'); + }); + + it('should successfully update inelasticInteractionRateAvg of pp run', async () => { + await goToPage(page, 'run-detail', { queryParameters: { runNumber: 49 } }); + await pressElement(page, '#edit-run'); + await fillInput(page, '#Run-inelasticInteractionRateAvg input', 100000); + + await page.click('#save-run'); + await page.waitForNetworkIdle(); + + 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 reloadPage(page); + await goToPage(page, 'run-detail', { queryParameters: { id: 1 } }); await pressElement(page, '#edit-run'); await waitForTimeout(100); const element = await page.$('#lhc-fill-fillNumber>strong'); @@ -326,6 +355,13 @@ module.exports = () => { expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('25:00:00'); }); + it('should successfully display duration without warning popover when run has trigger OFF', async () => { + await goToPage(page, 'run-detail', { queryParameters: { id: 107 } }); + const runDurationCell = await page.$('#runDurationValue'); + expect(await runDurationCell.$('.popover-trigger')).to.be.null; + expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('25:00:00'); + }); + it('should successfully display UNKNOWN without warning popover when run last for more than 48 hours', async () => { await goToPage(page, 'run-detail', { queryParameters: { id: 105 } }); const runDurationCell = await page.$('#runDurationValue'); diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 33fa6f6f01..3ec1a101ff 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -324,6 +324,35 @@ module.exports = () => { expect(table.length).to.equal(2); }); + it('should successfully filter on tags', async () => { + await pressElement(page, '#reset-filters'); + + // Open filter toggle + await page.waitForSelector('.tags-filter .dropdown-trigger'); + await page.$eval('.tags-filter .dropdown-trigger', (element) => element.click()); + await pressElement(page, '#tag-dropdown-option-FOOD'); + await pressElement(page, '#tag-dropdown-option-RUN'); + await waitForTimeout(300); + + table = await page.$$('tbody tr'); + expect(table.length).to.equal(1); + + await page.$eval('#tag-filter-combination-operator-radio-button-or', (element) => element.click()); + await page.$eval('.tags-filter .dropdown-trigger', (element) => element.click()); + await pressElement(page, '#tag-dropdown-option-RUN'); + await pressElement(page, '#tag-dropdown-option-TEST-TAG-41'); + await page.waitForSelector('tbody tr:nth-child(2)', { timeout: 500 }); + + table = await page.$$('tbody tr'); + expect(table.length).to.equal(2); + + await page.$eval('#tag-filter-combination-operator-radio-button-none-of', (element) => element.click()); + await page.waitForSelector('tbody tr:nth-child(2)', { hidden: true, timeout: 500 }); + + // Multiple pages, not very representative + expectInnerText('#totalRowsCount', '108'); + }); + it('should successfully filter on definition', async () => { await goToPage(page, 'run-overview'); const filterInputSelectorPrefix = '#runDefinitionCheckbox'; @@ -639,7 +668,7 @@ module.exports = () => { * @param {string[]} authorizedRunQualities the list of valid run qualities * @return {void} */ - const checkTableRunQualities = async (rows, authorizedRunQualities) => { + const checkTableTriggerValue = async (rows, authorizedRunQualities) => { for (const row of rows) { expect(await row.evaluate((rowItem) => { const rowId = rowItem.id; @@ -657,12 +686,12 @@ module.exports = () => { table = await page.$$('tbody tr'); expect(table.length).to.equal(8); - await checkTableRunQualities(table, ['OFF']); + await checkTableTriggerValue(table, ['OFF']); await page.$eval(ltuFilterSelector, (element) => element.click()); await waitForTimeout(300); table = await page.$$('tbody tr'); - await checkTableRunQualities(table, ['OFF', 'LTU']); + await checkTableTriggerValue(table, ['OFF', 'LTU']); await page.$eval(ltuFilterSelector, (element) => element.click()); await waitForTimeout(300); @@ -670,7 +699,7 @@ module.exports = () => { expect(table.length).to.equal(8); - await checkTableRunQualities(table, ['OFF']); + await checkTableTriggerValue(table, ['OFF']); }); it('should successfully filter on a list of run numbers and inform the user about it', async () => { @@ -1152,6 +1181,13 @@ module.exports = () => { expect(urlParameters).to.contain(`fillNumber=${fillNumber}`); }); + it('should successfully display duration without warning popover when run has trigger OFF', async () => { + await goToPage(page, 'run-overview'); + const runDurationCell = await page.$('#row107-runDuration'); + expect(await runDurationCell.$('.popover-trigger')).to.be.null; + expect(await runDurationCell.evaluate((element) => element.innerText)).to.equal('25:00:00'); + }); + it('should successfully display duration without warning popover when run has both trigger start and stop', async () => { await goToPage(page, 'run-overview'); const runDurationCell = await page.$('#row106-runDuration'); @@ -1192,6 +1228,7 @@ module.exports = () => { `${popoverSelector} a:nth-child(3)`, ({ href }) => href, // eslint-disable-next-line max-len - )).to.equal('http://localhost:8082/?page=layoutShow&runNumber=104&definition=COMMISSIONING&detector=CPV&pdpBeamType=cosmic&runType=COSMICS'); + )).to.equal('http://localhost:8082/' + + '?page=layoutShow&runNumber=104&definition=COMMISSIONING&detector=CPV&pdpBeamType=cosmic&runType=COSMICS'); }); }; diff --git a/test/public/runs/runsPerDataPass.overview.test.js b/test/public/runs/runsPerDataPass.overview.test.js index fb2ebcfa7b..25a098d7f5 100644 --- a/test/public/runs/runsPerDataPass.overview.test.js +++ b/test/public/runs/runsPerDataPass.overview.test.js @@ -82,7 +82,7 @@ module.exports = () => { }); it('shows correct datatypes in respective columns', async () => { - await reloadPage(page); + await goToPage(page, 'runs-per-data-pass', { queryParameters: { dataPassId: 3 } }); table = await page.$$('tr'); firstRowId = await getFirstRow(table, page); @@ -96,6 +96,12 @@ module.exports = () => { timeTrgEnd: (date) => !isNaN(Date.parse(date)), aliceL3Current: (current) => !isNaN(Number(current)), aliceL3Dipole: (current) => !isNaN(Number(current)), + + muInelasticInteractionRate: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), + inelasticInteractionRateAvg: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), + inelasticInteractionRateAtStart: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), + inelasticInteractionRateAtMid: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), + inelasticInteractionRateAtEnd: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), ...Object.fromEntries(DETECTORS.map((detectorName) => [detectorName, (quality) => expect(quality).to.be.oneOf(['QC', ''])])), }; diff --git a/test/public/runs/runsPerPeriod.overview.test.js b/test/public/runs/runsPerPeriod.overview.test.js index 3411e74959..80a3ab92e2 100644 --- a/test/public/runs/runsPerPeriod.overview.test.js +++ b/test/public/runs/runsPerPeriod.overview.test.js @@ -80,7 +80,7 @@ module.exports = () => { }); it('shows correct datatypes in respective columns', async () => { - await reloadPage(page); + await goToPage(page, 'runs-per-lhc-period', { queryParameters: { lhcPeriodName: 'LHC22a' } }); table = await page.$$('tr'); firstRowId = await getFirstRow(table, page); @@ -94,6 +94,12 @@ module.exports = () => { timeTrgEnd: (date) => !isNaN(Date.parse(date)), aliceL3Current: (current) => !isNaN(Number(current)), aliceL3Dipole: (current) => !isNaN(Number(current)), + + muInelasticInteractionRate: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), + inelasticInteractionRateAvg: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), + inelasticInteractionRateAtStart: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), + inelasticInteractionRateAtMid: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), + inelasticInteractionRateAtEnd: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), ...Object.fromEntries(DETECTORS.map((detectorName) => [detectorName, (quality) => expect(quality).oneOf([...RUN_QUALITIES, ''])])), }; diff --git a/test/public/runs/runsPerSimulationPass.overview.test.js b/test/public/runs/runsPerSimulationPass.overview.test.js index b6a4298a45..12933887ea 100644 --- a/test/public/runs/runsPerSimulationPass.overview.test.js +++ b/test/public/runs/runsPerSimulationPass.overview.test.js @@ -97,6 +97,12 @@ module.exports = () => { aliceL3Current: (current) => !isNaN(Number(current.replace(/,/g, ''))), dipoleCurrent: (current) => !isNaN(Number(current.replace(/,/g, ''))), + + muInelasticInteractionRate: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), + inelasticInteractionRateAvg: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), + inelasticInteractionRateAtStart: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), + inelasticInteractionRateAtMid: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), + inelasticInteractionRateAtEnd: (value) => value === '-' || !isNaN(Number(value.replace(/,/g, ''))), ...Object.fromEntries(DETECTORS.map((detectorName) => [detectorName, (quality) => expect(quality).to.be.oneOf(['QC', ''])])), }; diff --git a/test/public/simulationPasses/overviewAnchoredSimulationPasses.test.js b/test/public/simulationPasses/overviewAnchoredSimulationPasses.test.js index 0609349d67..90e30cab65 100644 --- a/test/public/simulationPasses/overviewAnchoredSimulationPasses.test.js +++ b/test/public/simulationPasses/overviewAnchoredSimulationPasses.test.js @@ -57,8 +57,10 @@ module.exports = () => { const dataSizeUnits = new Set(['B', 'KB', 'MB', 'GB', 'TB']); const tableDataValidators = { name: (name) => periodNameRegex.test(name), - jiraId: (jiraId) => /[A-Z][A-Z0-9]+-[0-9]+/.test(jiraId), + associatedRuns: (display) => /(No runs)|(\d+)/.test(display), + associatedDataPasses: (display) => /(No anchorage)|(\d+)/.test(display), pwg: (pwg) => /PWG.+/.test(pwg), + jiraId: (jiraId) => /[A-Z][A-Z0-9]+-[0-9]+/.test(jiraId), requestedEventsCount: (requestedEventsCount) => !isNaN(requestedEventsCount.replace(/,/g, '')), generatedEventsCount: (generatedEventsCount) => !isNaN(generatedEventsCount.replace(/,/g, '')), outputSize: (outpuSize) => { diff --git a/test/public/simulationPasses/overviewPerLhcPeriod.test.js b/test/public/simulationPasses/overviewPerLhcPeriod.test.js index e7189b742d..b4918d749e 100644 --- a/test/public/simulationPasses/overviewPerLhcPeriod.test.js +++ b/test/public/simulationPasses/overviewPerLhcPeriod.test.js @@ -20,6 +20,7 @@ const { testTableSortingByColumn, pressElement, expectColumnValues, + validateTableData, } = require('../defaults'); const { expect } = chai; @@ -53,39 +54,22 @@ module.exports = () => { }); it('shows correct datatypes in respective columns', async () => { - // Expectations of header texts being of a certain datatype + const dataSizeUnits = new Set(['B', 'KB', 'MB', 'GB', 'TB']); const headerDatatypes = { name: (name) => periodNameRegex.test(name), - year: (year) => !isNaN(year), + associatedRuns: (display) => /(No runs)|(\d+)/.test(display), + associatedDataPasses: (display) => /(No anchorage)|(\d+)/.test(display), pwg: (pwg) => /PWG.+/.test(pwg), - requestedEventsCount: (requestedEventsCount) => !isNaN(requestedEventsCount), - generatedEventsCount: (generatedEventsCount) => !isNaN(generatedEventsCount), - outpuSize: (outpuSize) => !isNaN(outpuSize), + jiraId: (jiraId) => /[A-Z]+[A-Z0-9]+-\d+/.test(jiraId), + requestedEventsCount: (requestedEventsCount) => !isNaN(requestedEventsCount.replace(/,/g, '')), + generatedEventsCount: (generatedEventsCount) => !isNaN(generatedEventsCount.replace(/,/g, '')), + outputSize: (outpuSize) => { + const [number, unit] = outpuSize.split(' '); + return !isNaN(number) && dataSizeUnits.has(unit.trim()); + }, }; - // We find the headers matching the datatype keys - await page.waitForSelector('th'); - const headers = await page.$$('th'); - const headerIndices = {}; - for (const [index, header] of headers.entries()) { - const headerContent = await page.evaluate((element) => element.id, header); - const matchingDatatype = Object.keys(headerDatatypes).find((key) => headerContent === key); - if (matchingDatatype !== undefined) { - headerIndices[index] = matchingDatatype; - } - } - - // We expect every value of a header matching a datatype key to actually be of that datatype - - // Use the third row because it is where statistics are present - const firstRowCells = await page.$$('tr:nth-of-type(3) td'); - for (const [index, cell] of firstRowCells.entries()) { - if (index in headerIndices) { - const cellContent = await page.evaluate((element) => element.innerText, cell); - const expectedDatatype = headerDatatypes[headerIndices[index]](cellContent); - expect(expectedDatatype).to.be.true; - } - } + await validateTableData(page, new Map(Object.entries(headerDatatypes))); }); it('Should display the correct items counter at the bottom of the page', async () => {