From bb5a9c3845fe2de0ed76e2c584314b634529c551 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:51:18 +0100 Subject: [PATCH 01/20] [O2B-1545] Add GAQ summary models, adapters and migration --- lib/database/adapters/GaqSummaryAdapter.js | 56 +++++++++++++ .../adapters/GaqSummaryInvalidationAdapter.js | 46 +++++++++++ lib/database/adapters/index.js | 6 ++ ...0260223120000-create-gaq-summary-tables.js | 82 +++++++++++++++++++ lib/database/models/gaqSummary.js | 52 ++++++++++++ lib/database/models/gaqSummaryInvalidation.js | 37 +++++++++ lib/database/models/index.js | 4 + 7 files changed, 283 insertions(+) create mode 100644 lib/database/adapters/GaqSummaryAdapter.js create mode 100644 lib/database/adapters/GaqSummaryInvalidationAdapter.js create mode 100644 lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js create mode 100644 lib/database/models/gaqSummary.js create mode 100644 lib/database/models/gaqSummaryInvalidation.js diff --git a/lib/database/adapters/GaqSummaryAdapter.js b/lib/database/adapters/GaqSummaryAdapter.js new file mode 100644 index 0000000000..f069b0bfa7 --- /dev/null +++ b/lib/database/adapters/GaqSummaryAdapter.js @@ -0,0 +1,56 @@ +/** + * @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. + */ + +/** + * GaqSummaryAdapter + */ +class GaqSummaryAdapter { + /** + * Constructor + */ + constructor() { + this.toEntity = this.toEntity.bind(this); + } + + /** + * Converts the given database object to an entity object. + * + * @param {SequelizeGaqSummary} databaseObject Object to convert. + * @returns {GaqSummary} Converted entity object. + */ + toEntity(databaseObject) { + const { + dataPassId, + runNumber, + badEffectiveRunCoverage, + explicitlyNotBadEffectiveRunCoverage, + mcReproducible, + missingVerificationsCount, + undefinedQualityPeriodsCount, + computedAt, + } = databaseObject; + + return { + dataPassId, + runNumber, + badEffectiveRunCoverage, + explicitlyNotBadEffectiveRunCoverage, + mcReproducible, + missingVerificationsCount, + undefinedQualityPeriodsCount, + computedAt, + }; + } +} + +module.exports = { GaqSummaryAdapter }; diff --git a/lib/database/adapters/GaqSummaryInvalidationAdapter.js b/lib/database/adapters/GaqSummaryInvalidationAdapter.js new file mode 100644 index 0000000000..63d3ebce73 --- /dev/null +++ b/lib/database/adapters/GaqSummaryInvalidationAdapter.js @@ -0,0 +1,46 @@ +/** + * @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. + */ + +/** + * GaqSummaryInvalidationAdapter + */ +class GaqSummaryInvalidationAdapter { + /** + * Constructor + */ + constructor() { + this.toEntity = this.toEntity.bind(this); + } + + /** + * Converts the given database object to an entity object. + * + * @param {SequelizeGaqSummaryInvalidation} databaseObject Object to convert. + * @returns {GaqSummaryInvalidation} Converted entity object. + */ + toEntity(databaseObject) { + const { + dataPassId, + runNumber, + invalidatedAt, + } = databaseObject; + + return { + dataPassId, + runNumber, + invalidatedAt, + }; + } +} + +module.exports = { GaqSummaryInvalidationAdapter }; diff --git a/lib/database/adapters/index.js b/lib/database/adapters/index.js index 5ff6404b3d..97d22b927f 100644 --- a/lib/database/adapters/index.js +++ b/lib/database/adapters/index.js @@ -27,6 +27,8 @@ const EorReasonAdapter = require('./EorReasonAdapter'); const FlpRoleAdapter = require('./FlpRoleAdapter'); const { HostAdapter } = require('./HostAdapter.js'); const { GaqDetectorAdapter } = require('./GaqDetectorAdapter.js'); +const { GaqSummaryAdapter } = require('./GaqSummaryAdapter.js'); +const { GaqSummaryInvalidationAdapter } = require('./GaqSummaryInvalidationAdapter.js'); const { LhcFillAdapter } = require('./LhcFillAdapter.js'); const { LhcFillStatisticsAdapter } = require('./LhcFillStatisticsAdapter.js'); const LhcPeriodAdapter = require('./LhcPeriodAdapter'); @@ -63,6 +65,8 @@ const environmentHistoryItemAdapter = new EnvironmentHistoryItemAdapter(); const eorReasonAdapter = new EorReasonAdapter(); const flpRoleAdapter = new FlpRoleAdapter(); const gaqDetectorAdapter = new GaqDetectorAdapter(); +const gaqSummaryAdapter = new GaqSummaryAdapter(); +const gaqSummaryInvalidationAdapter = new GaqSummaryInvalidationAdapter(); const hostAdapter = new HostAdapter(); const lhcFillAdapter = new LhcFillAdapter(); const lhcFillStatisticsAdapter = new LhcFillStatisticsAdapter(); @@ -159,6 +163,8 @@ module.exports = { eorReasonAdapter, flpRoleAdapter, gaqDetectorAdapter, + gaqSummaryAdapter, + gaqSummaryInvalidationAdapter, hostAdapter, lhcFillAdapter, lhcFillStatisticsAdapter, diff --git a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js new file mode 100644 index 0000000000..9ead5d5f32 --- /dev/null +++ b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js @@ -0,0 +1,82 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface, Sequelize) => queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.createTable('gaq_summaries', { + data_pass_id: { + type: Sequelize.INTEGER, + primaryKey: true, + allowNull: false, + references: { + model: 'data_passes', + key: 'id', + }, + }, + run_number: { + type: Sequelize.INTEGER, + primaryKey: true, + allowNull: false, + references: { + model: 'runs', + key: 'run_number', + }, + }, + bad_effective_run_coverage: { + type: Sequelize.FLOAT, + allowNull: false, + }, + explicitly_not_bad_effective_run_coverage: { + type: Sequelize.FLOAT, + allowNull: false, + }, + mc_reproducible: { + type: Sequelize.BOOLEAN, + allowNull: false, + }, + missing_verifications_count: { + type: Sequelize.INTEGER, + allowNull: false, + }, + undefined_quality_periods_count: { + type: Sequelize.INTEGER, + allowNull: false, + }, + computed_at: { + type: Sequelize.DATE(3), + allowNull: false, + }, + }, { transaction }); + + await queryInterface.createTable('gaq_summary_invalidations', { + data_pass_id: { + type: Sequelize.INTEGER, + primaryKey: true, + allowNull: false, + references: { + model: 'data_passes', + key: 'id', + }, + }, + run_number: { + type: Sequelize.INTEGER, + primaryKey: true, + allowNull: false, + references: { + model: 'runs', + key: 'run_number', + }, + }, + invalidated_at: { + type: Sequelize.DATE(3), + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP(3)'), + }, + }, { transaction }); + }), + + down: async (queryInterface) => queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.dropTable('gaq_summary_invalidations', { transaction }); + await queryInterface.dropTable('gaq_summaries', { transaction }); + }), +}; diff --git a/lib/database/models/gaqSummary.js b/lib/database/models/gaqSummary.js new file mode 100644 index 0000000000..80e21f0145 --- /dev/null +++ b/lib/database/models/gaqSummary.js @@ -0,0 +1,52 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +module.exports = (sequelize) => { + const Sequelize = require('sequelize'); + + const GaqSummary = sequelize.define('GaqSummary', { + dataPassId: { + type: Sequelize.INTEGER, + }, + runNumber: { + type: Sequelize.INTEGER, + }, + badEffectiveRunCoverage: { + type: Sequelize.FLOAT, + }, + explicitlyNotBadEffectiveRunCoverage: { + type: Sequelize.FLOAT, + }, + mcReproducible: { + type: Sequelize.BOOLEAN, + }, + missingVerificationsCount: { + type: Sequelize.INTEGER, + }, + undefinedQualityPeriodsCount: { + type: Sequelize.INTEGER, + }, + computedAt: { + type: Sequelize.DATE(3), + }, + }, { tableName: 'gaq_summaries' }); + + GaqSummary.removeAttribute('id'); + + GaqSummary.associate = (models) => { + GaqSummary.belongsTo(models.Run, { foreignKey: 'runNumber', as: 'run' }); + GaqSummary.belongsTo(models.DataPass, { foreignKey: 'dataPassId', as: 'dataPass' }); + }; + + return GaqSummary; +}; diff --git a/lib/database/models/gaqSummaryInvalidation.js b/lib/database/models/gaqSummaryInvalidation.js new file mode 100644 index 0000000000..f99b5d0895 --- /dev/null +++ b/lib/database/models/gaqSummaryInvalidation.js @@ -0,0 +1,37 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +module.exports = (sequelize) => { + const Sequelize = require('sequelize'); + + const GaqSummaryInvalidation = sequelize.define('GaqSummaryInvalidation', { + dataPassId: { + type: Sequelize.INTEGER, + }, + runNumber: { + type: Sequelize.INTEGER, + }, + invalidatedAt: { + type: Sequelize.DATE(3), + }, + }, { tableName: 'gaq_summary_invalidations' }); + + GaqSummaryInvalidation.removeAttribute('id'); + + GaqSummaryInvalidation.associate = (models) => { + GaqSummaryInvalidation.belongsTo(models.Run, { foreignKey: 'runNumber', as: 'run' }); + GaqSummaryInvalidation.belongsTo(models.DataPass, { foreignKey: 'dataPassId', as: 'dataPass' }); + }; + + return GaqSummaryInvalidation; +}; diff --git a/lib/database/models/index.js b/lib/database/models/index.js index 87d793fac3..36736ce905 100644 --- a/lib/database/models/index.js +++ b/lib/database/models/index.js @@ -27,6 +27,8 @@ const EorReason = require('./eorreason'); const EpnRoleSession = require('./epnrolesession'); const FlpRole = require('./flprole'); const GaqDetector = require('./gaqDetector.js'); +const GaqSummary = require('./gaqSummary.js'); +const GaqSummaryInvalidation = require('./gaqSummaryInvalidation.js'); const Host = require('./host.js'); const LhcFill = require('./lhcFill'); const LhcFillStatistics = require('./lhcFillStatistics.js'); @@ -66,6 +68,8 @@ module.exports = (sequelize) => { EpnRoleSessionkey: EpnRoleSession(sequelize), FlpRole: FlpRole(sequelize), GaqDetector: GaqDetector(sequelize), + GaqSummary: GaqSummary(sequelize), + GaqSummaryInvalidation: GaqSummaryInvalidation(sequelize), Host: Host(sequelize), LhcFill: LhcFill(sequelize), LhcFillStatistics: LhcFillStatistics(sequelize), From 4fade0f12b6ac96baa67f298431e7025cafd7cca Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:08:20 +0100 Subject: [PATCH 02/20] [O2B-1545] Add GAQ summary repositories and timestamps Add GaqSummaryRepository and GaqSummaryInvalidationRepository and export them from the repositories index. Update migration to replace the previous invalidated_at field with created_at and add updated_at to mirror default Sequelize tables. --- .../adapters/GaqSummaryInvalidationAdapter.js | 6 ++-- ...0260223120000-create-gaq-summary-tables.js | 7 +++- lib/database/models/gaqSummaryInvalidation.js | 3 -- .../GaqSummaryInvalidationRepository.js | 33 +++++++++++++++++++ .../repositories/GaqSummaryRepository.js | 33 +++++++++++++++++++ lib/database/repositories/index.js | 4 +++ 6 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 lib/database/repositories/GaqSummaryInvalidationRepository.js create mode 100644 lib/database/repositories/GaqSummaryRepository.js diff --git a/lib/database/adapters/GaqSummaryInvalidationAdapter.js b/lib/database/adapters/GaqSummaryInvalidationAdapter.js index 63d3ebce73..dbd76df3a8 100644 --- a/lib/database/adapters/GaqSummaryInvalidationAdapter.js +++ b/lib/database/adapters/GaqSummaryInvalidationAdapter.js @@ -32,13 +32,15 @@ class GaqSummaryInvalidationAdapter { const { dataPassId, runNumber, - invalidatedAt, + createdAt, + updatedAt, } = databaseObject; return { dataPassId, runNumber, - invalidatedAt, + createdAt, + updatedAt, }; } } diff --git a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js index 9ead5d5f32..7f08357a89 100644 --- a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js +++ b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js @@ -67,11 +67,16 @@ module.exports = { key: 'run_number', }, }, - invalidated_at: { + created_at: { type: Sequelize.DATE(3), allowNull: false, defaultValue: Sequelize.literal('CURRENT_TIMESTAMP(3)'), }, + updated_at: { + type: Sequelize.DATE(3), + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), + }, }, { transaction }); }), diff --git a/lib/database/models/gaqSummaryInvalidation.js b/lib/database/models/gaqSummaryInvalidation.js index f99b5d0895..3c0944b9bc 100644 --- a/lib/database/models/gaqSummaryInvalidation.js +++ b/lib/database/models/gaqSummaryInvalidation.js @@ -21,9 +21,6 @@ module.exports = (sequelize) => { runNumber: { type: Sequelize.INTEGER, }, - invalidatedAt: { - type: Sequelize.DATE(3), - }, }, { tableName: 'gaq_summary_invalidations' }); GaqSummaryInvalidation.removeAttribute('id'); diff --git a/lib/database/repositories/GaqSummaryInvalidationRepository.js b/lib/database/repositories/GaqSummaryInvalidationRepository.js new file mode 100644 index 0000000000..c6608f9e83 --- /dev/null +++ b/lib/database/repositories/GaqSummaryInvalidationRepository.js @@ -0,0 +1,33 @@ +/** + * @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: { + GaqSummaryInvalidation, + }, +} = require('..'); +const Repository = require('./Repository'); + +/** + * GaqSummaryInvalidation repository + */ +class GaqSummaryInvalidationRepository extends Repository { + /** + * Creates a new `GaqSummaryInvalidationRepository` instance. + */ + constructor() { + super(GaqSummaryInvalidation); + } +} + +module.exports = new GaqSummaryInvalidationRepository(); diff --git a/lib/database/repositories/GaqSummaryRepository.js b/lib/database/repositories/GaqSummaryRepository.js new file mode 100644 index 0000000000..98986de513 --- /dev/null +++ b/lib/database/repositories/GaqSummaryRepository.js @@ -0,0 +1,33 @@ +/** + * @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: { + GaqSummary, + }, +} = require('..'); +const Repository = require('./Repository'); + +/** + * GaqSummary repository + */ +class GaqSummaryRepository extends Repository { + /** + * Creates a new `GaqSummaryRepository` instance. + */ + constructor() { + super(GaqSummary); + } +} + +module.exports = new GaqSummaryRepository(); diff --git a/lib/database/repositories/index.js b/lib/database/repositories/index.js index 0c79279752..417ccd5375 100644 --- a/lib/database/repositories/index.js +++ b/lib/database/repositories/index.js @@ -27,6 +27,8 @@ const EnvironmentRepository = require('./EnvironmentRepository'); const EorReasonRepository = require('./EorReasonRepository'); const FlpRoleRepository = require('./FlpRoleRepository'); const GaqDetectorRepository = require('./GaqDetectorRepository.js'); +const GaqSummaryInvalidationRepository = require('./GaqSummaryInvalidationRepository.js'); +const GaqSummaryRepository = require('./GaqSummaryRepository.js'); const HostRepository = require('./HostRepository.js'); const LhcFillRepository = require('./LhcFillRepository'); const LhcFillStatisticsRepository = require('./LhcFillStatisticsRepository.js'); @@ -70,6 +72,8 @@ module.exports = { EorReasonRepository, FlpRoleRepository, GaqDetectorRepository, + GaqSummaryInvalidationRepository, + GaqSummaryRepository, HostRepository, LhcFillRepository, LhcFillStatisticsRepository, From 15afdaac85826129a0b127db26be1a3d355c5fdf Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:15:14 +0100 Subject: [PATCH 03/20] [O2B-1545] Use default Sequelize createdAt/updatedAt instead of computedAt --- lib/database/adapters/GaqSummaryAdapter.js | 6 ++++-- .../v1/20260223120000-create-gaq-summary-tables.js | 8 +++++++- lib/database/models/gaqSummary.js | 3 --- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/database/adapters/GaqSummaryAdapter.js b/lib/database/adapters/GaqSummaryAdapter.js index f069b0bfa7..88d7394126 100644 --- a/lib/database/adapters/GaqSummaryAdapter.js +++ b/lib/database/adapters/GaqSummaryAdapter.js @@ -37,7 +37,8 @@ class GaqSummaryAdapter { mcReproducible, missingVerificationsCount, undefinedQualityPeriodsCount, - computedAt, + createdAt, + updatedAt, } = databaseObject; return { @@ -48,7 +49,8 @@ class GaqSummaryAdapter { mcReproducible, missingVerificationsCount, undefinedQualityPeriodsCount, - computedAt, + createdAt, + updatedAt, }; } } diff --git a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js index 7f08357a89..1e404d3645 100644 --- a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js +++ b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js @@ -42,9 +42,15 @@ module.exports = { type: Sequelize.INTEGER, allowNull: false, }, - computed_at: { + created_at: { type: Sequelize.DATE(3), allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP(3)'), + }, + updated_at: { + type: Sequelize.DATE(3), + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), }, }, { transaction }); diff --git a/lib/database/models/gaqSummary.js b/lib/database/models/gaqSummary.js index 80e21f0145..55b33b3521 100644 --- a/lib/database/models/gaqSummary.js +++ b/lib/database/models/gaqSummary.js @@ -36,9 +36,6 @@ module.exports = (sequelize) => { undefinedQualityPeriodsCount: { type: Sequelize.INTEGER, }, - computedAt: { - type: Sequelize.DATE(3), - }, }, { tableName: 'gaq_summaries' }); GaqSummary.removeAttribute('id'); From 363f09bed1b972877e491ca6f5c7a224617b8392 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:03:35 +0100 Subject: [PATCH 04/20] [O2B-1545] Use mcReproducibleCoverage float instead of boolean Rename and change mcReproducible to be the coverage float not the boolean. --- lib/database/adapters/GaqSummaryAdapter.js | 4 ++-- .../migrations/v1/20260223120000-create-gaq-summary-tables.js | 4 ++-- lib/database/models/gaqSummary.js | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/database/adapters/GaqSummaryAdapter.js b/lib/database/adapters/GaqSummaryAdapter.js index 88d7394126..fd9ba4aa0e 100644 --- a/lib/database/adapters/GaqSummaryAdapter.js +++ b/lib/database/adapters/GaqSummaryAdapter.js @@ -34,7 +34,7 @@ class GaqSummaryAdapter { runNumber, badEffectiveRunCoverage, explicitlyNotBadEffectiveRunCoverage, - mcReproducible, + mcReproducibleCoverage, missingVerificationsCount, undefinedQualityPeriodsCount, createdAt, @@ -46,7 +46,7 @@ class GaqSummaryAdapter { runNumber, badEffectiveRunCoverage, explicitlyNotBadEffectiveRunCoverage, - mcReproducible, + mcReproducibleCoverage, missingVerificationsCount, undefinedQualityPeriodsCount, createdAt, diff --git a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js index 1e404d3645..8122607a37 100644 --- a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js +++ b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js @@ -30,8 +30,8 @@ module.exports = { type: Sequelize.FLOAT, allowNull: false, }, - mc_reproducible: { - type: Sequelize.BOOLEAN, + mc_reproducible_coverage: { + type: Sequelize.FLOAT, allowNull: false, }, missing_verifications_count: { diff --git a/lib/database/models/gaqSummary.js b/lib/database/models/gaqSummary.js index 55b33b3521..de91be13f2 100644 --- a/lib/database/models/gaqSummary.js +++ b/lib/database/models/gaqSummary.js @@ -30,6 +30,9 @@ module.exports = (sequelize) => { mcReproducible: { type: Sequelize.BOOLEAN, }, + mcReproducibleCoverage: { + type: Sequelize.FLOAT, + }, missingVerificationsCount: { type: Sequelize.INTEGER, }, From 002f3012a18f31ebaff85827935529156ce2909f Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:49:41 +0100 Subject: [PATCH 05/20] [O2B-1545] Remove mcReproducible field Forgot to remove. --- lib/database/models/gaqSummary.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/database/models/gaqSummary.js b/lib/database/models/gaqSummary.js index de91be13f2..f3338e5557 100644 --- a/lib/database/models/gaqSummary.js +++ b/lib/database/models/gaqSummary.js @@ -27,9 +27,6 @@ module.exports = (sequelize) => { explicitlyNotBadEffectiveRunCoverage: { type: Sequelize.FLOAT, }, - mcReproducible: { - type: Sequelize.BOOLEAN, - }, mcReproducibleCoverage: { type: Sequelize.FLOAT, }, From 5bebbb5ac73f011462d327823b0e6c5f1ace192e Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:24:00 +0100 Subject: [PATCH 06/20] [O2B-1563] Invalidate GAQ summaries on related changes Add GAQ summary invalidation whenever underlying data affecting GAQ changes. These changes ensure GAQ summary caches/records are marked for recomputation whenever detectors, QC flags, or run QC times that influence GAQ summaries are modified. --- .../services/gaq/GaqDetectorsService.js | 16 +++++- .../qualityControlFlag/QcFlagService.js | 50 +++++++++++++++++-- lib/server/services/run/updateRun.js | 25 +++++++++- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/lib/server/services/gaq/GaqDetectorsService.js b/lib/server/services/gaq/GaqDetectorsService.js index 1ac2a0dc7e..6c19d1cd4a 100644 --- a/lib/server/services/gaq/GaqDetectorsService.js +++ b/lib/server/services/gaq/GaqDetectorsService.js @@ -12,7 +12,7 @@ */ const { gaqDetectorAdapter, detectorAdapter } = require('../../../database/adapters'); -const { GaqDetectorRepository, RunRepository, DetectorRepository } = require('../../../database/repositories'); +const { GaqDetectorRepository, RunRepository, DetectorRepository, GaqSummaryInvalidationRepository } = require('../../../database/repositories'); const { BadParameterError } = require('../../errors/BadParameterError'); const { dataSource } = require('../../../database/DataSource.js'); const { Op } = require('sequelize'); @@ -57,6 +57,13 @@ class GaqDetectorService { .flatMap((runNumber) => detectorIds .map((detectorId) => ({ dataPassId, runNumber, detectorId }))); const createdEntries = await GaqDetectorRepository.insertAll(gaqEntries); + + // Invalidate GAQ summaries for all affected runs + await Promise.all(runNumbers.map((runNumber) => GaqSummaryInvalidationRepository.upsert({ + dataPassId, + runNumber, + }))); + return createdEntries.map(gaqDetectorAdapter.toEntity); }); } @@ -101,6 +108,13 @@ class GaqDetectorService { .flatMap(({ runNumber, detectors }) => detectors .map(({ id: detectorId }) => ({ dataPassId, runNumber, detectorId }))); const createdEntries = await GaqDetectorRepository.insertAll(gaqEntries); + + // Invalidate GAQ summaries for all affected runs + await Promise.all(runNumbers.map((runNumber) => GaqSummaryInvalidationRepository.upsert({ + dataPassId, + runNumber, + }))); + return createdEntries.map(gaqDetectorAdapter.toEntity); }, { transaction }); } diff --git a/lib/server/services/qualityControlFlag/QcFlagService.js b/lib/server/services/qualityControlFlag/QcFlagService.js index 3d34e1446b..3533716572 100644 --- a/lib/server/services/qualityControlFlag/QcFlagService.js +++ b/lib/server/services/qualityControlFlag/QcFlagService.js @@ -20,6 +20,7 @@ const { RunRepository, QcFlagVerificationRepository, QcFlagEffectivePeriodRepository, + GaqSummaryInvalidationRepository, }, } = require('../../../database/index.js'); const { dataSource } = require('../../../database/DataSource.js'); @@ -209,6 +210,14 @@ class QcFlagService { ], }); + if (dataPass) { + // Invalidate GAQ summary for the dataPass and runNumber of the created flag + await GaqSummaryInvalidationRepository.upsert({ + dataPassId: dataPassIdentifier, + runNumber, + }); + } + createdFlags.push(qcFlagAdapter.toEntity(createdFlag)); } catch (error) { this._logger.warnMessage(`Failed to create QC flag with properties: ${JSON.stringify(qcFlag)}. Error: ${error}`); @@ -284,6 +293,15 @@ class QcFlagService { { const { id, from, to, origin, createdById, runNumber, dplDetectorId, flagTypeId, createdAt } = qcFlag; + + if (dataPassId) { + // Invalidate GAQ summary for the dataPass and runNumber of the created flag + await GaqSummaryInvalidationRepository.upsert({ + dataPassId: dataPassId, + runNumber, + }); + } + const qcFlagPropertiesToLog = { id, from, @@ -338,8 +356,8 @@ class QcFlagService { await QcFlagEffectivePeriodRepository.removeAll({ where: { id: { [Op.in]: effectivePeriodIds } } }); // Sequelize update requires a where and can't work only using association - const qcFlagIds = (await QcFlagRepository.findAll({ - attributes: ['id'], + const qcFlagIdsToRunNumbers = (await QcFlagRepository.findAll({ + attributes: ['id', 'runNumber'], include: { association: 'dataPasses', attributes: [], @@ -349,13 +367,23 @@ class QcFlagService { }, }, raw: true, - })).map(({ id }) => id); + })).map(({ id, runNumber }) => ({ id, runNumber })); + + const qcFlagIds = qcFlagIdsToRunNumbers.map(({ id }) => id); + const runNumbers = new Set(); + qcFlagIdsToRunNumbers.map(({ runNumber }) => runNumbers.add(runNumber)); await QcFlagRepository.updateAll( { deleted: true }, { where: { id: qcFlagIds } }, ); + // Invalidate GAQ summary for the dataPass and all runNumbers of the deleted flags + await Promise.all(Array.from(runNumbers).map((runNumber) => GaqSummaryInvalidationRepository.upsert({ + dataPassId, + runNumber, + }))); + return qcFlagIds.length; }); } @@ -389,7 +417,21 @@ class QcFlagService { createdById: user.id, }); - return await this.getOneOrFail(flagId); + const updatedQcFlag = await this.getOneOrFail(flagId); + + // Get data pass id for the flag (if exists) to invalidate only related GAQ summaries + const dataPassQcFlag = await DataPassQcFlagRepository.findOne({ where: { qualityControlFlagId: flagId } }); + const dataPassId = dataPassQcFlag?.dataPassId; + + // Invalidate GAQ summary if it's the first verification + if (dataPassId && (!qcFlag.verifications || qcFlag.verifications.length === 0)) { + await GaqSummaryInvalidationRepository.upsert({ + dataPassId, + runNumber: updatedQcFlag.runNumber, + }); + } + + return updatedQcFlag; }, { transaction }); } diff --git a/lib/server/services/run/updateRun.js b/lib/server/services/run/updateRun.js index 2e8b50048c..98c7247c53 100644 --- a/lib/server/services/run/updateRun.js +++ b/lib/server/services/run/updateRun.js @@ -12,6 +12,8 @@ */ const RunRepository = require('../../../database/repositories/RunRepository.js'); +const GaqSummaryInvalidationRepository = require('../../../database/repositories/GaqSummaryInvalidationRepository.js'); +const DataPassRunRepository = require('../../../database/repositories/DataPassRunRepository.js'); const { getRunOrFail } = require('./getRunOrFail.js'); const { utilities: { TransactionHelper } } = require('../../../database'); const { checkLhcFill } = require('../../../usecases/lhcFill/checkLhcFill.js'); @@ -82,7 +84,12 @@ exports.updateRun = async (identifier, payload, transaction) => { } // Store the run quality to create a log if it changed - const previousRun = { runQuality: runModel?.runQuality, calibrationStatus: runModel?.calibrationStatus }; + const previousRun = { + runQuality: runModel?.runQuality, + calibrationStatus: runModel?.calibrationStatus, + qcTimeStart: runModel?.qcTimeStart, + qcTimeEnd: runModel?.qcTimeEnd, + }; runPatch.definition = runPatch.definition ?? getRunDefinition({ ...runModel.dataValues, @@ -204,6 +211,22 @@ exports.updateRun = async (identifier, payload, transaction) => { } } + // Need to reload the runModel as qcTimeStart and qcTimeEnd are virtual columns not in the patch so will stay stale after update + await runModel.reload(); + if (previousRun.qcTimeStart?.getTime() !== runModel.qcTimeStart?.getTime() + || previousRun.qcTimeEnd?.getTime() !== runModel.qcTimeEnd?.getTime()) { + const dataPassRuns = await DataPassRunRepository.findAll({ + attributes: ['dataPassId'], + where: { runNumber: runModel.runNumber }, + }); + for (const { dataPassId } of dataPassRuns) { + await GaqSummaryInvalidationRepository.upsert({ + dataPassId, + runNumber: runModel.runNumber, + }); + } + } + return runModel; }, { transaction }); }; From 16d3563ef0813e8c10de405ef7d9cda50eed8b3c Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:34:34 +0100 Subject: [PATCH 07/20] [O2B-1545] Rename GAQ summary coverage fields --- lib/database/adapters/GaqSummaryAdapter.js | 8 ++++---- .../v1/20260223120000-create-gaq-summary-tables.js | 4 ++-- lib/database/models/gaqSummary.js | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/database/adapters/GaqSummaryAdapter.js b/lib/database/adapters/GaqSummaryAdapter.js index fd9ba4aa0e..77ff939d69 100644 --- a/lib/database/adapters/GaqSummaryAdapter.js +++ b/lib/database/adapters/GaqSummaryAdapter.js @@ -32,8 +32,8 @@ class GaqSummaryAdapter { const { dataPassId, runNumber, - badEffectiveRunCoverage, - explicitlyNotBadEffectiveRunCoverage, + badRunCoverage, + explicitlyNotBadRunCoverage, mcReproducibleCoverage, missingVerificationsCount, undefinedQualityPeriodsCount, @@ -44,8 +44,8 @@ class GaqSummaryAdapter { return { dataPassId, runNumber, - badEffectiveRunCoverage, - explicitlyNotBadEffectiveRunCoverage, + badRunCoverage, + explicitlyNotBadRunCoverage, mcReproducibleCoverage, missingVerificationsCount, undefinedQualityPeriodsCount, diff --git a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js index 8122607a37..b734f32e32 100644 --- a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js +++ b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js @@ -22,11 +22,11 @@ module.exports = { key: 'run_number', }, }, - bad_effective_run_coverage: { + bad_run_coverage: { type: Sequelize.FLOAT, allowNull: false, }, - explicitly_not_bad_effective_run_coverage: { + explicitly_not_bad_run_coverage: { type: Sequelize.FLOAT, allowNull: false, }, diff --git a/lib/database/models/gaqSummary.js b/lib/database/models/gaqSummary.js index f3338e5557..480a9a4943 100644 --- a/lib/database/models/gaqSummary.js +++ b/lib/database/models/gaqSummary.js @@ -21,10 +21,10 @@ module.exports = (sequelize) => { runNumber: { type: Sequelize.INTEGER, }, - badEffectiveRunCoverage: { + badRunCoverage: { type: Sequelize.FLOAT, }, - explicitlyNotBadEffectiveRunCoverage: { + explicitlyNotBadRunCoverage: { type: Sequelize.FLOAT, }, mcReproducibleCoverage: { From 170c1b37f6556e0e39c8f9b54e62bf737523587b Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:41:48 +0200 Subject: [PATCH 08/20] [O2B-1563] Add GAQ summary invalidation trigger tests Validate GAQ summary invalidation behaviour on QC flag create/verify/delete, deleteAllForDataPass, explicit/default GAQ detector changes, and run QC time updates. --- .../server/services/gaq/GaqSummary.test.js | 121 ++++++++++++++++++ test/lib/server/services/gaq/index.js | 2 + test/lib/server/services/index.js | 4 +- 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 test/lib/server/services/gaq/GaqSummary.test.js diff --git a/test/lib/server/services/gaq/GaqSummary.test.js b/test/lib/server/services/gaq/GaqSummary.test.js new file mode 100644 index 0000000000..bffd5c5f05 --- /dev/null +++ b/test/lib/server/services/gaq/GaqSummary.test.js @@ -0,0 +1,121 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const { expect } = require('chai'); +const { resetDatabaseContent } = require('../../../../utilities/resetDatabaseContent.js'); +const { repositories: { GaqSummaryInvalidationRepository } } = require('../../../../../lib/database'); +const { qcFlagService } = require('../../../../../lib/server/services/qualityControlFlag/QcFlagService.js'); +const { gaqDetectorService } = require('../../../../../lib/server/services/gaq/GaqDetectorsService.js'); +const { updateRun } = require('../../../../../lib/server/services/run/updateRun.js'); + +module.exports = () => { + // Test resets the database before running and clears the invalidation table between each case + before(async () => { + await resetDatabaseContent(); + }); + + const relations = { user: { roles: ['admin'], externalUserId: 1 } }; + const dataPassId = 4; // LHC22a_apass2 + const runNumber = 56; + + /** + * Check whether an invalidation entry exists for a given data pass and run + * + * @param {number} expectedDataPassId + * @param {number} expectedRunNumber + * @param {boolean} toBeNull + * + * @return {Promise} + */ + const expectInvalidation = async (expectedDataPassId, expectedRunNumber, toBeNull = false) => { + const invalidation = await GaqSummaryInvalidationRepository.findOne({ + where: { dataPassId: expectedDataPassId, runNumber: expectedRunNumber }, + }); + if (toBeNull) { + expect(invalidation, `Expected no invalidation for dataPassId=${expectedDataPassId} runNumber=${expectedRunNumber}`).to.be.null; + } else { + expect(invalidation, `Expected invalidation for dataPassId=${expectedDataPassId} runNumber=${expectedRunNumber}`).to.not.be.null; + } + }; + + describe("GAQ Summary Invalidation", async () => { + afterEach(async () => { + await GaqSummaryInvalidationRepository.removeAll({ truncate: true }); + }); + + it('should invalidate GAQ summary when a QC flag is created for a data pass', async () => { + await qcFlagService.create( + [{ from: null, to: null, flagTypeId: 3 }], + { runNumber, detectorIdentifier: { detectorId: 7 }, dataPassIdentifier: { id: dataPassId } }, + relations, + ); + + await expectInvalidation(dataPassId, runNumber); + }); + + it('should invalidate GAQ summary when a QC flag is verified for the first time for a data pass', async () => { + const flagId = 8; // Seeded flag in data pass 4, run 100, with no verifications + + await qcFlagService.verifyFlag({ flagId }, relations); + + await expectInvalidation(dataPassId, 100); + + // Clear invalidation + await GaqSummaryInvalidationRepository.removeAll({ where: { dataPassId, runNumber: 100 } }); + + // Verify again to check that no new invalidation is created when the flag is already verified + await qcFlagService.verifyFlag({ flagId }, relations); + await expectInvalidation(dataPassId, 100, true); + }); + + it('should invalidate GAQ summary when a QC flag is deleted for a data pass', async () => { + const flagId = 9; // Seeded flag in data pass 4, run 105, with no verifications (so deletion is allowed) + await qcFlagService.delete(flagId); + + await expectInvalidation(dataPassId, 105); + }); + + it('should invalidate GAQ summary for all runs when all QC flags are deleted for a data pass', async () => { + await qcFlagService.deleteAllForDataPass(dataPassId); + + await expectInvalidation(dataPassId, 100); + await expectInvalidation(dataPassId, 105); + }); + + it('should invalidate GAQ summary when GAQ detectors are explicitly set for a data pass and run', async () => { + const gaqDataPassId = 3; // LHC22a_apass1 (has run 56 and detectors set up in GaqDetectorService tests) + const detectorIds = [4, 7]; + + await gaqDetectorService.setGaqDetectors(gaqDataPassId, [runNumber], detectorIds); + + await expectInvalidation(gaqDataPassId, runNumber); + }); + + it('should invalidate GAQ summary when default GAQ detectors are used for a data pass and run', async () => { + const gaqDataPassId = 3; + + await gaqDetectorService.useDefaultGaqDetectors(gaqDataPassId, [runNumber]); + + await expectInvalidation(gaqDataPassId, runNumber); + }); + + it('should invalidate GAQ summary when the QC time of a run changes', async () => { + await updateRun( + { runNumber }, + { runPatch: { timeTrgStart: new Date('2019-08-08 20:30:00') } }, + ); + + await expectInvalidation(dataPassId, runNumber); + }); + }); +}; diff --git a/test/lib/server/services/gaq/index.js b/test/lib/server/services/gaq/index.js index 985f80fe84..93e10b2ac6 100644 --- a/test/lib/server/services/gaq/index.js +++ b/test/lib/server/services/gaq/index.js @@ -12,7 +12,9 @@ */ const GaqDetectorServiceSuite = require('./GaqDetectorService.test.js'); +const GaqSummarySuite = require('./GaqSummary.test.js'); module.exports = () => { describe('GaqDetectorService', GaqDetectorServiceSuite); + describe('GaqSummary', GaqSummarySuite); }; diff --git a/test/lib/server/services/index.js b/test/lib/server/services/index.js index 01df33fcaf..38aabbf6dd 100644 --- a/test/lib/server/services/index.js +++ b/test/lib/server/services/index.js @@ -32,7 +32,7 @@ const UserSuite = require('./user/index.js'); const SimulationPassesSuite = require('./simulationPasses/index.js'); const QcFlagsSuite = require('./qualityControlFlag/index.js'); const CtpTriggerCountersSuite = require('./ctpTriggerCounters/index.js'); -const GaqDetectorSuite = require('./gaq'); +const GaqSuite = require('./gaq'); module.exports = () => { before(resetDatabaseContent); @@ -46,7 +46,7 @@ module.exports = () => { describe('Environment history item', EnvironmentHistoryItemSuite); describe('EOS report', EosReportSuite); describe('Flp role', FlpRoleSuite); - describe('GaqDetector', GaqDetectorSuite); + describe('GAQ', GaqSuite); describe('LHC fill suite', LhcFillSuite); describe('Logs', LogSuite); describe('RunType', RunTypeSuite); From 78a2181986fd52f57a28435efdf160897919c9c1 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:46:57 +0200 Subject: [PATCH 09/20] [O2B-1563] Use dataPass.id in GAQ invalidation TYPO --- lib/server/services/qualityControlFlag/QcFlagService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/server/services/qualityControlFlag/QcFlagService.js b/lib/server/services/qualityControlFlag/QcFlagService.js index 3533716572..3f0ca96d79 100644 --- a/lib/server/services/qualityControlFlag/QcFlagService.js +++ b/lib/server/services/qualityControlFlag/QcFlagService.js @@ -213,7 +213,7 @@ class QcFlagService { if (dataPass) { // Invalidate GAQ summary for the dataPass and runNumber of the created flag await GaqSummaryInvalidationRepository.upsert({ - dataPassId: dataPassIdentifier, + dataPassId: dataPass.id, runNumber, }); } From c2ccbd476f83ce5ffb60097db8d1f373c61812b5 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 17 Apr 2026 15:52:40 +0200 Subject: [PATCH 10/20] Add calculationFailed to GAQ summaries Add a calculation_failed boolean column to gaq_summaries. This allows us to know whether a summary has been attempted to be calculated but unsuccessful due to limited data etc. --- lib/database/adapters/GaqSummaryAdapter.js | 2 ++ .../v1/20260223120000-create-gaq-summary-tables.js | 5 +++++ lib/database/models/gaqSummary.js | 3 +++ 3 files changed, 10 insertions(+) diff --git a/lib/database/adapters/GaqSummaryAdapter.js b/lib/database/adapters/GaqSummaryAdapter.js index 77ff939d69..21cd7b7004 100644 --- a/lib/database/adapters/GaqSummaryAdapter.js +++ b/lib/database/adapters/GaqSummaryAdapter.js @@ -37,6 +37,7 @@ class GaqSummaryAdapter { mcReproducibleCoverage, missingVerificationsCount, undefinedQualityPeriodsCount, + calculationFailed, createdAt, updatedAt, } = databaseObject; @@ -49,6 +50,7 @@ class GaqSummaryAdapter { mcReproducibleCoverage, missingVerificationsCount, undefinedQualityPeriodsCount, + calculationFailed, createdAt, updatedAt, }; diff --git a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js index b734f32e32..3a787b02e7 100644 --- a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js +++ b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js @@ -42,6 +42,11 @@ module.exports = { type: Sequelize.INTEGER, allowNull: false, }, + calculation_failed: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, created_at: { type: Sequelize.DATE(3), allowNull: false, diff --git a/lib/database/models/gaqSummary.js b/lib/database/models/gaqSummary.js index 480a9a4943..aab9a04dfd 100644 --- a/lib/database/models/gaqSummary.js +++ b/lib/database/models/gaqSummary.js @@ -36,6 +36,9 @@ module.exports = (sequelize) => { undefinedQualityPeriodsCount: { type: Sequelize.INTEGER, }, + calculationFailed: { + type: Sequelize.BOOLEAN, + }, }, { tableName: 'gaq_summaries' }); GaqSummary.removeAttribute('id'); From 35112e5e34b7c8f75240e086dd1cd100abc395da Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:15:47 +0200 Subject: [PATCH 11/20] [O2B-1545] Make GAQ summary columns nullable Remove not-null constraints from several GAQ summary columns in the migration to allow NULL when values are unavailable. --- .../v1/20260223120000-create-gaq-summary-tables.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js index 3a787b02e7..4540a01c4a 100644 --- a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js +++ b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js @@ -24,23 +24,18 @@ module.exports = { }, bad_run_coverage: { type: Sequelize.FLOAT, - allowNull: false, }, explicitly_not_bad_run_coverage: { type: Sequelize.FLOAT, - allowNull: false, }, mc_reproducible_coverage: { type: Sequelize.FLOAT, - allowNull: false, }, missing_verifications_count: { type: Sequelize.INTEGER, - allowNull: false, }, undefined_quality_periods_count: { type: Sequelize.INTEGER, - allowNull: false, }, calculation_failed: { type: Sequelize.BOOLEAN, From 5059082f89dae13ed149d910b413e0757e87c0b3 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Mon, 4 May 2026 14:20:48 +0200 Subject: [PATCH 12/20] [O2B-1545] Add invalidatedAt to gaq_summaries, remove invalidation table Store invalidation timestamp directly on gaq_summaries instead of a separate gaq_summary_invalidations table. --- lib/database/adapters/GaqSummaryAdapter.js | 2 ++ ...0260223120000-create-gaq-summary-tables.js | 33 ++----------------- lib/database/models/gaqSummary.js | 3 ++ 3 files changed, 8 insertions(+), 30 deletions(-) diff --git a/lib/database/adapters/GaqSummaryAdapter.js b/lib/database/adapters/GaqSummaryAdapter.js index 21cd7b7004..262fceb5be 100644 --- a/lib/database/adapters/GaqSummaryAdapter.js +++ b/lib/database/adapters/GaqSummaryAdapter.js @@ -38,6 +38,7 @@ class GaqSummaryAdapter { missingVerificationsCount, undefinedQualityPeriodsCount, calculationFailed, + invalidatedAt, createdAt, updatedAt, } = databaseObject; @@ -51,6 +52,7 @@ class GaqSummaryAdapter { missingVerificationsCount, undefinedQualityPeriodsCount, calculationFailed, + invalidatedAt, createdAt, updatedAt, }; diff --git a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js index 4540a01c4a..05b2693702 100644 --- a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js +++ b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js @@ -42,36 +42,10 @@ module.exports = { allowNull: false, defaultValue: false, }, - created_at: { + invalidated_at: { type: Sequelize.DATE(3), - allowNull: false, - defaultValue: Sequelize.literal('CURRENT_TIMESTAMP(3)'), - }, - updated_at: { - type: Sequelize.DATE(3), - allowNull: false, - defaultValue: Sequelize.literal('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), - }, - }, { transaction }); - - await queryInterface.createTable('gaq_summary_invalidations', { - data_pass_id: { - type: Sequelize.INTEGER, - primaryKey: true, - allowNull: false, - references: { - model: 'data_passes', - key: 'id', - }, - }, - run_number: { - type: Sequelize.INTEGER, - primaryKey: true, - allowNull: false, - references: { - model: 'runs', - key: 'run_number', - }, + allowNull: true, + defaultValue: null, }, created_at: { type: Sequelize.DATE(3), @@ -87,7 +61,6 @@ module.exports = { }), down: async (queryInterface) => queryInterface.sequelize.transaction(async (transaction) => { - await queryInterface.dropTable('gaq_summary_invalidations', { transaction }); await queryInterface.dropTable('gaq_summaries', { transaction }); }), }; diff --git a/lib/database/models/gaqSummary.js b/lib/database/models/gaqSummary.js index aab9a04dfd..97099f256d 100644 --- a/lib/database/models/gaqSummary.js +++ b/lib/database/models/gaqSummary.js @@ -39,6 +39,9 @@ module.exports = (sequelize) => { calculationFailed: { type: Sequelize.BOOLEAN, }, + invalidatedAt: { + type: Sequelize.DATE(3), + }, }, { tableName: 'gaq_summaries' }); GaqSummary.removeAttribute('id'); From 9893667ba7ccb7629c8c66d1b77cec5fb11c580d Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Mon, 4 May 2026 14:23:39 +0200 Subject: [PATCH 13/20] [O2B-1545] Rename calculationFailed to notComputable --- lib/database/adapters/GaqSummaryAdapter.js | 4 ++-- .../migrations/v1/20260223120000-create-gaq-summary-tables.js | 2 +- lib/database/models/gaqSummary.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/database/adapters/GaqSummaryAdapter.js b/lib/database/adapters/GaqSummaryAdapter.js index 262fceb5be..53b6baf61a 100644 --- a/lib/database/adapters/GaqSummaryAdapter.js +++ b/lib/database/adapters/GaqSummaryAdapter.js @@ -37,7 +37,7 @@ class GaqSummaryAdapter { mcReproducibleCoverage, missingVerificationsCount, undefinedQualityPeriodsCount, - calculationFailed, + notComputable, invalidatedAt, createdAt, updatedAt, @@ -51,7 +51,7 @@ class GaqSummaryAdapter { mcReproducibleCoverage, missingVerificationsCount, undefinedQualityPeriodsCount, - calculationFailed, + notComputable, invalidatedAt, createdAt, updatedAt, diff --git a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js index 05b2693702..9366d050ce 100644 --- a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js +++ b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js @@ -37,7 +37,7 @@ module.exports = { undefined_quality_periods_count: { type: Sequelize.INTEGER, }, - calculation_failed: { + not_computable: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false, diff --git a/lib/database/models/gaqSummary.js b/lib/database/models/gaqSummary.js index 97099f256d..efa76e04fd 100644 --- a/lib/database/models/gaqSummary.js +++ b/lib/database/models/gaqSummary.js @@ -36,7 +36,7 @@ module.exports = (sequelize) => { undefinedQualityPeriodsCount: { type: Sequelize.INTEGER, }, - calculationFailed: { + notComputable: { type: Sequelize.BOOLEAN, }, invalidatedAt: { From 47449a7da2403962c4e7417076d06807f38ad453 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Mon, 4 May 2026 14:31:43 +0200 Subject: [PATCH 14/20] [O2B-1545] Remove unused GaqSummaryInvalidation files and imports --- .../adapters/GaqSummaryInvalidationAdapter.js | 48 ------------------- lib/database/adapters/index.js | 3 -- lib/database/models/gaqSummaryInvalidation.js | 34 ------------- lib/database/models/index.js | 2 - .../GaqSummaryInvalidationRepository.js | 33 ------------- lib/database/repositories/index.js | 2 - 6 files changed, 122 deletions(-) delete mode 100644 lib/database/adapters/GaqSummaryInvalidationAdapter.js delete mode 100644 lib/database/models/gaqSummaryInvalidation.js delete mode 100644 lib/database/repositories/GaqSummaryInvalidationRepository.js diff --git a/lib/database/adapters/GaqSummaryInvalidationAdapter.js b/lib/database/adapters/GaqSummaryInvalidationAdapter.js deleted file mode 100644 index dbd76df3a8..0000000000 --- a/lib/database/adapters/GaqSummaryInvalidationAdapter.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -/** - * GaqSummaryInvalidationAdapter - */ -class GaqSummaryInvalidationAdapter { - /** - * Constructor - */ - constructor() { - this.toEntity = this.toEntity.bind(this); - } - - /** - * Converts the given database object to an entity object. - * - * @param {SequelizeGaqSummaryInvalidation} databaseObject Object to convert. - * @returns {GaqSummaryInvalidation} Converted entity object. - */ - toEntity(databaseObject) { - const { - dataPassId, - runNumber, - createdAt, - updatedAt, - } = databaseObject; - - return { - dataPassId, - runNumber, - createdAt, - updatedAt, - }; - } -} - -module.exports = { GaqSummaryInvalidationAdapter }; diff --git a/lib/database/adapters/index.js b/lib/database/adapters/index.js index 97d22b927f..9c858e1bc2 100644 --- a/lib/database/adapters/index.js +++ b/lib/database/adapters/index.js @@ -28,7 +28,6 @@ const FlpRoleAdapter = require('./FlpRoleAdapter'); const { HostAdapter } = require('./HostAdapter.js'); const { GaqDetectorAdapter } = require('./GaqDetectorAdapter.js'); const { GaqSummaryAdapter } = require('./GaqSummaryAdapter.js'); -const { GaqSummaryInvalidationAdapter } = require('./GaqSummaryInvalidationAdapter.js'); const { LhcFillAdapter } = require('./LhcFillAdapter.js'); const { LhcFillStatisticsAdapter } = require('./LhcFillStatisticsAdapter.js'); const LhcPeriodAdapter = require('./LhcPeriodAdapter'); @@ -66,7 +65,6 @@ const eorReasonAdapter = new EorReasonAdapter(); const flpRoleAdapter = new FlpRoleAdapter(); const gaqDetectorAdapter = new GaqDetectorAdapter(); const gaqSummaryAdapter = new GaqSummaryAdapter(); -const gaqSummaryInvalidationAdapter = new GaqSummaryInvalidationAdapter(); const hostAdapter = new HostAdapter(); const lhcFillAdapter = new LhcFillAdapter(); const lhcFillStatisticsAdapter = new LhcFillStatisticsAdapter(); @@ -164,7 +162,6 @@ module.exports = { flpRoleAdapter, gaqDetectorAdapter, gaqSummaryAdapter, - gaqSummaryInvalidationAdapter, hostAdapter, lhcFillAdapter, lhcFillStatisticsAdapter, diff --git a/lib/database/models/gaqSummaryInvalidation.js b/lib/database/models/gaqSummaryInvalidation.js deleted file mode 100644 index 3c0944b9bc..0000000000 --- a/lib/database/models/gaqSummaryInvalidation.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -module.exports = (sequelize) => { - const Sequelize = require('sequelize'); - - const GaqSummaryInvalidation = sequelize.define('GaqSummaryInvalidation', { - dataPassId: { - type: Sequelize.INTEGER, - }, - runNumber: { - type: Sequelize.INTEGER, - }, - }, { tableName: 'gaq_summary_invalidations' }); - - GaqSummaryInvalidation.removeAttribute('id'); - - GaqSummaryInvalidation.associate = (models) => { - GaqSummaryInvalidation.belongsTo(models.Run, { foreignKey: 'runNumber', as: 'run' }); - GaqSummaryInvalidation.belongsTo(models.DataPass, { foreignKey: 'dataPassId', as: 'dataPass' }); - }; - - return GaqSummaryInvalidation; -}; diff --git a/lib/database/models/index.js b/lib/database/models/index.js index 36736ce905..2549209c5b 100644 --- a/lib/database/models/index.js +++ b/lib/database/models/index.js @@ -28,7 +28,6 @@ const EpnRoleSession = require('./epnrolesession'); const FlpRole = require('./flprole'); const GaqDetector = require('./gaqDetector.js'); const GaqSummary = require('./gaqSummary.js'); -const GaqSummaryInvalidation = require('./gaqSummaryInvalidation.js'); const Host = require('./host.js'); const LhcFill = require('./lhcFill'); const LhcFillStatistics = require('./lhcFillStatistics.js'); @@ -69,7 +68,6 @@ module.exports = (sequelize) => { FlpRole: FlpRole(sequelize), GaqDetector: GaqDetector(sequelize), GaqSummary: GaqSummary(sequelize), - GaqSummaryInvalidation: GaqSummaryInvalidation(sequelize), Host: Host(sequelize), LhcFill: LhcFill(sequelize), LhcFillStatistics: LhcFillStatistics(sequelize), diff --git a/lib/database/repositories/GaqSummaryInvalidationRepository.js b/lib/database/repositories/GaqSummaryInvalidationRepository.js deleted file mode 100644 index c6608f9e83..0000000000 --- a/lib/database/repositories/GaqSummaryInvalidationRepository.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const { - models: { - GaqSummaryInvalidation, - }, -} = require('..'); -const Repository = require('./Repository'); - -/** - * GaqSummaryInvalidation repository - */ -class GaqSummaryInvalidationRepository extends Repository { - /** - * Creates a new `GaqSummaryInvalidationRepository` instance. - */ - constructor() { - super(GaqSummaryInvalidation); - } -} - -module.exports = new GaqSummaryInvalidationRepository(); diff --git a/lib/database/repositories/index.js b/lib/database/repositories/index.js index 417ccd5375..4be7600145 100644 --- a/lib/database/repositories/index.js +++ b/lib/database/repositories/index.js @@ -27,7 +27,6 @@ const EnvironmentRepository = require('./EnvironmentRepository'); const EorReasonRepository = require('./EorReasonRepository'); const FlpRoleRepository = require('./FlpRoleRepository'); const GaqDetectorRepository = require('./GaqDetectorRepository.js'); -const GaqSummaryInvalidationRepository = require('./GaqSummaryInvalidationRepository.js'); const GaqSummaryRepository = require('./GaqSummaryRepository.js'); const HostRepository = require('./HostRepository.js'); const LhcFillRepository = require('./LhcFillRepository'); @@ -72,7 +71,6 @@ module.exports = { EorReasonRepository, FlpRoleRepository, GaqDetectorRepository, - GaqSummaryInvalidationRepository, GaqSummaryRepository, HostRepository, LhcFillRepository, From 0b8b161e72b0787da3ce644529ce7b0bb5d7c1d9 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Mon, 4 May 2026 15:05:13 +0200 Subject: [PATCH 15/20] [O2B-1563] Use GaqSummaryRepository invalidation Replace usage of GaqSummaryInvalidationRepository with GaqSummaryRepository across services and tests. Upsert calls now set an invalidatedAt timestamp to mark GAQ summaries as invalidated. --- lib/server/services/gaq/GaqDetectorsService.js | 8 +++++--- .../services/qualityControlFlag/QcFlagService.js | 14 +++++++++----- lib/server/services/run/updateRun.js | 5 +++-- test/lib/server/services/gaq/GaqSummary.test.js | 14 +++++++------- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/server/services/gaq/GaqDetectorsService.js b/lib/server/services/gaq/GaqDetectorsService.js index 6c19d1cd4a..4411acdefa 100644 --- a/lib/server/services/gaq/GaqDetectorsService.js +++ b/lib/server/services/gaq/GaqDetectorsService.js @@ -12,7 +12,7 @@ */ const { gaqDetectorAdapter, detectorAdapter } = require('../../../database/adapters'); -const { GaqDetectorRepository, RunRepository, DetectorRepository, GaqSummaryInvalidationRepository } = require('../../../database/repositories'); +const { GaqDetectorRepository, RunRepository, DetectorRepository, GaqSummaryRepository } = require('../../../database/repositories'); const { BadParameterError } = require('../../errors/BadParameterError'); const { dataSource } = require('../../../database/DataSource.js'); const { Op } = require('sequelize'); @@ -59,9 +59,10 @@ class GaqDetectorService { const createdEntries = await GaqDetectorRepository.insertAll(gaqEntries); // Invalidate GAQ summaries for all affected runs - await Promise.all(runNumbers.map((runNumber) => GaqSummaryInvalidationRepository.upsert({ + await Promise.all(runNumbers.map((runNumber) => GaqSummaryRepository.upsert({ dataPassId, runNumber, + invalidatedAt: new Date(), }))); return createdEntries.map(gaqDetectorAdapter.toEntity); @@ -110,9 +111,10 @@ class GaqDetectorService { const createdEntries = await GaqDetectorRepository.insertAll(gaqEntries); // Invalidate GAQ summaries for all affected runs - await Promise.all(runNumbers.map((runNumber) => GaqSummaryInvalidationRepository.upsert({ + await Promise.all(runNumbers.map((runNumber) => GaqSummaryRepository.upsert({ dataPassId, runNumber, + invalidatedAt: new Date(), }))); return createdEntries.map(gaqDetectorAdapter.toEntity); diff --git a/lib/server/services/qualityControlFlag/QcFlagService.js b/lib/server/services/qualityControlFlag/QcFlagService.js index 3f0ca96d79..e3dd1a9dda 100644 --- a/lib/server/services/qualityControlFlag/QcFlagService.js +++ b/lib/server/services/qualityControlFlag/QcFlagService.js @@ -20,7 +20,7 @@ const { RunRepository, QcFlagVerificationRepository, QcFlagEffectivePeriodRepository, - GaqSummaryInvalidationRepository, + GaqSummaryRepository, }, } = require('../../../database/index.js'); const { dataSource } = require('../../../database/DataSource.js'); @@ -212,9 +212,10 @@ class QcFlagService { if (dataPass) { // Invalidate GAQ summary for the dataPass and runNumber of the created flag - await GaqSummaryInvalidationRepository.upsert({ + await GaqSummaryRepository.upsert({ dataPassId: dataPass.id, runNumber, + invalidatedAt: new Date(), }); } @@ -296,9 +297,10 @@ class QcFlagService { if (dataPassId) { // Invalidate GAQ summary for the dataPass and runNumber of the created flag - await GaqSummaryInvalidationRepository.upsert({ + await GaqSummaryRepository.upsert({ dataPassId: dataPassId, runNumber, + invalidatedAt: new Date(), }); } @@ -379,9 +381,10 @@ class QcFlagService { ); // Invalidate GAQ summary for the dataPass and all runNumbers of the deleted flags - await Promise.all(Array.from(runNumbers).map((runNumber) => GaqSummaryInvalidationRepository.upsert({ + await Promise.all(Array.from(runNumbers).map((runNumber) => GaqSummaryRepository.upsert({ dataPassId, runNumber, + invalidatedAt: new Date(), }))); return qcFlagIds.length; @@ -425,9 +428,10 @@ class QcFlagService { // Invalidate GAQ summary if it's the first verification if (dataPassId && (!qcFlag.verifications || qcFlag.verifications.length === 0)) { - await GaqSummaryInvalidationRepository.upsert({ + await GaqSummaryRepository.upsert({ dataPassId, runNumber: updatedQcFlag.runNumber, + invalidatedAt: new Date(), }); } diff --git a/lib/server/services/run/updateRun.js b/lib/server/services/run/updateRun.js index 98c7247c53..a7833ed107 100644 --- a/lib/server/services/run/updateRun.js +++ b/lib/server/services/run/updateRun.js @@ -12,7 +12,7 @@ */ const RunRepository = require('../../../database/repositories/RunRepository.js'); -const GaqSummaryInvalidationRepository = require('../../../database/repositories/GaqSummaryInvalidationRepository.js'); +const GaqSummaryRepository = require('../../../database/repositories/GaqSummaryRepository.js'); const DataPassRunRepository = require('../../../database/repositories/DataPassRunRepository.js'); const { getRunOrFail } = require('./getRunOrFail.js'); const { utilities: { TransactionHelper } } = require('../../../database'); @@ -220,9 +220,10 @@ exports.updateRun = async (identifier, payload, transaction) => { where: { runNumber: runModel.runNumber }, }); for (const { dataPassId } of dataPassRuns) { - await GaqSummaryInvalidationRepository.upsert({ + await GaqSummaryRepository.upsert({ dataPassId, runNumber: runModel.runNumber, + invalidatedAt: new Date(), }); } } diff --git a/test/lib/server/services/gaq/GaqSummary.test.js b/test/lib/server/services/gaq/GaqSummary.test.js index bffd5c5f05..b4f6ae920d 100644 --- a/test/lib/server/services/gaq/GaqSummary.test.js +++ b/test/lib/server/services/gaq/GaqSummary.test.js @@ -13,13 +13,12 @@ const { expect } = require('chai'); const { resetDatabaseContent } = require('../../../../utilities/resetDatabaseContent.js'); -const { repositories: { GaqSummaryInvalidationRepository } } = require('../../../../../lib/database'); +const { repositories: { GaqSummaryRepository } } = require('../../../../../lib/database'); const { qcFlagService } = require('../../../../../lib/server/services/qualityControlFlag/QcFlagService.js'); const { gaqDetectorService } = require('../../../../../lib/server/services/gaq/GaqDetectorsService.js'); const { updateRun } = require('../../../../../lib/server/services/run/updateRun.js'); module.exports = () => { - // Test resets the database before running and clears the invalidation table between each case before(async () => { await resetDatabaseContent(); }); @@ -38,19 +37,20 @@ module.exports = () => { * @return {Promise} */ const expectInvalidation = async (expectedDataPassId, expectedRunNumber, toBeNull = false) => { - const invalidation = await GaqSummaryInvalidationRepository.findOne({ + const summary = await GaqSummaryRepository.findOne({ where: { dataPassId: expectedDataPassId, runNumber: expectedRunNumber }, }); if (toBeNull) { - expect(invalidation, `Expected no invalidation for dataPassId=${expectedDataPassId} runNumber=${expectedRunNumber}`).to.be.null; + expect(summary?.invalidatedAt, `Expected no invalidation for dataPassId=${expectedDataPassId} runNumber=${expectedRunNumber}`).to.be.null; } else { - expect(invalidation, `Expected invalidation for dataPassId=${expectedDataPassId} runNumber=${expectedRunNumber}`).to.not.be.null; + expect(summary?.invalidatedAt, `Expected invalidation for dataPassId=${expectedDataPassId} runNumber=${expectedRunNumber}`).to.not.be.null; } }; describe("GAQ Summary Invalidation", async () => { + // Resetting the invalidated column between each case afterEach(async () => { - await GaqSummaryInvalidationRepository.removeAll({ truncate: true }); + await GaqSummaryRepository.updateAll({ invalidatedAt: null }, { where: {} }); }); it('should invalidate GAQ summary when a QC flag is created for a data pass', async () => { @@ -71,7 +71,7 @@ module.exports = () => { await expectInvalidation(dataPassId, 100); // Clear invalidation - await GaqSummaryInvalidationRepository.removeAll({ where: { dataPassId, runNumber: 100 } }); + await GaqSummaryRepository.updateAll({ invalidatedAt: null }, { where: { dataPassId, runNumber: 100 } }); // Verify again to check that no new invalidation is created when the flag is already verified await qcFlagService.verifyFlag({ flagId }, relations); From 772a96c179e35f1f705a6a04e6f41a518c627856 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 5 May 2026 10:23:03 +0200 Subject: [PATCH 16/20] [O2B-1545] Add index on gaq_summaries.invalidated_at Create an index on invalidated_at column of gaq_summaries table to improve query performance, as it is commonly used to fetch the invalidated summary queue. --- .../v1/20260223120000-create-gaq-summary-tables.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js index 9366d050ce..639db07c5c 100644 --- a/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js +++ b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js @@ -58,6 +58,11 @@ module.exports = { defaultValue: Sequelize.literal('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), }, }, { transaction }); + + await queryInterface.addIndex('gaq_summaries', { + name: 'gaq_summaries_invalidated_at_idx', + fields: ['invalidated_at'], + }, { transaction }); }), down: async (queryInterface) => queryInterface.sequelize.transaction(async (transaction) => { From 92e3c54af043f2eeb6a00c09dba982f05e8e4698 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Tue, 5 May 2026 12:32:20 +0200 Subject: [PATCH 17/20] [O2B-1563] Stealth fix: We do not need to soft delete flags that are already soft deleted The simplest way of invalidating GAQ summaries affected by the deletion of a dataPass' Flags is to just take the list of deleted flags. But this is inefficient if some flags were already deleted and thus had already had their summaries at the time. --- lib/server/services/qualityControlFlag/QcFlagService.js | 1 + test/api/qcFlags.test.js | 2 +- test/lib/server/services/gaq/GaqSummary.test.js | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/server/services/qualityControlFlag/QcFlagService.js b/lib/server/services/qualityControlFlag/QcFlagService.js index e3dd1a9dda..b1484df9b8 100644 --- a/lib/server/services/qualityControlFlag/QcFlagService.js +++ b/lib/server/services/qualityControlFlag/QcFlagService.js @@ -368,6 +368,7 @@ class QcFlagService { id: dataPassId, }, }, + where: { deleted: false }, raw: true, })).map(({ id, runNumber }) => ({ id, runNumber })); diff --git a/test/api/qcFlags.test.js b/test/api/qcFlags.test.js index ee67961439..092df4d883 100644 --- a/test/api/qcFlags.test.js +++ b/test/api/qcFlags.test.js @@ -1110,7 +1110,7 @@ module.exports = () => { .delete(`/api/qcFlags/perDataPass?dataPassId=${dataPassId}&token=${BkpRoles.DPG_ASYNC_QC_ADMIN}`); expect(response.status).to.be.equal(200); - expect(response.body.data.deletedCount).to.equal(11); // 9 from seeders, 2 created in POST requests previously in this test + expect(response.body.data.deletedCount).to.equal(10); // 9 from seeders, 2 created in POST requests previously in this test, 1 already soft-deleted }); }); diff --git a/test/lib/server/services/gaq/GaqSummary.test.js b/test/lib/server/services/gaq/GaqSummary.test.js index b4f6ae920d..62ad2a980e 100644 --- a/test/lib/server/services/gaq/GaqSummary.test.js +++ b/test/lib/server/services/gaq/GaqSummary.test.js @@ -89,7 +89,6 @@ module.exports = () => { await qcFlagService.deleteAllForDataPass(dataPassId); await expectInvalidation(dataPassId, 100); - await expectInvalidation(dataPassId, 105); }); it('should invalidate GAQ summary when GAQ detectors are explicitly set for a data pass and run', async () => { From f5f686c906213b99e563f1ceb81882c53bb2d345 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:15:32 +0200 Subject: [PATCH 18/20] [O2B-1545] Add a composite primary key on GAQSummary table Sequelize model omitted the primaryKey that migration file defines. --- lib/database/models/gaqSummary.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/database/models/gaqSummary.js b/lib/database/models/gaqSummary.js index efa76e04fd..3a12f62f33 100644 --- a/lib/database/models/gaqSummary.js +++ b/lib/database/models/gaqSummary.js @@ -17,9 +17,11 @@ module.exports = (sequelize) => { const GaqSummary = sequelize.define('GaqSummary', { dataPassId: { type: Sequelize.INTEGER, + primaryKey: true, }, runNumber: { type: Sequelize.INTEGER, + primaryKey: true, }, badRunCoverage: { type: Sequelize.FLOAT, From 9c1116b0944266fac0fe6b769d7ff4bce5230105 Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:18:25 +0200 Subject: [PATCH 19/20] [O2B-1563] Centralise GAQ summary invalidation and skip redundant work Extract invalidation into helpers. Guard GAQ updateRun reload so don't run reload unnecessarily. Add to notComputable field in model allowNull default params. Add more test coverage for invalidation on run patch. --- lib/database/models/gaqSummary.js | 2 + .../repositories/GaqSummaryRepository.js | 22 +++++++++++ .../services/gaq/GaqDetectorsService.js | 14 +------ .../qualityControlFlag/QcFlagService.js | 36 ++++++------------ lib/server/services/run/updateRun.js | 37 ++++++++++++------- .../server/services/gaq/GaqSummary.test.js | 20 ++++++++++ 6 files changed, 81 insertions(+), 50 deletions(-) diff --git a/lib/database/models/gaqSummary.js b/lib/database/models/gaqSummary.js index 3a12f62f33..ca2a1f69b8 100644 --- a/lib/database/models/gaqSummary.js +++ b/lib/database/models/gaqSummary.js @@ -40,6 +40,8 @@ module.exports = (sequelize) => { }, notComputable: { type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, }, invalidatedAt: { type: Sequelize.DATE(3), diff --git a/lib/database/repositories/GaqSummaryRepository.js b/lib/database/repositories/GaqSummaryRepository.js index 98986de513..81463d31c3 100644 --- a/lib/database/repositories/GaqSummaryRepository.js +++ b/lib/database/repositories/GaqSummaryRepository.js @@ -28,6 +28,28 @@ class GaqSummaryRepository extends Repository { constructor() { super(GaqSummary); } + + /** + * Mark the summary for a given (dataPassId, runNumber) as invalidated, creating the row if it does not yet exist + * + * @param {number} dataPassId data pass id + * @param {number} runNumber run number + * @return {Promise} resolves once the summary is invalidated + */ + async invalidate(dataPassId, runNumber) { + await this.upsert({ dataPassId, runNumber, invalidatedAt: new Date() }); + } + + /** + * Mark a list of summaries as invalidated in parallel + * + * @param {{ dataPassId: number, runNumber: number }[]} pairs the (dataPassId, runNumber) pairs to invalidate + * @return {Promise} resolves once all summaries are invalidated + */ + async invalidateMany(pairs) { + const invalidatedAt = new Date(); + await Promise.all(pairs.map(({ dataPassId, runNumber }) => this.upsert({ dataPassId, runNumber, invalidatedAt }))); + } } module.exports = new GaqSummaryRepository(); diff --git a/lib/server/services/gaq/GaqDetectorsService.js b/lib/server/services/gaq/GaqDetectorsService.js index 4411acdefa..4f6d466503 100644 --- a/lib/server/services/gaq/GaqDetectorsService.js +++ b/lib/server/services/gaq/GaqDetectorsService.js @@ -58,12 +58,7 @@ class GaqDetectorService { .map((detectorId) => ({ dataPassId, runNumber, detectorId }))); const createdEntries = await GaqDetectorRepository.insertAll(gaqEntries); - // Invalidate GAQ summaries for all affected runs - await Promise.all(runNumbers.map((runNumber) => GaqSummaryRepository.upsert({ - dataPassId, - runNumber, - invalidatedAt: new Date(), - }))); + await GaqSummaryRepository.invalidateMany(runNumbers.map((runNumber) => ({ dataPassId, runNumber }))); return createdEntries.map(gaqDetectorAdapter.toEntity); }); @@ -110,12 +105,7 @@ class GaqDetectorService { .map(({ id: detectorId }) => ({ dataPassId, runNumber, detectorId }))); const createdEntries = await GaqDetectorRepository.insertAll(gaqEntries); - // Invalidate GAQ summaries for all affected runs - await Promise.all(runNumbers.map((runNumber) => GaqSummaryRepository.upsert({ - dataPassId, - runNumber, - invalidatedAt: new Date(), - }))); + await GaqSummaryRepository.invalidateMany(runNumbers.map((runNumber) => ({ dataPassId, runNumber }))); return createdEntries.map(gaqDetectorAdapter.toEntity); }, { transaction }); diff --git a/lib/server/services/qualityControlFlag/QcFlagService.js b/lib/server/services/qualityControlFlag/QcFlagService.js index b1484df9b8..b6a647290a 100644 --- a/lib/server/services/qualityControlFlag/QcFlagService.js +++ b/lib/server/services/qualityControlFlag/QcFlagService.js @@ -210,13 +210,12 @@ class QcFlagService { ], }); - if (dataPass) { - // Invalidate GAQ summary for the dataPass and runNumber of the created flag - await GaqSummaryRepository.upsert({ - dataPassId: dataPass.id, - runNumber, - invalidatedAt: new Date(), - }); + /** + * Invalidate GAQ summary for the dataPass and runNumber of the created flag. + * Skip when `verify` is true: verifyFlag() above already invalidated the same (dataPassId, runNumber). + */ + if (dataPass && !verify) { + await GaqSummaryRepository.invalidate(dataPass.id, runNumber); } createdFlags.push(qcFlagAdapter.toEntity(createdFlag)); @@ -296,12 +295,8 @@ class QcFlagService { const { id, from, to, origin, createdById, runNumber, dplDetectorId, flagTypeId, createdAt } = qcFlag; if (dataPassId) { - // Invalidate GAQ summary for the dataPass and runNumber of the created flag - await GaqSummaryRepository.upsert({ - dataPassId: dataPassId, - runNumber, - invalidatedAt: new Date(), - }); + // Invalidate GAQ summary for the dataPass and runNumber of the deleted flag + await GaqSummaryRepository.invalidate(dataPassId, runNumber); } const qcFlagPropertiesToLog = { @@ -373,8 +368,7 @@ class QcFlagService { })).map(({ id, runNumber }) => ({ id, runNumber })); const qcFlagIds = qcFlagIdsToRunNumbers.map(({ id }) => id); - const runNumbers = new Set(); - qcFlagIdsToRunNumbers.map(({ runNumber }) => runNumbers.add(runNumber)); + const runNumbers = new Set(qcFlagIdsToRunNumbers.map(({ runNumber }) => runNumber)); await QcFlagRepository.updateAll( { deleted: true }, @@ -382,11 +376,7 @@ class QcFlagService { ); // Invalidate GAQ summary for the dataPass and all runNumbers of the deleted flags - await Promise.all(Array.from(runNumbers).map((runNumber) => GaqSummaryRepository.upsert({ - dataPassId, - runNumber, - invalidatedAt: new Date(), - }))); + await GaqSummaryRepository.invalidateMany(Array.from(runNumbers, (runNumber) => ({ dataPassId, runNumber }))); return qcFlagIds.length; }); @@ -429,11 +419,7 @@ class QcFlagService { // Invalidate GAQ summary if it's the first verification if (dataPassId && (!qcFlag.verifications || qcFlag.verifications.length === 0)) { - await GaqSummaryRepository.upsert({ - dataPassId, - runNumber: updatedQcFlag.runNumber, - invalidatedAt: new Date(), - }); + await GaqSummaryRepository.invalidate(dataPassId, updatedQcFlag.runNumber); } return updatedQcFlag; diff --git a/lib/server/services/run/updateRun.js b/lib/server/services/run/updateRun.js index a7833ed107..f4b9f0e69a 100644 --- a/lib/server/services/run/updateRun.js +++ b/lib/server/services/run/updateRun.js @@ -37,6 +37,20 @@ const { logSpecificRunTag } = require('./logEntriesCreation/logSpecificRunTag.js */ const TAGS_TO_LOG = ['Not for physics']; +/** + * Run patch fields whose values feed the qc_time_start / qc_time_end virtual columns + * + * @type {string[]} + */ +const QC_TIME_SOURCE_FIELDS = [ + 'firstTfTimestamp', + 'lastTfTimestamp', + 'timeTrgStart', + 'timeTrgEnd', + 'timeO2Start', + 'timeO2End', +]; + /** * Update the given run * @@ -211,20 +225,17 @@ exports.updateRun = async (identifier, payload, transaction) => { } } - // Need to reload the runModel as qcTimeStart and qcTimeEnd are virtual columns not in the patch so will stay stale after update - await runModel.reload(); - if (previousRun.qcTimeStart?.getTime() !== runModel.qcTimeStart?.getTime() - || previousRun.qcTimeEnd?.getTime() !== runModel.qcTimeEnd?.getTime()) { - const dataPassRuns = await DataPassRunRepository.findAll({ - attributes: ['dataPassId'], - where: { runNumber: runModel.runNumber }, - }); - for (const { dataPassId } of dataPassRuns) { - await GaqSummaryRepository.upsert({ - dataPassId, - runNumber: runModel.runNumber, - invalidatedAt: new Date(), + // Only check for invalidation when the patch could have changed them + if (QC_TIME_SOURCE_FIELDS.some((field) => field in runPatch)) { + // Reload because qcTimeStart/qcTimeEnd are virtual columns not in the patch and stay stale after update + await runModel.reload(); + if (previousRun.qcTimeStart?.getTime() !== runModel.qcTimeStart?.getTime() + || previousRun.qcTimeEnd?.getTime() !== runModel.qcTimeEnd?.getTime()) { + const dataPassRuns = await DataPassRunRepository.findAll({ + attributes: ['dataPassId'], + where: { runNumber: runModel.runNumber }, }); + await GaqSummaryRepository.invalidateMany(dataPassRuns.map(({ dataPassId }) => ({ dataPassId, runNumber: runModel.runNumber }))); } } diff --git a/test/lib/server/services/gaq/GaqSummary.test.js b/test/lib/server/services/gaq/GaqSummary.test.js index 62ad2a980e..c681b62b93 100644 --- a/test/lib/server/services/gaq/GaqSummary.test.js +++ b/test/lib/server/services/gaq/GaqSummary.test.js @@ -116,5 +116,25 @@ module.exports = () => { await expectInvalidation(dataPassId, runNumber); }); + + it('should NOT invalidate GAQ summary when updateRun patch does not touch QC time source fields', async () => { + await updateRun( + { runNumber }, + { runPatch: { definition: 'PHYSICS' } }, + ); + + await expectInvalidation(dataPassId, runNumber, true); + }); + + it('should NOT invalidate GAQ summary when a QC time source field is patched but qcTimeStart/qcTimeEnd do not change', async () => { + // Run 56 has a non-null time_trg_start, so qc_time_start resolves to time_trg_start regardless of time_o2_start. + // Patching only time_o2_start moves a source field but does not change the virtual qcTimeStart / qcTimeEnd, so no invalidation should fire. + await updateRun( + { runNumber }, + { runPatch: { timeO2Start: new Date('2019-08-08 18:00:00') } }, + ); + + await expectInvalidation(dataPassId, runNumber, true); + }); }); }; From 23f6e0442e16511db656ed942338df95e424f9fc Mon Sep 17 00:00:00 2001 From: Isaac Hill <71404865+isaachilly@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:08:09 +0200 Subject: [PATCH 20/20] Revert "Merge branch 'main' into feature/O2B-1563/Create-GAQ-summary-invalidation-mechanism" This reverts commit ce0ac8cc3179e92f0e7b317d330ac445e999c903, reversing changes made to 9c1116b0944266fac0fe6b769d7ff4bce5230105. --- .github/CODEOWNERS | 1 - .github/workflows/bookkeeping.yml | 8 +- .github/workflows/docker.yml | 4 - Dockerfile | 2 +- ...260305110000-add-qcf-run-detector-index.js | 28 - .../seeders/20240404100811-qc-flags.js | 21 - lib/domain/dtos/GetAllLogsDto.js | 19 +- .../dtos/filters/EnvironmentsFilterDto.js | 14 +- lib/domain/dtos/filters/RunFilterDto.js | 25 +- .../enums/NonPhysicsProductionsNamesWords.js | 2 - lib/public/Model.js | 18 +- lib/public/app.css | 68 - .../LhcFillsFilter/BeamTypeFilterModel.js | 6 +- .../LhcFillsFilter/StableBeamFilterModel.js | 76 + .../Filters/LhcFillsFilter/beamTypeFilter.js | 6 +- .../LhcFillsFilter/fillNumberFilter.js | 25 + .../LhcFillsFilter/schemeNameFilter.js | 25 + .../LhcFillsFilter/stableBeamFilter.js | 49 + .../LogsFilter/author/AuthorFilterModel.js | 21 +- .../Filters/LogsFilter/author/authorFilter.js | 33 +- .../components/Filters/LogsFilter/created.js | 53 + .../Filters/LogsFilter/environments.js | 28 + .../components/Filters/LogsFilter/runs.js | 28 + .../Filters/RunsFilter/BeamModeFilterModel.js | 37 +- .../RunsFilter/DetectorsFilterModel.js | 10 +- .../RunsFilter/EorReasonFilterModel.js | 9 - .../Filters/RunsFilter/GaqFilterModel.js | 97 - .../RunsFilter/MagnetsFilteringModel.js | 79 +- .../RunsFilter/MultiCompositionFilterModel.js | 110 - .../RunsFilter/RunDefinitionFilterModel.js | 11 +- .../Filters/RunsFilter/TimeRangeFilter.js | 7 - .../components/Filters/RunsFilter/dcs.js | 50 + .../components/Filters/RunsFilter/ddflp.js | 50 + .../components/Filters/RunsFilter/epn.js | 50 + .../Filters/RunsFilter/runDefinitionFilter.js | 5 +- .../Filters/RunsFilter/runNumbersFilter.js | 25 + .../Filters/RunsFilter/triggerValueFilter.js | 21 + .../components/Filters/common/FilterModel.js | 20 - .../Filters/common/FilteringModel.js | 183 +- .../Filters/common/RadioButtonFilterModel.js | 48 - .../Filters/common/TagFilterModel.js | 12 +- .../common/filters/FilterInputModel.js | 119 ++ .../filters/NumericalComparisonFilterModel.js | 19 +- .../common/filters/ProcessedTextInputModel.js | 21 - .../common/filters/RawTextFilterModel.js | 7 - .../common/filters/SelectionFilterModel.js | 63 + .../filters/TextComparisonFilterModel.js | 12 +- .../common/filters/TextTokensFilterModel.js | 7 - .../common/filters/TimeRangeInputModel.js | 8 - .../common/filters/ToggleFilterModel.js | 74 - .../Filters/common/filters/checkboxFilter.js | 26 + .../common/filters/radioButtonFilter.js | 38 - .../Filters/common/filters/textFilter.js | 8 +- .../Filters/common/filters/textInputFilter.js | 26 - .../Filters/common/filters/toggleFilter.js | 45 - .../Filters/common/filtersPanelPopover.js | 131 +- .../common/form/inputs/DateTimeInputModel.js | 10 +- .../components/common/form/switchInput.js | 4 +- .../common/messages/warningComponent.js | 35 - .../common/selection/SelectionModel.js | 69 +- .../runEorReasons/runEorReasonSelection.js | 5 +- .../runTypes/RunTypesFilterModel.js | 37 +- lib/public/domain/enums/DetectorOrders.js | 43 - .../models/FilterableOverviewPageModel.js | 132 -- lib/public/models/OverviewModel.js | 19 +- .../services/detectors/detectorsProvider.js | 18 +- .../ActiveColumns/dataPassesActiveColumns.js | 9 +- .../views/DataPasses/DataPassesModel.js | 9 +- .../DataPasses/DataPassesOverviewModel.js | 62 +- .../DataPassesPerLhcPeriodOverviewModel.js | 6 +- .../DataPassesPerLhcPeriodOverviewPage.js | 21 +- ...ataPassesPerSimulationPassOverviewModel.js | 6 +- ...DataPassesPerSimulationPassOverviewPage.js | 19 +- .../environmentsActiveColumns.js | 25 +- .../views/Environments/EnvironmentModel.js | 3 +- .../Overview/EnvironmentOverviewModel.js | 109 +- .../Overview/environmentOverviewComponent.js | 7 +- lib/public/views/Home/Overview/HomePage.js | 2 +- .../views/Home/Overview/HomePageModel.js | 8 +- .../ActiveColumns/lhcFillsActiveColumns.js | 26 +- lib/public/views/LhcFills/LhcFills.js | 3 +- .../Overview/LhcFillsOverviewModel.js | 102 +- lib/public/views/LhcFills/Overview/index.js | 17 +- .../Logs/ActiveColumns/logsActiveColumns.js | 87 +- lib/public/views/Logs/LogsModel.js | 5 +- .../views/Logs/Overview/LogsOverviewModel.js | 409 +++- lib/public/views/Logs/Overview/index.js | 15 +- .../ActiveColumns/qcFlagTypesActiveColumns.js | 17 +- .../Overview/QcFlagTypesOverviewModel.js | 105 +- .../Overview/QcFlagTypesOverviewPage.js | 19 +- .../views/QcFlagTypes/QcFlagTypesModel.js | 3 +- .../synchronousQcFlagsActiveColumns.js | 61 - .../SynchronousQcFlagsOverviewPage.js | 16 +- .../views/QcFlags/format/formatQcFlagEnd.js | 5 +- .../views/QcFlags/format/formatQcFlagStart.js | 5 +- .../runDetectorsAsyncQcActiveColumns.js | 2 +- .../Runs/ActiveColumns/runsActiveColumns.js | 73 +- lib/public/views/Runs/Details/RunPatch.js | 11 +- .../views/Runs/Details/runDetailsComponent.js | 7 +- .../FixedPdpBeamTypeRunsOverviewModel.js | 5 +- .../views/Runs/Overview/RunsOverviewModel.js | 316 ++- .../views/Runs/Overview/RunsOverviewPage.js | 20 +- .../views/Runs/Overview/RunsWithQcModel.js | 136 +- .../RunsPerDataPassOverviewModel.js | 43 +- .../RunsPerDataPassOverviewPage.js | 350 ++-- .../RunsPerLhcPeriodOverviewModel.js | 38 +- .../RunsPerLhcPeriodOverviewPage.js | 69 +- lib/public/views/Runs/RunsModel.js | 30 +- .../RunsPerSimulationPassOverviewModel.js | 38 +- .../RunsPerSimulationPassOverviewPage.js | 129 +- .../views/Runs/format/editRunEorReasons.js | 15 +- .../views/Runs/format/formatRunEorReason.js | 36 - .../Runs/mcReproducibleAsNotBadToggle.js | 28 + .../simulationPassesActiveColumns.js | 4 +- .../AnchoredSimulationPassesOverviewModel.js | 65 +- .../AnchoredSimulationPassesOverviewPage.js | 12 +- ...mulationPassesPerLhcPeriodOverviewModel.js | 65 +- ...imulationPassesPerLhcPeriodOverviewPage.js | 19 +- .../SimulationPasses/SimulationPassesModel.js | 9 +- .../ActiveColumns/lhcPeriodsActiveColumns.js | 12 +- .../views/lhcPeriods/LhcPeriodsModel.js | 6 +- .../Overview/LhcPeriodsOverviewModel.js | 96 +- .../Overview/LhcPeriodsOverviewPage.js | 24 +- lib/server/Loggers/FilterLogger.js | 60 - .../controllers/dataPasses.controller.js | 16 +- .../lhcPeriodStatistics.controller.js | 2 +- .../InfoLoggerListener.middleware.js | 23 - lib/server/routers/dataPasses.router.js | 4 +- lib/server/routers/environments.router.js | 4 +- lib/server/routers/lhcFills.router.js | 4 +- .../routers/lhcPeriodsStatistics.router.js | 4 +- lib/server/routers/logs.router.js | 4 +- lib/server/routers/qcFlag.router.js | 4 +- lib/server/routers/runs.router.js | 4 +- lib/server/routers/simulationPasses.router.js | 4 +- .../services/dataPasses/DataPassService.js | 55 +- .../lhcPeriod/LhcPeriodStatisticsService.js | 21 +- .../environment/GetAllEnvironmentsUseCase.js | 50 +- lib/usecases/lhcFill/GetAllLhcFillsUseCase.js | 9 +- lib/usecases/log/GetAllLogsUseCase.js | 74 +- lib/usecases/run/GetAllRunsUseCase.js | 57 +- lib/utilities/setTimeRangeQuery.js | 25 - package-lock.json | 1833 ++++++++++++----- package.json | 32 +- test/api/dataPasses.test.js | 4 +- test/api/logs.test.js | 149 +- test/api/qcFlags.test.js | 10 +- test/api/runs.test.js | 28 +- .../qualityControlFlag/QcFlagService.test.js | 16 +- .../GetAllEnvironmentsUseCase.test.js | 36 - .../usecases/log/GetAllLogsUseCase.test.js | 52 +- .../usecases/run/GetAllRunsUseCase.test.js | 13 +- test/public/Filters/FilteringModel.test.js | 157 -- test/public/Filters/filtersToUrl.test.js | 529 ----- test/public/Filters/urlToFilter.test.js | 372 ---- .../components/filtersPopoverPanel.test.js | 70 - test/public/components/index.js | 4 - test/public/components/warnings.test.js | 85 - .../dataPasses/overviewPerLhcPeriod.test.js | 2 +- .../overviewPerSimulationPass.test.js | 2 +- test/public/defaults.js | 60 +- test/public/index.js | 2 - test/public/logs/overview.test.js | 333 +-- test/public/qcFlagTypes/overview.test.js | 2 +- .../qcFlags/synchronousOverview.test.js | 50 +- test/public/runs/detail.test.js | 19 +- test/public/runs/overview.test.js | 16 +- .../runs/runsPerDataPass.overview.test.js | 43 +- .../runs/runsPerLhcPeriod.overview.test.js | 33 +- .../runsPerSimulationPass.overview.test.js | 22 +- 170 files changed, 4641 insertions(+), 4956 deletions(-) delete mode 100644 lib/database/migrations/v1/20260305110000-add-qcf-run-detector-index.js rename test/public/Filters/index.js => lib/domain/dtos/filters/EnvironmentsFilterDto.js (58%) create mode 100644 lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js create mode 100644 lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js create mode 100644 lib/public/components/Filters/LhcFillsFilter/schemeNameFilter.js create mode 100644 lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js create mode 100644 lib/public/components/Filters/LogsFilter/created.js create mode 100644 lib/public/components/Filters/LogsFilter/environments.js create mode 100644 lib/public/components/Filters/LogsFilter/runs.js delete mode 100644 lib/public/components/Filters/RunsFilter/GaqFilterModel.js delete mode 100644 lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js create mode 100644 lib/public/components/Filters/RunsFilter/dcs.js create mode 100644 lib/public/components/Filters/RunsFilter/ddflp.js create mode 100644 lib/public/components/Filters/RunsFilter/epn.js create mode 100644 lib/public/components/Filters/RunsFilter/runNumbersFilter.js create mode 100644 lib/public/components/Filters/RunsFilter/triggerValueFilter.js delete mode 100644 lib/public/components/Filters/common/RadioButtonFilterModel.js create mode 100644 lib/public/components/Filters/common/filters/FilterInputModel.js create mode 100644 lib/public/components/Filters/common/filters/SelectionFilterModel.js delete mode 100644 lib/public/components/Filters/common/filters/ToggleFilterModel.js delete mode 100644 lib/public/components/Filters/common/filters/radioButtonFilter.js delete mode 100644 lib/public/components/Filters/common/filters/textInputFilter.js delete mode 100644 lib/public/components/Filters/common/filters/toggleFilter.js delete mode 100644 lib/public/components/common/messages/warningComponent.js delete mode 100644 lib/public/domain/enums/DetectorOrders.js delete mode 100644 lib/public/models/FilterableOverviewPageModel.js delete mode 100644 lib/public/views/QcFlags/ActiveColumns/synchronousQcFlagsActiveColumns.js delete mode 100644 lib/public/views/Runs/format/formatRunEorReason.js create mode 100644 lib/public/views/Runs/mcReproducibleAsNotBadToggle.js delete mode 100644 lib/server/Loggers/FilterLogger.js delete mode 100644 lib/server/middleware/InfoLoggerListener.middleware.js delete mode 100644 lib/utilities/setTimeRangeQuery.js delete mode 100644 test/public/Filters/FilteringModel.test.js delete mode 100644 test/public/Filters/filtersToUrl.test.js delete mode 100644 test/public/Filters/urlToFilter.test.js delete mode 100644 test/public/components/filtersPopoverPanel.test.js delete mode 100644 test/public/components/warnings.test.js diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3be7e511ff..b806224b85 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1 @@ * @graduta -* @isaachilly diff --git a/.github/workflows/bookkeeping.yml b/.github/workflows/bookkeeping.yml index d4ee530717..c3c2358c27 100644 --- a/.github/workflows/bookkeeping.yml +++ b/.github/workflows/bookkeeping.yml @@ -10,10 +10,6 @@ on: branches: - main -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - jobs: parallel_tests: name: ${{ matrix.test_type }} @@ -43,7 +39,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Set up Docker - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@v3 - name: Create Coverage Directory run: mkdir -p ${{ github.workspace }}/coverage @@ -75,7 +71,7 @@ jobs: env: TEST_TYPE: ${{ matrix.test_type }} - name: Upload Coverage to Codecov - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v5 with: files: ./coverage/lcov.info env: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0521614f2a..69dd060eb0 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -9,10 +9,6 @@ on: permissions: contents: read -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - jobs: linter: runs-on: ubuntu-latest diff --git a/Dockerfile b/Dockerfile index d4f9caed24..b0373db06a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN apk add --no-cache \ freetype=2.13.2-r0 \ freetype-dev=2.13.2-r0 \ harfbuzz=8.5.0-r0 \ - ca-certificates=20260413-r0 + ca-certificates=20250911-r0 # Tell Puppeteer to skip installing Chrome. We'll be using the installed package. ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true diff --git a/lib/database/migrations/v1/20260305110000-add-qcf-run-detector-index.js b/lib/database/migrations/v1/20260305110000-add-qcf-run-detector-index.js deleted file mode 100644 index 4c04e5920e..0000000000 --- a/lib/database/migrations/v1/20260305110000-add-qcf-run-detector-index.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -'use strict'; - -/** @type {import('sequelize-cli').Migration} */ -module.exports = { - up: async (queryInterface) => queryInterface.sequelize.transaction(async (transaction) => { - await queryInterface.addIndex('quality_control_flags', { - name: 'quality_control_flags_run_detector_idx', - fields: ['run_number', 'detector_id'], - }, { transaction }); - }), - - down: async (queryInterface) => queryInterface.sequelize.transaction(async (transaction) => { - await queryInterface.removeIndex('quality_control_flags', 'quality_control_flags_run_detector_idx', { transaction }); - }), -}; diff --git a/lib/database/seeders/20240404100811-qc-flags.js b/lib/database/seeders/20240404100811-qc-flags.js index 560cb644bc..b66ca15bce 100644 --- a/lib/database/seeders/20240404100811-qc-flags.js +++ b/lib/database/seeders/20240404100811-qc-flags.js @@ -281,21 +281,6 @@ module.exports = { created_at: '2024-08-12 12:00:10', updated_at: '2024-08-12 12:00:10', }, - { - id: 103, - deleted: true, - from: null, - to: '2019-08-08 20:50:00', - comment: 'deleted flag', - - run_number: 56, - flag_type_id: 13, // Bad - created_by_id: 2, - detector_id: 7, // FT0 - - created_at: '2024-08-12 12:00:15', - updated_at: '2024-08-12 12:00:15', - }, // Run : 56, ITS { @@ -409,12 +394,6 @@ module.exports = { from: '2019-08-08 20:50:00', to: null, }, - { - id: 103, - flag_id: 103, - from: null, - to: '2019-08-08 20:50:00', - }, // Run : 56, ITS { diff --git a/lib/domain/dtos/GetAllLogsDto.js b/lib/domain/dtos/GetAllLogsDto.js index 7a0ef08306..8f6be452d7 100644 --- a/lib/domain/dtos/GetAllLogsDto.js +++ b/lib/domain/dtos/GetAllLogsDto.js @@ -17,10 +17,17 @@ const PaginationDto = require('./PaginationDto'); const { CustomJoi } = require('./CustomJoi.js'); const { TagsFilterDto } = require('./filters/TagsFilterDto.js'); const { FromToFilterDto } = require('./filters/FromToFilterDto.js'); +const { EnvironmentsFilterDto } = require('./filters/EnvironmentsFilterDto'); -const RunFilterDto = CustomJoi.stringArray().items(EntityIdDto).single(); -const EnvironmentsFilterDto = CustomJoi.stringArray().items(Joi.string()).single(); -const LhcFillFilterDto = CustomJoi.stringArray().items(EntityIdDto).single(); +const RunFilterDto = Joi.object({ + values: CustomJoi.stringArray().items(EntityIdDto).single().required(), + operation: Joi.string().valid('and', 'or').required(), +}); + +const LhcFillFilterDto = Joi.object({ + values: CustomJoi.stringArray().items(EntityIdDto).single().required(), + operation: Joi.string().valid('and', 'or').required(), +}); const FilterDto = Joi.object({ title: Joi.string().trim(), @@ -28,14 +35,14 @@ const FilterDto = Joi.object({ author: Joi.string().trim(), created: FromToFilterDto, tags: TagsFilterDto, - fillNumbers: LhcFillFilterDto, - runNumbers: RunFilterDto, + lhcFills: LhcFillFilterDto, + run: RunFilterDto, origin: Joi.string() .valid('human', 'process'), parentLog: EntityIdDto, rootLog: EntityIdDto, rootOnly: Joi.boolean(), - environmentIds: EnvironmentsFilterDto, + environments: EnvironmentsFilterDto, }); const SortDto = Joi.object({ diff --git a/test/public/Filters/index.js b/lib/domain/dtos/filters/EnvironmentsFilterDto.js similarity index 58% rename from test/public/Filters/index.js rename to lib/domain/dtos/filters/EnvironmentsFilterDto.js index 1023d11de8..3baa97a747 100644 --- a/test/public/Filters/index.js +++ b/lib/domain/dtos/filters/EnvironmentsFilterDto.js @@ -11,12 +11,10 @@ * or submit itself to any jurisdiction. */ -const ToUrlSuite = require('./filtersToUrl.test.js'); -const ToFilterSuite = require('./urlToFilter.test.js'); -const FilteringModelSuite = require('./filteringModel.test.js'); +const Joi = require('joi'); +const { CustomJoi } = require('../CustomJoi.js'); -module.exports = () => { - describe('Filters to URL', ToUrlSuite); - describe('URL to Filters', ToFilterSuite); - describe('FilteringModel', FilteringModelSuite); -}; +exports.EnvironmentsFilterDto = Joi.object({ + values: CustomJoi.stringArray().items(Joi.string()).single().required(), + operation: Joi.string().valid('and', 'or').required(), +}); diff --git a/lib/domain/dtos/filters/RunFilterDto.js b/lib/domain/dtos/filters/RunFilterDto.js index e67106d9f0..c66a194778 100644 --- a/lib/domain/dtos/filters/RunFilterDto.js +++ b/lib/domain/dtos/filters/RunFilterDto.js @@ -88,27 +88,26 @@ exports.RunFilterDto = Joi.object({ inelasticInteractionRateAtEnd: FloatComparisonDto, gaq: Joi.object({ - notBadFraction: FloatComparisonDto.custom((value, helpers) => { - const [, { dataPassIds }] = helpers.state.ancestors; - - if (!dataPassIds || dataPassIds.length !== 1) { - return helpers.message('Filtering by GAQ is enabled only when filtering with one dataPassId'); - } - - return value; - }), + notBadFraction: FloatComparisonDto.when( + 'dataPassIds', + { + is: Joi.array().length(1), + then: FloatComparisonDto, + otherwise: Joi.forbidden().error(new Error('Filtering by GAQ is enabled only when filtering with one dataPassId')), + }, + ), mcReproducibleAsNotBad: Joi.boolean().optional(), }), - detectorsQcNotBadFraction: Joi.object() + detectorsQc: Joi.object() .pattern( Joi.string().regex(/^_\d+$/), // Detector id with '_' prefix - FloatComparisonDto, + Joi.object({ notBadFraction: FloatComparisonDto }), ) .keys({ mcReproducibleAsNotBad: Joi.boolean().optional(), }) - .custom((detectorsQcNotBadFractionObj, helpers) => { + .custom((detectorsQcObj, helpers) => { const [{ dataPassIds, simulationPassIds, lhcPeriodIds }] = helpers.state.ancestors; singleRunsCollectionCustomCheck( @@ -118,6 +117,6 @@ exports.RunFilterDto = Joi.object({ 'the dataPassIds, simulationPassIds and lhcPeriodIds filters collectively contain exactly one ID', ); - return detectorsQcNotBadFractionObj; + return detectorsQcObj; }), }); diff --git a/lib/domain/enums/NonPhysicsProductionsNamesWords.js b/lib/domain/enums/NonPhysicsProductionsNamesWords.js index 0d76a56d09..fb3d55cc07 100644 --- a/lib/domain/enums/NonPhysicsProductionsNamesWords.js +++ b/lib/domain/enums/NonPhysicsProductionsNamesWords.js @@ -23,5 +23,3 @@ const NonPhysicsProductionsNamesWords = Object.freeze({ module.exports.NonPhysicsProductionsNamesWords = NonPhysicsProductionsNamesWords; module.exports.NON_PHYSICS_PRODUCTIONS_NAMES_WORDS = Object.values(NonPhysicsProductionsNamesWords); - -module.exports.NON_PHYSICS_PRODUCTIONS_NAMES_TOTAL_LENGTH = Object.values(NonPhysicsProductionsNamesWords).join(',').length; diff --git a/lib/public/Model.js b/lib/public/Model.js index 0d0ae222f3..6818118c81 100644 --- a/lib/public/Model.js +++ b/lib/public/Model.js @@ -95,27 +95,21 @@ export default class Model extends Observable { this._appConfiguration$ = new Observable(); this._inputDebounceTime = INPUT_DEBOUNCE_TIME; - // Setup router - this.router = new QueryRouter(); - this.router.observe(this.handleLocationChange.bind(this)); - this.router.bubbleTo(this); - registerFrontLinkListener((e) => this.router.handleLinkEvent(e)); - // Models this.home = new HomePageModel(this); this.home.bubbleTo(this); - this.lhcPeriods = new LhcPeriodsModel(this.router); + this.lhcPeriods = new LhcPeriodsModel(this); this.lhcPeriods.bubbleTo(this); - this.dataPasses = new DataPassesModel(this.router); + this.dataPasses = new DataPassesModel(this); this.dataPasses.bubbleTo(this); this.qcFlags = new QcFlagsModel(this); this.qcFlags.bubbleTo(this); - this.simulationPasses = new SimulationPassesModel(this.router); + this.simulationPasses = new SimulationPassesModel(this); this.simulationPasses.bubbleTo(this); this.qcFlagTypes = new QcFlagTypesModel(this); @@ -184,6 +178,12 @@ export default class Model extends Observable { this.errorModel = new ErrorModel(); this.errorModel.bubbleTo(this); + // Setup router + this.router = new QueryRouter(); + this.router.observe(this.handleLocationChange.bind(this)); + this.router.bubbleTo(this); + registerFrontLinkListener((e) => this.router.handleLinkEvent(e)); + // Init pages this.handleLocationChange(); this.window.addEventListener('resize', debounce(() => this.notify(), 100)); diff --git a/lib/public/app.css b/lib/public/app.css index 0e88f93174..ec66c3717c 100644 --- a/lib/public/app.css +++ b/lib/public/app.css @@ -266,12 +266,6 @@ th.text-center, td.text-center { border-color: #f5c6cb; } -.alert-warning { - color: var(--color-warning); - background-color: #ffe8c8; - border-color: #fdd69f; -} - .alert-danger hr { border-top-color: #f1b0b7; } @@ -718,68 +712,6 @@ label { opacity: 0.5; } -.active-filters-indicator { - position: relative; - z-index: 10; - background-color: white; - border-radius: .25rem; - padding: var(--space-xs) var(--space-s) var(--space-xs) var(--space-s); - margin: 0 0 0 var(--space-s); -} - -.active-filters-indicator:has(+ .clear-filter-icon-container) { - border-right: 0; - border-radius: .25rem 0 0 .25rem -} - -.clear-filter-icon-container { - background-color: white; - border-radius: 0 .25rem .25rem 0; - font-weight: 700; - cursor: pointer; -} - -.clear-filter-icon { - padding: var(--space-xs); - background-color: white; - color: var(--color-danger); - position: relative; - border-radius: 0 .25rem .25rem 0; - z-index: 10; -} - -.clear-filter-icon:hover { - background-color: var(--color-danger); - color: white; -} - -.inactive { - opacity: 0.5; - pointer-events: none; -} - -.pulse-green { - --pulse-color: 102, 255, 7; - animation: pulse 2s infinite; -} - -.pulse-red { - --pulse-color: 206, 42, 42; - animation: pulse 2s infinite; -} - -@keyframes pulse { - 0% { - box-shadow: 0 0 0px rgba(var(--pulse-color), 0.6); - } - 50% { - box-shadow: 0 0 10px rgba(var(--pulse-color), 0.9); - } - 100% { - box-shadow: 0 0 0px rgba(var(--pulse-color), 0.6); - } -} - /** * Breakpoints : * small : x < 600 (default styles) diff --git a/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js index fc0964da04..18be7af40d 100644 --- a/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js +++ b/lib/public/components/Filters/LhcFillsFilter/BeamTypeFilterModel.js @@ -12,12 +12,12 @@ */ import { beamTypesProvider } from '../../../services/beamTypes/beamTypesProvider.js'; -import { SelectionModel } from '../../common/selection/SelectionModel.js'; +import { SelectionFilterModel } from '../common/filters/SelectionFilterModel.js'; /** * Beam type filter model */ -export class BeamTypeFilterModel extends SelectionModel { +export class BeamTypeFilterModel extends SelectionFilterModel { /** * Constructor */ @@ -28,7 +28,7 @@ export class BeamTypeFilterModel extends SelectionModel { beamTypesProvider.items$.getCurrent().apply({ Success: (types) => { const beamTypes = types.map((type) => ({ value: type.beam_type })); - this.setAvailableOptions(beamTypes); + this._selectionModel.setAvailableOptions(beamTypes); }, }); }); diff --git a/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js new file mode 100644 index 0000000000..1bc3f8aed2 --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/StableBeamFilterModel.js @@ -0,0 +1,76 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE Trg. 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-Trg.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 { SelectionModel } from '../../common/selection/SelectionModel.js'; + +/** + * Stable beam filter model + * Holds true or false value + */ +export class StableBeamFilterModel extends SelectionModel { + /** + * Constructor + */ + constructor() { + super({ availableOptions: [{ value: true }, { value: false }], + defaultSelection: [{ value: false }], + multiple: false, + allowEmpty: false }); + } + + /** + * Returns true if the current filter is stable beams only + * + * @return {boolean} true if filter is stable beams only + */ + isStableBeamsOnly() { + return this.current; + } + + /** + * Sets the current filter to stable beams only + * + * @param {boolean} value value to set this stable beams only filter with + * @return {void} + */ + setStableBeamsOnly(value) { + this.select({ value }); + } + + /** + * Get normalized selected option + */ + get normalized() { + return this.current; + } + + /** + * Overrides SelectionModel.isEmpty to respect the fact that stable beam filter cannot be empty. + * @returns {boolean} true if the current value of the filter is false. + */ + get isEmpty() { + return this.current === false; + } + + /** + * Reset the filter to default values + * + * @return {void} + */ + resetDefaults() { + if (!this.isEmpty) { + this.reset(); + this.notify(); + } + } +} diff --git a/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js b/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js index 83f1487922..7872734704 100644 --- a/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js +++ b/lib/public/components/Filters/LhcFillsFilter/beamTypeFilter.js @@ -19,4 +19,8 @@ import { checkboxes } from '../common/filters/checkboxFilter.js'; * @param {BeamTypeFilterModel} beamTypeFilterModel beamTypeFilterModel * @return {Component} the filter */ -export const beamTypeFilter = (beamTypeFilterModel) => checkboxes(beamTypeFilterModel, { selector: 'beam-types' }); +export const beamTypeFilter = (beamTypeFilterModel) => + checkboxes( + beamTypeFilterModel.selectionModel, + { selector: 'beam-types' }, + ); diff --git a/lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js b/lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js new file mode 100644 index 0000000000..de13af7586 --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/fillNumberFilter.js @@ -0,0 +1,25 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE Trg. 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-Trg.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 { rawTextFilter } from '../common/filters/rawTextFilter.js'; + +/** + * Component to filter LHC-fills by fill number + * + * @param {RawTextFilterModel} filterModel the filter model + * @returns {Component} the text field + */ +export const fillNumberFilter = (filterModel) => rawTextFilter( + filterModel, + { classes: ['w-100', 'fill-numbers-filter'], placeholder: 'e.g. 11392, 11383, 7625' }, +); diff --git a/lib/public/components/Filters/LhcFillsFilter/schemeNameFilter.js b/lib/public/components/Filters/LhcFillsFilter/schemeNameFilter.js new file mode 100644 index 0000000000..7b644f382a --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/schemeNameFilter.js @@ -0,0 +1,25 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE Trg. 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-Trg.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 { rawTextFilter } from '../common/filters/rawTextFilter.js'; + +/** + * Component to filter LHC-fills by scheme name + * + * @param {RawTextFilterModel} filterModel the filter model + * @returns {Component} the text field + */ +export const schemeNameFilter = (filterModel) => rawTextFilter( + filterModel, + { classes: ['w-100'], placeholder: 'e.g. Single_12b_8_1024_8_2018' }, +); diff --git a/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js new file mode 100644 index 0000000000..b4429c002c --- /dev/null +++ b/lib/public/components/Filters/LhcFillsFilter/stableBeamFilter.js @@ -0,0 +1,49 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE Trg. 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-Trg.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 { switchInput } from '../../common/form/switchInput.js'; +import { radioButton } from '../../common/form/inputs/radioButton.js'; + +/** + * Display a toggle switch or radio buttons to filter stable beams only + * + * @param {StableBeamFilterModel} stableBeamFilterModel the stableBeamFilterModel + * @param {boolean} radioButtonMode define whether or not to return radio buttons or a switch. + * @returns {Component} the toggle switch + */ +export const toggleStableBeamOnlyFilter = (stableBeamFilterModel, radioButtonMode = false) => { + const name = 'stableBeamsOnlyRadio'; + const labelOff = 'OFF'; + const labelOn = 'ON'; + if (radioButtonMode) { + return h('.form-group-header.flex-row.w-100', [ + radioButton({ + label: labelOff, + isChecked: !stableBeamFilterModel.isStableBeamsOnly(), + action: () => stableBeamFilterModel.setStableBeamsOnly(false), + name: name, + }), + radioButton({ + label: labelOn, + isChecked: stableBeamFilterModel.isStableBeamsOnly(), + action: () => stableBeamFilterModel.setStableBeamsOnly(true), + name: name, + }), + ]); + } else { + return switchInput(stableBeamFilterModel.isStableBeamsOnly(), (newState) => { + stableBeamFilterModel.setStableBeamsOnly(newState); + }, { labelAfter: 'STABLE BEAM ONLY' }); + } +}; diff --git a/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js b/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js index f41a4458e2..1b7a133916 100644 --- a/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js +++ b/lib/public/components/Filters/LogsFilter/author/AuthorFilterModel.js @@ -11,12 +11,12 @@ * or submit itself to any jurisdiction. */ -import { RawTextFilterModel } from '../../common/filters/RawTextFilterModel.js'; +import { FilterInputModel } from '../../common/filters/FilterInputModel.js'; /** * Model to handle the state of the Author Filter */ -export class AuthorFilterModel extends RawTextFilterModel { +export class AuthorFilterModel extends FilterInputModel { /** * Constructor * @@ -32,7 +32,7 @@ export class AuthorFilterModel extends RawTextFilterModel { * @return {boolean} true if '!Anonymous' is included in the raw filter string, false otherwise. */ isAnonymousExcluded() { - return this._value.includes('!Anonymous'); + return this._raw.includes('!Anonymous'); } /** @@ -42,25 +42,28 @@ export class AuthorFilterModel extends RawTextFilterModel { */ toggleAnonymousFilter() { if (this.isAnonymousExcluded()) { - this._value = this._value.split(',') + this._raw = this._raw.split(',') .filter((author) => author.trim() !== '!Anonymous') .join(','); } else { - this._value += super.isEmpty ? '!Anonymous' : ', !Anonymous'; + this._raw += super.isEmpty ? '!Anonymous' : ', !Anonymous'; } + this._value = this.valueFromRaw(this._raw); this.notify(); } /** - * Reset the filter to its default value and notify the observers if the reset changed anything. + * Reset the filter to its default value and notify the observers. * * @return {void} */ clear() { - if (!this.isEmpty) { - super.reset(); - this.notify(); + if (this.isEmpty) { + return; } + + super.reset(); + this.notify(); } } diff --git a/lib/public/components/Filters/LogsFilter/author/authorFilter.js b/lib/public/components/Filters/LogsFilter/author/authorFilter.js index f40d2c160d..d5fe5a7a45 100644 --- a/lib/public/components/Filters/LogsFilter/author/authorFilter.js +++ b/lib/public/components/Filters/LogsFilter/author/authorFilter.js @@ -14,7 +14,19 @@ import { h } from '/js/src/index.js'; import { iconX } from '/js/src/icons.js'; import { switchInput } from '../../../common/form/switchInput.js'; -import { rawTextFilter } from '../../common/filters/rawTextFilter.js'; + +/** + * Returns a text input field that can be used to filter logs by author + * + * @param {AuthorFilterModel} authorFilterModel The author filter model object + * @returns {Component} A text box that allows the user to enter an author substring to match against all logs + */ +const authorFilterTextInput = (authorFilterModel) => h('input.w-40', { + type: 'text', + id: 'authorFilterText', + value: authorFilterModel.raw, + oninput: (e) => authorFilterModel.update(e.target.value), +}); /** * Returns a button that can be used to reset the author filter. @@ -22,8 +34,11 @@ import { rawTextFilter } from '../../common/filters/rawTextFilter.js'; * @param {AuthorFilterModel} authorFilterModel The author filter model object * @return {Component} A button that can be used to reset the author filter */ -const resetAuthorFilterButton = (authorFilterModel) => - h('.btn.btn-pill.f7', { disabled: authorFilterModel.isEmpty, onclick: () => authorFilterModel.clear() }, iconX()); +const resetAuthorFilterButton = (authorFilterModel) => h( + '.btn.btn-pill.f7', + { disabled: authorFilterModel.isEmpty, onclick: () => authorFilterModel.clear() }, + iconX(), +); /** * Returns a toggle that can be used to exclude anonymous authors @@ -40,11 +55,11 @@ export const excludeAnonymousLogAuthorToggle = (authorFilterModel) => switchInpu /** * Returns a authorFilter component with text input, reset button, and anonymous exclusion button. * - * @param {AuthorFilterModel} authorFilterModel the authorFilterModel - * @return {Component} the author filter component + * @param {LogModel} logModel the log model object + * @returns {Component} the author filter component */ -export const authorFilter = (authorFilterModel) => h('.flex-row.items-center.g3', [ - rawTextFilter(authorFilterModel, { classes: ['w-50'], id: 'authorFilterText', value: authorFilterModel.raw, placeholder: 'e.g. John Doe' }), - resetAuthorFilterButton(authorFilterModel), - excludeAnonymousLogAuthorToggle(authorFilterModel), +export const authorFilter = ({ authorFilter }) => h('.flex-row.items-center.g3', [ + authorFilterTextInput(authorFilter), + resetAuthorFilterButton(authorFilter), + excludeAnonymousLogAuthorToggle(authorFilter), ]); diff --git a/lib/public/components/Filters/LogsFilter/created.js b/lib/public/components/Filters/LogsFilter/created.js new file mode 100644 index 0000000000..3a86526c85 --- /dev/null +++ b/lib/public/components/Filters/LogsFilter/created.js @@ -0,0 +1,53 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { h } from '/js/src/index.js'; + +const DATE_FORMAT = 'YYYY-MM-DD'; + +let today = new Date(); +today.setMinutes(today.getMinutes() - today.getTimezoneOffset()); +[today] = today.toISOString().split('T'); + +/** + * Returns the creation date filter components + * @param {LogModel} logModel the log model object + * @return {vnode} Two date selection boxes to control the minimum and maximum creation dates for the log filters + */ +const createdFilter = (logModel) => { + const createdFrom = logModel.getCreatedFilterFrom(); + const createdTo = logModel.getCreatedFilterTo(); + return h('', [ + h('.f6', 'From:'), + h('input.w-75.mv1', { + type: 'date', + id: 'createdFilterFrom', + placeholder: DATE_FORMAT, + max: createdTo || today, + value: createdFrom, + oninput: (e) => logModel.setCreatedFilter('From', e.target.value, e.target.validity.valid), + }, ''), + h('.f6', 'To:'), + h('input.w-75.mv1', { + type: 'date', + id: 'createdFilterTo', + placeholder: DATE_FORMAT, + min: createdFrom, + max: today, + value: createdTo, + oninput: (e) => logModel.setCreatedFilter('To', e.target.value, e.target.validity.valid), + }, ''), + ]); +}; + +export default createdFilter; diff --git a/lib/public/components/Filters/LogsFilter/environments.js b/lib/public/components/Filters/LogsFilter/environments.js new file mode 100644 index 0000000000..665ae9eb44 --- /dev/null +++ b/lib/public/components/Filters/LogsFilter/environments.js @@ -0,0 +1,28 @@ +/** + * @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'; + +/** + * Returns a filter component to filter on environment Ids, either a coma separated list of specific ids or a substring + * search + * @param {LogsOverviewModel} logModel The global model object + * @return {vnode} A text box that allows the user to enter an environment substring to match against all runs or a + * list of environment ids + */ +export const environmentFilter = (logModel) => h('input.w-75.mt1', { + type: 'text', + value: logModel.getEnvFilterRaw(), + placeholder: 'e.g. Dxi029djX, TDI59So3d...', + oninput: (e) => logModel.setEnvFilter(e.target.value), +}, ''); diff --git a/lib/public/components/Filters/LogsFilter/runs.js b/lib/public/components/Filters/LogsFilter/runs.js new file mode 100644 index 0000000000..659d04a401 --- /dev/null +++ b/lib/public/components/Filters/LogsFilter/runs.js @@ -0,0 +1,28 @@ +/** + * @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'; + +/** + * Returns the runs filter component + * @param {LogModel} logsModel the log model object + * @return {vnode} A text box that allows the user to enter runNumbers to filter the logs + */ +const runsFilter = (logsModel) => h('input.w-75.mt1', { + type: 'text', + id: 'runsFilterText', + value: logsModel.getRunsFilterRaw(), + oninput: (e) => logsModel.setRunsFilter(e.target.value), +}, ''); + +export default runsFilter; diff --git a/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js index 626644ae88..0704fc684d 100644 --- a/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/BeamModeFilterModel.js @@ -12,17 +12,50 @@ */ import { ObservableBasedSelectionDropdownModel } from '../../detector/ObservableBasedSelectionDropdownModel.js'; +import { FilterModel } from '../common/FilterModel.js'; /** * Beam mode filter model */ -export class BeamModeFilterModel extends ObservableBasedSelectionDropdownModel { +export class BeamModeFilterModel extends FilterModel { /** * Constructor * * @param {ObservableData>} beamModes$ observable remote data of objects representing beam modes */ constructor(beamModes$) { - super(beamModes$, ({ name }) => ({ value: name })); + super(); + this._selectionDropdownModel = new ObservableBasedSelectionDropdownModel(beamModes$, ({ name }) => ({ value: name })); + this._addSubmodel(this._selectionDropdownModel); + } + + /** + * @inheritDoc + */ + reset() { + this._selectionDropdownModel.reset(); + } + + /** + * @inheritDoc + */ + get isEmpty() { + return this._selectionDropdownModel.isEmpty; + } + + /** + * Return the underlying dropdown model + * + * @return {ObservableDropDownModel} the underlying dropdown model + */ + get selectionDropdownModel() { + return this._selectionDropdownModel; + } + + /** + * @inheritDoc + */ + get normalized() { + return this._selectionDropdownModel.selected; } } diff --git a/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js b/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js index 7d75c417c8..432ecc58df 100644 --- a/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/DetectorsFilterModel.js @@ -62,19 +62,11 @@ export class DetectorsFilterModel extends FilterModel { operator: this._combinationOperatorModel.current, }; if (!this.isNone()) { - normalized.values = this._dropdownModel.normalized; + normalized.values = this._dropdownModel.selected.join(); } return normalized; } - /** - * @inheritDoc - */ - set normalized({ operator, values }) { - this._combinationOperatorModel.normalized = operator; - this._dropdownModel.normalized = values; - } - /** * Return true if the current combination operator is none * diff --git a/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js b/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js index b3b1e649bf..f57c810cce 100644 --- a/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/EorReasonFilterModel.js @@ -66,15 +66,6 @@ export class EorReasonFilterModel extends FilterModel { return ret; } - /** - * @inheritDoc - */ - set normalized({ category, title, description }) { - this._category = category; - this._title = title; - this._description = description; - } - /** * Returns the EOR reason filter category * diff --git a/lib/public/components/Filters/RunsFilter/GaqFilterModel.js b/lib/public/components/Filters/RunsFilter/GaqFilterModel.js deleted file mode 100644 index fe80fb8745..0000000000 --- a/lib/public/components/Filters/RunsFilter/GaqFilterModel.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE Trg. 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-Trg.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 { FilterModel } from '../common/FilterModel.js'; -import { NumericalComparisonFilterModel } from '../common/filters/NumericalComparisonFilterModel.js'; - -/** - * FilterModel that filters by the fraction of gaq that was not bad - */ -export class GaqFilterModel extends FilterModel { - /** - * Constructor - * @param {ToggleFilterModel} mcReproducibleAsNotBad model that determines if a 'not bad' status was reproduceable for a Monte Carlo. - * This param is required as multiple other filters models need to make use of the same ToggleFilterModel instance - */ - constructor(mcReproducibleAsNotBad) { - super(); - - this._notBadFraction = new NumericalComparisonFilterModel({ scale: 0.01, integer: false }); - this._addSubmodel(this._notBadFraction); - this._mcReproducibleAsNotBad = mcReproducibleAsNotBad; - - /** - * _mcReproducableAsNotBad will only be added to the normalize call notBadFraction is not empty - * So, notifying when it is empty will just send an unneeded request. - */ - this._mcReproducibleAsNotBad.visualChange$.bubbleTo(this._visualChange$); - this._mcReproducibleAsNotBad.observe(() => { - if (!this.notBadFraction.isEmpty) { - this.notify(); - } - }); - } - - /** - * @inheritDoc - */ - reset() { - this._notBadFraction.reset(); - } - - /** - * @inheritDoc - */ - get isEmpty() { - return this._notBadFraction.isEmpty; - } - - /** - * @inheritDoc - */ - get normalized() { - const normalized = { notBadFraction: this._notBadFraction.normalized }; - - if (!this.isEmpty) { - normalized.mcReproducibleAsNotBad = this._mcReproducibleAsNotBad.isToggled; - } - - return normalized; - } - - /** - * @inheritDoc - */ - set normalized({ notBadFraction, mcReproducibleAsNotBad }) { - this._notBadFraction.normalized = notBadFraction; - this._mcReproducibleAsNotBad.normalized = mcReproducibleAsNotBad; - } - - /** - * Return the underlying notBadFraction model - * - * @return {NumericalComparisonFilterModel} the filter model - */ - get notBadFraction() { - return this._notBadFraction; - } - - /** - * Return the underlying mcReproducibleAsNotBad model - * - * @return {ToggleFilterModel} the filter model - */ - get mcReproducibleAsNotBad() { - return this._mcReproducibleAsNotBad; - } -} diff --git a/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js b/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js index 9e38dfbbf3..015f991286 100644 --- a/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js +++ b/lib/public/components/Filters/RunsFilter/MagnetsFilteringModel.js @@ -11,31 +11,21 @@ * or submit itself to any jurisdiction. */ +import { FilterModel } from '../common/FilterModel.js'; import { ObservableBasedSelectionDropdownModel } from '../../detector/ObservableBasedSelectionDropdownModel.js'; /** * Return the option value corresponding to a given magnets current level * * @param {MagnetsCurrentLevels} currentLevels the current levels - * @return {object} the option's value + * @return {string} the option's value */ -const magnetsCurrentLevelsToKey = ({ l3, dipole }) => ({ value: `${l3}kA/${dipole}kA` }); - -/** - * Return the magnets current lever based on a key string - * - * @param {object} value string containing the current levels - * @return {MagnetsCurrentLevels} - */ -const keyToMagnetsCurrentLevels = (value) => { - const [l3, dipole] = value.split('/').map((str) => parseFloat(str.slice(0, -2))); - return { l3, dipole }; -}; +const magnetsCurrentLevelsToOptionValue = ({ l3, dipole }) => `${l3}kA/${dipole}kA`; /** * AliceL3AndDipoleFilteringModel */ -export class MagnetsFilteringModel extends ObservableBasedSelectionDropdownModel { +export class MagnetsFilteringModel extends FilterModel { /** * Constructor * @@ -43,31 +33,64 @@ export class MagnetsFilteringModel extends ObservableBasedSelectionDropdownModel * levels */ constructor(magnetsCurrentLevels$) { - super(magnetsCurrentLevels$, magnetsCurrentLevelsToKey, { multiple: false }); + super(); + this._selectionDropdownModel = new ObservableBasedSelectionDropdownModel( + magnetsCurrentLevels$, + (magnetsCurrentLevels) => ({ value: magnetsCurrentLevelsToOptionValue(magnetsCurrentLevels) }), + { multiple: false }, + ); + this._addSubmodel(this._selectionDropdownModel); + + this._valueToFilteringParamsMap = new Map(); + magnetsCurrentLevels$.observe(() => { + magnetsCurrentLevels$.getCurrent().match({ + + /** + * Fill map indexing current level by their corresponding value + * + * @param {MagnetsCurrentLevels[]} currentLevels the current levels to map + * @return {void} + */ + Success: (currentLevels) => { + this._valueToFilteringParamsMap = new Map(currentLevels.map(({ l3, dipole }) => [ + magnetsCurrentLevelsToOptionValue({ l3, dipole }), + { l3, dipole }, + ])); + }, + Other: () => { + this._valueToFilteringParamsMap = new Map(); + }, + }); + }); } /** * @inheritDoc */ - get normalized() { - const [selectedOption] = this.selected; + reset() { + this._selectionDropdownModel.reset(); + } - if (selectedOption === undefined) { - return null; - } + /** + * @inheritDoc + */ + get isEmpty() { + return this._selectionDropdownModel.isEmpty; + } - return keyToMagnetsCurrentLevels(selectedOption); + /** + * @inheritDoc + */ + get normalized() { + return this._valueToFilteringParamsMap.get(this._selectionDropdownModel.selected[0]) ?? null; } /** - * Sets selected options based on an object containing l3 and dipole fields. - * Accounts for the options being either RemoteData or an array. + * Return the underlying selection dropdown model * - * @param {MagnetsCurrentLevels} value the magnets current levels - * @param {number} value.l3 the L3 current level in kA - * @param {number} value.dipole the dipole current level in kA + * @return {SelectionDropdownModel} the dropdown model */ - set normalized(value) { - super.normalized = magnetsCurrentLevelsToKey(value).value; + get selectionDropdownModel() { + return this._selectionDropdownModel; } } diff --git a/lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js b/lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js deleted file mode 100644 index 80aafc8644..0000000000 --- a/lib/public/components/Filters/RunsFilter/MultiCompositionFilterModel.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE Trg. 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-Trg.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 { FilterModel } from '../common/FilterModel.js'; - -/** - * FilterModel that allows devs to create custom filters from multiple other filters during instantiation, or using putFilter - */ -export class MultiCompositionFilterModel extends FilterModel { - /** - * Constructor - * @param {Object} filters the filters that will make up the composite filter - */ - constructor(filters = {}) { - super(); - - /** - * @type {Object} - */ - this._filters = {}; - - Object.entries(filters).forEach(([key, filter]) => this.putFilter(key, filter)); - } - - /** - * Return a subfilter by key - * - * @param {string} key the key of the subfilter - * @return {FilterModel} the subfilter - */ - putFilter(key, filterModel) { - if (key in this._filters) { - return; - } - - this._filters[key] = filterModel; - this._addSubmodel(filterModel); - } - - /** - * Add new subfilter - * - * @param {string} key key of the subfilter - * @param {FilterModel} filter the the subfilter - */ - getFilter(key) { - if (!(key in this._filters)) { - throw new Error(`No filter found with key ${key}`); - } - - return this._filters[key]; - } - - /** - * @inheritDoc - */ - reset() { - Object.values(this._filters).forEach((filter) => filter.reset()); - } - - /** - * @inheritDoc - */ - get isEmpty() { - return Object.values(this._filters).every((filter) => filter.isEmpty); - } - - /** - * @inheritDoc - */ - get isInactive() { - return Object.values(this._filters).every((filter) => filter.isInactive); - } - - /** - * @inheritDoc - */ - get normalized() { - const normalized = {}; - - for (const [id, filter] of Object.entries(this._filters)) { - if (!filter.isEmpty) { - normalized[id] = filter.normalized; - } - } - - return normalized; - } - - /** - * @inheritDoc - */ - set normalized(filters) { - for (const [key, value] of Object.entries(filters)) { - if (key in this._filters) { - this._filters[key].normalized = value; - } - } - } -} diff --git a/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js b/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js index ac41defd53..8fb9347735 100644 --- a/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js +++ b/lib/public/components/Filters/RunsFilter/RunDefinitionFilterModel.js @@ -1,10 +1,10 @@ import { RUN_DEFINITIONS, RunDefinition } from '../../../domain/enums/RunDefinition.js'; -import { SelectionModel } from '../../common/selection/SelectionModel.js'; +import { SelectionFilterModel } from '../common/filters/SelectionFilterModel.js'; /** * Run definition filter model */ -export class RunDefinitionFilterModel extends SelectionModel { +export class RunDefinitionFilterModel extends SelectionFilterModel { /** * Constructor */ @@ -18,7 +18,7 @@ export class RunDefinitionFilterModel extends SelectionModel { * @return {boolean} true if filter is physics only */ isPhysicsOnly() { - const selectedOptions = this.selected; + const selectedOptions = this._selectionModel.selected; return selectedOptions.length === 1 && selectedOptions[0] === RunDefinition.Physics; } @@ -29,8 +29,9 @@ export class RunDefinitionFilterModel extends SelectionModel { */ setPhysicsOnly() { if (!this.isPhysicsOnly()) { - this.selectedOptions = []; - this.select(RunDefinition.Physics); + this._selectionModel.selectedOptions = []; + this._selectionModel.select(RunDefinition.Physics); + this.notify(); } } diff --git a/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js b/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js index 296e4f4753..e765137afa 100644 --- a/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js +++ b/lib/public/components/Filters/RunsFilter/TimeRangeFilter.js @@ -45,13 +45,6 @@ export class TimeRangeFilterModel extends FilterModel { return normalized; } - /** - * @inheritDoc - */ - set normalized({ from, to }) { - this._timeRangeInputModel.normalized = { from, to }; - } - /** * Return the underlying time range input model * diff --git a/lib/public/components/Filters/RunsFilter/dcs.js b/lib/public/components/Filters/RunsFilter/dcs.js new file mode 100644 index 0000000000..590eb81b78 --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/dcs.js @@ -0,0 +1,50 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE Trg. 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-Trg.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 { radioButton } from '../../common/form/inputs/radioButton.js'; +import { h } from '/js/src/index.js'; + +/** + * Filter panel for DCS toggle; ON/OFF/ANY + * @param {RunsOverviewModel} runModel the run model object + * @return {vnode} Three radio buttons inline + */ +const dcsOperationRadioButtons = (runModel) => { + const state = runModel.getDcsFilterOperation(); + const name = 'dcsFilterRadio'; + const labelAny = 'ANY'; + const labelOff = 'OFF'; + const labelOn = 'ON'; + return h('.form-group-header.flex-row.w-100', [ + radioButton({ + label: labelAny, + isChecked: state === '', + action: () => runModel.removeDcs(), + name, + }), + radioButton({ + label: labelOff, + isChecked: state === false, + action: () => runModel.setDcsFilterOperation(false), + name, + }), + radioButton({ + label: labelOn, + isChecked: state === true, + action: () => runModel.setDcsFilterOperation(true), + name, + }), + ]); +}; + +export default dcsOperationRadioButtons; diff --git a/lib/public/components/Filters/RunsFilter/ddflp.js b/lib/public/components/Filters/RunsFilter/ddflp.js new file mode 100644 index 0000000000..74bf28f4ba --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/ddflp.js @@ -0,0 +1,50 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE Trg. 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-Trg.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 { radioButton } from '../../common/form/inputs/radioButton.js'; +import { h } from '/js/src/index.js'; + +/** + * Filter panel for Data Distribution toggle; ON/OFF/ANY + * @param {RunsOverviewModel} runModel the run model object + * @return {vnode} Three radio buttons inline + */ +const ddflpOperationRadioButtons = (runModel) => { + const state = runModel.getDdflpFilterOperation(); + const name = 'ddFlpFilterRadio'; + const labelAny = 'ANY'; + const labelOff = 'OFF'; + const labelOn = 'ON'; + return h('.form-group-header.flex-row.w-100', [ + radioButton({ + label: labelAny, + isChecked: state === '', + action: () => runModel.removeDdflp(), + name, + }), + radioButton({ + label: labelOff, + isChecked: state === false, + action: () => runModel.setDdflpFilterOperation(false), + name, + }), + radioButton({ + label: labelOn, + isChecked: state === true, + action: () => runModel.setDdflpFilterOperation(true), + name, + }), + ]); +}; + +export default ddflpOperationRadioButtons; diff --git a/lib/public/components/Filters/RunsFilter/epn.js b/lib/public/components/Filters/RunsFilter/epn.js new file mode 100644 index 0000000000..5e639d8afb --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/epn.js @@ -0,0 +1,50 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE Trg. 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-Trg.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 { radioButton } from '../../common/form/inputs/radioButton.js'; +import { h } from '/js/src/index.js'; + +/** + * Filter panel for EPN toggle; ON/OFF/ANY + * @param {RunsOverviewModel} runModel the run model object + * @return {vnode} Three radio buttons inline + */ +const epnOperationRadioButtons = (runModel) => { + const state = runModel.getEpnFilterOperation(); + const name = 'epnFilterRadio'; + const labelAny = 'ANY'; + const labelOff = 'OFF'; + const labelOn = 'ON'; + return h('.form-group-header.flex-row.w-100', [ + radioButton({ + label: labelAny, + isChecked: state === '', + action: () => runModel.removeEpn(), + name, + }), + radioButton({ + label: labelOff, + isChecked: state === false, + action: () => runModel.setEpnFilterOperation(false), + name, + }), + radioButton({ + label: labelOn, + isChecked: state === true, + action: () => runModel.setEpnFilterOperation(true), + name, + }), + ]); +}; + +export default epnOperationRadioButtons; diff --git a/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js b/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js index 2a799ff675..d53ba62428 100644 --- a/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js +++ b/lib/public/components/Filters/RunsFilter/runDefinitionFilter.js @@ -19,4 +19,7 @@ import { checkboxes } from '../common/filters/checkboxFilter.js'; * @param {RunDefinitionFilterModel} runDefinitionFilterModel run definition filter model * @return {Component} the filter */ -export const runDefinitionFilter = (runDefinitionFilterModel) => checkboxes(runDefinitionFilterModel, { selector: 'run-definition' }); +export const runDefinitionFilter = (runDefinitionFilterModel) => checkboxes( + runDefinitionFilterModel.selectionModel, + { selector: 'run-definition' }, +); diff --git a/lib/public/components/Filters/RunsFilter/runNumbersFilter.js b/lib/public/components/Filters/RunsFilter/runNumbersFilter.js new file mode 100644 index 0000000000..1beeadee0a --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/runNumbersFilter.js @@ -0,0 +1,25 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { rawTextFilter } from '../common/filters/rawTextFilter.js'; + +/** + * Component to filter runs on run number + * + * @param {RawTextFilterModel} filterModel the filter model + * @return {Component} the filter + */ +export const runNumbersFilter = (filterModel) => rawTextFilter( + filterModel, + { classes: ['w-100', 'run-numbers-filter'], placeholder: 'e.g. 534454, 534455...' }, +); diff --git a/lib/public/components/Filters/RunsFilter/triggerValueFilter.js b/lib/public/components/Filters/RunsFilter/triggerValueFilter.js new file mode 100644 index 0000000000..5addab02fe --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/triggerValueFilter.js @@ -0,0 +1,21 @@ +import { checkboxFilter } from '../common/filters/checkboxFilter.js'; +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. + */ +export const triggerValueFilter = (runModel) => checkboxFilter( + 'triggerValue', + TRIGGER_VALUES, + (value) => runModel.triggerValuesFilters.has(value), + (e, value) => { + if (e.target.checked) { + runModel.triggerValuesFilters.add(value); + } else { + runModel.triggerValuesFilters.delete(value); + } + runModel.triggerValuesFilters = Array.from(runModel.triggerValuesFilters); + }, +); diff --git a/lib/public/components/Filters/common/FilterModel.js b/lib/public/components/Filters/common/FilterModel.js index d16f1226f7..cc7badb53c 100644 --- a/lib/public/components/Filters/common/FilterModel.js +++ b/lib/public/components/Filters/common/FilterModel.js @@ -57,17 +57,6 @@ export class FilterModel extends Observable { throw new Error('Abstract function call'); } - /** - * Sets filters from normalised values to submodels in needed. - * - * @param {string|number|object|string[]|number[]|null} _value The value used to set filters - * @return {void} the normalized value - * @abstract - */ - set normalized(_value) { - throw new Error('Abstract function call'); - } - /** * Returns the observable notified any time there is a visual change which has no impact on the actual filter value * @@ -77,15 +66,6 @@ export class FilterModel extends Observable { return this._visualChange$; } - /** - * States if the filter is active. By default this is equivalent to isEmpty - * - * @return {boolean} true if the filter is active - */ - get isInactive() { - return this.isEmpty; - } - /** * Utility function to register a filter model as sub-filter model * diff --git a/lib/public/components/Filters/common/FilteringModel.js b/lib/public/components/Filters/common/FilteringModel.js index 2f196d4f7c..e937786456 100644 --- a/lib/public/components/Filters/common/FilteringModel.js +++ b/lib/public/components/Filters/common/FilteringModel.js @@ -12,16 +12,7 @@ */ import { expandQueryLikeNestedKey } from '../../../utilities/expandNestedKey.js'; -import { SelectionModel } from '../../common/selection/SelectionModel.js'; -import { FilterModel } from './FilterModel.js'; -import { buildUrl, Observable, parseUrlParameters } from '/js/src/index.js'; - -const WARNING_TYPES = Object.freeze({ - PAGE_MISMATCH: 'Page-Filter mismatch', - UNKNOWN_FILTERS: 'Unknown Filters', - UNPARSABLE_URL: 'Unparseable URL', - UNPARSABLE_FILTERS: 'Unparsable Filters', -}); +import { Observable } from '/js/src/index.js'; /** * Model representing a filtering system, including filter inputs visibility, filters values and so on @@ -30,45 +21,28 @@ export class FilteringModel extends Observable { /** * Constructor * - * @param {QueryRouter} router router that controls the application's page navigation * @param {Object} filters the filters with their label and model - * @param {Map} warnings object reference used to define warnings. */ - constructor(router, filters, warnings) { + constructor(filters) { super(); - this._visualChange$ = new Observable(); - this._pageIdentifier = null; - this._warnings = warnings; - this._router = router; - this._filters = {}; - this._filterModels = []; - Object.entries(filters).forEach(([key, model]) => this.put(key, model)); - } + this._visualChange$ = new Observable(); - /** - * Sets the page identifiers - * - * @param {string} identifier a string identifies a page from the router params. - * Used to prevent unneeded reads/writes from/to the url - * @returns {void} - */ - set pageIdentifier(identifier) { - this._pageIdentifier = identifier; + this._filters = filters; + this._filterModels = Object.values(filters); + for (const model of this._filterModels) { + model.bubbleTo(this); + model.visualChange$?.bubbleTo(this._visualChange$); + } } /** * Reset the filters * * @param {boolean} [notify=false] if true the model notifies its observers - * @param {boolean} [clearUrl=false] if true filters will be removed from the url * @return {void} */ - reset(notify = false, clearUrl = false) { - if (!this.isAnyFilterActive()) { - return; - } - + reset(notify = false) { for (const model of this._filterModels) { model.reset(); } @@ -76,13 +50,6 @@ export class FilteringModel extends Observable { if (notify) { this.notify(); } - - if (clearUrl) { - this._clearWarnings(); - const { params } = this._router; - params.filter = this.normalized; - this._router.go(buildUrl('?', params), false, true); - } } /** @@ -107,7 +74,12 @@ export class FilteringModel extends Observable { * @return {boolean} true if at least one filter is active */ isAnyFilterActive() { - return !this._filterModels.every((model) => model.isInactive); + for (const model of this._filterModels) { + if (!model.isEmpty) { + return true; + } + } + return false; } /** @@ -133,123 +105,6 @@ export class FilteringModel extends Observable { return this._filters[key]; } - /** - * When the user updates the displayed Objects, the filters should be placed in the URL as well - * @returns {undefined} - */ - setFilterToURL() { - const { params } = this._router; - const newParams = { ...params }; - newParams.filter = this.normalized; - - if (this._pageIdentifier === params.page) { - this._router.go(buildUrl('?', newParams), false, true); - } - - this.notify(); - } - - /** - * Compute seach parameters based a url or router - * - * @param {string} url the url that is to be parsed - * @returns {object} the serach parameters object - */ - _computeParameters(url) { - try { - return parseUrlParameters(new URL(url).searchParams); - } catch { - this._warnings.set(WARNING_TYPES.UNPARSABLE_URL, `URL could not be parsed. URL: ${url}`); - this.notify(); - return {}; - } - } - - /** - * Look for parameters used for filtering in URL and apply them in the layout if it exists - * - * @param {boolean} notify if observers should be notified after setting the filters - * @param {string|null} [url=null] the url that is to be parsed into active filters - * @returns {undefined} - */ - setFilterFromURL(notify = false, url = null) { - this._clearWarnings(); - - const params = url ? this._computeParameters(url) : this._router.params; - const { page, filter } = params; - - if (this._pageIdentifier !== page) { - if (url && page) { // 'page' might be undefined if the url is unparsable - this._warnings.set(WARNING_TYPES.PAGE_MISMATCH, `The filters provided were meant for ${page}`); - } - } else { - if (!filter) { - this.reset(); - return; - } - - const { setFilterErrors, unknownFilters } = this._setFilters(filter); - - if (setFilterErrors.length > 0) { - this._warnings.set( - WARNING_TYPES.UNPARSABLE_FILTERS, - `The following filter-value pairs could not be parsed: [${setFilterErrors.join(', ')}]`, - ); - } - - if (unknownFilters.length > 0) { - this._warnings.set( - WARNING_TYPES.UNKNOWN_FILTERS, - `The filters: [${unknownFilters.join(', ')}]; are not reccognised. Check if they are spelled correctly.`, - ); - } - - if (url) { - this._router.go(buildUrl('?', params), false, true); - } - } - - if (notify) { - this.notify(); - } - } - - /** - * Clear all filter-related warnings from the warnings map - * - * @returns {undefined} - */ - _clearWarnings() { - for (const key in Object.keys(WARNING_TYPES)) { - this._warnings.delete(key); - } - } - - /** - * Sets all filters using their normalized setters - * - * @param {object} an object containging the uknown filters and the filters that failed to parse - */ - _setFilters(filters) { - const unknownFilters = []; - const setFilterErrors = []; - - for (const [key, value] of Object.entries(filters)) { - if (key in this._filters) { - try { - this._filters[key].normalized = value; - } catch { - setFilterErrors.push(`${buildUrl('', { [key]: value }).slice(1)}`); - } - } else { - unknownFilters.push(`'${key}'`); - } - } - - return { unknownFilters, setFilterErrors }; - } - /** * Add new filter * @@ -263,13 +118,9 @@ export class FilteringModel extends Observable { return; } - if (!(filter instanceof FilterModel || filter instanceof SelectionModel)) { - throw new Error('Filter must extend FilterModel or SelectionModel'); - } - this._filters[key] = filter; this._filterModels.push(filter); - filter.observe(() => this.setFilterToURL()); + filter.bubbleTo(this); filter.visualChange$?.bubbleTo(this._visualChange$); } } diff --git a/lib/public/components/Filters/common/RadioButtonFilterModel.js b/lib/public/components/Filters/common/RadioButtonFilterModel.js deleted file mode 100644 index 0aaa6e70af..0000000000 --- a/lib/public/components/Filters/common/RadioButtonFilterModel.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { SelectionModel } from '../../common/selection/SelectionModel.js'; - -/** - * Model for managing a radiobutton view and state - */ -export class RadioButtonFilterModel extends SelectionModel { - /** - * Constructor - * - * @param {SelectionOption[]} [availableOptions] the list of possible operators - * @param {function} [setDefault] function that selects the default from the list of available options. Selects first entry by default - * @param {boolean} [defaultIsEmpty] if true, the default selection will be treated as empty - */ - constructor(availableOptions, setDefault = (options) => [options[0]], defaultIsEmpty = true) { - super({ - availableOptions, - defaultSelection: setDefault(availableOptions), - multiple: false, - allowEmpty: false, - }); - - this._defaultIsEmpty = defaultIsEmpty; - } - - /** - * @inheritdoc - */ - get isEmpty() { - if (this._defaultIsEmpty) { - return this.hasOnlyDefaultSelection(); - } - - return false; - } -} diff --git a/lib/public/components/Filters/common/TagFilterModel.js b/lib/public/components/Filters/common/TagFilterModel.js index e92d129eed..c3ce81e09f 100644 --- a/lib/public/components/Filters/common/TagFilterModel.js +++ b/lib/public/components/Filters/common/TagFilterModel.js @@ -58,19 +58,11 @@ export class TagFilterModel extends FilterModel { */ get normalized() { return { - values: this._selectionModel.normalized, - operation: this._combinationOperatorModel.normalized, + values: this.selected.join(), + operation: this.combinationOperator, }; } - /** - * @inheritDoc - */ - set normalized({ values, operation }) { - this._selectionModel.normalized = values; - this._combinationOperatorModel.normalized = operation; - } - /** * Return the model handling tag selection state * diff --git a/lib/public/components/Filters/common/filters/FilterInputModel.js b/lib/public/components/Filters/common/filters/FilterInputModel.js new file mode 100644 index 0000000000..8860edf61d --- /dev/null +++ b/lib/public/components/Filters/common/filters/FilterInputModel.js @@ -0,0 +1,119 @@ +/** + * @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 { Observable } from '/js/src/index.js'; + +/** + * Model for a generic filter input + */ +export class FilterInputModel extends Observable { + /** + * Constructor + */ + constructor() { + super(); + + this._value = null; + this._raw = ''; + + this._visualChange$ = new Observable(); + } + + /** + * Define the current value of the filter + * + * @param {string} raw the raw value of the filter + * @return {void} + */ + update(raw) { + const previousValues = this.value; + + this._value = this.valueFromRaw(raw); + this._raw = raw; + + if (this.areValuesEquals(this.value, previousValues)) { + // Only raw value changed + this._visualChange$.notify(); + } else { + this.notify(); + } + } + + /** + * Reset the filter to its default value + * + * @return {void} + */ + reset() { + this._value = null; + this._raw = ''; + } + + /** + * Returns the raw value of the filter (the user input) + * + * @return {string} the raw value + */ + get raw() { + return this._raw; + } + + /** + * Return the parsed values of the filter + * + * @return {*} the parsed values + */ + get value() { + return this._value; + } + + /** + * States if the filter has been filled + * + * @return {boolean} true if the filter has been filled + */ + get isEmpty() { + return !this.value; + } + + /** + * Returns the observable notified any time there is a visual change which has no impact on the actual filter value + * + * @return {Observable} the observable + */ + get visualChange$() { + return this._visualChange$; + } + + /** + * Returns the processed value from raw input + * + * @param {string} raw the raw input value + * @return {*} the processed value + * @protected + */ + valueFromRaw(raw) { + return raw.trim(); + } + + /** + * Compares two values + * + * @param {*} first the first value + * @param {*} second the second value + * @return {boolean} true if the values are equals + * @protected + */ + areValuesEquals(first, second) { + return first === second; + } +} diff --git a/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js b/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js index 843500ad1f..ee00126389 100644 --- a/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js +++ b/lib/public/components/Filters/common/filters/NumericalComparisonFilterModel.js @@ -27,7 +27,6 @@ export class NumericalComparisonFilterModel extends FilterModel { constructor(options) { super(); const { scale = 1, integer = false } = options || {}; - this._scale = scale; this._operatorSelectionModel = new ComparisonSelectionModel(); this._operatorSelectionModel.visualChange$.bubbleTo(this._visualChange$); @@ -83,25 +82,11 @@ export class NumericalComparisonFilterModel extends FilterModel { */ get normalized() { return { - operator: this._operatorSelectionModel.normalized, - limit: this._operandInputModel.normalized, + operator: this._operatorSelectionModel.current, + limit: this._operandInputModel.value, }; } - /** - * @inheritDoc - */ - set normalized({ operator, limit }) { - const numericLimit = parseFloat(limit); - const scaledLimit = numericLimit / this._scale; - - if (!isNaN(numericLimit) || !isNaN(scaledLimit)) { - this._operandInputModel.normalized = { value: numericLimit, raw: scaledLimit }; - } - - this._operatorSelectionModel.normalized = operator; - } - /** * @inheritDoc */ diff --git a/lib/public/components/Filters/common/filters/ProcessedTextInputModel.js b/lib/public/components/Filters/common/filters/ProcessedTextInputModel.js index d9488cd8f1..9e46fe95b5 100644 --- a/lib/public/components/Filters/common/filters/ProcessedTextInputModel.js +++ b/lib/public/components/Filters/common/filters/ProcessedTextInputModel.js @@ -98,27 +98,6 @@ export class ProcessedTextInputModel extends Observable { this._value = null; } - /** - * Returns the normalized value of the filter, that can be used as URL parameter - * @returns {string} - */ - get normalized() { - return this._value; - } - - /** - * Sets filters from normalised values. - * - * @param {string} value The value used to set the parsed value - * @param {string} raw The value used to set the raw value - * @return {void} - * @abstract - */ - set normalized({ value, raw }) { - this._value = value; - this._raw = raw; - } - /** * Return the visual change observable * diff --git a/lib/public/components/Filters/common/filters/RawTextFilterModel.js b/lib/public/components/Filters/common/filters/RawTextFilterModel.js index d156c86e10..f996b7b976 100644 --- a/lib/public/components/Filters/common/filters/RawTextFilterModel.js +++ b/lib/public/components/Filters/common/filters/RawTextFilterModel.js @@ -35,13 +35,6 @@ export class RawTextFilterModel extends FilterModel { return this._value; } - /** - * @inheritDoc - */ - set normalized(value) { - this._value = value; - } - /** * Return the filter current value * diff --git a/lib/public/components/Filters/common/filters/SelectionFilterModel.js b/lib/public/components/Filters/common/filters/SelectionFilterModel.js new file mode 100644 index 0000000000..4bb602d7aa --- /dev/null +++ b/lib/public/components/Filters/common/filters/SelectionFilterModel.js @@ -0,0 +1,63 @@ +/** + * @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 { FilterModel } from '../FilterModel.js'; +import { SelectionModel } from '../../../common/selection/SelectionModel.js'; + +/** + * Filter model based on a selection model + */ +export class SelectionFilterModel extends FilterModel { + /** + * Constructor + * + * @param {object} [configuration] the selection filter configuration + * @param {SelectionOption[]} [configuration.availableOptions=[]] the list of available options + */ + constructor(configuration) { + super(); + + this._selectionModel = new SelectionModel({ availableOptions: configuration.availableOptions }); + this._selectionModel.bubbleTo(this); + } + + /** + * @inheritDoc + */ + reset() { + this._selectionModel.reset(); + } + + /** + * @inheritDoc + */ + get isEmpty() { + return this._selectionModel.isEmpty; + } + + /** + * @inheritDoc + */ + get normalized() { + return this._selectionModel.selected.join(','); + } + + /** + * Return the underlying selection model + * + * @return {SelectionModel} the underlying selection model + */ + get selectionModel() { + return this._selectionModel; + } +} diff --git a/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js b/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js index 7f843d6295..b6510f8fae 100644 --- a/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js +++ b/lib/public/components/Filters/common/filters/TextComparisonFilterModel.js @@ -64,19 +64,11 @@ export class TextComparisonFilterModel extends FilterModel { */ get normalized() { return { - operator: this._operatorSelectionModel.normalized, - limit: this._operandInputModel.normalized, + operator: this._operatorSelectionModel.current, + limit: this._operandInputModel.value, }; } - /** - * @inheritDoc - */ - set normalized({ operator, limit }) { - this._operatorSelectionModel.normalized = operator; - this._operandInputModel.normalized = limit; - } - /** * @inheritDoc */ diff --git a/lib/public/components/Filters/common/filters/TextTokensFilterModel.js b/lib/public/components/Filters/common/filters/TextTokensFilterModel.js index 8c838e5abf..60e192febe 100644 --- a/lib/public/components/Filters/common/filters/TextTokensFilterModel.js +++ b/lib/public/components/Filters/common/filters/TextTokensFilterModel.js @@ -78,13 +78,6 @@ export class TextTokensFilterModel extends FilterModel { .filter((token) => token.length > 0); } - /** - * @inheritDoc - */ - set normalized(value) { - this._raw = value.join(TOKENS_DELIMITER); - } - /** * Returns the observable notified any time there is a visual change which has no impact on the actual filter value * @return {Observable} the observable diff --git a/lib/public/components/Filters/common/filters/TimeRangeInputModel.js b/lib/public/components/Filters/common/filters/TimeRangeInputModel.js index 66a4481847..54ee3fe7b0 100644 --- a/lib/public/components/Filters/common/filters/TimeRangeInputModel.js +++ b/lib/public/components/Filters/common/filters/TimeRangeInputModel.js @@ -142,14 +142,6 @@ export class TimeRangeInputModel extends FilterModel { }; } - /** - * @inheritDoc - */ - set normalized({ from, to }) { - this._fromTimeInputModel.setValue(parseInt(from, 10), true); - this._toTimeInputModel.setValue(parseInt(to, 10), true); - } - /** * States if the filter value is valid * diff --git a/lib/public/components/Filters/common/filters/ToggleFilterModel.js b/lib/public/components/Filters/common/filters/ToggleFilterModel.js deleted file mode 100644 index ee22703852..0000000000 --- a/lib/public/components/Filters/common/filters/ToggleFilterModel.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ -import { SelectionModel } from '../../../common/selection/SelectionModel.js'; - -/** - * SelectionModel that restricts the selection to a boolean toggle (true/false). - */ -export class ToggleFilterModel extends SelectionModel { - /** - * Constructor - * @param {boolean} toggledByDefault If the filter should be toggled by default - * @param {boolean} defaultIsInactive if true, will treat the untoggled state (false) as empty. - */ - constructor(toggledByDefault = false, defaultIsInactive = false) { - super({ - availableOptions: [{ value: true }, { value: false }], - defaultSelection: [{ value: toggledByDefault }], - multiple: false, - allowEmpty: false, - }); - - this._defaultIsInactive = defaultIsInactive; - } - - /** - * Returns true if the current value is set to true - * - * @return {boolean} true if filter is stable beams only - */ - get isToggled() { - return this.current; - } - - /** - * Toggles the filter state - * - * @return {void} - */ - toggle() { - this.select({ value: !this.current }); - } - - /** - * Toggles are always filled, as 'false' / untoggled is also considered a value - * - * @return {boolean} `false` - */ - get isEmpty() { - return false; - } - - /** - * Returns if the toggle filter is considered 'inactive' - * - * @return {boolean} - */ - get isInactive() { - if (this._defaultIsInactive) { - return this.hasOnlyDefaultSelection(); - } - - return false; - } -} diff --git a/lib/public/components/Filters/common/filters/checkboxFilter.js b/lib/public/components/Filters/common/filters/checkboxFilter.js index 2cf550c091..dcfcb4a95b 100644 --- a/lib/public/components/Filters/common/filters/checkboxFilter.js +++ b/lib/public/components/Filters/common/filters/checkboxFilter.js @@ -14,6 +14,32 @@ import { h } from '/js/src/index.js'; +/** + * A general component for generating checkboxes. + * + * @param {string} name The general name of the element. + * @param {Array} values the list of options to display + * @param {function} isChecked true if the checkbox is checked, else false + * @param {function} onChange the handler called once the checkbox state changes (change event is passed as first parameter, value as second) + * @param {Object} [additionalProperties] Additional options that can be given to the class. + * @returns {vnode} An object that has one or multiple checkboxes. + * @deprecated use checkboxes + */ +export const checkboxFilter = (name, values, isChecked, onChange, additionalProperties) => + h('.flex-row.flex-wrap', values.map((value) => h('.form-check.flex-grow', [ + h('input.form-check-input', { + id: `${name}Checkbox${value}`, + class: name, + type: 'checkbox', + checked: isChecked(value), + onchange: (e) => onChange(e, value), + ...additionalProperties || {}, + }), + h('label.form-check-label', { + for: `${name}Checkbox${value}`, + }, value.toUpperCase()), + ]))); + /** * Display a filter composed of checkbox listing pre-defined options * @param {SelectionModel} selectionModel filter model diff --git a/lib/public/components/Filters/common/filters/radioButtonFilter.js b/lib/public/components/Filters/common/filters/radioButtonFilter.js deleted file mode 100644 index 88f42c610a..0000000000 --- a/lib/public/components/Filters/common/filters/radioButtonFilter.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE Trg. 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-Trg.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 { radioButton } from '../../../common/form/inputs/radioButton.js'; -import { h } from '/js/src/index.js'; - -/** - * Radio button filter component - * - * @param {RadioSelectionModel} selectionModel the a selectionmodel - * @param {string} filterName the name of the filter - * @return {vnode} A number of radio buttons corresponding with the selection options - */ -const radioButtonFilter = (selectionModel, filterName) => { - const name = `${filterName}FilterRadio`; - return h( - '.flex-row.w-100', - selectionModel.options.map((option) => { - const { label } = option; - const action = () => selectionModel.select(option); - const isChecked = selectionModel.isSelected(option); - - return radioButton({ label, isChecked, action, name }); - }), - ); -}; - -export default radioButtonFilter; diff --git a/lib/public/components/Filters/common/filters/textFilter.js b/lib/public/components/Filters/common/filters/textFilter.js index d6ae0cdfa4..6b288d54ac 100644 --- a/lib/public/components/Filters/common/filters/textFilter.js +++ b/lib/public/components/Filters/common/filters/textFilter.js @@ -16,13 +16,13 @@ import { h } from '/js/src/index.js'; /** * Returns a text filter component * - * @param {TextTokensFilterModel} textTokensFilterModel the model of the text filter + * @param {FilterInputModel|TextTokensFilterModel} filterInputModel the model of the text filter * @param {Object} attributes the additional attributes to pass to the component, such as id and classes * @return {Component} the filter component */ -export const textFilter = (textTokensFilterModel, attributes) => h('input', { +export const textFilter = (filterInputModel, attributes) => h('input', { ...attributes, type: 'text', - value: textTokensFilterModel.raw, - oninput: (e) => textTokensFilterModel.update(e.target.value), + value: filterInputModel.raw, + oninput: (e) => filterInputModel.update(e.target.value), }, ''); diff --git a/lib/public/components/Filters/common/filters/textInputFilter.js b/lib/public/components/Filters/common/filters/textInputFilter.js deleted file mode 100644 index 27a36f112d..0000000000 --- a/lib/public/components/Filters/common/filters/textInputFilter.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { rawTextFilter } from './rawTextFilter.js'; - -/** - * Standardised component for a rawTextFilter that span the width of their container - * - * @param {FilteringModel} filteringModel the page's filteringModel - * @param {string} key the identifier to serve as css selector and to fetch the correct filter from the filteringModel - * @param {string} placeholder placeholder text for the input element - * @param {string} width class that determines the width of the input - * @return {Component} the filter - */ -export const textInputFilter = (filteringModel, key, placeholder, widthClass = 'w-100') => - rawTextFilter(filteringModel.get(key), { classes: [widthClass, `${key}-textFilter`], placeholder }); diff --git a/lib/public/components/Filters/common/filters/toggleFilter.js b/lib/public/components/Filters/common/filters/toggleFilter.js deleted file mode 100644 index ac37063779..0000000000 --- a/lib/public/components/Filters/common/filters/toggleFilter.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE Trg. 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-Trg.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 { switchInput } from '../../../common/form/switchInput.js'; -import { radioButton } from '../../../common/form/inputs/radioButton.js'; - -/** - * Display a toggle switch or radio buttons for toggle filters - * - * @param {ToggleFilterModel} toggleFilterModel a ToggleFilterModel - * @param {name} toggleFilterModel the name used to identify and label the filter - * @param {boolean} radioButtonMode define whether or not to return radio buttons or a switch. - * @returns {Component} the toggle switch - */ -export const toggleFilter = (toggleFilterModel, name, id, radioButtonMode = false) => { - if (radioButtonMode) { - return h('.flex-row.w-100', [ - radioButton({ - label: 'OFF', - isChecked: !toggleFilterModel.isToggled, - action: () => toggleFilterModel.toggle(), - name, - }), - radioButton({ - label: 'ON', - isChecked: toggleFilterModel.isToggled, - action: () => toggleFilterModel.toggle(), - name, - }), - ]); - } - - return h('', switchInput(toggleFilterModel.isToggled, () => toggleFilterModel.toggle(), { labelAfter: name, id })); -}; diff --git a/lib/public/components/Filters/common/filtersPanelPopover.js b/lib/public/components/Filters/common/filtersPanelPopover.js index 648427db34..e0c0a7490c 100644 --- a/lib/public/components/Filters/common/filtersPanelPopover.js +++ b/lib/public/components/Filters/common/filtersPanelPopover.js @@ -10,8 +10,7 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ -import { h, info, popover, PopoverAnchors, PopoverTriggerPreConfiguration, DropdownComponent, CopyToClipboardComponent } from '/js/src/index.js'; -import { iconCaretBottom } from '/js/src/icons.js'; +import { h, info, popover, PopoverAnchors, PopoverTriggerPreConfiguration } from '/js/src/index.js'; import { profiles } from '../../common/table/profiles.js'; import { applyProfile } from '../../../utilities/applyProfile.js'; import { tooltip } from '../../common/popover/tooltip.js'; @@ -36,27 +35,7 @@ import { tooltip } from '../../common/popover/tooltip.js'; * * @return {Component} the button component */ -const filtersToggleTrigger = () => h('button#openFilterToggle.btn.btn.btn-primary.first-item', 'Filters'); - -/** - * Button component that resets all filters upon click - * - * @param {FilteringModel|OverviewPageModel} filteringModel the FilteringModel - * @param {bool} [isIcon=false] if the component is rendered as a regular button with text or as a component with an 'X' icon - * @returns {Component} the reset button component - */ -const resetFiltersButton = (filteringModel, isIcon = false) => { - const attributes = { - disabled: !filteringModel.isAnyFilterActive(), - onclick: () => filteringModel.resetFiltering - ? filteringModel.resetFiltering(true, true) - : filteringModel.reset(true, true), - }; - - return isIcon - ? h('.clear-filter-icon-container.btn-group-item.last-item.pulse-red', attributes, h('.clear-filter-icon.b1.b-danger', 'X')) - : h('button#reset-filters.btn.btn-danger', attributes, 'Reset all filters'); -}; +const filtersToggleTrigger = () => h('button#openFilterToggle.btn.btn.btn-primary', 'Filters'); /** * Create main header of the filters panel @@ -65,7 +44,16 @@ const resetFiltersButton = (filteringModel, isIcon = false) => { */ const filtersToggleContentHeader = (filteringModel) => h('.flex-row.justify-between', [ h('.f4', 'Filters'), - resetFiltersButton(filteringModel), + h( + 'button#reset-filters.btn.btn-danger', + { + onclick: () => filteringModel.resetFiltering + ? filteringModel.resetFiltering() + : filteringModel.reset(true), + disabled: !filteringModel.isAnyFilterActive(), + }, + 'Reset all filters', + ), ]); /** @@ -126,9 +114,9 @@ const filtersToggleContent = ( * @param {FiltersConfiguration} filtersConfiguration filters configuration * @param {object} [configuration] optional configuration * @param {string} [configuration.profile] specify for which profile filtering should be enabled - * @return {Component} the filter button component + * @return {Component} the filter component */ -const filtersPanelButton = (filteringModel, filtersConfiguration, configuration) => popover( +export const filtersPanelPopover = (filteringModel, filtersConfiguration, configuration) => popover( filtersToggleTrigger(), filtersToggleContent(filteringModel, filtersConfiguration, configuration), { @@ -136,94 +124,3 @@ const filtersPanelButton = (filteringModel, filtersConfiguration, configuration) anchor: PopoverAnchors.RIGHT_START, }, ); - -/** - * A button component that lets the user copy the url if there are active filters. - * - * @param {boolean} activeFilters if false, will disable the button - * @returns {Component} the copy button component - */ -const copyButtonOption = (activeFilters) => h( - '', - { style: activeFilters ? {} : { opacity: 0.5, pointerEvents: 'none' } }, - h(CopyToClipboardComponent, { value: location.href, id: 'filters' }, 'Copy Active Filters'), -); - -/** - * A button component that lets the user paste the first entry of their clipboard as a filter url. - * - * @param {FilteringModel|OverviewPageModel} model the FilteringModel - * @returns {Component} the paste button component - */ -const pasteButtonOption = (model) => { - const clipboardSupported = navigator?.clipboard && window.isSecureContext; - - // Sometimes, the overview model is passed to filterPanelPopover instead of the filteringmodel (e.g. envirionments) - const { filteringModel = model } = model; - - return h('button.btn.btn-primary', { - onclick: async () => { - const url = await navigator.clipboard.readText(); - filteringModel.setFilterFromURL(true, url); - }, - disabled: !clipboardSupported, - id: 'paste-filters', - }, 'Paste filters'); -}; - -/** - * A indicates if any filters are currently active on the page - * - * @param {FilteringModel} model the filtering model - * @returns {Component} the active filters indicator - */ -const activeFilterIndicator = (model) => { - // Sometimes, the overview model is passed to filterPanelPopover instead of the filteringmodel (e.g. envirionments) - const { filteringModel = model } = model; - - const hasActiveFilters = filteringModel.isAnyFilterActive(); - const innerText = `Filters ${hasActiveFilters ? 'Active' : 'Inactive'}`; - - let indicator = '.active-filters-indicator.b1'; - indicator += hasActiveFilters ? '.b-success.success.pulse-green' : '.inactive'; - - const children = [h(indicator, innerText)]; - - if (hasActiveFilters) { - children.push(resetFiltersButton(filteringModel, true)); - } - - return h('.flex-row.items-center', children); -}; - -/** - * Return component composed of the filter popover button and a dropdown trigger - * - * @param {FilteringModel} filteringModel the filtering model - * @param {FiltersConfiguration} filtersConfiguration filters configuration - * @param {object} [configuration] optional configuration - * @param {string} [configuration.profile] specify for which profile filtering should be enabled - * @return {Component} the filter component - */ -export const filtersPanelPopover = (filteringModel, filtersConfiguration, configuration) => { - const hasActiveFilters = filteringModel.isAnyFilterActive(); - - return h( - '.flex-row.items-center.btn-group', - [ - filtersPanelButton(filteringModel, filtersConfiguration, configuration), - DropdownComponent( - h('.btn.btn-group-item.last-item', iconCaretBottom()), - h( - '.flex-column.p2.g2', - [ - copyButtonOption(hasActiveFilters), - pasteButtonOption(filteringModel), - resetFiltersButton(filteringModel), - ], - ), - ), - activeFilterIndicator(filteringModel), - ], - ); -}; diff --git a/lib/public/components/common/form/inputs/DateTimeInputModel.js b/lib/public/components/common/form/inputs/DateTimeInputModel.js index 69456fd95d..2aec85f59f 100644 --- a/lib/public/components/common/form/inputs/DateTimeInputModel.js +++ b/lib/public/components/common/form/inputs/DateTimeInputModel.js @@ -65,15 +65,13 @@ export class DateTimeInputModel extends Observable { */ update(raw) { this._raw = raw; - const hasDateAndTime = raw.date && raw.time; - try { - this._value = hasDateAndTime ? extractTimestampFromDateTimeInput(raw) : null; + this._value = raw.date && raw.time ? extractTimestampFromDateTimeInput(raw, { seconds: this._seconds }) : null; } catch { this._value = null; } - hasDateAndTime && this.notify(); + this.notify(); } /** @@ -123,10 +121,6 @@ export class DateTimeInputModel extends Observable { return; } - if (isNaN(value)) { - return; - } - this._value = value; this._raw = value !== null ? formatTimestampForDateTimeInput(value, this._seconds) diff --git a/lib/public/components/common/form/switchInput.js b/lib/public/components/common/form/switchInput.js index f06cb5154a..ad7f7f8135 100644 --- a/lib/public/components/common/form/switchInput.js +++ b/lib/public/components/common/form/switchInput.js @@ -32,7 +32,7 @@ import { h } from '/js/src/index.js'; * @return {Component} the switch component */ export const switchInput = (value, onChange, options) => { - const { key, labelAfter, labelBefore, color, id } = options || {}; + const { key, labelAfter, labelBefore, color } = options || {}; const attributes = { ...key ? { key } : {} }; return h( @@ -40,7 +40,7 @@ export const switchInput = (value, onChange, options) => { attributes, [ labelBefore, - h('.switch', { id }, [ + h('.switch', [ h('input', { onchange: (e) => onChange(e.target.checked), type: 'checkbox', diff --git a/lib/public/components/common/messages/warningComponent.js b/lib/public/components/common/messages/warningComponent.js deleted file mode 100644 index 1c37ddf5d7..0000000000 --- a/lib/public/components/common/messages/warningComponent.js +++ /dev/null @@ -1,35 +0,0 @@ -import { h } from '/js/src/index.js'; -import { iconX } from '/js/src/icons.js'; - -/** - * Component to display whenever a page has warnings. - * - * @param {OverviewPageModel} overviewModel model that controlls an overview page - * @returns {Component} the warning componen - */ -export const warningComponent = (overviewModel) => { - const { warnings } = overviewModel; - - if (!warnings.size) { - return null; - } - - return h('details.alert.alert-warning', { open: true }, [ - h('summary', 'Warnings'), - h('ul', warnings.entries().toArray().map(([key, message]) => - h('li.flex-row.items-center', [ - h( - '.btn.btn-pill.alert-warning.mh1', - { - onclick: () => { - warnings.delete(key); - overviewModel.notify(); - }, - }, - iconX(), - ), - h('strong.mh1', `${key}:`), - h('span', message), - ]))), - ]); -}; diff --git a/lib/public/components/common/selection/SelectionModel.js b/lib/public/components/common/selection/SelectionModel.js index b9926b4f32..8b28aa28d1 100644 --- a/lib/public/components/common/selection/SelectionModel.js +++ b/lib/public/components/common/selection/SelectionModel.js @@ -42,12 +42,6 @@ export class SelectionModel extends Observable { super(); const { availableOptions = [], defaultSelection = [], multiple = true, allowEmpty = true } = configuration || {}; - /** - * @type {SelectionOption[]} - * @protected - */ - this._selectionBacklog = []; - /** * @type {RemoteData|SelectionOption[]} * @protected @@ -113,15 +107,6 @@ export class SelectionModel extends Observable { return selected.length === defaultSelection.length && selected.every((item) => defaultSelection.includes(item)); } - /** - * States if the filter is active. By default this is equivalent to isEmpty - * - * @return {boolean} true if the filter is active - */ - get isInactive() { - return this.isEmpty; - } - /** * Reset the selection to the default * @@ -258,7 +243,7 @@ export class SelectionModel extends Observable { } /** - * Defines the list of available options and if there is a selection backlog, these will be applied + * Defines the list of available options * * @param {RemoteData|SelectionOption[]} availableOptions the new available options * @return {void} @@ -266,11 +251,6 @@ export class SelectionModel extends Observable { setAvailableOptions(availableOptions) { this._availableOptions = availableOptions; this.visualChange$.notify(); - - if (this._selectionBacklog.length) { - this.selectedOptions = this._selectionBacklog; - this.notify(); - } } /** @@ -335,19 +315,12 @@ export class SelectionModel extends Observable { } /** - * Define (overrides) the list of currently selected options. - * Invalid selection options are excluded + * Define (overrides) the list of currently selected options * * @param {SelectionOption[]} selected the list of selected options */ set selectedOptions(selected) { - let { options } = this; - - if (this.options instanceof RemoteData) { - options = options.isSuccess() ? options.payload : []; - } - - this._selectedOptions = options.filter((option) => selected.some(({ value }) => String(value) === String(option.value)));; + this._selectedOptions = selected; } /** @@ -358,40 +331,4 @@ export class SelectionModel extends Observable { get optionsSelectedByDefault() { return this._defaultSelection; } - - /** - * Sets selected options based on a comma-seperated string. - * Accounts for the options being either RemoteData or an array. - * - * @param {string} value the value that is to be set. - */ - set normalized(value) { - const options = value.split(',').map((option) => ({ value: option.trim() })); - const isRemoteData = this.options instanceof RemoteData; - const noOptions = !this.options?.length; - - if (isRemoteData) { - this._availableOptions.match({ - Success: (_) => { - this.selectedOptions = options; - }, - Other: () => { - this._selectionBacklog = options; - }, - }); - } else if (noOptions) { - this._selectionBacklog = options; - } else { - this.selectedOptions = options; - } - } - - /** - * Returns the normalized value of the selection - * - * @return {string|string[]|boolean|boolean[]|number|number[]|SelectionOption|SelectionOption[]} the normalized value - */ - get normalized() { - return (this._allowEmpty || this._multiple) ? this.selected.join(',') : this.current; - } } diff --git a/lib/public/components/runEorReasons/runEorReasonSelection.js b/lib/public/components/runEorReasons/runEorReasonSelection.js index c7a3ad14a3..dbe86cde87 100644 --- a/lib/public/components/runEorReasons/runEorReasonSelection.js +++ b/lib/public/components/runEorReasons/runEorReasonSelection.js @@ -22,7 +22,6 @@ import { h } from '/js/src/index.js'; */ export const eorReasonFilterComponent = (eorReasonFilterModel, eorReasonTypes) => { const eorReasonsCategories = [...new Set(eorReasonTypes.map(({ category }) => category))]; - const { category: currentCategory, title: currentTitle } = eorReasonFilterModel; return [ h('.flex-row', [ @@ -37,7 +36,7 @@ export const eorReasonFilterComponent = (eorReasonFilterModel, eorReasonTypes) = h('option', { selected: eorReasonFilterModel.category === '', value: '' }, '-'), eorReasonsCategories.map((category, index) => h( `option#eorCategory${index}`, - { key: category, value: category, selected: category === currentCategory }, + { key: category, value: category }, category, )), ], @@ -55,7 +54,7 @@ export const eorReasonFilterComponent = (eorReasonFilterModel, eorReasonTypes) = .filter((reason) => reason.category === eorReasonFilterModel.category) .map(({ title }, index) => h( `option#eorTitle${index}`, - { key: title, value: title, selected: title === currentTitle }, + { key: title, value: title }, title || '(empty)', )), ], diff --git a/lib/public/components/runTypes/RunTypesFilterModel.js b/lib/public/components/runTypes/RunTypesFilterModel.js index 9767fb0e08..60a923cbc6 100644 --- a/lib/public/components/runTypes/RunTypesFilterModel.js +++ b/lib/public/components/runTypes/RunTypesFilterModel.js @@ -12,18 +12,51 @@ */ import { runTypeToOption } from './runTypeToOption.js'; +import { FilterModel } from '../Filters/common/FilterModel.js'; import { ObservableBasedSelectionDropdownModel } from '../detector/ObservableBasedSelectionDropdownModel.js'; /** * Model storing state of a selection of run types picked from the list of all the existing run types */ -export class RunTypesFilterModel extends ObservableBasedSelectionDropdownModel { +export class RunTypesFilterModel extends FilterModel { /** * Constructor * * @param {ObservableData>} runTypes$ observable remote data of run types list */ constructor(runTypes$) { - super(runTypes$, runTypeToOption); + super(); + this._selectionDropdownModel = new ObservableBasedSelectionDropdownModel(runTypes$, runTypeToOption); + this._addSubmodel(this._selectionDropdownModel); + } + + /** + * @inheritDoc + */ + reset() { + this._selectionDropdownModel.reset(); + } + + /** + * @inheritDoc + */ + get isEmpty() { + return this._selectionDropdownModel.isEmpty; + } + + /** + * @inheritDoc + */ + get normalized() { + return this._selectionDropdownModel.selected; + } + + /** + * Return the underlying selection dropdown model + * + * @return {SelectionDropdownModel} the selection dropdown model + */ + get selectionDropdownModel() { + return this._selectionDropdownModel; } } diff --git a/lib/public/domain/enums/DetectorOrders.js b/lib/public/domain/enums/DetectorOrders.js deleted file mode 100644 index 90094c7f21..0000000000 --- a/lib/public/domain/enums/DetectorOrders.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { DetectorType } from './DetectorTypes.js'; - -/** - * Defines priority mappings for detector types. - * Each key is a mapping between {@link DetectorType} values and their numeric priority - * (larger values will appear first - see detectorsProvider LN88). - * - * - **DEFAULT**: Standard ordering used across most views. - * - **RCT**: Ordering used in the Run Condition Table, which prioritizes PHYSICAL detectors. - */ -export const DetectorOrders = Object.freeze({ - DEFAULT: { - [DetectorType.OTHER]: 0, - [DetectorType.VIRTUAL]: 1, - [DetectorType.PHYSICAL]: 2, - [DetectorType.AOT_GLO]: 3, - [DetectorType.AOT_EVENT]: 4, - [DetectorType.MUON_GLO]: 5, - [DetectorType.QC_ONLY]: 6, - }, - RCT: { - [DetectorType.OTHER]: 0, - [DetectorType.AOT_GLO]: 1, - [DetectorType.AOT_EVENT]: 2, - [DetectorType.MUON_GLO]: 3, - [DetectorType.VIRTUAL]: 4, - [DetectorType.PHYSICAL]: 5, - [DetectorType.QC_ONLY]: 6, - }, -}); diff --git a/lib/public/models/FilterableOverviewPageModel.js b/lib/public/models/FilterableOverviewPageModel.js deleted file mode 100644 index b3d954e05d..0000000000 --- a/lib/public/models/FilterableOverviewPageModel.js +++ /dev/null @@ -1,132 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { buildUrl } from '/js/src/index.js'; -import { OverviewPageModel } from './OverviewModel.js'; -import { FilteringModel } from '../components/Filters/common/FilteringModel.js'; - -/** - * Base model for a filterable overview page - * - * @template T the type of data displayed in the overview page - */ -export class FilterableOverviewPageModel extends OverviewPageModel { - /** - * Constructor - * @param {QueryRouter} router router that controls the application's page navigation - * @param {string} pageIdentifier string that indicates what page this model represents - * @param {Object} filters the filters with their label and model - */ - constructor(router, pageIdentifier, filters) { - super(); - this._filteringModel = new FilteringModel(router, filters, this._warnings); - - this._filteringModel.pageIdentifier = pageIdentifier; - this._filteringModel.visualChange$.bubbleTo(this); - this._filteringModel.observe(() => this._applyFilters()); - this._sortModel.unobserve(this._sortModelCallback); - this._sortModel.observe(() => this._applyFilters()); - this._debouncedLoad = (_time) => {}; // Abstract, does nothing on purpose - this._fetchInstantly = true; - } - - /** - * Builds a url string from filters and a base string - * - * @param {string} base the base string from which the endpoint will be built - * @return {string} - */ - buildRootEndpoint(base) { - return buildUrl(base, { filter: this.getFilterParams() }); - } - - /** - * Sets the fetchInstantly boolean - * @param {boolean} bool the value to set - * @return {void} - */ - set fetchInstantly(bool) { - this._fetchInstantly = bool; - } - - /** - * Returns all filtering, sorting and pagination settings to their default values - * @param {boolean} [fetch = true] whether to refetch all data after filters have been reset - * @return {void} - */ - reset(fetch = true) { - super.reset(); - this.resetFiltering(fetch); - } - - /** - * Reset all filtering models - * @param {boolean} fetch Whether to refetch all data after filters have been reset - * @param {boolean} [clearUrl=false] if true filters will be removed from the url - * @return {void} - */ - resetFiltering(fetch = true, clearUrl = false) { - this._filteringModel.reset(false, clearUrl); - - if (fetch) { - this._applyFilters(true); - } - } - - /** - * Checks if any filter value has been modified from their default (empty) - * @return {Boolean} If any filter is active - */ - isAnyFilterActive() { - return this._filteringModel.isAnyFilterActive(); - } - - /** - * Apply the current filtering and update the remote data list - * - * @param {boolean} now if true, filtering will be applied now without debouncing - * - * @return {void} - */ - _applyFilters() { - this._pagination.silentlySetCurrentPage(1); - this._fetchInstantly ? this.load() : this._debouncedLoad(); - } - - /** - * Set underlying FilteringModel's filters from the query parameters in the URL - * - * @param {boolean} notify if the FilteringModel should notify it's observers after finishing setting the filters - */ - setFilterFromURL(notify) { - this._filteringModel.setFilterFromURL(notify); - } - - /** - * Return the filtering model - * - * @return {FilteringModel} the filtering model - */ - get filteringModel() { - return this._filteringModel; - } - - /** - * Return filter params of base model - * - * @return {object} filter - */ - getFilterParams() { - return this._filteringModel.normalized; - } -} diff --git a/lib/public/models/OverviewModel.js b/lib/public/models/OverviewModel.js index 73334c204b..69ae0c3df3 100644 --- a/lib/public/models/OverviewModel.js +++ b/lib/public/models/OverviewModel.js @@ -38,15 +38,12 @@ export class OverviewPageModel extends Observable { */ constructor() { super(); - this._warnings = new Map(); - this._sortModel = new SortModel(); - this._sortModelCallback = () => { + this._sortModel = new SortModel(); + this._sortModel.observe(() => { this._pagination.silentlySetCurrentPage(1); this.load(); - }; - - this._sortModel.observe(this._sortModelCallback); + }); this._sortModel.visualChange$.bubbleTo(this); // Single page data handling @@ -100,7 +97,6 @@ export class OverviewPageModel extends Observable { reset() { this._item$.setCurrent(RemoteData.notAsked()); this._pagination.reset(); - this._warnings.clear(); } /** @@ -253,13 +249,4 @@ export class OverviewPageModel extends Observable { hasAnyData() { return this._item$.getCurrent().match({ Success: ({ length = 0 } = {}) => length > 0, Other: () => false }); } - - /** - * Returns the warnings object - * - * @return {object} the warning model - */ - get warnings() { - return this._warnings; - } } diff --git a/lib/public/services/detectors/detectorsProvider.js b/lib/public/services/detectors/detectorsProvider.js index 2370f19942..3825835d66 100644 --- a/lib/public/services/detectors/detectorsProvider.js +++ b/lib/public/services/detectors/detectorsProvider.js @@ -15,7 +15,6 @@ import { switchCase } from '/js/src/index.js'; import { getRemoteData } from '../../utilities/fetch/getRemoteData.js'; import { ObservableData } from '../../utilities/ObservableData.js'; import { DetectorType, DATA_TAKING_DETECTOR_TYPES, QC_DETECTORS } from '../../domain/enums/DetectorTypes.js'; -import { DetectorOrders } from '../../domain/enums/DetectorOrders.js'; import { NonPhysicalDetector } from '../../domain/enums/detectorsNames.mjs'; @@ -45,12 +44,9 @@ const getQcDetectorsFromAllDetectors = (allDetectors) => allDetectors export class DetectorsProvider extends RemoteDataProvider { /** * Constructor - * - * @param {DetectorOrders} detectorOrder the order to base sorting on, default is DetectorOrders.DEFAULT */ - constructor(detectorOrder = DetectorOrders.DEFAULT) { + constructor() { super(); - this._detectorOrder = detectorOrder; this._physical$ = ObservableData.builder() .source(this._items$) .apply((remoteDetectors) => remoteDetectors.apply({ @@ -78,14 +74,21 @@ export class DetectorsProvider extends RemoteDataProvider { */ async getRemoteData() { const { data: detectors } = await getRemoteData('/api/detectors'); - const typeToOrderingKey = (type) => switchCase(type, this._detectorOrder); + const typeToOrderingKey = (type) => switchCase(type, { + [DetectorType.OTHER]: 0, + [DetectorType.VIRTUAL]: 1, + [DetectorType.PHYSICAL]: 2, + [DetectorType.AOT_GLO]: 3, + [DetectorType.AOT_EVENT]: 4, + [DetectorType.MUON_GLO]: 5, + [DetectorType.QC_ONLY]: 6, + }); const orderingKey = (detector1, detector2) => { const specialPair = ['ZDC', 'TST']; if (specialPair.includes(detector1.name) && specialPair.includes(detector2.name)) { return detector1.name === 'ZDC' ? 1 : -1; } - // Note the negative sign to have larger priority types appear first return -(typeToOrderingKey(detector1.type) - typeToOrderingKey(detector2.type)) * 10 + detector1.name.localeCompare(detector2.name); }; @@ -158,4 +161,3 @@ export class DetectorsProvider extends RemoteDataProvider { } export const detectorsProvider = new DetectorsProvider(); -export const rctDetectorsProvider = new DetectorsProvider(DetectorOrders.RCT); diff --git a/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js b/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js index e0b6d87316..45d55bf6c6 100644 --- a/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js +++ b/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js @@ -20,7 +20,7 @@ import { h } from '/js/src/index.js'; import { formatDataPassName } from '../format/formatDataPassName.js'; import { formatDataPassStatusHistory } from '../format/formatStatusHistory.js'; import { checkboxes } from '../../../components/Filters/common/filters/checkboxFilter.js'; -import { textFilter } from '../../../components/Filters/common/filters/textFilter.js'; +import { rawTextFilter } from '../../../components/Filters/common/filters/rawTextFilter.js'; /** * List of active columns for a generic data passes table @@ -35,7 +35,10 @@ export const dataPassesActiveColumns = { visible: true, sortable: true, format: (_, dataPass) => formatDataPassName(dataPass), - filter: (filteringModel) => textFilter(filteringModel.get('names'), { class: 'w-75 mt1', placeholder: 'e.g. LHC22a, lhc23b, ...' }), + filter: (filteringModel) => rawTextFilter( + filteringModel.get('names'), + { classes: ['w-75', 'mt1'], placeholder: 'e.g. LHC22a_apass1, ...' }, + ), balloon: true, classes: 'w-20', }, @@ -102,7 +105,7 @@ export const dataPassesActiveColumns = { nonPhysicsProductions: { name: 'Include nonphysics productions', - filter: (filteringModel) => checkboxes(filteringModel.get('permittedNonPhysicsNames')), + filter: (filteringModel) => checkboxes(filteringModel.get('include[byName]').selectionModel), visible: false, }, }; diff --git a/lib/public/views/DataPasses/DataPassesModel.js b/lib/public/views/DataPasses/DataPassesModel.js index 42fed10c3a..5d987b31d7 100644 --- a/lib/public/views/DataPasses/DataPassesModel.js +++ b/lib/public/views/DataPasses/DataPassesModel.js @@ -21,15 +21,14 @@ import { DataPassesPerSimulationPassOverviewModel } from './PerSimulationPassOve export class DataPassesModel extends Observable { /** * The constructor of the model - * @param {QueryRouter} router router that controls the application's page navigation */ - constructor(router) { + constructor() { super(); - this._perLhcPeriodOverviewModel = new DataPassesPerLhcPeriodOverviewModel(router, 'data-passes-per-lhc-period-overview'); + this._perLhcPeriodOverviewModel = new DataPassesPerLhcPeriodOverviewModel(); this._perLhcPeriodOverviewModel.bubbleTo(this); - this._perSimulationPassOverviewModel = new DataPassesPerSimulationPassOverviewModel(router, 'data-passes-per-simulation-pass-overview'); + this._perSimulationPassOverviewModel = new DataPassesPerSimulationPassOverviewModel(); this._perSimulationPassOverviewModel.bubbleTo(this); } @@ -40,7 +39,6 @@ export class DataPassesModel extends Observable { * @returns {void} */ loadPerLhcPeriodOverview({ lhcPeriodId }) { - this._perLhcPeriodOverviewModel.setFilterFromURL(false); this._perLhcPeriodOverviewModel.load({ lhcPeriodId }); } @@ -69,7 +67,6 @@ export class DataPassesModel extends Observable { */ loadPerSimulationPassOverview({ simulationPassId }) { this._perSimulationPassOverviewModel.simulationPassId = parseInt(simulationPassId, 10); - this._perSimulationPassOverviewModel.setFilterFromURL(false); this._perSimulationPassOverviewModel.load(); } diff --git a/lib/public/views/DataPasses/DataPassesOverviewModel.js b/lib/public/views/DataPasses/DataPassesOverviewModel.js index 4d07c34e51..b85cc052d7 100644 --- a/lib/public/views/DataPasses/DataPassesOverviewModel.js +++ b/lib/public/views/DataPasses/DataPassesOverviewModel.js @@ -10,30 +10,60 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ -import { SelectionModel } from '../../components/common/selection/SelectionModel.js'; +import { FilteringModel } from '../../components/Filters/common/FilteringModel.js'; +import { SelectionFilterModel } from '../../components/Filters/common/filters/SelectionFilterModel.js'; import { TextTokensFilterModel } from '../../components/Filters/common/filters/TextTokensFilterModel.js'; import { NON_PHYSICS_PRODUCTIONS_NAMES_WORDS } from '../../domain/enums/NonPhysicsProductionsNamesWords.js'; -import { FilterableOverviewPageModel } from '../../models/FilterableOverviewPageModel.js'; +import { OverviewPageModel } from '../../models/OverviewModel.js'; /** * Data Passes overview model */ -export class DataPassesOverviewModel extends FilterableOverviewPageModel { +export class DataPassesOverviewModel extends OverviewPageModel { /** * Constructor - * @param {QueryRouter} router router that controls the application's page navigation - * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(router, pageIdentifier) { - super( - router, - pageIdentifier, - { - names: new TextTokensFilterModel(), - permittedNonPhysicsNames: new SelectionModel({ - availableOptions: NON_PHYSICS_PRODUCTIONS_NAMES_WORDS.map((word) => ({ label: word.toUpperCase(), value: word })), - }), - }, - ); + constructor() { + super(); + this._filteringModel = new FilteringModel({ + names: new TextTokensFilterModel(), + 'include[byName]': new SelectionFilterModel({ + availableOptions: NON_PHYSICS_PRODUCTIONS_NAMES_WORDS.map((word) => ({ label: word.toUpperCase(), value: word })), + }), + }); + + this._filteringModel.visualChange$.bubbleTo(this); + this._filteringModel.observe(() => { + this._pagination.currentPage = 1; + this.load(); + }); + } + + /** + * Return filter params of base model + * + * @return {object} filter + */ + getFilterParams() { + return this._filteringModel.normalized; + } + + /** + * Reset this model to its default + * + * @returns {void} + */ + reset() { + this._filteringModel.reset(); + super.reset(); + } + + /** + * Return the filtering model + * + * @return {FilteringModel} the filtering model + */ + get filteringModel() { + return this._filteringModel; } } diff --git a/lib/public/views/DataPasses/PerLhcPeriodOverview/DataPassesPerLhcPeriodOverviewModel.js b/lib/public/views/DataPasses/PerLhcPeriodOverview/DataPassesPerLhcPeriodOverviewModel.js index 6da2205751..dc125e1a94 100644 --- a/lib/public/views/DataPasses/PerLhcPeriodOverview/DataPassesPerLhcPeriodOverviewModel.js +++ b/lib/public/views/DataPasses/PerLhcPeriodOverview/DataPassesPerLhcPeriodOverviewModel.js @@ -19,11 +19,9 @@ import { buildUrl } from '/js/src/index.js'; export class DataPassesPerLhcPeriodOverviewModel extends DataPassesOverviewModel { /** * Constructor - * @param {QueryRouter} router router that controls the application's page navigation - * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(router, pageIdentifier) { - super(router, pageIdentifier); + constructor() { + super(); this._lhcPeriodId = null; } diff --git a/lib/public/views/DataPasses/PerLhcPeriodOverview/DataPassesPerLhcPeriodOverviewPage.js b/lib/public/views/DataPasses/PerLhcPeriodOverview/DataPassesPerLhcPeriodOverviewPage.js index 7cb3e5fb65..e97dca2170 100644 --- a/lib/public/views/DataPasses/PerLhcPeriodOverview/DataPassesPerLhcPeriodOverviewPage.js +++ b/lib/public/views/DataPasses/PerLhcPeriodOverview/DataPassesPerLhcPeriodOverviewPage.js @@ -19,7 +19,6 @@ import { filtersPanelPopover } from '../../../components/Filters/common/filtersP import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; import { dataPassesActiveColumns } from '../ActiveColumns/dataPassesActiveColumns.js'; import { DataPassVersionStatus } from '../../../domain/enums/DataPassVersionStatus.js'; -import { warningComponent } from '../../../components/common/messages/warningComponent.js'; const TABLEROW_HEIGHT = 42; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -43,18 +42,24 @@ const getRowClasses = ({ versions }) => { * @returns {Component} The overview screen */ export const DataPassesPerLhcPeriodOverviewPage = ({ dataPasses: { perLhcPeriodOverviewModel: dataPassesPerLhcPeriodOverviewModel } }) => { - const { filteringModel, sortModel, pagination, items } = dataPassesPerLhcPeriodOverviewModel; - - pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount(TABLEROW_HEIGHT, PAGE_USED_HEIGHT)); + dataPassesPerLhcPeriodOverviewModel.pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount( + TABLEROW_HEIGHT, + PAGE_USED_HEIGHT, + )); return h('', { onremove: () => dataPassesPerLhcPeriodOverviewModel.reset(), }, [ - h('.flex-row.header-container.pv2', filtersPanelPopover(filteringModel, dataPassesActiveColumns)), - warningComponent(dataPassesPerLhcPeriodOverviewModel), + h('.flex-row.header-container.pv2', filtersPanelPopover(dataPassesPerLhcPeriodOverviewModel.filteringModel, dataPassesActiveColumns)), h('.w-100.flex-column', [ - table(items, dataPassesActiveColumns, { classes: getRowClasses }, null, { sort: sortModel }), - paginationComponent(pagination), + table( + dataPassesPerLhcPeriodOverviewModel.items, + dataPassesActiveColumns, + { classes: getRowClasses }, + null, + { sort: dataPassesPerLhcPeriodOverviewModel.sortModel }, + ), + paginationComponent(dataPassesPerLhcPeriodOverviewModel.pagination), ]), ]); }; diff --git a/lib/public/views/DataPasses/PerSimulationPassOverview/DataPassesPerSimulationPassOverviewModel.js b/lib/public/views/DataPasses/PerSimulationPassOverview/DataPassesPerSimulationPassOverviewModel.js index d9b1008552..30fd3c616c 100644 --- a/lib/public/views/DataPasses/PerSimulationPassOverview/DataPassesPerSimulationPassOverviewModel.js +++ b/lib/public/views/DataPasses/PerSimulationPassOverview/DataPassesPerSimulationPassOverviewModel.js @@ -21,11 +21,9 @@ import { DataPassesOverviewModel } from '../DataPassesOverviewModel.js'; export class DataPassesPerSimulationPassOverviewModel extends DataPassesOverviewModel { /** * Constructor - * @param {QueryRouter} router router that controls the application's page navigation - * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(router, pageIdentifier) { - super(router, pageIdentifier); + constructor() { + super(); this._simulationPass = new ObservableData(RemoteData.notAsked()); this._simulationPass.bubbleTo(this); } diff --git a/lib/public/views/DataPasses/PerSimulationPassOverview/DataPassesPerSimulationPassOverviewPage.js b/lib/public/views/DataPasses/PerSimulationPassOverview/DataPassesPerSimulationPassOverviewPage.js index 6e11d594a8..2473f3383d 100644 --- a/lib/public/views/DataPasses/PerSimulationPassOverview/DataPassesPerSimulationPassOverviewPage.js +++ b/lib/public/views/DataPasses/PerSimulationPassOverview/DataPassesPerSimulationPassOverviewPage.js @@ -22,7 +22,6 @@ import { breadcrumbs } from '../../../components/common/navigation/breadcrumbs.j import spinner from '../../../components/common/spinner.js'; import { tooltip } from '../../../components/common/popover/tooltip.js'; import { DataPassVersionStatus } from '../../../domain/enums/DataPassVersionStatus.js'; -import { warningComponent } from '../../../components/common/messages/warningComponent.js'; const TABLEROW_HEIGHT = 42; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -47,9 +46,12 @@ const getRowClasses = ({ versions }) => { */ export const DataPassesPerSimulationPassOverviewPage = ({ dataPasses: { perSimulationPassOverviewModel: dataPassesPerSimulationPassOverviewModel } }) => { - const { items, simulationPass, pagination, filteringModel, sortModel } = dataPassesPerSimulationPassOverviewModel; + dataPassesPerSimulationPassOverviewModel.pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount( + TABLEROW_HEIGHT, + PAGE_USED_HEIGHT, + )); - pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount(TABLEROW_HEIGHT, PAGE_USED_HEIGHT)); + const { items, simulationPass, pagination } = dataPassesPerSimulationPassOverviewModel; const commonTitle = h('h2#breadcrumb-header', 'Data Passes per MC'); @@ -57,7 +59,7 @@ export const DataPassesPerSimulationPassOverviewPage = ({ dataPasses: { onremove: () => dataPassesPerSimulationPassOverviewModel.reset(), }, [ h('.flex-row.items-center.g2', [ - filtersPanelPopover(filteringModel, dataPassesActiveColumns), + filtersPanelPopover(dataPassesPerSimulationPassOverviewModel.filteringModel, dataPassesActiveColumns), h( '.flex-row.g1.items-center', simulationPass.match({ @@ -68,9 +70,14 @@ export const DataPassesPerSimulationPassOverviewPage = ({ dataPasses: { }), ), ]), - warningComponent(dataPassesPerSimulationPassOverviewModel), h('.w-100.flex-column', [ - table(items, dataPassesActiveColumns, { classes: getRowClasses }, null, { sort: sortModel }), + table( + items, + dataPassesActiveColumns, + { classes: getRowClasses }, + null, + { sort: dataPassesPerSimulationPassOverviewModel.sortModel }, + ), paginationComponent(pagination), ]), ]); diff --git a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js index 1226d7f7cb..d392ffb53a 100644 --- a/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js +++ b/lib/public/views/Environments/ActiveColumns/environmentsActiveColumns.js @@ -26,7 +26,7 @@ import { aliEcsEnvironmentLinkComponent } from '../../../components/common/exter import { StatusAcronym } from '../../../domain/enums/statusAcronym.mjs'; import { timeRangeFilter } from '../../../components/Filters/common/filters/timeRangeFilter.js'; import { checkboxes } from '../../../components/Filters/common/filters/checkboxFilter.js'; -import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; +import { rawTextFilter } from '../../../components/Filters/common/filters/rawTextFilter.js'; /** * List of active columns for a generic Environments component @@ -60,10 +60,13 @@ export const environmentsActiveColumns = { /** * Environment IDs filter component * - * @param {EnvironmentOverviewModel} environmentOverviewModel.filteringModel the filtering model + * @param {EnvironmentOverviewModel} environmentOverviewModel the environment overview model * @return {Component} the filter component */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'ids', 'e.g. CmCvjNbg, TDI59So3d...'), + filter: (environmentOverviewModel) => rawTextFilter( + environmentOverviewModel.filteringModel.get('ids'), + { classes: ['w-100'], placeholder: 'e.g. CmCvjNbg, TDI59So3d...' }, + ), }, runs: { name: 'Runs', @@ -76,10 +79,13 @@ export const environmentsActiveColumns = { /** * Run numbers filter component * - * @param {EnvironmentOverviewModel} environmentOverviewModel.filteringModel the filtering model + * @param {EnvironmentOverviewModel} environmentOverviewModel the environment overview model * @return {Component} the filter component */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'runNumbers', 'e.g. 553203, 553221, ...'), + filter: (environmentOverviewModel) => rawTextFilter( + environmentOverviewModel.filteringModel.get('runNumbers'), + { classes: ['w-100'], placeholder: 'e.g. 553203, 553221, ...' }, + ), }, updatedAt: { name: 'Last Update', @@ -117,7 +123,7 @@ export const environmentsActiveColumns = { * @param {EnvironmentOverviewModel} environmentOverviewModel the environment overview model * @return {Component} the filter component */ - filter: (environmentOverviewModel) => checkboxes(environmentOverviewModel.filteringModel.get('currentStatus')), + filter: (environmentOverviewModel) => checkboxes(environmentOverviewModel.filteringModel.get('currentStatus').selectionModel), }, historyItems: { name: h('.flex-row.g2.items-center', ['Status History', infoTooltip(environmentStatusHistoryLegendComponent())]), @@ -134,9 +140,12 @@ export const environmentsActiveColumns = { /** * Status history filter component * - * @param {EnvironmentOverviewModel} environmentOverviewModel.filteringModel the filtering model + * @param {EnvironmentOverviewModel} environmentOverviewModel the environment overview model * @return {Component} the filter component */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'statusHistory', 'e.g. D-R-X'), + filter: (environmentOverviewModel) => rawTextFilter( + environmentOverviewModel.filteringModel.get('statusHistory'), + { classes: ['w-100'], placeholder: 'e.g. D-R-X' }, + ), }, }; diff --git a/lib/public/views/Environments/EnvironmentModel.js b/lib/public/views/Environments/EnvironmentModel.js index 1cc7fa484d..ba4b1e86bf 100644 --- a/lib/public/views/Environments/EnvironmentModel.js +++ b/lib/public/views/Environments/EnvironmentModel.js @@ -29,7 +29,7 @@ export class EnvironmentModel extends Observable { super(); // Sub-models - this._overviewModel = new EnvironmentOverviewModel(model, 'env-overview'); + this._overviewModel = new EnvironmentOverviewModel(model); this._overviewModel.bubbleTo(this); this._detailsModel = new EnvironmentDetailsModel(); @@ -42,7 +42,6 @@ export class EnvironmentModel extends Observable { */ loadOverview() { if (!this._overviewModel.pagination.isInfiniteScrollEnabled) { - this._overviewModel.setFilterFromURL(false); this._overviewModel.load(); } } diff --git a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js index 9621e4df33..8498a02d79 100644 --- a/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js +++ b/lib/public/views/Environments/Overview/EnvironmentOverviewModel.js @@ -11,47 +11,58 @@ * or submit itself to any jurisdiction. */ +import { buildUrl } from '/js/src/index.js'; +import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; +import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { TimeRangeInputModel } from '../../../components/Filters/common/filters/TimeRangeInputModel.js'; +import { SelectionFilterModel } from '../../../components/Filters/common/filters/SelectionFilterModel.js'; import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; +import { debounce } from '../../../utilities/debounce.js'; import { coloredEnvironmentStatusComponent } from '../ColoredEnvironmentStatusComponent.js'; import { StatusAcronym } from '../../../domain/enums/statusAcronym.mjs'; -import { SelectionModel } from '../../../components/common/selection/SelectionModel.js'; -import { FilterableOverviewPageModel } from '../../../models/FilterableOverviewPageModel.js'; /** * Environment overview page model */ -export class EnvironmentOverviewModel extends FilterableOverviewPageModel { +export class EnvironmentOverviewModel extends OverviewPageModel { /** * Constructor * @param {Model} model global model - * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(model, pageIdentifier) { - super( - model.router, - pageIdentifier, - { - created: new TimeRangeInputModel(), - runNumbers: new RawTextFilterModel(), - statusHistory: new RawTextFilterModel(), - currentStatus: new SelectionModel({ - availableOptions: Object.keys(StatusAcronym).map((status) => ({ - value: status, - label: coloredEnvironmentStatusComponent(status), - rawLabel: status, - })), - }), - ids: new RawTextFilterModel(), - }, - ); + constructor(model) { + super(); + + this._filteringModel = new FilteringModel({ + created: new TimeRangeInputModel(), + runNumbers: new RawTextFilterModel(), + statusHistory: new RawTextFilterModel(), + currentStatus: new SelectionFilterModel({ + availableOptions: Object.keys(StatusAcronym).map((status) => ({ + value: status, + label: coloredEnvironmentStatusComponent(status), + rawLabel: status, + })), + }), + ids: new RawTextFilterModel(), + }); + + this._filteringModel.observe(() => this._applyFilters(true)); + this._filteringModel.visualChange$?.bubbleTo(this); + + this.reset(false); + const updateDebounceTime = () => { + this._debouncedLoad = debounce(this.load.bind(this), model.inputDebounceTime); + }; + + model.appConfiguration$.observe(() => updateDebounceTime()); + updateDebounceTime(); } /** * @inheritDoc */ getRootEndpoint() { - return this.buildRootEndpoint('/api/environments'); + return buildUrl('/api/environments', { filter: this.filteringModel.normalized }); } /** @@ -62,4 +73,56 @@ export class EnvironmentOverviewModel extends FilterableOverviewPageModel { get environments() { return this.items; } + + /** + * Returns all filtering, sorting and pagination settings to their default values + * @param {boolean} [fetch = true] whether to refetch all data after filters have been reset + * @return {void} + */ + reset(fetch = true) { + super.reset(); + this.resetFiltering(fetch); + } + + /** + * Reset all filtering models + * @param {boolean} fetch Whether to refetch all data after filters have been reset + * @return {void} + */ + resetFiltering(fetch = true) { + this._filteringModel.reset(); + + if (fetch) { + this._applyFilters(true); + } + } + + /** + * Checks if any filter value has been modified from their default (empty) + * @return {Boolean} If any filter is active + */ + isAnyFilterActive() { + return this._filteringModel.isAnyFilterActive(); + } + + /** + * Return the filtering model + * + * @return {FilteringModel} the filtering model + */ + get filteringModel() { + return this._filteringModel; + } + + /** + * Apply the current filtering and update the remote data list + * + * @param {boolean} now if true, filtering will be applied now without debouncing + * + * @return {void} + */ + _applyFilters(now = false) { + this._pagination.currentPage = 1; + now ? this.load() : this._debouncedLoad(true); + } } diff --git a/lib/public/views/Environments/Overview/environmentOverviewComponent.js b/lib/public/views/Environments/Overview/environmentOverviewComponent.js index df8f5a332d..7cc60ecd22 100644 --- a/lib/public/views/Environments/Overview/environmentOverviewComponent.js +++ b/lib/public/views/Environments/Overview/environmentOverviewComponent.js @@ -17,7 +17,6 @@ import { environmentsActiveColumns } from '../ActiveColumns/environmentsActiveCo import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; -import { warningComponent } from '../../../components/common/messages/warningComponent.js'; const TABLEROW_HEIGHT = 58; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -31,14 +30,16 @@ const PAGE_USED_HEIGHT = 181; export const environmentOverviewComponent = (envsOverviewModel) => { const { pagination, environments } = envsOverviewModel; - pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount(TABLEROW_HEIGHT, PAGE_USED_HEIGHT)); + pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount( + TABLEROW_HEIGHT, + PAGE_USED_HEIGHT, + )); return h('', [ h( '.flex-row.header-container.g2.pv2', filtersPanelPopover(envsOverviewModel, environmentsActiveColumns), ), - warningComponent(envsOverviewModel), h('.w-100.flex-column', [ h('.header-container.pv2'), table(environments, environmentsActiveColumns, { classes: 'table-sm' }), diff --git a/lib/public/views/Home/Overview/HomePage.js b/lib/public/views/Home/Overview/HomePage.js index 3768f6cf6d..92705e6973 100644 --- a/lib/public/views/Home/Overview/HomePage.js +++ b/lib/public/views/Home/Overview/HomePage.js @@ -46,7 +46,7 @@ export const HomePage = ({ home: { logsOverviewModel, runsOverviewModel, lhcFill h('.flex-row.g2', [ h('.flex-column', [ h('h3', 'Log Entries'), - h('.f6#logs-panel', table(logsOverviewModel.items, logsActiveColumns, null, { profile: 'home' })), + h('.f6#logs-panel', table(logsOverviewModel.logs, logsActiveColumns, null, { profile: 'home' })), ]), h('.flex-column', [ h('h3', 'LHC Fills'), diff --git a/lib/public/views/Home/Overview/HomePageModel.js b/lib/public/views/Home/Overview/HomePageModel.js index e40fe38952..40b6cfac85 100644 --- a/lib/public/views/Home/Overview/HomePageModel.js +++ b/lib/public/views/Home/Overview/HomePageModel.js @@ -26,13 +26,13 @@ export class HomePageModel extends Observable { */ constructor(model) { super(); - this._runsOverviewModel = new RunsOverviewModel(model, 'home'); + this._runsOverviewModel = new RunsOverviewModel(model); this._runsOverviewModel.bubbleTo(this); - this._logsOverviewModel = new LogsOverviewModel(model, true, 'home'); + this._logsOverviewModel = new LogsOverviewModel(model, true); this._logsOverviewModel.bubbleTo(this); - this._lhcFillsOverviewModel = new LhcFillsOverviewModel(model.router, true, 'home'); + this._lhcFillsOverviewModel = new LhcFillsOverviewModel(true); this._lhcFillsOverviewModel.bubbleTo(this); } @@ -42,7 +42,7 @@ export class HomePageModel extends Observable { */ loadOverview() { this._runsOverviewModel.load(); - this._logsOverviewModel.load(true); + this._logsOverviewModel.fetchLogs(true); this._lhcFillsOverviewModel.load(); } diff --git a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js index be4311d7e4..b2657c8cfd 100644 --- a/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js +++ b/lib/public/views/LhcFills/ActiveColumns/lhcFillsActiveColumns.js @@ -23,11 +23,12 @@ import { buttonLinkWithDropdown } from '../../../components/common/selection/inf import { infologgerLinksComponents } from '../../../components/common/externalLinks/infologgerLinksComponents.js'; import { formatBeamType } from '../../../utilities/formatting/formatBeamType.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; -import { toggleFilter } from '../../../components/Filters/common/filters/toggleFilter.js'; +import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; +import { fillNumberFilter } from '../../../components/Filters/LhcFillsFilter/fillNumberFilter.js'; import { durationFilter } from '../../../components/Filters/LhcFillsFilter/durationFilter.js'; import { beamTypeFilter } from '../../../components/Filters/LhcFillsFilter/beamTypeFilter.js'; +import { schemeNameFilter } from '../../../components/Filters/LhcFillsFilter/schemeNameFilter.js'; import { timeRangeFilter } from '../../../components/Filters/common/filters/timeRangeFilter.js'; -import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; /** * List of active columns for a lhc fills table @@ -53,14 +54,7 @@ export const lhcFillsActiveColumns = { ), ], ), - - /** - * FillNumber filter component - * - * @param {FilteringModel} LhcFillsOverviewModel.filteringModel the filtering model - * @return {Component} the filter component - */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'fillNumbers', 'e.g. 7966, 7954, 7948...'), + filter: (lhcFillModel) => fillNumberFilter(lhcFillModel.filteringModel.get('fillNumbers')), profiles: { lhcFill: true, environment: true, @@ -117,8 +111,7 @@ export const lhcFillsActiveColumns = { name: 'Stable Beams Only', visible: false, format: (boolean) => boolean ? 'On' : 'Off', - filter: (lhcFillModel) => - toggleFilter(lhcFillModel.filteringModel.get('hasStableBeams'), 'stableBeamsOnlyRadio', 'stableBeamsOnlyRadio', true), + filter: (lhcFillModel) => toggleStableBeamOnlyFilter(lhcFillModel.filteringModel.get('hasStableBeams'), true), }, stableBeamsDuration: { name: 'SB Duration', @@ -200,14 +193,7 @@ export const lhcFillsActiveColumns = { visible: true, size: 'w-10', format: (value) => value ? value : '-', - - /** - * Schema filter component - * - * @param {FilteringModel} LhcFillsOverviewModel.filteringModel the filtering model - * @return {Component} the filter component - */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'schemeName', 'e.g. Single_12b_8_1024_8_2018'), + filter: (lhcFillModel) => schemeNameFilter(lhcFillModel.filteringModel.get('schemeName')), balloon: true, }, runs: { diff --git a/lib/public/views/LhcFills/LhcFills.js b/lib/public/views/LhcFills/LhcFills.js index a4343be26a..70b6c5eb3d 100644 --- a/lib/public/views/LhcFills/LhcFills.js +++ b/lib/public/views/LhcFills/LhcFills.js @@ -29,7 +29,7 @@ export default class LhcFills extends Observable { this.model = model; // Sub-models - this._overviewModel = new LhcFillsOverviewModel(model.router, true, 'lhc-fill-overview'); + this._overviewModel = new LhcFillsOverviewModel(true); this._overviewModel.bubbleTo(this); this._detailsModel = new LhcFillDetailsModel(); @@ -42,7 +42,6 @@ export default class LhcFills extends Observable { * @returns {void} */ loadOverview() { - this._overviewModel.setFilterFromURL(false); this._overviewModel.load(); } diff --git a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js index 3e73c2fa0f..c57ae69c25 100644 --- a/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js +++ b/lib/public/views/LhcFills/Overview/LhcFillsOverviewModel.js @@ -11,42 +11,49 @@ * or submit itself to any jurisdiction. */ +import { buildUrl } from '/js/src/index.js'; +import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; +import { StableBeamFilterModel } from '../../../components/Filters/LhcFillsFilter/StableBeamFilterModel.js'; import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; +import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { addStatisticsToLhcFill } from '../../../services/lhcFill/addStatisticsToLhcFill.js'; import { BeamTypeFilterModel } from '../../../components/Filters/LhcFillsFilter/BeamTypeFilterModel.js'; import { TextComparisonFilterModel } from '../../../components/Filters/common/filters/TextComparisonFilterModel.js'; import { TimeRangeFilterModel } from '../../../components/Filters/RunsFilter/TimeRangeFilter.js'; -import { ToggleFilterModel } from '../../../components/Filters/common/filters/ToggleFilterModel.js'; -import { FilterableOverviewPageModel } from '../../../models/FilterableOverviewPageModel.js'; /** * Model for the LHC fills overview page * * @implements {OverviewModel} */ -export class LhcFillsOverviewModel extends FilterableOverviewPageModel { +export class LhcFillsOverviewModel extends OverviewPageModel { /** * Constructor * - * @param {QueryRouter} router router that controls the application's page navigation * @param {boolean} [stableBeamsOnly=false] if true, overview will load stable beam only - * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(router, stableBeamsOnly = false, pageIdentifier) { - super( - router, - pageIdentifier, - { - fillNumbers: new RawTextFilterModel(), - beamDuration: new TextComparisonFilterModel(), - runDuration: new TextComparisonFilterModel(), - hasStableBeams: new ToggleFilterModel(stableBeamsOnly, true), - stableBeamsStart: new TimeRangeFilterModel(), - stableBeamsEnd: new TimeRangeFilterModel(), - beamTypes: new BeamTypeFilterModel(), - schemeName: new RawTextFilterModel(), - }, - ); + constructor(stableBeamsOnly = false) { + super(); + + this._filteringModel = new FilteringModel({ + fillNumbers: new RawTextFilterModel(), + beamDuration: new TextComparisonFilterModel(), + runDuration: new TextComparisonFilterModel(), + hasStableBeams: new StableBeamFilterModel(), + stableBeamsStart: new TimeRangeFilterModel(), + stableBeamsEnd: new TimeRangeFilterModel(), + beamTypes: new BeamTypeFilterModel(), + schemeName: new RawTextFilterModel(), + }); + + this._filteringModel.observe(() => this._applyFilters()); + this._filteringModel.visualChange$.bubbleTo(this); + + this.reset(false); + + if (stableBeamsOnly) { + this._filteringModel.get('hasStableBeams').setStableBeamsOnly(true); + } } /** @@ -63,6 +70,59 @@ export class LhcFillsOverviewModel extends FilterableOverviewPageModel { * @inheritDoc */ getRootEndpoint() { - return this.buildRootEndpoint('/api/lhcFills'); + const params = { + filter: this.filteringModel.normalized, + }; + return buildUrl('/api/lhcFills', params); + } + + /** + * Returns all filtering, sorting and pagination settings to their default values + * @param {boolean} [fetch = true] whether to refetch all data after filters have been reset + * @return {void} + */ + reset(fetch = true) { + super.reset(); + this.resetFiltering(fetch); + } + + /** + * Reset all filtering models + * @param {boolean} fetch Whether to refetch all data after filters have been reset + * @return {void} + */ + resetFiltering(fetch = true) { + this._filteringModel.reset(); + + if (fetch) { + this._applyFilters(); + } + } + + /** + * Checks if any filter value has been modified from their default (empty) + * @return {Boolean} If any filter is active + */ + isAnyFilterActive() { + return this._filteringModel.isAnyFilterActive(); + } + + /** + * Return the filtering model + * + * @return {FilteringModel} the filtering model + */ + get filteringModel() { + return this._filteringModel; + } + + /** + * Apply the current filtering and update the remote data list + * + * @return {void} + */ + _applyFilters() { + this._pagination.currentPage = 1; + this.load(); } } diff --git a/lib/public/views/LhcFills/Overview/index.js b/lib/public/views/LhcFills/Overview/index.js index f790bb9957..e81409f06c 100644 --- a/lib/public/views/LhcFills/Overview/index.js +++ b/lib/public/views/LhcFills/Overview/index.js @@ -18,8 +18,7 @@ import { lhcFillsActiveColumns } from '../ActiveColumns/lhcFillsActiveColumns.js import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; -import { toggleFilter } from '../../../components/Filters/common/filters/toggleFilter.js'; -import { warningComponent } from '../../../components/common/messages/warningComponent.js'; +import { toggleStableBeamOnlyFilter } from '../../../components/Filters/LhcFillsFilter/stableBeamFilter.js'; const TABLEROW_HEIGHT = 53.3; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -42,18 +41,20 @@ export const Index = (model) => h('', { * @returns {Object} Html page */ const showLhcFillsTable = (lhcFillsOverviewModel) => { - const { items, pagination, filteringModel } = lhcFillsOverviewModel; - pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount(TABLEROW_HEIGHT, PAGE_USED_HEIGHT, 1)); + lhcFillsOverviewModel.pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount( + TABLEROW_HEIGHT, + PAGE_USED_HEIGHT, + 1, + )); return [ h('.flex-row.header-container.g2.pv2', [ filtersPanelPopover(lhcFillsOverviewModel, lhcFillsActiveColumns), - toggleFilter(filteringModel.get('hasStableBeams'), 'STABLE BEAM ONLY'), + toggleStableBeamOnlyFilter(lhcFillsOverviewModel.filteringModel.get('hasStableBeams')), ]), - warningComponent(lhcFillsOverviewModel), h('.w-100.flex-column', [ - table(items, lhcFillsActiveColumns, null, { tableClasses: '.table-sm' }), - paginationComponent(pagination), + table(lhcFillsOverviewModel.items, lhcFillsActiveColumns, null, { tableClasses: '.table-sm' }), + paginationComponent(lhcFillsOverviewModel.pagination), ]), ]; }; diff --git a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js index 6d99d48fd5..c43b04b917 100644 --- a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js +++ b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js @@ -15,16 +15,19 @@ import { h } from '/js/src/index.js'; import { iconCommentSquare, iconPaperclip } from '/js/src/icons.js'; import { authorFilter } from '../../../components/Filters/LogsFilter/author/authorFilter.js'; +import createdFilter from '../../../components/Filters/LogsFilter/created.js'; +import runsFilter from '../../../components/Filters/LogsFilter/runs.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { frontLinks } from '../../../components/common/navigation/frontLinks.js'; import { tagFilter } from '../../../components/Filters/common/filters/tagFilter.js'; import { formatRunsList } from '../../Runs/format/formatRunsList.js'; import { profiles } from '../../../components/common/table/profiles.js'; +import { textFilter } from '../../../components/Filters/common/filters/textFilter.js'; +import { environmentFilter } from '../../../components/Filters/LogsFilter/environments.js'; import { formatLhcFillsList } from '../../LhcFills/format/formatLhcFillsList.js'; +import { lhcFillsFilter } from '../../../components/Filters/LogsFilter/lhcFill.js'; import { formatTagsList } from '../../Tags/format/formatTagsList.js'; -import { timeRangeFilter } from '../../../components/Filters/common/filters/timeRangeFilter.js'; -import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; /** * A method to display a small and simple number/icon collection as a column @@ -68,14 +71,13 @@ export const logsActiveColumns = { visible: true, sortable: true, size: 'w-30', - - /** - * Title filter component - * - * @param {FilteringModel} logOverviewModel.filteringModel filtering model - * @return {Component} the filter component - */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'title', 'e.g. Report on runs: ...'), + filter: ({ titleFilter }) => textFilter( + titleFilter, + { + id: 'titleFilterText', + class: 'w-75 mt1', + }, + ), balloon: true, profiles: { embeded: true, @@ -90,14 +92,13 @@ export const logsActiveColumns = { name: 'Content', visible: false, size: 'w-10', - - /** - * Content filter component - * - * @param {FilteringModel} logOverviewModel.filteringModel the filtering model - * @return {Component} the filter component - */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'content', 'e.g. Quality of run 52...'), + filter: ({ contentFilter }) => textFilter( + contentFilter, + { + id: 'contentFilterText', + class: 'w-75 mt1', + }, + ), }, author: { name: 'Author', @@ -105,14 +106,7 @@ export const logsActiveColumns = { sortable: true, size: 'w-15', format: (author) => author.name, - - /** - * Author filter component - * - * @param {FilteringModel} logOverviewModel.filteringModel filtering model - * @return {Component} the filter component - */ - filter: ({ filteringModel }) => authorFilter(filteringModel.get('author')), + filter: authorFilter, profiles: [profiles.none, 'embeded'], }, createdAt: { @@ -121,14 +115,7 @@ export const logsActiveColumns = { sortable: true, size: 'w-10', format: (timestamp) => formatTimestamp(timestamp, false), - - /** - * Created filter component - * - * @param {FilteringModel} logOverviewModel.filteringModel filtering model - * @return {Component} the filter component - */ - filter: ({ filteringModel }) => timeRangeFilter(filteringModel.get('created')), + filter: createdFilter, profiles: { embeded: { format: (timestamp) => formatTimestamp(timestamp), @@ -150,11 +137,10 @@ export const logsActiveColumns = { /** * Tag filter component - * - * @param {FilteringModel} logOverviewModel.filteringModel filtering model + * @param {LogsOverviewModel} logsModel the log model * @return {Component} the filter component */ - filter: ({ filteringModel }) => tagFilter(filteringModel.get('tags')), + filter: (logsModel) => tagFilter(logsModel.listingTagsFilterModel), balloon: true, profiles: [profiles.none, 'embeded'], }, @@ -164,14 +150,7 @@ export const logsActiveColumns = { sortable: true, size: 'w-15', format: formatRunsList, - - /** - * Runs filter component - * - * @param {FilteringModel} logOverviewModel.filteringModel filtering model - * @return {Component} the filter component - */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'runNumbers', 'e.g. 553203, 553221, ...'), + filter: runsFilter, balloon: true, profiles: [profiles.none, 'embeded'], }, @@ -188,14 +167,7 @@ export const logsActiveColumns = { parameters: { environmentId: id }, }), ), - - /** - * Environment filter component - * - * @param {FilteringModel} logOverviewModel.filteringModel filtering model - * @return {Component} the filter component - */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'environmentIds', 'e.g. Dxi029djX, TDI59So3d...'), + filter: environmentFilter, balloon: true, profiles: [profiles.none, 'embeded'], }, @@ -205,14 +177,7 @@ export const logsActiveColumns = { sortable: false, size: 'w-10', format: formatLhcFillsList, - - /** - * LhcFills filter component - * - * @param {FilteringModel} logOverviewModel.filteringModel filtering model - * @return {Component} the filter component - */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'fillNumbers', 'e.g. 11392, 11383, 7625'), + filter: lhcFillsFilter, balloon: true, profiles: [profiles.none, 'embeded'], }, diff --git a/lib/public/views/Logs/LogsModel.js b/lib/public/views/Logs/LogsModel.js index 1c894620a4..b4f9342d42 100644 --- a/lib/public/views/Logs/LogsModel.js +++ b/lib/public/views/Logs/LogsModel.js @@ -30,7 +30,7 @@ export class LogsModel extends Observable { super(); this.model = model; - this._overviewModel = new LogsOverviewModel(model, false, 'log-overview'); + this._overviewModel = new LogsOverviewModel(model); this._overviewModel.bubbleTo(this); this._treeViewModel = new LogTreeViewModel(); @@ -55,8 +55,7 @@ export class LogsModel extends Observable { */ loadOverview() { if (!this._overviewModel.pagination.isInfiniteScrollEnabled) { - this._overviewModel.setFilterFromURL(false); - this._overviewModel.load(); + this._overviewModel.fetchLogs(); } } diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index f8244d42a8..cce376438b 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -11,49 +11,410 @@ * or submit itself to any jurisdiction. */ +import { buildUrl, Observable, RemoteData } from '/js/src/index.js'; import { TagFilterModel } from '../../../components/Filters/common/TagFilterModel.js'; +import { SortModel } from '../../../components/common/table/SortModel.js'; +import { debounce } from '../../../utilities/debounce.js'; +import { FilterInputModel } from '../../../components/Filters/common/filters/FilterInputModel.js'; import { AuthorFilterModel } from '../../../components/Filters/LogsFilter/author/AuthorFilterModel.js'; +import { PaginationModel } from '../../../components/Pagination/PaginationModel.js'; +import { getRemoteDataSlice } from '../../../utilities/fetch/getRemoteDataSlice.js'; import { tagsProvider } from '../../../services/tag/tagsProvider.js'; -import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; -import { TimeRangeInputModel } from '../../../components/Filters/common/filters/TimeRangeInputModel.js'; -import { FilterableOverviewPageModel } from '../../../models/FilterableOverviewPageModel.js'; /** * Model representing handlers for log entries page * * @implements {OverviewModel} */ -export class LogsOverviewModel extends FilterableOverviewPageModel { +export class LogsOverviewModel extends Observable { /** * The constructor of the Overview model object * * @param {Model} model global model * @param {boolean} excludeAnonymous Whether to exclude anonymous logs - * @param {string} pageIdentifier string that indicates what page this model represents - */ - constructor(model, excludeAnonymous = false, pageIdentifier) { - super( - model.router, - pageIdentifier, - { - author: new AuthorFilterModel(), - title: new RawTextFilterModel(), - content: new RawTextFilterModel(), - tags: new TagFilterModel(tagsProvider.items$), - runNumbers: new RawTextFilterModel(), - environmentIds: new RawTextFilterModel(), - fillNumbers: new RawTextFilterModel(), - created: new TimeRangeInputModel(), - }, + */ + constructor(model, excludeAnonymous = false) { + super(); + + this.model = model; + + // Sub-models + this._listingTagsFilterModel = new TagFilterModel(tagsProvider.items$); + this._listingTagsFilterModel.observe(() => this._applyFilters()); + this._listingTagsFilterModel.visualChange$.bubbleTo(this); + + this._overviewSortModel = new SortModel(); + this._overviewSortModel.observe(() => this._applyFilters(true)); + this._overviewSortModel.visualChange$.bubbleTo(this); + + this._pagination = new PaginationModel(); + this._pagination.observe(() => this.fetchLogs()); + this._pagination.itemsPerPageSelector$.observe(() => this.notify()); + + // Filtering models + this._authorFilter = new AuthorFilterModel(); + this._registerFilter(this._authorFilter); + + this._titleFilter = new FilterInputModel(); + this._registerFilter(this._titleFilter); + + this._contentFilter = new FilterInputModel(); + this._registerFilter(this._contentFilter); + + this._logs = RemoteData.NotAsked(); + + const updateDebounceTime = () => { + this._debouncedFetchAllLogs = debounce(this.fetchLogs.bind(this), model.inputDebounceTime); + }; + model.appConfiguration$.observe(() => updateDebounceTime()); + updateDebounceTime(); + + excludeAnonymous && this._authorFilter.update('!Anonymous'); + + this.reset(false); + } + + /** + * Retrieve every relevant log from the API + * @returns {Promise} Injects the data object with the response data + */ + async fetchLogs() { + const keepExisting = this._pagination.currentPage > 1 && this._pagination.isInfiniteScrollEnabled; + + if (!keepExisting) { + this._logs = RemoteData.loading(); + this.notify(); + } + + const params = { + ...this._getFilterQueryParams(), + 'page[offset]': this._pagination.firstItemOffset, + 'page[limit]': this._pagination.itemsPerPage, + }; + + const endpoint = buildUrl('/api/logs', params); + + try { + const { items, totalCount } = await getRemoteDataSlice(endpoint); + const concatenateWith = keepExisting ? this._logs.payload ?? [] : []; + this._logs = RemoteData.success([...concatenateWith, ...items]); + this._pagination.itemsCount = totalCount; + } catch (errors) { + this._logs = RemoteData.failure(errors); + } + + this.notify(); + } + + /** + * Return current logs + * @return {RemoteData<*[]>} current data + */ + get logs() { + return this._logs; + } + + /** + * Reset all filtering, sorting and pagination settings to their default values + * + * @param {boolean} fetch Whether to refetch all logs after filters have been reset + * @return {undefined} + */ + reset(fetch = true) { + this.titleFilter.reset(); + this.contentFilter.reset(); + this.authorFilter.reset(); + + this.createdFilterFrom = ''; + this.createdFilterTo = ''; + + this.listingTagsFilterModel.reset(); + + this.runFilterOperation = 'AND'; + this.runFilterValues = []; + this._runFilterRawValue = ''; + + this.environmentFilterOperation = 'AND'; + this.environmentFilterValues = []; + this._environmentFilterRawValue = ''; + + this.lhcFillFilterOperation = 'AND'; + this.lhcFillFilterValues = []; + this._lhcFillFilterRawValue = ''; + + this._pagination.reset(); + + if (fetch) { + this._applyFilters(true); + } + } + + /** + * Checks if any filter value has been modified from their default (empty) + * @returns {boolean} If any filter is active + */ + isAnyFilterActive() { + return ( + !this._titleFilter.isEmpty + || !this._contentFilter.isEmpty + || !this._authorFilter.isEmpty + || this.createdFilterFrom !== '' + || this.createdFilterTo !== '' + || !this.listingTagsFilterModel.isEmpty + || this.runFilterValues.length !== 0 + || this.environmentFilterValues.length !== 0 + || this.lhcFillFilterValues.length !== 0 ); + } + + /** + * Returns the current title substring filter + * @returns {string} The current title substring filter + */ + getRunsFilterRaw() { + return this._runFilterRawValue; + } + + /** + * Add a run to the filter + * @param {string} rawRuns The runs to be added to the filter criteria + * @returns {undefined} + */ + setRunsFilter(rawRuns) { + this._runFilterRawValue = rawRuns; + const runs = []; + const valuesRegex = /([0-9]+),?/g; + + let match = valuesRegex.exec(rawRuns); + while (match) { + runs.push(parseInt(match[1], 10)); + match = valuesRegex.exec(rawRuns); + } + + // Allow empty runs only if raw runs is an empty string + if (runs.length > 0 || rawRuns.length === 0) { + this.runFilterValues = runs; + this._applyFilters(); + } + } + + /** + * Returns the raw current environment filter + * @returns {string} the raw current environment filter + */ + getEnvFilterRaw() { + return this._environmentFilterRawValue; + } + + /** + * Returns the current environment filter + * @returns {string[]} The current environment filter + */ + getEnvFilter() { + return this.environmentFilterValues; + } + + /** + * Sets the environment filter + * @param {string} rawEnvironments The environments to apply to the filter + * @returns {undefined} + */ + setEnvFilter(rawEnvironments) { + this._environmentFilterRawValue = rawEnvironments; + const envs = rawEnvironments + .split(/[ ,]+/) + .filter(Boolean) + .map((id) => id.trim()); + + if (envs.length > 0 || rawEnvironments.length === 0) { + this.environmentFilterValues = envs; + this._applyFilters(); + } + } + + /** + * Returns the current title substring filter + * @returns {string} The current title substring filter + */ + getLhcFillsFilterRaw() { + return this._lhcFillFilterRawValue; + } + + /** + * Add a lhcFill to the filter + * @param {string} rawLhcFills The LHC fills to be added to the filter criteria + * @returns {void} + */ + setLhcFillsFilter(rawLhcFills) { + this._lhcFillFilterRawValue = rawLhcFills; + + // Split the lhc fills string by comma or whitespace, remove falsy values like empty strings, and convert to int + const lhcFills = rawLhcFills + .split(/[ ,]+/) + .filter(Boolean) + .map((fillNumberStr) => parseInt(fillNumberStr.trim(), 10)); + + // Allow empty lhcFills only if raw lhcFills is an empty string + if (lhcFills.length > 0 || rawLhcFills.length === 0) { + this.lhcFillFilterValues = lhcFills; + this._applyFilters(); + } + } + + /** + * Returns the current minimum creation datetime + * @returns {Integer} The current minimum creation datetime + */ + getCreatedFilterFrom() { + return this.createdFilterFrom; + } - excludeAnonymous && this._filteringModel.get('author').update('!Anonymous'); + /** + * Returns the current maximum creation datetime + * @returns {Integer} The current maximum creation datetime + */ + getCreatedFilterTo() { + return this.createdFilterTo; } /** - * @inheritdoc + * Set a datetime for the creation datetime filter + * @param {string} key The filter value to apply the datetime to + * @param {Object} date The datetime to be applied to the creation datetime filter + * @param {boolean} valid Whether the inserted date passes validity check + * @returns {undefined} + */ + setCreatedFilter(key, date, valid) { + if (valid) { + this[`createdFilter${key}`] = date; + this._applyFilters(); + } + } + + /** + * Return the model handling the filtering on tags + * + * @return {TagFilterModel} the filtering model */ - getRootEndpoint() { - return this.buildRootEndpoint('/api/logs'); + get listingTagsFilterModel() { + return this._listingTagsFilterModel; + } + + /** + * Returns the model handling the overview page table sort + * + * @return {SortModel} the sort model + */ + get overviewSortModel() { + return this._overviewSortModel; + } + + /** + * Returns the filter model for author filter + * + * @return {FilterInputModel} the filter model + */ + get authorFilter() { + return this._authorFilter; + } + + /** + * Returns the filter model for title filter + * + * @return {FilterInputModel} the filter model + */ + get titleFilter() { + return this._titleFilter; + } + + /** + * Returns the model for body filter + * @return {FilterInputModel} the filter model + */ + get contentFilter() { + return this._contentFilter; + } + + /** + * Returns the pagination model + * + * @return {PaginationModel} the pagination model + */ + get pagination() { + return this._pagination; + } + + /** + * Apply the current filtering and update the remote data list + * + * @param {boolean} now if true, filtering will be applied now without debouncing + * + * @return {void} + */ + _applyFilters(now = false) { + this._pagination.silentlySetCurrentPage(1); + now ? this.fetchLogs() : this._debouncedFetchAllLogs(); + } + + /** + * Register a new filter model + * @param {FilterInputModel} filter the filter to register + * @return {void} + * @private + */ + _registerFilter(filter) { + filter.visualChange$.bubbleTo(this); + filter.observe(() => this._applyFilters()); + } + + /** + * Returns the list of URL params corresponding to the currently applied filter + * + * @return {Object} the URL params + * + * @private + */ + _getFilterQueryParams() { + const sortOn = this._overviewSortModel.appliedOn; + const sortDirection = this._overviewSortModel.appliedDirection; + + return { + ...!this._titleFilter.isEmpty && { + 'filter[title]': this._titleFilter.value, + }, + ...!this._contentFilter.isEmpty && { + 'filter[content]': this._contentFilter.value, + }, + ...!this._authorFilter.isEmpty && { + 'filter[author]': this._authorFilter.value, + }, + ...this.createdFilterFrom && { + 'filter[created][from]': + new Date(`${this.createdFilterFrom.replace(/\//g, '-')}T00:00:00.000`).getTime(), + }, + ...this.createdFilterTo && { + 'filter[created][to]': + new Date(`${this.createdFilterTo.replace(/\//g, '-')}T23:59:59.999`).getTime(), + }, + ...!this.listingTagsFilterModel.isEmpty && { + 'filter[tags][values]': this.listingTagsFilterModel.selected.join(), + 'filter[tags][operation]': this.listingTagsFilterModel.combinationOperator, + }, + ...this.runFilterValues.length > 0 && { + 'filter[run][values]': this.runFilterValues.join(), + 'filter[run][operation]': this.runFilterOperation.toLowerCase(), + }, + ...this.environmentFilterValues.length > 0 && { + 'filter[environments][values]': this.environmentFilterValues, + 'filter[environments][operation]': this.environmentFilterOperation.toLowerCase(), + }, + ...this.lhcFillFilterValues.length > 0 && { + 'filter[lhcFills][values]': this.lhcFillFilterValues.join(), + 'filter[lhcFills][operation]': this.lhcFillFilterOperation.toLowerCase(), + }, + ...sortOn && sortDirection && { + [`sort[${sortOn}]`]: sortDirection, + }, + }; } } diff --git a/lib/public/views/Logs/Overview/index.js b/lib/public/views/Logs/Overview/index.js index bf72f81c3a..012f6e7bfe 100644 --- a/lib/public/views/Logs/Overview/index.js +++ b/lib/public/views/Logs/Overview/index.js @@ -19,7 +19,6 @@ import { paginationComponent } from '../../../components/Pagination/paginationCo import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; import { excludeAnonymousLogAuthorToggle } from '../../../components/Filters/LogsFilter/author/authorFilter.js'; -import { warningComponent } from '../../../components/common/messages/warningComponent.js'; const TABLEROW_HEIGHT = 69; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -31,22 +30,22 @@ const PAGE_USED_HEIGHT = 215; * @return {Component} Returns a vnode with the table containing the logs */ const logOverviewScreen = ({ logs: { overviewModel: logsOverviewModel } }) => { - const { pagination, filteringModel, items, sortModel } = logsOverviewModel; - - pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount(TABLEROW_HEIGHT, PAGE_USED_HEIGHT)); + logsOverviewModel.pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount( + TABLEROW_HEIGHT, + PAGE_USED_HEIGHT, + )); return h('', [ h('#main-action-bar.flex-row.justify-between.header-container.pv2', [ h('.flex-row.g3', [ filtersPanelPopover(logsOverviewModel, logsActiveColumns), - excludeAnonymousLogAuthorToggle(filteringModel.get('author')), + excludeAnonymousLogAuthorToggle(logsOverviewModel.authorFilter), ]), actionButtons(), ]), - warningComponent(logsOverviewModel), h('.w-100.flex-column', [ - table(items, logsActiveColumns, null, null, { sort: sortModel }), - paginationComponent(pagination), + table(logsOverviewModel.logs, logsActiveColumns, null, null, { sort: logsOverviewModel.overviewSortModel }), + paginationComponent(logsOverviewModel.pagination), ]), ]); }; diff --git a/lib/public/views/QcFlagTypes/ActiveColumns/qcFlagTypesActiveColumns.js b/lib/public/views/QcFlagTypes/ActiveColumns/qcFlagTypesActiveColumns.js index 6f1ae72b84..7f4ae8aa69 100644 --- a/lib/public/views/QcFlagTypes/ActiveColumns/qcFlagTypesActiveColumns.js +++ b/lib/public/views/QcFlagTypes/ActiveColumns/qcFlagTypesActiveColumns.js @@ -14,8 +14,8 @@ import { h } from '/js/src/index.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; import { textFilter } from '../../../components/Filters/common/filters/textFilter.js'; +import { checkboxes } from '../../../components/Filters/common/filters/checkboxFilter.js'; import { qcFlagTypeColoredBadge } from '../../../components/qcFlags/qcFlagTypeColoredBadge.js'; -import radioButtonFilter from '../../../components/Filters/common/filters/radioButtonFilter.js'; /** * List of active columns for a QC Flag Types table @@ -30,7 +30,10 @@ export const qcFlagTypesActiveColumns = { name: { name: 'Name', visible: true, - filter: ({ filteringModel }) => textFilter(filteringModel.get('names'), { class: 'w-75 mt1', placeholder: 'e.g. BadPID, ...' }), + filter: ({ namesFilterModel }) => textFilter( + namesFilterModel, + { class: 'w-75 mt1', placeholder: 'e.g. BadPID, ...' }, + ), classes: 'f6', sortable: true, format: (_, qcFlagType) => qcFlagTypeColoredBadge(qcFlagType), @@ -40,7 +43,10 @@ export const qcFlagTypesActiveColumns = { name: 'Method', visible: true, sortable: true, - filter: ({ filteringModel }) => textFilter(filteringModel.get('methods'), { class: 'w-75 mt1', placeholder: 'e.g. Bad PID, ...' }), + filter: ({ methodsFilterModel }) => textFilter( + methodsFilterModel, + { class: 'w-75 mt1', placeholder: 'e.g. Bad PID, ...' }, + ), classes: 'f6', }, @@ -48,7 +54,10 @@ export const qcFlagTypesActiveColumns = { name: 'Bad', visible: true, sortable: true, - filter: ({ filteringModel }) => radioButtonFilter(filteringModel.get('bad'), 'bad'), + filter: ({ isBadFilterModel }) => checkboxes( + isBadFilterModel, + { class: 'w-75 mt1', selector: 'qc-flag-type-bad-filter' }, + ), classes: 'f6 w-5', format: (bad) => bad ? h('.danger', 'Yes') : h('.success', 'No'), }, diff --git a/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js b/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js index 8f12861e8f..6c80ada996 100644 --- a/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js +++ b/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewModel.js @@ -12,34 +12,107 @@ */ import { TextTokensFilterModel } from '../../../components/Filters/common/filters/TextTokensFilterModel.js'; -import { RadioButtonFilterModel } from '../../../components/Filters/common/RadioButtonFilterModel.js'; -import { FilterableOverviewPageModel } from '../../../models/FilterableOverviewPageModel.js'; +import { OverviewPageModel } from '../../../models/OverviewModel.js'; +import { SelectionModel } from '../../../components/common/selection/SelectionModel.js'; +import { buildUrl } from '/js/src/index.js'; /** * QcFlagTypesOverviewModel */ -export class QcFlagTypesOverviewModel extends FilterableOverviewPageModel { +export class QcFlagTypesOverviewModel extends OverviewPageModel { /** * Constructor - * @param {QueryRouter} router router that controls the application's page navigation - * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(router, pageIdentifier) { - super( - router, - pageIdentifier, - { - names: new TextTokensFilterModel(), - methods: new TextTokensFilterModel(), - bad: new RadioButtonFilterModel([{ label: 'Any' }, { label: 'Bad', value: true }, { label: 'Not Bad', value: false }]), - }, - ); + constructor() { + super(); + + this._namesFilterModel = new TextTokensFilterModel(); + this._registerFilter(this._namesFilterModel); + this._methodsFilterModel = new TextTokensFilterModel(); + this._registerFilter(this._methodsFilterModel); + this._isBadFilterModel = + new SelectionModel({ availableOptions: [{ label: 'Bad', value: true }, { label: 'Not Bad', value: false }] }); + this._registerFilter(this._isBadFilterModel); } /** * @inheritdoc */ getRootEndpoint() { - return this.buildRootEndpoint('/api/qcFlagTypes'); + const params = {}; + if (this.isAnyFilterActive()) { + params.filter = { + names: this._namesFilterModel.normalized, + methods: this._methodsFilterModel.normalized, + bad: this._isBadFilterModel.selected.length === 2 + ? undefined + : this._isBadFilterModel.selected[0], + }; + } + + return buildUrl('/api/qcFlagTypes', params); + } + + /** + * Get names filter model + * + * @return {TextTokensFilterModel} names filter model + */ + get namesFilterModel() { + return this._namesFilterModel; + } + + /** + * Get methods filter model + * + * @return {TextTokensFilterModel} methods filter model + */ + get methodsFilterModel() { + return this._methodsFilterModel; + } + + /** + * Returns filter model for filtering bad and not bad flags + * + * @return {TextTokensFilterModel} filter model for filtering bad and not bad flags + */ + get isBadFilterModel() { + return this._isBadFilterModel; + } + + /** + * Register a new filter model + * + * @param {FilterModel} filterModel the filter model to register + * @return {void} + * @private + */ + _registerFilter(filterModel) { + filterModel.visualChange$.bubbleTo(this); + filterModel.observe(() => { + this._pagination.silentlySetCurrentPage(1); + this.load(); + }); + } + + /** + * States whether any filter is active + * + * @return {boolean} true if any filter is active + */ + isAnyFilterActive() { + return !this._namesFilterModel.isEmpty || !this._methodsFilterModel.isEmpty || this._isBadFilterModel.selected.length; + } + + /** + * Reset this model to its default + * + * @returns {void} + */ + reset() { + this._methodsFilterModel.reset(); + this._namesFilterModel.reset(); + this._isBadFilterModel.reset(); + super.reset(); } } diff --git a/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewPage.js b/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewPage.js index 0c3fb2a71e..6b2a818527 100644 --- a/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewPage.js +++ b/lib/public/views/QcFlagTypes/Overview/QcFlagTypesOverviewPage.js @@ -19,7 +19,6 @@ import { qcFlagTypesActiveColumns } from '../ActiveColumns/qcFlagTypesActiveColu import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; import { BkpRoles } from '../../../domain/enums/BkpRoles.js'; -import { warningComponent } from '../../../components/common/messages/warningComponent.js'; const TABLEROW_HEIGHT = 30; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -31,9 +30,12 @@ const PAGE_USED_HEIGHT = 215; * @return {Component} The overview page */ export const QcFlagTypesOverviewPage = ({ qcFlagTypes: { overviewModel } }) => { - const { items: qcFlagTypes, pagination, sortModel } = overviewModel; + overviewModel.pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount( + TABLEROW_HEIGHT, + PAGE_USED_HEIGHT, + )); - pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount(TABLEROW_HEIGHT, PAGE_USED_HEIGHT)); + const { items: qcFlagTypes } = overviewModel; return h('', [ h('.flex-row.justify-between.items-center.g2', [ @@ -48,10 +50,15 @@ export const QcFlagTypesOverviewPage = ({ qcFlagTypes: { overviewModel } }) => { }), ], ]), - warningComponent(overviewModel), h('.flex-column.w-100', [ - table(qcFlagTypes, qcFlagTypesActiveColumns, { classes: '.table-sm' }, null, { sort: sortModel }), - paginationComponent(pagination), + table( + qcFlagTypes, + qcFlagTypesActiveColumns, + { classes: '.table-sm' }, + null, + { sort: overviewModel.sortModel }, + ), + paginationComponent(overviewModel.pagination), ]), ]); }; diff --git a/lib/public/views/QcFlagTypes/QcFlagTypesModel.js b/lib/public/views/QcFlagTypes/QcFlagTypesModel.js index 9fe8118a76..43468d3e34 100644 --- a/lib/public/views/QcFlagTypes/QcFlagTypesModel.js +++ b/lib/public/views/QcFlagTypes/QcFlagTypesModel.js @@ -29,7 +29,7 @@ export class QcFlagTypesModel extends Observable { this.model = model; // Overview - this._overviewModel = new QcFlagTypesOverviewModel(model.router, 'qc-flag-types-overview'); + this._overviewModel = new QcFlagTypesOverviewModel(); this._overviewModel.bubbleTo(this); } @@ -38,7 +38,6 @@ export class QcFlagTypesModel extends Observable { * @return {void} */ loadOverview() { - this._overviewModel.setFilterFromURL(false); this._overviewModel.load(); } diff --git a/lib/public/views/QcFlags/ActiveColumns/synchronousQcFlagsActiveColumns.js b/lib/public/views/QcFlags/ActiveColumns/synchronousQcFlagsActiveColumns.js deleted file mode 100644 index e087e2b780..0000000000 --- a/lib/public/views/QcFlags/ActiveColumns/synchronousQcFlagsActiveColumns.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { h } from '/js/src/index.js'; -import { qcFlagsActiveColumns } from './qcFlagsActiveColumns.js'; -import { formatQcFlagStart } from '../format/formatQcFlagStart.js'; -import { formatQcFlagEnd } from '../format/formatQcFlagEnd.js'; -import { formatQcFlagCreatedBy } from '../format/formatQcFlagCreatedBy.js'; -import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; - -/** - * Active columns configuration for synchronous QC flags table - */ -export const synchronousQcFlagsActiveColumns = { - id: { - name: 'Id', - visible: false, - }, - flagType: { - ...qcFlagsActiveColumns.flagType, - classes: 'w-15', - }, - from: { - name: 'From/To', - visible: true, - format: (_, qcFlag) => h('', [ - h('.flex-row', ['From: ', formatQcFlagStart(qcFlag, true)]), - h('.flex-row', ['To: ', formatQcFlagEnd(qcFlag, true)]), - ]), - classes: 'w-15', - }, - comment: { - ...qcFlagsActiveColumns.comment, - balloon: true, - }, - deleted: { - name: 'Deleted', - visible: true, - classes: 'w-5', - format: (deleted) => deleted ? h('.danger', 'Yes') : 'No', - }, - createdBy: { - name: 'Created', - visible: true, - balloon: true, - format: (_, qcFlag) => h('', [ - h('.flex-row', ['By: ', formatQcFlagCreatedBy(qcFlag)]), - h('.flex-row', ['At: ', formatTimestamp(qcFlag.createdAt)]), - ]), - }, -}; diff --git a/lib/public/views/QcFlags/Synchronous/SynchronousQcFlagsOverviewPage.js b/lib/public/views/QcFlags/Synchronous/SynchronousQcFlagsOverviewPage.js index 8fa5058b25..b8937c51ba 100644 --- a/lib/public/views/QcFlags/Synchronous/SynchronousQcFlagsOverviewPage.js +++ b/lib/public/views/QcFlags/Synchronous/SynchronousQcFlagsOverviewPage.js @@ -16,7 +16,7 @@ import { h } from '/js/src/index.js'; import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; import { table } from '../../../components/common/table/table.js'; import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; -import { synchronousQcFlagsActiveColumns } from '../ActiveColumns/synchronousQcFlagsActiveColumns.js'; +import { qcFlagsActiveColumns } from '../ActiveColumns/qcFlagsActiveColumns.js'; import { qcFlagsBreadcrumbs } from '../../../components/qcFlags/qcFlagsBreadcrumbs.js'; import { mergeRemoteData } from '../../../utilities/mergeRemoteData.js'; import errorAlert from '../../../components/common/errorAlert.js'; @@ -46,6 +46,16 @@ export const SynchronousQcFlagsOverviewPage = ({ qcFlags: { synchronousOverviewM PAGE_USED_HEIGHT, )); + const activeColumns = { + qcFlagId: { + name: 'Id', + visible: false, + classes: 'w-5', + }, + ...qcFlagsActiveColumns, + }; + delete activeColumns.verified; + return h( '', { onremove: () => synchronousOverviewModel.reset() }, @@ -60,8 +70,8 @@ export const SynchronousQcFlagsOverviewPage = ({ qcFlags: { synchronousOverviewM h('.w-100.flex-column', [ table( qcFlags, - synchronousQcFlagsActiveColumns, - { classes: '.table-sm.f6' }, + activeColumns, + { classes: '.table-sm' }, null, { sort: sortModel }, ), diff --git a/lib/public/views/QcFlags/format/formatQcFlagEnd.js b/lib/public/views/QcFlags/format/formatQcFlagEnd.js index 9cb2a7857d..dac1426802 100644 --- a/lib/public/views/QcFlags/format/formatQcFlagEnd.js +++ b/lib/public/views/QcFlags/format/formatQcFlagEnd.js @@ -17,12 +17,11 @@ import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.j * Format QC flag `to` timestamp * * @param {QcFlag} qcFlag QC flag - * @param {boolean} inline if true, date and time are on a single line * @return {Component} formatted `to` timestamp */ -export const formatQcFlagEnd = ({ from, to }, inline = false) => { +export const formatQcFlagEnd = ({ from, to }) => { if (to) { - return formatTimestamp(to, inline); + return formatTimestamp(to, false); } else { return from ? 'Until run end' diff --git a/lib/public/views/QcFlags/format/formatQcFlagStart.js b/lib/public/views/QcFlags/format/formatQcFlagStart.js index bf9e8ccae5..b5a11b9b6d 100644 --- a/lib/public/views/QcFlags/format/formatQcFlagStart.js +++ b/lib/public/views/QcFlags/format/formatQcFlagStart.js @@ -17,12 +17,11 @@ import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.j * Format QC flag `from` timestamp * * @param {QcFlag} qcFlag QC flag - * @param {boolean} inline if true, date and time are on a single line * @return {Component} formatted `from` timestamp */ -export const formatQcFlagStart = ({ from, to }, inline = false) => { +export const formatQcFlagStart = ({ from, to }) => { if (from) { - return formatTimestamp(from, inline); + return formatTimestamp(from, false); } else { return to ? 'Since run start' diff --git a/lib/public/views/Runs/ActiveColumns/runDetectorsAsyncQcActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runDetectorsAsyncQcActiveColumns.js index 2647f8589a..f4497010c4 100644 --- a/lib/public/views/Runs/ActiveColumns/runDetectorsAsyncQcActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runDetectorsAsyncQcActiveColumns.js @@ -161,7 +161,7 @@ export const createRunDetectorsAsyncQcActiveColumns = ( visible: false, profiles: profile, filter: (filteringModel) => { - const filterModel = filteringModel.get('detectorsQcNotBadFraction').getFilter(`_${dplDetectorId}`); + const filterModel = filteringModel.get(`detectorsQc[_${dplDetectorId}][notBadFraction]`); return filterModel ? numericalComparisonFilter(filterModel, { step: 0.1, selectorPrefix: `detectorsQc-for-${dplDetectorId}-notBadFraction` }) : null; diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 9992a93407..eefe0f006f 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -12,7 +12,11 @@ */ import { CopyToClipboardComponent, h } from '/js/src/index.js'; +import { runNumbersFilter } from '../../../components/Filters/RunsFilter/runNumbersFilter.js'; import { displayRunEorReasonsOverview } from '../format/displayRunEorReasonOverview.js'; +import ddflpFilter from '../../../components/Filters/RunsFilter/ddflp.js'; +import dcsFilter from '../../../components/Filters/RunsFilter/dcs.js'; +import epnFilter from '../../../components/Filters/RunsFilter/epn.js'; import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; import { displayRunDuration } from '../format/displayRunDuration.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; @@ -40,10 +44,10 @@ import { isRunConsideredRunning } from '../../../services/run/isRunConsideredRun import { aliEcsEnvironmentLinkComponent } from '../../../components/common/externalLinks/aliEcsEnvironmentLinkComponent.js'; import { detectorsFilterComponent } from '../../../components/Filters/RunsFilter/detectorsFilterComponent.js'; import { timeRangeFilter } from '../../../components/Filters/common/filters/timeRangeFilter.js'; +import { rawTextFilter } from '../../../components/Filters/common/filters/rawTextFilter.js'; import { numericalComparisonFilter } from '../../../components/Filters/common/filters/numericalComparisonFilter.js'; import { checkboxes } from '../../../components/Filters/common/filters/checkboxFilter.js'; -import radioButtonFilter from '../../../components/Filters/common/filters/radioButtonFilter.js'; -import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; +import { triggerValueFilter } from '../../../components/Filters/RunsFilter/triggerValueFilter.js'; /** * List of active columns for a generic runs table @@ -63,10 +67,10 @@ export const runsActiveColumns = { /** * Run numbers filter component * - * @param {FilteringModel} runsOverviewModel.filteringModel the filtering model + * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the filter component */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'runNumbers', 'e.g. 534454, 534455...'), + filter: (runsOverviewModel) => runNumbersFilter(runsOverviewModel.filteringModel.get('runNumbers')), format: (runNumber, run) => buttonLinkWithDropdown( runNumber, 'run-detail', @@ -158,7 +162,8 @@ export const runsActiveColumns = { * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the beam modes filter component */ - filter: (runsOverviewModel) => selectionDropdown(runsOverviewModel.filteringModel.get('beamModes'), { selectorPrefix: 'beam-mode' }), + filter: (runsOverviewModel) => + selectionDropdown(runsOverviewModel.filteringModel.get('beamModes').selectionDropdownModel, { selectorPrefix: 'beam-mode' }), }, fillNumber: { name: 'Fill No.', @@ -184,10 +189,13 @@ export const runsActiveColumns = { /** * Fill number filter component * - * @param {FilteringModel} RunsOverviewModel.filteringModel the filtering model + * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the filter component */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'fillNumbers', 'e.g. 7966, 7954, 7948...'), + filter: (runsOverviewModel) => rawTextFilter( + runsOverviewModel.filteringModel.get('fillNumbers'), + { classes: ['w-100', 'fill-numbers-filter'], placeholder: 'e.g. 7966, 7954, 7948...' }, + ), }, lhcPeriod: { name: 'LHC Period', @@ -199,10 +207,13 @@ export const runsActiveColumns = { /** * LHC Periods filter * - * @param {FilteringModel} RunsOverviewModel.filteringModel the filtering model + * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the filter component */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'lhcPeriods', 'e.g. LHC22b, LHC22a...'), + filter: (runsOverviewModel) => rawTextFilter( + runsOverviewModel.filteringModel.get('lhcPeriods'), + { classes: ['w-100'], placeholder: 'e.g. LHC22b, LHC22a...' }, + ), }, timeO2Start: { name: 'Start', @@ -388,10 +399,13 @@ export const runsActiveColumns = { /** * Environment ids filter component * - * @param {FilteringModel} RunsOverviewModel.filteringModel the filtering model - * @return {Component} the filter component + * @param {RunsOverviewModel} runsOverviewModel the runs overview model + * @return {Component} the environment ids filter component */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'environmentIds', 'e.g. Dxi029djX, TDI59So3d...'), + filter: (runsOverviewModel) => rawTextFilter( + runsOverviewModel.filteringModel.get('environmentIds'), + { classes: ['environment-ids-filter', 'w-100'], placeholder: 'e.g. Dxi029djX, TDI59So3d...' }, + ), format: (id) => id ? frontLink(id, 'env-details', { environmentId: id }) : '-', }, runType: { @@ -406,7 +420,10 @@ export const runsActiveColumns = { * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the run types filter component */ - filter: (runsOverviewModel) => selectionDropdown(runsOverviewModel.filteringModel.get('runTypes'), { selectorPrefix: 'run-types' }), + filter: (runsOverviewModel) => selectionDropdown( + runsOverviewModel.filteringModel.get('runTypes').selectionDropdownModel, + { selectorPrefix: 'run-types' }, + ), }, runQuality: { name: 'Quality', @@ -437,7 +454,7 @@ export const runsActiveColumns = { * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the run quality filter component */ - filter: (runsOverviewModel) => checkboxes(runsOverviewModel.filteringModel.get('runQualities')), + filter: (runsOverviewModel) => checkboxes(runsOverviewModel.filteringModel.get('runQualities').selectionModel), }, nDetectors: { name: 'DETs #', @@ -508,7 +525,7 @@ export const runsActiveColumns = { classes: 'w-2 f6 w-wrapped', format: (boolean) => boolean ? 'On' : 'Off', exportFormat: (boolean) => boolean ? 'On' : 'Off', - filter: ({ filteringModel }) => radioButtonFilter(filteringModel.get('ddflp'), 'ddFlp'), + filter: ddflpFilter, }, dcs: { name: 'DCS', @@ -517,21 +534,14 @@ export const runsActiveColumns = { classes: 'w-2 f6 w-wrapped', format: (boolean) => boolean ? 'On' : 'Off', exportFormat: (boolean) => boolean ? 'On' : 'Off', - filter: ({ filteringModel }) => radioButtonFilter(filteringModel.get('dcs'), 'dcs'), + filter: dcsFilter, }, triggerValue: { name: 'TRG', visible: true, profiles: [profiles.none, 'lhcFill', 'environment'], classes: 'w-5 f6 w-wrapped', - - /** - * TriggerValue filter component - * - * @param {RunsOverviewModel} runsOverviewModel the runs overview model - * @return {Component} the trigger value filter component - */ - filter: ({ filteringModel }) => checkboxes(filteringModel.get('triggerValues'), { selector: 'triggerValue' }), + filter: triggerValueFilter, format: (trgValue) => trgValue ? trgValue : '-', }, epn: { @@ -541,7 +551,7 @@ export const runsActiveColumns = { classes: 'w-2 f6 w-wrapped', format: (boolean) => boolean ? 'On' : 'Off', exportFormat: (boolean) => boolean ? 'On' : 'Off', - filter: ({ filteringModel }) => radioButtonFilter(filteringModel.get('epn'), 'epn'), + filter: epnFilter, }, epnTopology: { name: 'EPN Topology', @@ -558,10 +568,13 @@ export const runsActiveColumns = { /** * ODC topology full name filter component * - * @param {FilteringModel} RunsOverviewModel.filteringModel the filtering model + * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the filter component */ - filter: ({ filteringModel }) => textInputFilter(filteringModel, 'odcTopologyFullName'), + filter: (runsOverviewModel) => rawTextFilter( + runsOverviewModel.filteringModel.get('odcTopologyFullName'), + { classes: ['w-100'] }, + ), balloon: true, }, eorReasons: { @@ -663,8 +676,10 @@ export const runsActiveColumns = { * @param {RunsOverviewModel} runsOverviewModel the runs overview model * @return {Component} the run types filter component */ - filter: (runsOverviewModel) => - selectionDropdown(runsOverviewModel.filteringModel.get('magnets'), { selectorPrefix: 'l3-dipole-current' }), + filter: (runsOverviewModel) => selectionDropdown( + runsOverviewModel.filteringModel.get('magnets').selectionDropdownModel, + { selectorPrefix: 'l3-dipole-current' }, + ), profiles: ['runsPerLhcPeriod', 'runsPerDataPass', 'runsPerSimulationPass', profiles.none], }, diff --git a/lib/public/views/Runs/Details/RunPatch.js b/lib/public/views/Runs/Details/RunPatch.js index 54f7e347c6..3b000b8223 100644 --- a/lib/public/views/Runs/Details/RunPatch.js +++ b/lib/public/views/Runs/Details/RunPatch.js @@ -9,7 +9,6 @@ import { RunQualities } from '../../../domain/enums/RunQualities.js'; * @property {string} category * @property {string} title * @property {string} description - * @property {string|null} [lastEditedName] */ /** @@ -76,8 +75,7 @@ export class RunPatch extends Observable { } if (this._eorReasons.length !== this._run.eorReasons.length || this._eorReasons.some(({ id }) => id === undefined)) { - // Strip lastEditedName — the server's EorReasonDto only accepts id, reasonTypeId, and description - ret.eorReasons = this._eorReasons.map(({ id, reasonTypeId, description }) => ({ id, reasonTypeId, description })); + ret.eorReasons = this._eorReasons; } if (this._hasRunQualityChange()) { @@ -128,12 +126,7 @@ export class RunPatch extends Observable { } = this._run || {}; this._runQuality = runQuality; - this._eorReasons = eorReasons.map(({ id, description, reasonTypeId, lastEditedName }) => ({ - id, - description, - reasonTypeId, - lastEditedName, - })); + this._eorReasons = eorReasons.map(({ id, description, reasonTypeId }) => ({ id, description, reasonTypeId })); this._tags = tags.map(({ text }) => text); this.formData = { diff --git a/lib/public/views/Runs/Details/runDetailsComponent.js b/lib/public/views/Runs/Details/runDetailsComponent.js index fe370ced70..ebb7ae4fe7 100644 --- a/lib/public/views/Runs/Details/runDetailsComponent.js +++ b/lib/public/views/Runs/Details/runDetailsComponent.js @@ -40,7 +40,7 @@ import { RunDefinition } from '../../../domain/enums/RunDefinition.js'; import { formatFloat } from '../../../utilities/formatting/formatFloat.js'; import { formatEditableNumber } from '../format/formatEditableNumber.js'; import { editRunEorReasons } from '../format/editRunEorReasons.js'; -import { formatRunEorReason } from '../format/formatRunEorReason.js'; +import { formatEorReason } from '../format/formatEorReason.mjs'; import { selectionDropdown } from '../../../components/common/selection/dropdown/selectionDropdown.js'; import { formatRunCalibrationStatus } from '../format/formatRunCalibrationStatus.js'; import { BeamModes } from '../../../domain/enums/BeamModes.js'; @@ -533,10 +533,7 @@ export const runDetailsComponent = (runDetailsModel, router) => runDetailsModel. h('#eor-reasons.flex-row', [ runDetailsModel.isEditModeEnabled ? editRunEorReasons(runDetailsModel) - : h( - '.flex-column.g2.w-100', - run.eorReasons.map((eorReason) => h('.eor-reason', formatRunEorReason(eorReason))), - ), + : h('.flex-column.g2', run.eorReasons.map((eorReason) => h('.eor-reason', formatEorReason(eorReason)))), ]), ]), ]), diff --git a/lib/public/views/Runs/Overview/FixedPdpBeamTypeRunsOverviewModel.js b/lib/public/views/Runs/Overview/FixedPdpBeamTypeRunsOverviewModel.js index 5edce4c14e..82eaf9e819 100644 --- a/lib/public/views/Runs/Overview/FixedPdpBeamTypeRunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/FixedPdpBeamTypeRunsOverviewModel.js @@ -23,10 +23,9 @@ export class FixedPdpBeamTypeRunsOverviewModel extends RunsWithQcModel { /** * Constructor * @param {Model} model global model - * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(model, pageIdentifier) { - super(model, pageIdentifier); + constructor(model) { + super(model); this._pdpBeamTypes = []; } diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index c6912513b6..0249c66085 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -11,15 +11,18 @@ * or submit itself to any jurisdiction. */ +import { buildUrl } from '/js/src/index.js'; import { TagFilterModel } from '../../../components/Filters/common/TagFilterModel.js'; +import { debounce } from '../../../utilities/debounce.js'; import { DetectorsFilterModel } from '../../../components/Filters/RunsFilter/DetectorsFilterModel.js'; import { RunTypesFilterModel } from '../../../components/runTypes/RunTypesFilterModel.js'; import { EorReasonFilterModel } from '../../../components/Filters/RunsFilter/EorReasonFilterModel.js'; -import { FilterableOverviewPageModel } from '../../../models/FilterableOverviewPageModel.js'; +import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { CombinationOperator } from '../../../components/Filters/common/CombinationOperatorChoiceModel.js'; import { NumericalComparisonFilterModel } from '../../../components/Filters/common/filters/NumericalComparisonFilterModel.js'; import { detectorsProvider } from '../../../services/detectors/detectorsProvider.js'; import { MagnetsFilteringModel } from '../../../components/Filters/RunsFilter/MagnetsFilteringModel.js'; +import { FilteringModel } from '../../../components/Filters/common/FilteringModel.js'; import { tagsProvider } from '../../../services/tag/tagsProvider.js'; import { eorReasonTypeProvider } from '../../../services/eorReason/eorReasonTypeProvider.js'; import { runTypesProvider } from '../../../services/runTypes/runTypesProvider.js'; @@ -28,75 +31,74 @@ import { magnetsCurrentLevelsProvider } from '../../../services/magnets/magnetsC import { RawTextFilterModel } from '../../../components/Filters/common/filters/RawTextFilterModel.js'; import { RunDefinitionFilterModel } from '../../../components/Filters/RunsFilter/RunDefinitionFilterModel.js'; import { RUN_QUALITIES } from '../../../domain/enums/RunQualities.js'; +import { SelectionFilterModel } from '../../../components/Filters/common/filters/SelectionFilterModel.js'; import { DataExportModel } from '../../../models/DataExportModel.js'; import { runsActiveColumns as dataExportConfiguration } from '../ActiveColumns/runsActiveColumns.js'; import { BeamModeFilterModel } from '../../../components/Filters/RunsFilter/BeamModeFilterModel.js'; import { beamModesProvider } from '../../../services/beamModes/beamModesProvider.js'; -import { RadioButtonFilterModel } from '../../../components/Filters/common/RadioButtonFilterModel.js'; -import { SelectionModel } from '../../../components/common/selection/SelectionModel.js'; -import { TRIGGER_VALUES } from '../../../domain/enums/TriggerValue.js'; /** * Model representing handlers for runs page * * @implements {OverviewModel} */ -export class RunsOverviewModel extends FilterableOverviewPageModel { +export class RunsOverviewModel extends OverviewPageModel { /** * The constructor of the Overview model object * @param {Model} model global model - * @param {string} pageIdentifier string that indicates what page this model represents - */ - constructor(model, pageIdentifier) { - super( - model.router, - pageIdentifier, - { - runNumbers: new RawTextFilterModel(), - detectors: new DetectorsFilterModel(detectorsProvider.dataTaking$), - tags: new TagFilterModel( - tagsProvider.items$, - [ - CombinationOperator.AND, - CombinationOperator.OR, - CombinationOperator.NONE_OF, - ], - ), - fillNumbers: new RawTextFilterModel(), - lhcPeriods: new RawTextFilterModel(), - o2start: new TimeRangeFilterModel(), - o2end: new TimeRangeFilterModel(), - definitions: new RunDefinitionFilterModel(), - runDuration: new NumericalComparisonFilterModel({ scale: 60 * 1000 }), - environmentIds: new RawTextFilterModel(), - runTypes: new RunTypesFilterModel(runTypesProvider.items$), - beamModes: new BeamModeFilterModel(beamModesProvider.items$), - runQualities: new SelectionModel({ - availableOptions: RUN_QUALITIES.map((quality) => ({ - label: quality.toUpperCase(), - value: quality, - })), - }), - nDetectors: new NumericalComparisonFilterModel({ integer: true }), - nEpns: new NumericalComparisonFilterModel({ integer: true }), - nFlps: new NumericalComparisonFilterModel({ integer: true }), - ctfFileCount: new NumericalComparisonFilterModel({ integer: true }), - tfFileCount: new NumericalComparisonFilterModel({ integer: true }), - otherFileCount: new NumericalComparisonFilterModel({ integer: true }), - odcTopologyFullName: new RawTextFilterModel(), - eorReason: new EorReasonFilterModel(eorReasonTypeProvider.items$), - magnets: new MagnetsFilteringModel(magnetsCurrentLevelsProvider.items$), - muInelasticInteractionRate: new NumericalComparisonFilterModel(), - inelasticInteractionRateAvg: new NumericalComparisonFilterModel(), - inelasticInteractionRateAtStart: new NumericalComparisonFilterModel(), - inelasticInteractionRateAtMid: new NumericalComparisonFilterModel(), - inelasticInteractionRateAtEnd: new NumericalComparisonFilterModel(), - ddflp: new RadioButtonFilterModel([{ label: 'ANY' }, { label: 'ON', value: true }, { label: 'OFF', value: false }]), - dcs: new RadioButtonFilterModel([{ label: 'ANY' }, { label: 'ON', value: true }, { label: 'OFF', value: false }]), - epn: new RadioButtonFilterModel([{ label: 'ANY' }, { label: 'ON', value: true }, { label: 'OFF', value: false }]), - triggerValues: new SelectionModel({ availableOptions: TRIGGER_VALUES.map((value) => ({ label: value, value })) }), - }, - ); + */ + constructor(model) { + super(); + + this._filteringModel = new FilteringModel({ + runNumbers: new RawTextFilterModel(), + detectors: new DetectorsFilterModel(detectorsProvider.dataTaking$), + tags: new TagFilterModel( + tagsProvider.items$, + [ + CombinationOperator.AND, + CombinationOperator.OR, + CombinationOperator.NONE_OF, + ], + ), + fillNumbers: new RawTextFilterModel(), + lhcPeriods: new RawTextFilterModel(), + o2start: new TimeRangeFilterModel(), + o2end: new TimeRangeFilterModel(), + definitions: new RunDefinitionFilterModel(), + runDuration: new NumericalComparisonFilterModel({ scale: 60 * 1000 }), + environmentIds: new RawTextFilterModel(), + runTypes: new RunTypesFilterModel(runTypesProvider.items$), + beamModes: new BeamModeFilterModel(beamModesProvider.items$), + runQualities: new SelectionFilterModel({ + availableOptions: RUN_QUALITIES.map((quality) => ({ + label: quality.toUpperCase(), + value: quality, + })), + }), + nDetectors: new NumericalComparisonFilterModel({ integer: true }), + nEpns: new NumericalComparisonFilterModel({ integer: true }), + nFlps: new NumericalComparisonFilterModel({ integer: true }), + ctfFileCount: new NumericalComparisonFilterModel({ integer: true }), + tfFileCount: new NumericalComparisonFilterModel({ integer: true }), + otherFileCount: new NumericalComparisonFilterModel({ integer: true }), + odcTopologyFullName: new RawTextFilterModel(), + eorReason: new EorReasonFilterModel(eorReasonTypeProvider.items$), + magnets: new MagnetsFilteringModel(magnetsCurrentLevelsProvider.items$), + muInelasticInteractionRate: new NumericalComparisonFilterModel(), + inelasticInteractionRateAvg: new NumericalComparisonFilterModel(), + inelasticInteractionRateAtStart: new NumericalComparisonFilterModel(), + inelasticInteractionRateAtMid: new NumericalComparisonFilterModel(), + inelasticInteractionRateAtEnd: new NumericalComparisonFilterModel(), + }); + + this._filteringModel.observe(() => this._applyFilters(true)); + this._filteringModel.visualChange$.bubbleTo(this); + + this.reset(false); + const updateDebounceTime = () => { + this._debouncedLoad = debounce(this.load.bind(this), model.inputDebounceTime); + }; this._exportModel = new DataExportModel(this._allItems$, dataExportConfiguration, () => this.loadAll()); this._exportModel.bubbleTo(this); @@ -104,14 +106,9 @@ export class RunsOverviewModel extends FilterableOverviewPageModel { this._exportModel.setDisabled(!this.hasAnyData()); this._exportModel.setTotalExistingItemsCount(this._pagination.itemsCount); }); - } - /** - * @inheritdoc - */ - reset(fetch = true) { - this._exportModel?.reset(); - super.reset(fetch); + model.appConfiguration$.observe(() => updateDebounceTime()); + updateDebounceTime(); } /** @@ -126,6 +123,195 @@ export class RunsOverviewModel extends FilterableOverviewPageModel { * @inheritdoc */ getRootEndpoint() { - return this.buildRootEndpoint('/api/runs'); + return buildUrl('/api/runs', { ...this._getFilterQueryParams(), ...{ filter: this.filteringModel.normalized } }); + } + + /** + * Returns all filtering, sorting and pagination settings to their default values + * @param {boolean} [fetch = true] whether to refetch all data after filters have been reset + * @return {void} + */ + reset(fetch = true) { + super.reset(); + this._exportModel?.reset(); + this.resetFiltering(fetch); + } + + /** + * Reset all filtering models + * @param {boolean} fetch Whether to refetch all data after filters have been reset + * @return {void} + */ + resetFiltering(fetch = true) { + this._filteringModel.reset(); + + this._triggerValuesFilters = new Set(); + + this.ddflpFilter = ''; + + this.dcsFilter = ''; + + this.epnFilter = ''; + + if (fetch) { + this._applyFilters(true); + } + } + + /** + * Checks if any filter value has been modified from their default (empty) + * @return {Boolean} If any filter is active + */ + isAnyFilterActive() { + return this._filteringModel.isAnyFilterActive() + || this._triggerValuesFilters.size !== 0 + || this.ddflpFilter !== '' + || this.dcsFilter !== '' + || this.epnFilter !== ''; + } + + /** + * Return the filtering model + * + * @return {FilteringModel} the filtering model + */ + get filteringModel() { + return this._filteringModel; + } + + /** + * Getter for the trigger values filter Set + * @return {Set} set of trigger filter values + */ + get triggerValuesFilters() { + return this._triggerValuesFilters; + } + + /** + * Setter for trigger values filter, this replaces the current set + * @param {Array} newTriggerValues new Set of values + * @return {undefined} + */ + set triggerValuesFilters(newTriggerValues) { + this._triggerValuesFilters = new Set(newTriggerValues); + this._applyFilters(); + } + + /** + * Returns the boolean of ddflp + * @return {Boolean} if ddflp is on + */ + getDdflpFilterOperation() { + return this.ddflpFilter; + } + + /** + * Sets the boolean of the filter if no new inputs were detected for 200 milliseconds + * @param {boolean} operation if the ddflp is on + * @return {undefined} + */ + setDdflpFilterOperation(operation) { + this.ddflpFilter = operation; + this._applyFilters(); + } + + /** + * Unchecks the ddflp checkbox and fetches all the runs. + * @return {undefined} + * + */ + removeDdflp() { + this.ddflpFilter = ''; + this._applyFilters(); + } + + /** + * Returns the boolean of dcs + * @return {Boolean} if dcs is on + */ + getDcsFilterOperation() { + return this.dcsFilter; + } + + /** + * Sets the boolean of the filter if no new inputs were detected for 200 milliseconds + * @param {boolean} operation if the dcs is on + * @return {undefined} + */ + setDcsFilterOperation(operation) { + this.dcsFilter = operation; + this._applyFilters(); + } + + /** + * Unchecks the dcs checkbox and fetches all the runs. + * @return {undefined} + */ + removeDcs() { + this.dcsFilter = ''; + this._applyFilters(); + } + + /** + * Returns the boolean of epn + * @return {Boolean} if epn is on + */ + getEpnFilterOperation() { + return this.epnFilter; + } + + /** + * Sets the boolean of the filter if no new inputs were detected for 200 milliseconds + * @param {boolean} operation if the epn is on + * @return {undefined} + */ + setEpnFilterOperation(operation) { + this.epnFilter = operation; + this._applyFilters(); + } + + /** + * Unchecks the epn checkbox and fetches all the runs. + * @return {undefined} + */ + removeEpn() { + this.epnFilter = ''; + this._applyFilters(); + } + + /** + * Returns the list of URL params corresponding to the currently applied filter + * + * @return {Object} the URL params + * + * @private + */ + _getFilterQueryParams() { + return { + ...this._triggerValuesFilters.size !== 0 && { + 'filter[triggerValues]': Array.from(this._triggerValuesFilters).join(), + }, + ...(this.ddflpFilter === true || this.ddflpFilter === false) && { + 'filter[ddflp]': this.ddflpFilter, + }, + ...(this.dcsFilter === true || this.dcsFilter === false) && { + 'filter[dcs]': this.dcsFilter, + }, + ...(this.epnFilter === true || this.epnFilter === false) && { + 'filter[epn]': this.epnFilter, + }, + }; + } + + /** + * Apply the current filtering and update the remote data list + * + * @param {boolean} now if true, filtering will be applied now without debouncing + * + * @return {void} + */ + _applyFilters(now = false) { + this._pagination.currentPage = 1; + now ? this.load() : this._debouncedLoad(true); } } diff --git a/lib/public/views/Runs/Overview/RunsOverviewPage.js b/lib/public/views/Runs/Overview/RunsOverviewPage.js index 4f76d417d9..ab43fcbfe9 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewPage.js +++ b/lib/public/views/Runs/Overview/RunsOverviewPage.js @@ -17,10 +17,9 @@ import { filtersPanelPopover } from '../../../components/Filters/common/filtersP import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; import { runsActiveColumns } from '../ActiveColumns/runsActiveColumns.js'; import { table } from '../../../components/common/table/table.js'; +import { runNumbersFilter } from '../../../components/Filters/RunsFilter/runNumbersFilter.js'; import { switchInput } from '../../../components/common/form/switchInput.js'; import { exportTriggerAndModal } from '../../../components/common/dataExport/exportTriggerAndModal.js'; -import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; -import { warningComponent } from '../../../components/common/messages/warningComponent.js'; const TABLEROW_HEIGHT = 59; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -47,20 +46,21 @@ export const togglePhysicsOnlyFilter = (runDefinitionFilterModel) => { * @return {Component} Returns a vnode with the table containing the runs */ export const RunsOverviewPage = ({ runs: { overviewModel: runsOverviewModel }, modalModel }) => { - const { pagination, items, exportModel, filteringModel } = runsOverviewModel; - pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount(TABLEROW_HEIGHT, PAGE_USED_HEIGHT)); + runsOverviewModel.pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount( + TABLEROW_HEIGHT, + PAGE_USED_HEIGHT, + )); return h('', [ h('.flex-row.header-container.g2.pv2', [ filtersPanelPopover(runsOverviewModel, runsActiveColumns), - h('.pl2#runOverviewFilter', textInputFilter(runsOverviewModel.filteringModel, 'runNumbers', 'e.g. 534454, 534455...')), - togglePhysicsOnlyFilter(filteringModel.get('definitions')), - exportTriggerAndModal(exportModel, modalModel), + h('.pl2#runOverviewFilter', runNumbersFilter(runsOverviewModel.filteringModel.get('runNumbers'))), + togglePhysicsOnlyFilter(runsOverviewModel.filteringModel.get('definitions')), + exportTriggerAndModal(runsOverviewModel.exportModel, modalModel), ]), - warningComponent(runsOverviewModel), h('.flex-column.w-100', [ - table(items, runsActiveColumns), - paginationComponent(pagination), + table(runsOverviewModel.items, runsActiveColumns), + paginationComponent(runsOverviewModel.pagination), ]), ]); }; diff --git a/lib/public/views/Runs/Overview/RunsWithQcModel.js b/lib/public/views/Runs/Overview/RunsWithQcModel.js index e9ec4f36c5..ad09ea4718 100644 --- a/lib/public/views/Runs/Overview/RunsWithQcModel.js +++ b/lib/public/views/Runs/Overview/RunsWithQcModel.js @@ -43,8 +43,6 @@ const qcFlagsExportConfigurationFactory = (detectors) => Object.fromEntries(dete import { ObservableData } from '../../../utilities/ObservableData.js'; import { DetectorType } from '../../../domain/enums/DetectorTypes.js'; import { mergeRemoteData } from '../../../utilities/mergeRemoteData.js'; -import { ToggleFilterModel } from '../../../components/Filters/common/filters/ToggleFilterModel.js'; -import { MultiCompositionFilterModel } from '../../../components/Filters/RunsFilter/MultiCompositionFilterModel.js'; /** * Merge QC summaries @@ -68,17 +66,11 @@ export class RunsWithQcModel extends RunsOverviewModel { /** * Constructor * @param {Model} model global model - * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(model, pageIdentifier) { - super(model, pageIdentifier); + constructor(model) { + super(model); - this._detectorsNotBadFractionRegistered = false; - this._detectorsForQcFlagRegistered = false; - - this._observablesQcFlagsSummaryDependsOn$ = null; - // This filter instance will be added as a sub-filter for a MultiCompositionFilter and a GaqFilter later. - this._mcReproducibleAsNotBad = new ToggleFilterModel(false, true); + this._mcReproducibleAsNotBad = false; this._runDetectorsSelectionModel = new RunDetectorsSelectionModel(); this._runDetectorsSelectionModel.bubbleTo(this); @@ -91,22 +83,35 @@ export class RunsWithQcModel extends RunsOverviewModel { verticalScrollEnabled: true, freezeFirstColumn: true, }); - - this._filteringModel - .put('detectorsQcNotBadFraction', new MultiCompositionFilterModel({ mcReproducibleAsNotBad: this._mcReproducibleAsNotBad })); } /** * @inheritdoc */ getRootEndpoint() { - return buildUrl(super.getRootEndpoint(), { include: { effectiveQcFlags: true } }); + const filter = {}; + filter.detectorsQc = { + mcReproducibleAsNotBad: this._mcReproducibleAsNotBad, + }; + + return buildUrl(super.getRootEndpoint(), { filter, include: { effectiveQcFlags: true } }); + } + + /** + * Set mcReproducibleAsNotBad + * + * @param {boolean} mcReproducibleAsNotBad new value + * @return {void} + */ + setMcReproducibleAsNotBad(mcReproducibleAsNotBad) { + this._mcReproducibleAsNotBad = mcReproducibleAsNotBad; + this.load(); } /** * Get mcReproducibleAsNotBad * - * @return {ToggleFilterModel} mcReproducibleAsNotBad + * @return {boolean} mcReproducibleAsNotBad */ get mcReproducibleAsNotBad() { return this._mcReproducibleAsNotBad; @@ -126,86 +131,57 @@ export class RunsWithQcModel extends RunsOverviewModel { */ async load() { this._runDetectorsSelectionModel.reset(); - // Only fetch QC summary manually if no observer is registered - if (!this._observablesQcFlagsSummaryDependsOn$) { - this._fetchQcSummary(); - } + this._fetchQcSummary(); super.load(); } /** - * Register not-bad fraction detectors filtering model and update it when detectors are loaded - * Also, trigger an immediate update if detectors are already loaded at the moment of registration + * Register not-bad fraction detectors filtering model * * @param {ObservableData>} detectors$ detectors remote data observable */ registerDetectorsNotBadFractionFilterModels(detectors$) { - const detectorsQcNotBadFraction = this._filteringModel.get('detectorsQcNotBadFraction'); - - const callback = (observableData) => { - const current = observableData.getCurrent(); - current?.apply({ - Success: (detectors) => detectors.forEach(({ id }) => - detectorsQcNotBadFraction.putFilter(`_${id}`, new NumericalComparisonFilterModel({ scale: 0.01, integer: false }))), - }); - - if (current?.isSuccess() && !this._detectorsNotBadFractionRegistered) { - this.filteringModel.setFilterFromURL(); - this._detectorsNotBadFractionRegistered = true; - } - }; - - if (!this._detectorsNotBadFractionRegistered) { - detectors$.observe(callback.bind(this)); - callback(detectors$); - } + detectors$.observe((observableData) => observableData.getCurrent().apply({ + Success: (detectors) => detectors.forEach(({ id }) => { + this._filteringModel.put(`detectorsQc[_${id}][notBadFraction]`, new NumericalComparisonFilterModel({ + scale: 0.01, + integer: false, + })); + }), + })); } /** - * Register detectors for QC flags data export and update export configuration when detectors are loaded - * Also, trigger an immediate update if detectors are already loaded at the moment of registration + * Register detectors for QC flags data export * * @param {ObservableData>} detectors$ detectors remote data observable */ registerDetectorsForQcFlagsDataExport(detectors$) { - const callback = (observableData) => { - const current = observableData.getCurrent(); - current?.apply({ - Success: (detectors) => { - this._detectorsForQcFlagRegistered = true; - this._exportModel.setDataExportConfiguration({ - ...baseDataExportConfiguration, - ...qcFlagsExportConfigurationFactory(detectors), - }); - }, - Other: () => null, - }); - }; - - if (!this._detectorsForQcFlagRegistered) { - detectors$.observe(callback.bind(this)); - callback(detectors$); - } + detectors$.observe((observableData) => observableData.getCurrent().apply({ + Success: (detectors) => { + this._exportModel.setDataExportConfiguration({ + ...baseDataExportConfiguration, + ...qcFlagsExportConfigurationFactory(detectors), + }); + }, + Other: () => null, + })); } /** - * Register observables data, which QC flags fetching operation success depends on + * Register obervables data, which QC flags fetching operation success dependes on * - * @param {ObservableData>} detectors$ observable data which QC flags fetching operation success depends on + * @param {ObservableData[]} observables obervable data list */ - registerObservablesQcSummaryDependsOn(detectors$) { - if (detectors$ === this._observablesQcFlagsSummaryDependsOn$) { - return; - } - - this._observablesQcFlagsSummaryDependsOn$ = detectors$; - const callback = (observableData) => { - const current = observableData.getCurrent(); - current?.apply({ Success: () => this._fetchQcSummary() }); - }; - this._observablesQcFlagsSummaryDependsOn$.observe(callback); - // Also trigger immediately if detectors are already loaded - callback(this._observablesQcFlagsSummaryDependsOn$); + registerObervablesQcSummaryDependesOn(observables) { + this._observablesQcFlagsSummaryDepndsOn$ = ObservableData + .builder() + .sources(observables) + .apply((remoteDataList) => mergeRemoteData(remoteDataList)) + .build(); + + this._observablesQcFlagsSummaryDepndsOn$ + .observe((observableData) => observableData.getCurrent().apply({ Success: () => this._fetchQcSummary() })); } /** @@ -232,8 +208,8 @@ export class RunsWithQcModel extends RunsOverviewModel { async _fetchQcSummary() { const qcSummaryScopeValid = Object.entries(this.qcSummaryScope).filter(([, id]) => id).length == 1; - if (qcSummaryScopeValid && this.detectors && this._observablesQcFlagsSummaryDependsOn$?.getCurrent()) { - mergeRemoteData([this.detectors, this._observablesQcFlagsSummaryDependsOn$.getCurrent()]).match({ + if (qcSummaryScopeValid && this.detectors && this._observablesQcFlagsSummaryDepndsOn$.getCurrent()) { + mergeRemoteData([this.detectors, this._observablesQcFlagsSummaryDepndsOn$.getCurrent()]).match({ Success: async ([detectors]) => { this._qcSummary$.setCurrent(RemoteData.loading()); try { @@ -242,7 +218,7 @@ export class RunsWithQcModel extends RunsOverviewModel { detectorIds: detectors .filter(({ type }) => type === DetectorType.PHYSICAL) .map(({ id }) => id).join(','), - mcReproducibleAsNotBad: this._mcReproducibleAsNotBad.isToggled, + mcReproducibleAsNotBad: this._mcReproducibleAsNotBad, })); const { data: qcSummary2 } = await getRemoteData(buildUrl('/api/qcFlags/summary', { @@ -256,7 +232,7 @@ export class RunsWithQcModel extends RunsOverviewModel { operator: 'none', }, }, - mcReproducibleAsNotBad: this._mcReproducibleAsNotBad.isToggled, + mcReproducibleAsNotBad: this._mcReproducibleAsNotBad, })); this._qcSummary$.setCurrent(RemoteData.success(mergeQcSummaries([qcSummary1, qcSummary2]))); } catch (error) { diff --git a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewModel.js b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewModel.js index 5bceeeeeb4..0aca49d627 100644 --- a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewModel.js +++ b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewModel.js @@ -14,16 +14,16 @@ import { buildUrl, RemoteData } from '/js/src/index.js'; import { ObservableData } from '../../../utilities/ObservableData.js'; import { getRemoteDataSlice } from '../../../utilities/fetch/getRemoteDataSlice.js'; import { getRemoteData } from '../../../utilities/fetch/getRemoteData.js'; -import { rctDetectorsProvider } from '../../../services/detectors/detectorsProvider.js'; +import { detectorsProvider } from '../../../services/detectors/detectorsProvider.js'; import { FixedPdpBeamTypeRunsOverviewModel } from '../Overview/FixedPdpBeamTypeRunsOverviewModel.js'; import { jsonPatch } from '../../../utilities/fetch/jsonPatch.js'; import { jsonPut } from '../../../utilities/fetch/jsonPut.js'; import { SkimmingStage } from '../../../domain/enums/SkimmingStage.js'; +import { NumericalComparisonFilterModel } from '../../../components/Filters/common/filters/NumericalComparisonFilterModel.js'; import { jsonFetch } from '../../../utilities/fetch/jsonFetch.js'; import { mergeRemoteData } from '../../../utilities/mergeRemoteData.js'; import { RemoteDataSource } from '../../../utilities/fetch/RemoteDataSource.js'; import { DetectorType } from '../../../domain/enums/DetectorTypes.js'; -import { GaqFilterModel } from '../../../components/Filters/RunsFilter/GaqFilterModel.js'; const ALL_CPASS_PRODUCTIONS_REGEX = /cpass\d+/; const DETECTOR_NAMES_NOT_IN_CPASSES = ['EVS']; @@ -35,16 +35,15 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo /** * Constructor * @param {Model} model global model - * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(model, pageIdentifier) { - super(model, pageIdentifier); + constructor(model) { + super(model); this._dataPass$ = new ObservableData(RemoteData.notAsked()); this._dataPass$.bubbleTo(this); this._detectors$ = ObservableData .builder() - .sources([rctDetectorsProvider.qc$, this._dataPass$]) + .sources([detectorsProvider.qc$, this._dataPass$]) .apply((remoteDataList) => mergeRemoteData(remoteDataList) .apply({ Success: ([detectors, dataPass]) => ALL_CPASS_PRODUCTIONS_REGEX.test(dataPass.name) ? detectors.filter(({ name, type }) => type !== DetectorType.AOT_GLO || DETECTOR_NAMES_NOT_IN_CPASSES.includes(name)) @@ -52,9 +51,10 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo })) .build(); - this._filteringModel.put('gaq', new GaqFilterModel(this._mcReproducibleAsNotBad)); - this._detectors$.bubbleTo(this); + this.registerDetectorsNotBadFractionFilterModels(this._detectors$); + this.registerDetectorsForQcFlagsDataExport(this._detectors$); + this.registerObervablesQcSummaryDependesOn([this._detectors$]); this._markAsSkimmableRequestResult$ = new ObservableData(RemoteData.notAsked()); this._markAsSkimmableRequestResult$.bubbleTo(this); @@ -68,6 +68,11 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo this._skimmableRuns$ = new ObservableData(RemoteData.notAsked()); this._skimmableRuns$.bubbleTo(this); + this._filteringModel.put('gaq[notBadFraction]', new NumericalComparisonFilterModel({ + scale: 0.01, + integer: false, + })); + this._freezeOrUnfreezeActionState$ = new ObservableData(RemoteData.notAsked()); this._freezeOrUnfreezeActionState$.bubbleTo(this); @@ -130,11 +135,6 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo }, Other: () => null, })); - - this.registerDetectorsNotBadFractionFilterModels(this._detectors$); - this.registerDetectorsForQcFlagsDataExport(this._detectors$); - this.registerObservablesQcSummaryDependsOn(this._detectors$); - super.load(); } @@ -142,15 +142,22 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo * @inheritdoc */ getRootEndpoint() { - const filter = { ...this._filteringModel.normalized, dataPassIds: [this._dataPassId] }; + const gaqNotBadFilter = this._filteringModel.get('gaq[notBadFraction]'); + const filter = { dataPassIds: [this._dataPassId] }; + if (!gaqNotBadFilter.isEmpty) { + filter.gaq = { + mcReproducibleAsNotBad: this._mcReproducibleAsNotBad, + }; + } + return buildUrl(super.getRootEndpoint(), { filter }); } /** * @inheritdoc */ - resetFiltering(fetch = true, clearUrl = false) { - super.resetFiltering(fetch, clearUrl); + resetFiltering(fetch = true) { + super.resetFiltering(fetch); } /** @@ -272,7 +279,7 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo * @param {number} dataPassId id of Data Pass */ set dataPassId(dataPassId) { - if (this._dataPassId && dataPassId !== this._dataPassId) { + if (dataPassId !== this._dataPassId) { this.reset(false); } this._dataPassId = dataPassId; @@ -356,7 +363,7 @@ export class RunsPerDataPassOverviewModel extends FixedPdpBeamTypeRunsOverviewMo }); const url = buildUrl('/api/qcFlags/summary/gaq', { dataPassId: this._dataPassId, - mcReproducibleAsNotBad: this._mcReproducibleAsNotBad.isToggled, + mcReproducibleAsNotBad: this._mcReproducibleAsNotBad, runNumber: runNumber, }); await this._gaqSummarySources[runNumber].fetch(url); diff --git a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js index fd847389f5..8f63fb608b 100644 --- a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js +++ b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js @@ -21,6 +21,7 @@ import { tooltip } from '../../../components/common/popover/tooltip.js'; import { breadcrumbs } from '../../../components/common/navigation/breadcrumbs.js'; import { createRunDetectorsAsyncQcActiveColumns } from '../ActiveColumns/runDetectorsAsyncQcActiveColumns.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; +import { runNumbersFilter } from '../../../components/Filters/RunsFilter/runNumbersFilter.js'; import { qcSummaryLegendTooltip } from '../../../components/qcFlags/qcSummaryLegendTooltip.js'; import { isRunNotSubjectToQc } from '../../../components/qcFlags/isRunNotSubjectToQc.js'; import { frontLink } from '../../../components/common/navigation/frontLink.js'; @@ -37,9 +38,7 @@ import { iconCaretBottom } from '/js/src/icons.js'; import { BkpRoles } from '../../../domain/enums/BkpRoles.js'; import { getInelasticInteractionRateColumns } from '../ActiveColumns/getInelasticInteractionRateActiveColumns.js'; import { exportTriggerAndModal } from '../../../components/common/dataExport/exportTriggerAndModal.js'; -import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; -import { toggleFilter } from '../../../components/Filters/common/filters/toggleFilter.js'; -import { warningComponent } from '../../../components/common/messages/warningComponent.js'; +import { mcReproducibleAsNotBadToggle } from '../mcReproducibleAsNotBadToggle.js'; const TABLEROW_HEIGHT = 59; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -118,194 +117,177 @@ export const RunsPerDataPassOverviewPage = ({ const commonTitle = h('h2#breadcrumb-header', { style: 'white-space: nowrap;' }, 'Physics Runs'); const runDetectorsSelectionIsEmpty = perDataPassOverviewModel.runDetectorsSelectionModel.selectedQueryString.length === 0; - const dataPass = remoteDataPass.match({ Other: () => null, Success: (data) => data }); - const detectors = remoteDetectors.match({ Other: () => null, Success: (data) => data }); - const qcSummary = remoteQcSummary.match({ Other: () => null, Success: (data) => data }); - /* - * The table drawing can be done without using mergeRemoteData, but that will redraw it - * each independent update to dataPass, detectors, or qcSummary. - * While this wouldn't necessarily be noticeable for users, it would detach nodes from - * the document, which would make writing integration test difficult and unreliable. - */ - const fullPageData = mergeRemoteData([remoteRuns, remoteDataPass, remoteDetectors, remoteQcSummary]); - - const activeColumns = { - ...runsActiveColumns, - ...getInelasticInteractionRateColumns(pdpBeamTypes), - - globalAggregatedQuality: { - name: 'GAQ', - information: h( - '', - h('', 'Global aggregated flag based on critical detectors.'), - h('', 'Default detectors: FT0, ITS, TPC (and ZDC for heavy-ion runs)'), - ), - visible: true, - - format: (_, { runNumber }) => { - const runGaqSummary = remoteGaqSummary[runNumber]; - const spinnerEl = h('.flex-row.items-center.justify-center.black', spinner({ size: 2, absolute: false })); - - return runGaqSummary.match({ - Success: (gaqSummary) => { - const gaqDisplay = - gaqSummary?.undefinedQualityPeriodsCount === 0 - ? getQcSummaryDisplay(gaqSummary) - : h('button.btn.btn-primary.w-100', 'GAQ'); - - return frontLink(gaqDisplay, 'gaq-flags', { dataPassId, runNumber }); - }, - Loading: () => tooltip(spinnerEl), - NotAsked: () => tooltip(spinnerEl), - Failure: () => - frontLink( - h('button.btn.btn-primary.w-100', [ - 'GAQ', - h( - '.d-inline-block.va-t-bottom', - tooltip( - h('.f7', iconWarning()), - 'GAQ Summary failed, please click to view GAQ flags', + return h( + '.intermediate-flex-column', + { onremove: () => { + perDataPassOverviewModel._abortGaqFetches(); + } }, + mergeRemoteData([remoteDataPass, remoteRuns, remoteDetectors, remoteQcSummary]).match({ + NotAsked: () => null, + Failure: (errors) => errorAlert(errors), + Success: ([dataPass, runs, detectors, qcSummary]) => { + const activeColumns = { + ...runsActiveColumns, + ...getInelasticInteractionRateColumns(pdpBeamTypes), + ...dataPass.skimmingStage === SkimmingStage.SKIMMABLE + ? { + readyForSkimming: { + name: 'Ready for skimming', + visible: true, + format: (_, { runNumber }) => remoteSkimmableRuns.match({ + Success: (skimmableRuns) => switchInput( + skimmableRuns[runNumber], + () => perDataPassOverviewModel.changeReadyForSkimmingFlagForRun({ + runNumber, + readyForSkimming: !skimmableRuns[runNumber], + }), + { + labelAfter: skimmableRuns[runNumber] + ? badge('ready', { color: Color.GREEN }) + : badge('not ready', { color: Color.WARNING_DARKER }), + }, ), - ), - ]), - 'gaq-flags', - { dataPassId, runNumber }, + Loading: () => h('.mh3.ph4', '... ...'), + Failure: () => tooltip(iconWarning(), 'Error occurred'), + NotAsked: () => tooltip(iconWarning(), 'Not asked for data'), + }), + profiles: ['runsPerDataPass'], + }, + } + : {}, + globalAggregatedQuality: { + name: 'GAQ', + information: h( + '', + h('', 'Global aggregated flag based on critical detectors.'), + h('', 'Default detectors: FT0, ITS, TPC (and ZDC for heavy-ion runs)'), ), - }); - }, - filter: ({ filteringModel }) => - numericalComparisonFilter(filteringModel.get('gaq').notBadFraction, { step: 0.1, selectorPrefix: 'gaqNotBadFraction' }), - filterTooltip: 'not-bad fraction expressed as a percentage', - profiles: ['runsPerDataPass'], - }, - ...dataPass?.skimmingStage === SkimmingStage.SKIMMABLE && { - readyForSkimming: { - name: 'Ready for skimming', - visible: true, + visible: true, + format: (_, { runNumber }) => { + const gaqLoadingSpinner = h('.flex-row.items-center.justify-center.black', spinner({ size: 2, absolute: false })); + const runGaqSummary = remoteGaqSummary[runNumber]; - format: (_, { runNumber }) => - remoteSkimmableRuns.match({ - Success: (skimmableRuns) => - switchInput( - skimmableRuns[runNumber], - () => - perDataPassOverviewModel.changeReadyForSkimmingFlagForRun({ - runNumber, - readyForSkimming: !skimmableRuns[runNumber], - }), - { - labelAfter: skimmableRuns[runNumber] - ? badge('ready', { color: Color.GREEN }) - : badge('not ready', { color: Color.WARNING_DARKER }), - }, - ), - - Loading: () => h('.mh3.ph4', '... ...'), - Failure: () => tooltip(iconWarning(), 'Error occurred'), - NotAsked: () => tooltip(iconWarning(), 'Not asked for data'), - }), + return runGaqSummary.match({ + Success: (gaqSummary) => { + const gaqDisplay = gaqSummary?.undefinedQualityPeriodsCount === 0 + ? getQcSummaryDisplay(gaqSummary) + : h('button.btn.btn-primary.w-100', 'GAQ'); - profiles: ['runsPerDataPass'], - }, - }, - ...detectors && dataPass && createRunDetectorsAsyncQcActiveColumns( - perDataPassOverviewModel.runDetectorsSelectionModel, - detectors, - remoteDplDetectorsUserHasAccessTo, - { dataPass }, - { - profile: 'runsPerDataPass', - qcSummary, - mcReproducibleAsNotBad, - }, - ), - }; - - return [ - h('.flex-row.justify-between.items-center.g2', [ - filtersPanelPopover(perDataPassOverviewModel, activeColumns, { profile: 'runsPerDataPass' }), - h('.pl2#runOverviewFilter', textInputFilter(perDataPassOverviewModel.filteringModel, 'runNumbers', 'e.g. 534454, 534455...')), - h( - '.flex-row.g1.items-center', - h('.flex-row.items-center.g1', [ - breadcrumbs([commonTitle, h('h2#breadcrumb-data-pass-name', dataPass?.name ?? spinner({ size: 1, absolute: false }))]), - h('#skimmableControl', dataPass && skimmableControl( - dataPass, - () => { - if (confirm('The data pass is going to be set as skimmable. Do you want to continue?')) { - perDataPassOverviewModel.markDataPassAsSkimmable(); - } + return frontLink(gaqDisplay, 'gaq-flags', { dataPassId, runNumber }); + }, + Loading: () => tooltip(gaqLoadingSpinner, 'Loading GAQ summary...'), + NotAsked: () => tooltip(gaqLoadingSpinner, 'Loading GAQ summary...'), + Failure: () => { + const gaqDisplay = h('button.btn.btn-primary.w-100', [ + 'GAQ', + h( + '.d-inline-block.va-t-bottom', + tooltip(h('.f7', iconWarning()), 'GAQ Summary failed, please click to view GAQ flags'), + ), + ]); + return frontLink(gaqDisplay, 'gaq-flags', { dataPassId, runNumber }); + }, + }); }, - markAsSkimmableRequestResult, - )), - ]), - ), - toggleFilter(mcReproducibleAsNotBad, h('em', 'MC.R as not-bad'), 'mcReproducibleAsNotBadToggle'), - h('.mlauto', qcSummaryLegendTooltip()), - h('#actions-dropdown-button', DropdownComponent( - h('button.btn.btn-primary', h('.flex-row.g2', ['Actions', iconCaretBottom()])), - h('.flex-column.p2.g2', [ - exportTriggerAndModal(perDataPassOverviewModel.exportModel, modalModel, { autoMarginLeft: false }), - frontLink( - h('button.btn.btn-primary.w-100.h2}#set-qc-flags-trigger', { - disabled: runDetectorsSelectionIsEmpty, - }, 'Set QC Flags'), - 'qc-flag-creation-for-data-pass', + filter: ({ filteringModel }) => numericalComparisonFilter( + filteringModel.get('gaq[notBadFraction]'), + { step: 0.1, selectorPrefix: 'gaqNotBadFraction' }, + ), + filterTooltip: 'not-bad fraction expressed as a percentage', + profiles: ['runsPerDataPass'], + }, + ...createRunDetectorsAsyncQcActiveColumns( + perDataPassOverviewModel.runDetectorsSelectionModel, + detectors, + remoteDplDetectorsUserHasAccessTo, + { dataPass }, { - runNumberDetectorsMap: perDataPassOverviewModel.runDetectorsSelectionModel.selectedQueryString, - dataPassId, + profile: 'runsPerDataPass', + qcSummary, + mcReproducibleAsNotBad, }, ), - sessionService.hasAccess([BkpRoles.DPG_ASYNC_QC_ADMIN]) && [ + }; + + return [ + h('.flex-row.justify-between.items-center.g2', [ + filtersPanelPopover(perDataPassOverviewModel, activeColumns, { profile: 'runsPerDataPass' }), + h('.pl2#runOverviewFilter', runNumbersFilter(perDataPassOverviewModel.filteringModel.get('runNumbers'))), h( - 'button.btn.btn-danger', - { - ...freezeOrUnfreezeActionState.match({ - Loading: () => ({ - disabled: true, - title: 'Loading', - }), - Other: () => ({}), - }), - onclick: () => dataPass?.isFrozen - ? perDataPassOverviewModel.unfreezeDataPass() - : perDataPassOverviewModel.freezeDataPass(), - }, - `${dataPass?.isFrozen ? 'Unfreeze' : 'Freeze'} the data pass`, + '.flex-row.g1.items-center', + h('.flex-row.items-center.g1', [ + breadcrumbs([commonTitle, h('h2#breadcrumb-data-pass-name', dataPass.name)]), + h('#skimmableControl', skimmableControl( + dataPass, + () => { + if (confirm('The data pass is going to be set as skimmable. Do you want to continue?')) { + perDataPassOverviewModel.markDataPassAsSkimmable(); + } + }, + markAsSkimmableRequestResult, + )), + ]), ), - h( - 'button.btn.btn-danger', - { - ...discardAllQcFlagsActionState.match({ - Loading: () => ({ - disabled: true, - title: 'Loading', - }), - Other: () => ({}), - }), - onclick: () => { - if (confirm('Are you sure you want to delete ALL the QC flags for this data pass?')) { - perDataPassOverviewModel.discardAllQcFlags(); - } - }, - }, - 'Delete ALL QC flags', + mcReproducibleAsNotBadToggle( + mcReproducibleAsNotBad, + () => perDataPassOverviewModel.setMcReproducibleAsNotBad(!mcReproducibleAsNotBad), ), - ], - ]), - { alignment: 'right' }, - )), - ]), - warningComponent(perDataPassOverviewModel), - h( - '.intermediate-flex-column', - { onremove: () => perDataPassOverviewModel._abortGaqFetches() }, - fullPageData.match({ - NotAsked: () => null, - Failure: (errors) => errorAlert(errors), - Success: ([runs]) => [ + h('.mlauto', qcSummaryLegendTooltip()), + h('#actions-dropdown-button', DropdownComponent( + h('button.btn.btn-primary', h('.flex-row.g2', ['Actions', iconCaretBottom()])), + h('.flex-column.p2.g2', [ + exportTriggerAndModal(perDataPassOverviewModel.exportModel, modalModel, { autoMarginLeft: false }), + frontLink( + h('button.btn.btn-primary.w-100.h2}#set-qc-flags-trigger', { + disabled: runDetectorsSelectionIsEmpty, + }, 'Set QC Flags'), + 'qc-flag-creation-for-data-pass', + { + runNumberDetectorsMap: perDataPassOverviewModel.runDetectorsSelectionModel.selectedQueryString, + dataPassId, + }, + ), + sessionService.hasAccess([BkpRoles.DPG_ASYNC_QC_ADMIN]) && [ + h( + 'button.btn.btn-danger', + { + ...freezeOrUnfreezeActionState.match({ + Loading: () => ({ + disabled: true, + title: 'Loading', + }), + Other: () => ({}), + }), + onclick: () => dataPass.isFrozen + ? perDataPassOverviewModel.unfreezeDataPass() + : perDataPassOverviewModel.freezeDataPass(), + }, + `${dataPass.isFrozen ? 'Unfreeze' : 'Freeze'} the data pass`, + ), + h( + 'button.btn.btn-danger', + { + ...discardAllQcFlagsActionState.match({ + Loading: () => ({ + disabled: true, + title: 'Loading', + }), + Other: () => ({}), + }), + onclick: () => { + if (confirm('Are you sure you want to delete ALL the QC flags for this data pass?')) { + perDataPassOverviewModel.discardAllQcFlags(); + } + }, + }, + 'Delete ALL QC flags', + ), + ], + ]), + { alignment: 'right' }, + )), + ]), markAsSkimmableRequestResult.match({ Failure: (errors) => errorAlert(errors), Other: () => null, @@ -329,9 +311,9 @@ export const RunsPerDataPassOverviewPage = ({ { sort: sortModel }, ), paginationComponent(perDataPassOverviewModel.pagination), - ], - Loading: () => spinner(), - }), - ), - ]; + ]; + }, + Loading: () => spinner(), + }), + ); }; diff --git a/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewModel.js b/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewModel.js index 2ae78a395c..b361522b8b 100644 --- a/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewModel.js +++ b/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewModel.js @@ -12,7 +12,7 @@ */ import { buildUrl, RemoteData } from '/js/src/index.js'; import { TabbedPanelModel } from '../../../components/TabbedPanel/TabbedPanelModel.js'; -import { rctDetectorsProvider } from '../../../services/detectors/detectorsProvider.js'; +import { detectorsProvider } from '../../../services/detectors/detectorsProvider.js'; import { jsonFetch } from '../../../utilities/fetch/jsonFetch.js'; import { DetectorType } from '../../../domain/enums/DetectorTypes.js'; import { ObservableData } from '../../../utilities/ObservableData.js'; @@ -31,25 +31,27 @@ export class RunsPerLhcPeriodOverviewModel extends FixedPdpBeamTypeRunsOverviewM * Constructor * * @param {Model} model global model - * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(model, pageIdentifier) { - super(model, pageIdentifier); + constructor(model) { + super(model); this._lhcPeriodId = null; this._lhcPeriodStatistics$ = new ObservableData(RemoteData.notAsked()); - this._onlineDetectors$ = rctDetectorsProvider.physical$; + this._onlineDetectors$ = detectorsProvider.physical$; this._syncDetectors$ = ObservableData .builder() - .source(rctDetectorsProvider.qc$) + .source(detectorsProvider.qc$) .apply((remoteDetectors) => remoteDetectors.apply({ Success: (detectors) => detectors.filter(({ type }) => [DetectorType.PHYSICAL, DetectorType.MUON_GLO].includes(type)), })) .build(); + this.registerDetectorsForQcFlagsDataExport(this._syncDetectors$); + this.registerObervablesQcSummaryDependesOn([this._syncDetectors$]); + this._syncDetectors$.bubbleTo(this); this._onlineDetectors$.bubbleTo(this); this._lhcPeriodStatistics$.bubbleTo(this); @@ -80,15 +82,12 @@ export class RunsPerLhcPeriodOverviewModel extends FixedPdpBeamTypeRunsOverviewM return; } - await this._fetchLhcPeriod(); - this._lhcPeriodStatistics$.getCurrent().match({ - Success: ({ pdpBeamTypes }) => this.setPdpBeamTypes(pdpBeamTypes), - Other: () => null, + await this._fetchLhcPeriod().then(() => { + this._lhcPeriodStatistics$.getCurrent().match({ + Success: ({ pdpBeamTypes }) => this.setPdpBeamTypes(pdpBeamTypes), + Other: () => null, + }); }); - - this.registerDetectorsForQcFlagsDataExport(this._syncDetectors$); - this.registerObservablesQcSummaryDependsOn(this._syncDetectors$); - super.load(); } @@ -96,8 +95,13 @@ export class RunsPerLhcPeriodOverviewModel extends FixedPdpBeamTypeRunsOverviewM * @inheritdoc */ getRootEndpoint() { - const filter = { lhcPeriodIds: [this._lhcPeriodId], runQualities: 'good', definitions: 'PHYSICS' }; - return buildUrl(super.getRootEndpoint(), { filter }); + return buildUrl(super.getRootEndpoint(), { + filter: { + lhcPeriodIds: [this._lhcPeriodId], + runQualities: 'good', + definitions: 'PHYSICS', + }, + }); } /** @@ -147,7 +151,7 @@ export class RunsPerLhcPeriodOverviewModel extends FixedPdpBeamTypeRunsOverviewM * @param {string} lhcPeriodId id of a LHC period */ set lhcPeriodId(lhcPeriodId) { - if (this._lhcPeriodId && lhcPeriodId !== this._lhcPeriodId) { + if (lhcPeriodId !== this._lhcPeriodId) { this.reset(false); } this._lhcPeriodId = lhcPeriodId; diff --git a/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js b/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js index 4a08a95565..7526324b35 100644 --- a/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js +++ b/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js @@ -26,10 +26,9 @@ import errorAlert from '../../../components/common/errorAlert.js'; import spinner from '../../../components/common/spinner.js'; import { getInelasticInteractionRateColumns } from '../ActiveColumns/getInelasticInteractionRateActiveColumns.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; +import { runNumbersFilter } from '../../../components/Filters/RunsFilter/runNumbersFilter.js'; +import { mcReproducibleAsNotBadToggle } from '../mcReproducibleAsNotBadToggle.js'; import { exportTriggerAndModal } from '../../../components/common/dataExport/exportTriggerAndModal.js'; -import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; -import { toggleFilter } from '../../../components/Filters/common/filters/toggleFilter.js'; -import { warningComponent } from '../../../components/common/messages/warningComponent.js'; const TABLEROW_HEIGHT = 62; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -51,6 +50,11 @@ const getRowClasses = (run) => isRunNotSubjectToQc(run) ? '.danger' : null; * @return {Component} The overview page */ export const RunsPerLhcPeriodOverviewPage = ({ runs: { perLhcPeriodOverviewModel }, modalModel }) => { + perLhcPeriodOverviewModel.pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount( + TABLEROW_HEIGHT, + PAGE_USED_HEIGHT, + )); + const { items: remoteRuns, lhcPeriodStatistics: remoteLhcPeriodStatistics, @@ -62,11 +66,8 @@ export const RunsPerLhcPeriodOverviewPage = ({ runs: { perLhcPeriodOverviewModel mcReproducibleAsNotBad, qcSummary: remoteQcSummary, pdpBeamTypes, - pagination, } = perLhcPeriodOverviewModel; - pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount(TABLEROW_HEIGHT, PAGE_USED_HEIGHT)); - /** * Render runs table with given detectors' active columns configuration * @@ -94,32 +95,30 @@ export const RunsPerLhcPeriodOverviewPage = ({ runs: { perLhcPeriodOverviewModel { sort: sortModel }, ); - const activeColumns = { - ...runsActiveColumns, - ...getInelasticInteractionRateColumns(pdpBeamTypes), - }; + return h( + '.intermediate-flex-column', + mergeRemoteData([remoteLhcPeriodStatistics, remoteRuns]).match({ + NotAsked: () => null, + Failure: (errors) => errorAlert(errors), + Loading: () => spinner(), + Success: ([lhcPeriodStatistics]) => { + const activeColumns = { + ...runsActiveColumns, + ...getInelasticInteractionRateColumns(pdpBeamTypes), - const lhcPeriodName = remoteLhcPeriodStatistics?.match({ - Success: (lhcPeriodStatistics) => lhcPeriodStatistics.lhcPeriod.name, - Other: () => spinner({ size: 1, absolute: false }), - }); + }; - return [ - h('.flex-row.justify-between.items-center.g2', [ - filtersPanelPopover(perLhcPeriodOverviewModel, activeColumns, { profile: 'runsPerLhcPeriod' }), - h('.pl2#runOverviewFilter', textInputFilter(perLhcPeriodOverviewModel.filteringModel, 'runNumbers', 'e.g. 534454, 534455...')), - h('h2', ['Good, physics runs of ', lhcPeriodName]), - warningComponent(perLhcPeriodOverviewModel), - toggleFilter(mcReproducibleAsNotBad, h('em', 'MC.R as not-bad'), 'mcReproducibleAsNotBadToggle'), - exportTriggerAndModal(perLhcPeriodOverviewModel.exportModel, modalModel), - ]), - h( - '.intermediate-flex-column', - remoteRuns.match({ - NotAsked: () => null, - Failure: (errors) => errorAlert(errors), - Loading: () => spinner(), - Success: () => [ + return [ + h('.flex-row.justify-between.items-center.g2', [ + filtersPanelPopover(perLhcPeriodOverviewModel, activeColumns, { profile: 'runsPerLhcPeriod' }), + h('.pl2#runOverviewFilter', runNumbersFilter(perLhcPeriodOverviewModel.filteringModel.get('runNumbers'))), + h('h2', `Good, physics runs of ${lhcPeriodStatistics.lhcPeriod.name}`), + mcReproducibleAsNotBadToggle( + mcReproducibleAsNotBad, + () => perLhcPeriodOverviewModel.setMcReproducibleAsNotBad(!mcReproducibleAsNotBad), + ), + exportTriggerAndModal(perLhcPeriodOverviewModel.exportModel, modalModel), + ]), ...tabbedPanelComponent( tabbedPanelModel, { @@ -153,8 +152,10 @@ export const RunsPerLhcPeriodOverviewPage = ({ runs: { perLhcPeriodOverviewModel }, { panelClass: ['scroll-auto'] }, ), - paginationComponent(pagination), - ] }), - ), - ]; + paginationComponent(perLhcPeriodOverviewModel.pagination), + ]; + }, + }), + + ); }; diff --git a/lib/public/views/Runs/RunsModel.js b/lib/public/views/Runs/RunsModel.js index 007a456368..ba30c3519a 100644 --- a/lib/public/views/Runs/RunsModel.js +++ b/lib/public/views/Runs/RunsModel.js @@ -32,13 +32,13 @@ export class RunsModel extends Observable { super(); this._detailsModel = new RunDetailsModel(); this._detailsModel.bubbleTo(this); - this._overviewModel = new RunsOverviewModel(model, 'run-overview'); + this._overviewModel = new RunsOverviewModel(model); this._overviewModel.bubbleTo(this); - this._perLhcPeriodOverviewModel = new RunsPerLhcPeriodOverviewModel(model, 'runs-per-lhc-period'); + this._perLhcPeriodOverviewModel = new RunsPerLhcPeriodOverviewModel(model); this._perLhcPeriodOverviewModel.bubbleTo(this); - this._perDataPassOverviewModel = new RunsPerDataPassOverviewModel(model, 'runs-per-data-pass'); + this._perDataPassOverviewModel = new RunsPerDataPassOverviewModel(model); this._perDataPassOverviewModel.bubbleTo(this); - this._perSimulationPassOverviewModel = new RunsPerSimulationPassOverviewModel(model, 'runs-per-simulation-pass'); + this._perSimulationPassOverviewModel = new RunsPerSimulationPassOverviewModel(model); this._perSimulationPassOverviewModel.bubbleTo(this); } @@ -48,7 +48,6 @@ export class RunsModel extends Observable { */ loadOverview() { if (! this._overviewModel.pagination.isInfiniteScrollEnabled) { - this._overviewModel.setFilterFromURL(false); this._overviewModel.load(); } } @@ -94,7 +93,6 @@ export class RunsModel extends Observable { this._perLhcPeriodOverviewModel.tabbedPanelModel.currentPanelKey = panel; if (!this._perLhcPeriodOverviewModel.pagination.isInfiniteScrollEnabled) { this._perLhcPeriodOverviewModel.lhcPeriodId = lhcPeriodId; - this._perLhcPeriodOverviewModel.setFilterFromURL(false); this._perLhcPeriodOverviewModel.load(); } } @@ -116,15 +114,7 @@ export class RunsModel extends Observable { loadPerDataPassOverview({ dataPassId }) { if (!this._perDataPassOverviewModel.pagination.isInfiniteScrollEnabled) { this._perDataPassOverviewModel.dataPassId = parseInt(dataPassId, 10); - if (this._perDataPassOverviewModel.pagination._defaultItemsPerPage) { - /** - * If the default items per page is set, it means model has loaded already once, - * so the pagination trigger will not refresh the data. - * Thus, we need to trigger the load here. - */ - this._perDataPassOverviewModel.setFilterFromURL(false); - this._perDataPassOverviewModel.load(); - } + this._perDataPassOverviewModel.load(); } } @@ -145,15 +135,7 @@ export class RunsModel extends Observable { loadPerSimulationPassOverview({ simulationPassId }) { if (!this._perSimulationPassOverviewModel.pagination.isInfiniteScrollEnabled) { this._perSimulationPassOverviewModel.simulationPassId = parseInt(simulationPassId, 10); - if (this._perSimulationPassOverviewModel.pagination._defaultItemsPerPage) { - /** - * If the default items per page is set, it means model has loaded already once, - * so the pagination trigger will not refresh the data. - * Thus, we need to trigger the load here. - */ - this._perSimulationPassOverviewModel.setFilterFromURL(false); - this._perSimulationPassOverviewModel.load(); - } + this._perSimulationPassOverviewModel.load(); } } diff --git a/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewModel.js b/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewModel.js index 084b57d130..9b8b982d4b 100644 --- a/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewModel.js +++ b/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewModel.js @@ -13,7 +13,7 @@ import { buildUrl, RemoteData } from '/js/src/index.js'; import { ObservableData } from '../../../utilities/ObservableData.js'; import { getRemoteData } from '../../../utilities/fetch/getRemoteData.js'; -import { rctDetectorsProvider } from '../../../services/detectors/detectorsProvider.js'; +import { detectorsProvider } from '../../../services/detectors/detectorsProvider.js'; import { FixedPdpBeamTypeRunsOverviewModel } from '../Overview/FixedPdpBeamTypeRunsOverviewModel.js'; /** @@ -23,14 +23,17 @@ export class RunsPerSimulationPassOverviewModel extends FixedPdpBeamTypeRunsOver /** * Constructor * @param {Model} model global model - * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(model, pageIdentifier) { - super(model, pageIdentifier); + constructor(model) { + super(model); this._simulationPass$ = new ObservableData(RemoteData.notAsked()); - this._detectors$ = rctDetectorsProvider.qc$; + this._detectors$ = detectorsProvider.qc$; + + this.registerObervablesQcSummaryDependesOn([this._detectors$]); + this.registerDetectorsNotBadFractionFilterModels(this._detectors$); + this.registerDetectorsForQcFlagsDataExport(this._detectors$); this._detectors$.bubbleTo(this); this._simulationPass$.bubbleTo(this); @@ -58,16 +61,12 @@ export class RunsPerSimulationPassOverviewModel extends FixedPdpBeamTypeRunsOver return; } - await this._fetchSimulationPass(); - this._simulationPass$.getCurrent().match({ - Success: ({ pdpBeamTypes }) => this.setPdpBeamTypes(pdpBeamTypes), - Other: () => null, + this._fetchSimulationPass().then(() => { + this._simulationPass$.getCurrent().match({ + Success: ({ pdpBeamTypes }) => this.setPdpBeamTypes(pdpBeamTypes), + Other: () => null, + }); }); - - this.registerDetectorsNotBadFractionFilterModels(this._detectors$); - this.registerDetectorsForQcFlagsDataExport(this._detectors$); - this.registerObservablesQcSummaryDependsOn(this._detectors$); - super.load(); } @@ -75,8 +74,13 @@ export class RunsPerSimulationPassOverviewModel extends FixedPdpBeamTypeRunsOver * @inheritdoc */ getRootEndpoint() { - const filter = { simulationPassIds: [this._simulationPassId] }; - return buildUrl(super.getRootEndpoint(), { filter }); + const params = { + filter: { + simulationPassIds: [this._simulationPassId], + }, + }; + + return buildUrl(super.getRootEndpoint(), params); } /** @@ -84,7 +88,7 @@ export class RunsPerSimulationPassOverviewModel extends FixedPdpBeamTypeRunsOver * @param {number} simulationPassId simulation pass id */ set simulationPassId(simulationPassId) { - if (this._simulationPassId && simulationPassId !== this._simulationPassId) { + if (simulationPassId !== this._simulationPassId) { this.reset(false); } this._simulationPassId = simulationPassId; diff --git a/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewPage.js b/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewPage.js index c64fcbe6c8..55d4cdb988 100644 --- a/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewPage.js +++ b/lib/public/views/Runs/RunsPerSimulationPass/RunsPerSimulationPassOverviewPage.js @@ -27,9 +27,8 @@ import errorAlert from '../../../components/common/errorAlert.js'; import { getInelasticInteractionRateColumns } from '../ActiveColumns/getInelasticInteractionRateActiveColumns.js'; import { exportTriggerAndModal } from '../../../components/common/dataExport/exportTriggerAndModal.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; -import { toggleFilter } from '../../../components/Filters/common/filters/toggleFilter.js'; -import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; -import { warningComponent } from '../../../components/common/messages/warningComponent.js'; +import { runNumbersFilter } from '../../../components/Filters/RunsFilter/runNumbersFilter.js'; +import { mcReproducibleAsNotBadToggle } from '../mcReproducibleAsNotBadToggle.js'; const TABLEROW_HEIGHT = 59; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -53,6 +52,11 @@ export const RunsPerSimulationPassOverviewPage = ({ dplDetectorsUserHasAccessTo: remoteDplDetectorsUserHasAccessTo, modalModel, }) => { + perSimulationPassOverviewModel.pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount( + TABLEROW_HEIGHT, + PAGE_USED_HEIGHT, + )); + const { items: remoteRuns, detectors: remoteDetectors, @@ -63,69 +67,60 @@ export const RunsPerSimulationPassOverviewPage = ({ sortModel, pdpBeamTypes, mcReproducibleAsNotBad, - pagination, } = perSimulationPassOverviewModel; - pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount(TABLEROW_HEIGHT, PAGE_USED_HEIGHT)); - const commonTitle = h('h2', 'Runs per MC'); - const fullPageData = mergeRemoteData([remoteRuns, remoteSimulationPass, remoteDetectors, remoteQcSummary]); - const simulationPass = remoteSimulationPass.match({ Other: () => null, Success: (data) => data }); - const detectors = remoteDetectors.match({ Other: () => null, Success: (data) => data }); - const qcSummary = remoteQcSummary.match({ Other: () => null, Success: (data) => data }); - - const activeColumns = { - ...runsActiveColumns, - ...getInelasticInteractionRateColumns(pdpBeamTypes), - ...detectors && qcSummary && createRunDetectorsAsyncQcActiveColumns( - perSimulationPassOverviewModel.runDetectorsSelectionModel, - detectors, - remoteDplDetectorsUserHasAccessTo, - { simulationPass }, - { - profile: 'runsPerSimulationPass', - qcSummary, - }, - ), - }; + return h( + '.intermediate-flex-column', + mergeRemoteData([remoteSimulationPass, remoteRuns, remoteDetectors, remoteQcSummary]).match({ + NotAsked: () => null, + Failure: (errors) => errorAlert(errors), + Success: ([simulationPass, runs, detectors, qcSummary]) => { + const activeColumns = { + ...runsActiveColumns, + ...getInelasticInteractionRateColumns(pdpBeamTypes), + ...createRunDetectorsAsyncQcActiveColumns( + perSimulationPassOverviewModel.runDetectorsSelectionModel, + detectors, + remoteDplDetectorsUserHasAccessTo, + { simulationPass }, + { + profile: 'runsPerSimulationPass', + qcSummary, + }, + ), + }; - return [ - h('.flex-row.justify-between.items-center.g2', [ - filtersPanelPopover(perSimulationPassOverviewModel, activeColumns, { profile: 'runsPerSimulationPass' }), - h('.pl2#runOverviewFilter', textInputFilter(perSimulationPassOverviewModel.filteringModel, 'runNumbers', 'e.g. 534454, 534455...')), - h( - '.flex-row.g1.items-center', - breadcrumbs([ - commonTitle, - h('h2#breadcrumb-simulation-pass-name', simulationPass?.name ?? spinner({ size: 1, absolute: false })), - ]), - ), - toggleFilter(mcReproducibleAsNotBad, h('em', 'MC.R as not-bad'), 'mcReproducibleAsNotBadToggle'), - h('.mlauto', qcSummaryLegendTooltip()), - exportTriggerAndModal(perSimulationPassOverviewModel.exportModel, modalModel, { autoMarginLeft: false }), - frontLink( - h( - 'button.btn.btn-primary.w-100.h2}#set-qc-flags-trigger', - { - disabled: perSimulationPassOverviewModel.runDetectorsSelectionModel.selectedQueryString.length < 1, - }, - 'Set QC Flags', - ), - 'qc-flag-creation-for-simulation-pass', - { - runNumberDetectorsMap: perSimulationPassOverviewModel.runDetectorsSelectionModel.selectedQueryString, - simulationPassId, - }, - ), - ]), - warningComponent(perSimulationPassOverviewModel), - h( - '.intermediate-flex-column', - fullPageData.match({ - NotAsked: () => null, - Failure: (errors) => errorAlert(errors), - Success: ([runs]) => [ + return [ + h('.flex-row.justify-between.items-center.g2', [ + filtersPanelPopover(perSimulationPassOverviewModel, activeColumns, { profile: 'runsPerSimulationPass' }), + h('.pl2#runOverviewFilter', runNumbersFilter(perSimulationPassOverviewModel.filteringModel.get('runNumbers'))), + h( + '.flex-row.g1.items-center', + breadcrumbs([commonTitle, h('h2#breadcrumb-simulation-pass-name', simulationPass.name)]), + ), + mcReproducibleAsNotBadToggle( + mcReproducibleAsNotBad, + () => perSimulationPassOverviewModel.setMcReproducibleAsNotBad(!mcReproducibleAsNotBad), + ), + h('.mlauto', qcSummaryLegendTooltip()), + exportTriggerAndModal(perSimulationPassOverviewModel.exportModel, modalModel, { autoMarginLeft: false }), + frontLink( + h( + 'button.btn.btn-primary.w-100.h2}#set-qc-flags-trigger', + { + disabled: perSimulationPassOverviewModel.runDetectorsSelectionModel.selectedQueryString.length < 1, + }, + 'Set QC Flags', + ), + 'qc-flag-creation-for-simulation-pass', + { + runNumberDetectorsMap: perSimulationPassOverviewModel.runDetectorsSelectionModel.selectedQueryString, + simulationPassId, + }, + ), + ]), table( runs, activeColumns, @@ -136,10 +131,10 @@ export const RunsPerSimulationPassOverviewPage = ({ }, { sort: sortModel }, ), - paginationComponent(pagination), - ], - Loading: () => spinner(), - }), - ), - ]; + paginationComponent(perSimulationPassOverviewModel.pagination), + ]; + }, + Loading: () => spinner(), + }), + ); }; diff --git a/lib/public/views/Runs/format/editRunEorReasons.js b/lib/public/views/Runs/format/editRunEorReasons.js index 6ba0d59e24..56c69e6f04 100644 --- a/lib/public/views/Runs/format/editRunEorReasons.js +++ b/lib/public/views/Runs/format/editRunEorReasons.js @@ -94,23 +94,20 @@ export const editRunEorReasons = (runDetailsModel) => { */ runDetailsModel.runPatch.eorReasons.length > 0 ? runDetailsModel.runPatch.eorReasons.map((eorReason) => { - const { reasonTypeId, description, lastEditedName } = eorReason; + const { reasonTypeId, description } = eorReason; const { category = '-', title } = eorReasonTypes.find((eorReasonType) => eorReasonType.id === reasonTypeId) || {}; const titleString = title ? ` - ${title}` : ''; const descriptionString = description ? ` - ${description}` : ''; return h( - '.flex-row.justify-between', + '.flex-row.items-center', { key: `${category} ${titleString} ${descriptionString}`, }, [ - h('.flex-row.items-center', [ - h('label.remove-eor-reason.danger.ph1.actionable-icon', { - onclick: () => runDetailsModel.runPatch.removeEorReason(eorReason), - }, iconTrash()), - h('.w-wrapped', `${category} ${titleString} ${descriptionString}`), - ]), - h('.w-wrapped', lastEditedName || null), + h('label.remove-eor-reason.danger.ph1.actionable-icon', { + onclick: () => runDetailsModel.runPatch.removeEorReason(eorReason), + }, iconTrash()), + h('.w-wrapped', `${category} ${titleString} ${descriptionString}`), ], ); }) diff --git a/lib/public/views/Runs/format/formatRunEorReason.js b/lib/public/views/Runs/format/formatRunEorReason.js deleted file mode 100644 index b97ab4a223..0000000000 --- a/lib/public/views/Runs/format/formatRunEorReason.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { h } from '/js/src/index.js'; -import { tooltip } from '../../../../components/common/popover/tooltip.js'; -import { formatEorReason } from './formatEorReason.mjs'; - -/** - * Display the given EoR reason as a vnode component with lastEditedName tooltip - * - * @param {Partial<{ - * category: string, - * title: string, - * description: string, - * lastEditedName: string, - * }>} eorReason the EoR reason to display - * @return {VNode} the vnode component - */ -export const formatRunEorReason = (eorReason) => { - const { lastEditedName } = eorReason; - const reasonText = formatEorReason(eorReason); - return h('.w-100.flex-row.justify-between', [ - h('', reasonText), - lastEditedName ? tooltip(h('.w-wrapped', lastEditedName), 'Last edited by') : null, - ]); -}; diff --git a/lib/public/views/Runs/mcReproducibleAsNotBadToggle.js b/lib/public/views/Runs/mcReproducibleAsNotBadToggle.js new file mode 100644 index 0000000000..636ed0f245 --- /dev/null +++ b/lib/public/views/Runs/mcReproducibleAsNotBadToggle.js @@ -0,0 +1,28 @@ +/** + * @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 { switchInput } from '../../components/common/form/switchInput.js'; +import { h } from '/js/src/index.js'; + +/** + * Display a toggle switch to change interpretation of MC.Reproducible flag type from bad to not-bad + * + * @param {boolean} value current value + * @param {function} onChange to be called when switching + * @returns {Component} the toggle switch + */ +export const mcReproducibleAsNotBadToggle = (value, onChange) => h('#mcReproducibleAsNotBadToggle', switchInput( + value, + onChange, + { labelAfter: h('em', 'MC.R as not-bad') }, +)); diff --git a/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js b/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js index 95f9940c22..05b796bcf8 100644 --- a/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js +++ b/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js @@ -31,8 +31,8 @@ export const simulationPassesActiveColumns = { name: 'Name', visible: true, sortable: true, - filter: ({ filteringModel }) => textFilter( - filteringModel.get('names'), + filter: ({ namesFilterModel }) => textFilter( + namesFilterModel, { class: 'w-75 mt1', placeholder: 'e.g. LHC23k5, ...' }, ), classes: 'w-10 f6', diff --git a/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js b/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js index 3054251391..ed6b776215 100644 --- a/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js +++ b/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewModel.js @@ -10,23 +10,24 @@ * granted to it by virtue of its status as an Intergovernmental Organization * or submit itself to any jurisdiction. */ -import { RemoteData } from '/js/src/index.js'; +import { buildUrl, RemoteData } from '/js/src/index.js'; import { TextTokensFilterModel } from '../../../components/Filters/common/filters/TextTokensFilterModel.js'; +import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { getRemoteData } from '../../../utilities/fetch/getRemoteData.js'; import { ObservableData } from '../../../utilities/ObservableData.js'; -import { FilterableOverviewPageModel } from '../../../models/FilterableOverviewPageModel.js'; /** * Simulation Passes Per Data Pass overview model */ -export class AnchoredSimulationPassesOverviewModel extends FilterableOverviewPageModel { +export class AnchoredSimulationPassesOverviewModel extends OverviewPageModel { /** * Constructor - * @param {QueryRouter} router router that controls the application's page navigation - * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(router, pageIdentifier) { - super(router, pageIdentifier, { names: new TextTokensFilterModel() }); + constructor() { + super(); + this._namesFilterModel = new TextTokensFilterModel(); + this._registerFilter(this._namesFilterModel); + this._dataPass = new ObservableData(RemoteData.notAsked()); } @@ -55,15 +56,25 @@ export class AnchoredSimulationPassesOverviewModel extends FilterableOverviewPag /** * @inheritdoc */ - getFilterParams() { - return { ...super.getFilterParams(), dataPassIds: [this._dataPassId] }; + getRootEndpoint() { + const params = { + filter: { + names: this._namesFilterModel.normalized, + dataPassIds: [this._dataPassId], + }, + }; + + return buildUrl('/api/simulationPasses', params); } /** - * @inheritdoc + * Reset this model to its default + * + * @returns {void} */ - getRootEndpoint() { - return this.buildRootEndpoint('/api/simulationPasses'); + reset() { + this._namesFilterModel.reset(); + super.reset(); } /** @@ -80,4 +91,34 @@ export class AnchoredSimulationPassesOverviewModel extends FilterableOverviewPag get dataPass() { return this._dataPass.getCurrent(); } + + /** + * Returns data passes names filter model + * @return {TextTokensFilterModel} data passes names filter model + */ + get namesFilterModel() { + return this._namesFilterModel; + } + + /** + * Register a new filter model + * @param {FilterModel} filterModel the filter model to register + * @return {void} + * @private + */ + _registerFilter(filterModel) { + filterModel.visualChange$.bubbleTo(this); + filterModel.observe(() => { + this._pagination.silentlySetCurrentPage(1); + this.load(); + }); + } + + /** + * States whether any filter is active + * @return {boolean} true if any filter is active + */ + isAnyFilterActive() { + return !this._namesFilterModel.isEmpty; + } } diff --git a/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewPage.js b/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewPage.js index f9f752836c..5894ba1a05 100644 --- a/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewPage.js +++ b/lib/public/views/SimulationPasses/AnchoredOverview/AnchoredSimulationPassesOverviewPage.js @@ -21,7 +21,6 @@ import { simulationPassesActiveColumns } from '../ActiveColumns/simulationPasses import { breadcrumbs } from '../../../components/common/navigation/breadcrumbs.js'; import spinner from '../../../components/common/spinner.js'; import { tooltip } from '../../../components/common/popover/tooltip.js'; -import { warningComponent } from '../../../components/common/messages/warningComponent.js'; const TABLEROW_HEIGHT = 42; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -35,9 +34,13 @@ const PAGE_USED_HEIGHT = 215; export const AnchoredSimulationPassesOverviewPage = ({ simulationPasses: { anchoredOverviewModel: anchoredSimulationPassesOverviewModel }, }) => { - const { items, dataPass, pagination, sortModel } = anchoredSimulationPassesOverviewModel; + anchoredSimulationPassesOverviewModel.pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount( + TABLEROW_HEIGHT, + PAGE_USED_HEIGHT, + )); + + const { items, dataPass, pagination } = anchoredSimulationPassesOverviewModel; - pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount(TABLEROW_HEIGHT, PAGE_USED_HEIGHT)); const commonTitle = h('h2#breadcrumb-header', { style: 'white-space: nowrap;' }, 'Anchored MC'); return h( @@ -58,14 +61,13 @@ export const AnchoredSimulationPassesOverviewPage = ({ }), ), ]), - warningComponent(anchoredSimulationPassesOverviewModel), h('.w-100.flex-column', [ table( items, simulationPassesActiveColumns, { classes: '.table-sm' }, null, - { sort: sortModel }, + { sort: anchoredSimulationPassesOverviewModel.sortModel }, ), paginationComponent(pagination), ]), diff --git a/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js index 0980a8c961..98e5d12059 100644 --- a/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js +++ b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js @@ -11,22 +11,23 @@ * or submit itself to any jurisdiction. */ import { TextTokensFilterModel } from '../../../components/Filters/common/filters/TextTokensFilterModel.js'; -import { RemoteData } from '/js/src/index.js'; +import { OverviewPageModel } from '../../../models/OverviewModel.js'; +import { buildUrl, RemoteData } from '/js/src/index.js'; import { ObservableData } from '../../../utilities/ObservableData.js'; import { getRemoteData } from '../../../utilities/fetch/getRemoteData.js'; -import { FilterableOverviewPageModel } from '../../../models/FilterableOverviewPageModel.js'; /** * Simulation Passes Per LHC Period overview model */ -export class SimulationPassesPerLhcPeriodOverviewModel extends FilterableOverviewPageModel { +export class SimulationPassesPerLhcPeriodOverviewModel extends OverviewPageModel { /** * Constructor - * @param {QueryRouter} router router that controls the application's page navigation - * @param {string} pageIdentifier string that indicates what page this model represents */ - constructor(router, pageIdentifier) { - super(router, pageIdentifier, { names: new TextTokensFilterModel() }); + constructor() { + super(); + + this._namesFilterModel = new TextTokensFilterModel(); + this._registerFilter(this._namesFilterModel); this._lhcPeriod = new ObservableData(RemoteData.notAsked()); this._lhcPeriod.bubbleTo(this); @@ -59,15 +60,25 @@ export class SimulationPassesPerLhcPeriodOverviewModel extends FilterableOvervie /** * @inheritdoc */ - getFilterParams() { - return { ...super.getFilterParams(), lhcPeriodIds: [this._lhcPeriodId] }; + getRootEndpoint() { + const params = { + filter: { + names: this._namesFilterModel.normalized, + lhcPeriodIds: [this._lhcPeriodId], + }, + }; + + return buildUrl('/api/simulationPasses', params); } /** - * @inheritdoc + * Reset this model to its default + * + * @returns {void} */ - getRootEndpoint() { - return this.buildRootEndpoint('/api/simulationPasses'); + reset() { + this._namesFilterModel.reset(); + super.reset(); } /** @@ -84,4 +95,34 @@ export class SimulationPassesPerLhcPeriodOverviewModel extends FilterableOvervie get lhcPeriod() { return this._lhcPeriod.getCurrent(); } + + /** + * Returns simulation passes names filter model + * @return {TextTokensFilterModel} simulation passes names filter model + */ + get namesFilterModel() { + return this._namesFilterModel; + } + + /** + * Register a new filter model + * @param {FilterModel} filterModel the filter model to register + * @return {void} + * @private + */ + _registerFilter(filterModel) { + filterModel.visualChange$.bubbleTo(this); + filterModel.observe(() => { + this._pagination.silentlySetCurrentPage(1); + this.load(); + }); + } + + /** + * States whether any filter is active + * @return {boolean} true if any filter is active + */ + isAnyFilterActive() { + return !this._namesFilterModel.isEmpty; + } } diff --git a/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewPage.js b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewPage.js index 0d2961b5f3..3cc12756d0 100644 --- a/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewPage.js +++ b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewPage.js @@ -21,7 +21,6 @@ import { simulationPassesActiveColumns } from '../ActiveColumns/simulationPasses import spinner from '../../../components/common/spinner.js'; import { tooltip } from '../../../components/common/popover/tooltip.js'; import { breadcrumbs } from '../../../components/common/navigation/breadcrumbs.js'; -import { warningComponent } from '../../../components/common/messages/warningComponent.js'; const TABLEROW_HEIGHT = 42; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -34,9 +33,12 @@ const PAGE_USED_HEIGHT = 215; */ export const SimulationPassesPerLhcPeriodOverviewPage = ({ simulationPasses: { perLhcPeriodOverviewModel: simulationPassesPerLhcPeriodOverviewModel } }) => { - const { items: simulationPasses, lhcPeriod, pagination, sortModel } = simulationPassesPerLhcPeriodOverviewModel; + simulationPassesPerLhcPeriodOverviewModel.pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount( + TABLEROW_HEIGHT, + PAGE_USED_HEIGHT, + )); - pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount(TABLEROW_HEIGHT, PAGE_USED_HEIGHT)); + const { items: simulationPasses, lhcPeriod } = simulationPassesPerLhcPeriodOverviewModel; const commonTitle = h('h2#breadcrumb-header', { style: 'white-space: nowrap;' }, 'Monte Carlo'); @@ -55,10 +57,15 @@ export const SimulationPassesPerLhcPeriodOverviewPage = ({ simulationPasses: { }), ), ]), - warningComponent(simulationPassesPerLhcPeriodOverviewModel), h('.w-100.flex-column', [ - table(simulationPasses, simulationPassesActiveColumns, { classes: '.table-sm' }, null, { sort: sortModel }), - paginationComponent(pagination), + table( + simulationPasses, + simulationPassesActiveColumns, + { classes: '.table-sm' }, + null, + { sort: simulationPassesPerLhcPeriodOverviewModel.sortModel }, + ), + paginationComponent(simulationPassesPerLhcPeriodOverviewModel.pagination), ]), ]); }; diff --git a/lib/public/views/SimulationPasses/SimulationPassesModel.js b/lib/public/views/SimulationPasses/SimulationPassesModel.js index 8ba624efd8..8e8d6e7969 100644 --- a/lib/public/views/SimulationPasses/SimulationPassesModel.js +++ b/lib/public/views/SimulationPasses/SimulationPassesModel.js @@ -21,15 +21,14 @@ import { AnchoredSimulationPassesOverviewModel } from './AnchoredOverview/Anchor export class SimulationPassesModel extends Observable { /** * The constructor of the model - * @param {QueryRouter} router router that controls the application's page navigation */ - constructor(router) { + constructor() { super(); - this._perLhcPeriodOverviewModel = new SimulationPassesPerLhcPeriodOverviewModel(router, 'simulation-passes-per-lhc-period-overview'); + this._perLhcPeriodOverviewModel = new SimulationPassesPerLhcPeriodOverviewModel(); this._perLhcPeriodOverviewModel.bubbleTo(this); - this._anchoredOverviewModel = new AnchoredSimulationPassesOverviewModel(router, 'anchored-simulation-passes-overview'); + this._anchoredOverviewModel = new AnchoredSimulationPassesOverviewModel(); this._anchoredOverviewModel.bubbleTo(this); } @@ -42,7 +41,6 @@ export class SimulationPassesModel extends Observable { loadPerLhcPeriodOverview({ lhcPeriodId }) { if (!this._perLhcPeriodOverviewModel.pagination.isInfiniteScrollEnabled) { this._perLhcPeriodOverviewModel.lhcPeriodId = lhcPeriodId; - this._perLhcPeriodOverviewModel.setFilterFromURL(false); this._perLhcPeriodOverviewModel.load(); } } @@ -72,7 +70,6 @@ export class SimulationPassesModel extends Observable { */ loadAnchoredOverview({ dataPassId }) { this._anchoredOverviewModel.dataPassId = dataPassId; - this._anchoredOverviewModel.setFilterFromURL(false); this._anchoredOverviewModel.load(); } diff --git a/lib/public/views/lhcPeriods/ActiveColumns/lhcPeriodsActiveColumns.js b/lib/public/views/lhcPeriods/ActiveColumns/lhcPeriodsActiveColumns.js index 6c312fbecf..6f757e25d0 100644 --- a/lib/public/views/lhcPeriods/ActiveColumns/lhcPeriodsActiveColumns.js +++ b/lib/public/views/lhcPeriods/ActiveColumns/lhcPeriodsActiveColumns.js @@ -30,8 +30,8 @@ export const lhcPeriodsActiveColumns = { name: 'Name', visible: true, sortable: true, - filter: ({ filteringModel }) => textFilter( - filteringModel.get('names'), + filter: ({ namesFilterModel }) => textFilter( + namesFilterModel, { class: 'w-75 mt1', placeholder: 'e.g. LHC22a, lhc23b, ...' }, ), classes: 'w-15', @@ -92,8 +92,8 @@ export const lhcPeriodsActiveColumns = { visible: true, sortable: true, format: (_, lhcPeriod) => formatLhcPeriodYear(lhcPeriod.name), - filter: ({ filteringModel }) => textFilter( - filteringModel.get('years'), + filter: ({ yearsFilterModel }) => textFilter( + yearsFilterModel, { class: 'w-75 mt1', placeholder: 'e.g. 2022, 2023, ...' }, ), classes: 'w-7', @@ -104,8 +104,8 @@ export const lhcPeriodsActiveColumns = { visible: true, sortable: true, format: (pdpBeamTypes) => pdpBeamTypes.length > 0 ? pdpBeamTypes.join(',') : '-', - filter: ({ filteringModel }) => textFilter( - filteringModel.get('pdpBeamTypes'), + filter: ({ pdpBeamTypesFilterModel }) => textFilter( + pdpBeamTypesFilterModel, { class: 'w-75 mt1', placeholder: 'e.g. pp, PbPb' }, ), classes: 'w-7', diff --git a/lib/public/views/lhcPeriods/LhcPeriodsModel.js b/lib/public/views/lhcPeriods/LhcPeriodsModel.js index 74df7b9dc7..4f9d0ed185 100644 --- a/lib/public/views/lhcPeriods/LhcPeriodsModel.js +++ b/lib/public/views/lhcPeriods/LhcPeriodsModel.js @@ -20,12 +20,11 @@ import { LhcPeriodsOverviewModel } from './Overview/LhcPeriodsOverviewModel.js'; export class LhcPeriodsModel extends Observable { /** * The constructor of the model - * @param {QueryRouter} router router that controls the application's page navigation */ - constructor(router) { + constructor() { super(); - this._overviewModel = new LhcPeriodsOverviewModel(router, 'lhc-period-overview'); + this._overviewModel = new LhcPeriodsOverviewModel(); this._overviewModel.bubbleTo(this); } @@ -35,7 +34,6 @@ export class LhcPeriodsModel extends Observable { * @returns {void} */ loadOverview() { - this._overviewModel.setFilterFromURL(false); this._overviewModel.load(); } diff --git a/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js b/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js index 88bf797877..eb2d5e48cd 100644 --- a/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js +++ b/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewModel.js @@ -12,36 +12,42 @@ */ import { TextTokensFilterModel } from '../../../components/Filters/common/filters/TextTokensFilterModel.js'; -import { FilterableOverviewPageModel } from '../../../models/FilterableOverviewPageModel.js'; +import { OverviewPageModel } from '../../../models/OverviewModel.js'; +import { buildUrl } from '/js/src/index.js'; /** * LHC Periods overview model * * @implements {OverviewModel} */ -export class LhcPeriodsOverviewModel extends FilterableOverviewPageModel { +export class LhcPeriodsOverviewModel extends OverviewPageModel { /** * The constructor of the Overview model object - * @param {QueryRouter} router router that controls the application's page navigation - * @param {string} pageIdentifier string that indicates what page this model represents - */ - constructor(router, pageIdentifier) { - super( - router, - pageIdentifier, - { - names: new TextTokensFilterModel(), - years: new TextTokensFilterModel(), - pdpBeamTypes: new TextTokensFilterModel(), - }, - ); + */ + constructor() { + super(); + + this._namesFilterModel = new TextTokensFilterModel(); + this._registerFilter(this._namesFilterModel); + this._yearsFilterModel = new TextTokensFilterModel(); + this._registerFilter(this._yearsFilterModel); + this._pdpBeamTypesFilterModel = new TextTokensFilterModel(); + this._registerFilter(this._pdpBeamTypesFilterModel); } /** * @inheritdoc */ getRootEndpoint() { - return this.buildRootEndpoint('/api/lhcPeriodsStatistics'); + const params = { + filter: { + names: this._namesFilterModel.normalized, + years: this._yearsFilterModel.normalized, + pdpBeamTypes: this._pdpBeamTypesFilterModel.normalized, + }, + }; + + return buildUrl('/api/lhcPeriodsStatistics', params); } /** @@ -59,4 +65,62 @@ export class LhcPeriodsOverviewModel extends FilterableOverviewPageModel { }; }); } + + /** + * Returns lhc periods names filter model + * @return {TextTokensFilterModel} lhc periods names filter model + */ + get namesFilterModel() { + return this._namesFilterModel; + } + + /** + * Returns lhc periods years filter model + * @return {TextTokensFilterModel} lhc periods years filter model + */ + get yearsFilterModel() { + return this._yearsFilterModel; + } + + /** + * Returns lhc periods beam type filter model + * @return {TextTokensFilterModel} lhc periods beam type filter model + */ + get pdpBeamTypesFilterModel() { + return this._pdpBeamTypesFilterModel; + } + + /** + * Reset this model to its default + * + * @returns {void} + */ + reset() { + super.reset(); + this._namesFilterModel.reset(); + this._yearsFilterModel.reset(); + this._pdpBeamTypesFilterModel.reset(); + } + + /** + * Register a new filter model + * @param {FilterModel} filterModel the filter model to register + * @return {void} + * @private + */ + _registerFilter(filterModel) { + filterModel.visualChange$.bubbleTo(this); + filterModel.observe(() => { + this._pagination.silentlySetCurrentPage(1); + this.load(); + }); + } + + /** + * States whether any filter is active + * @return {boolean} true if any filter is active + */ + isAnyFilterActive() { + return !this._namesFilterModel.isEmpty || !this._yearsFilterModel.isEmpty || !this._pdpBeamTypesFilterModel.isEmpty; + } } diff --git a/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewPage.js b/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewPage.js index 89c0def48c..b431c62d42 100644 --- a/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewPage.js +++ b/lib/public/views/lhcPeriods/Overview/LhcPeriodsOverviewPage.js @@ -18,7 +18,6 @@ import { lhcPeriodsActiveColumns } from '../ActiveColumns/lhcPeriodsActiveColumn import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; -import { warningComponent } from '../../../components/common/messages/warningComponent.js'; const TABLEROW_HEIGHT = 35; // Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; @@ -30,19 +29,22 @@ const PAGE_USED_HEIGHT = 215; * @returns {Component} The overview screen */ export const LhcPeriodsOverviewPage = ({ lhcPeriods: { overviewModel: lhcPeriodsOverviewModel } }) => { - const { sortModel, pagination, items } = lhcPeriodsOverviewModel; - - pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount(TABLEROW_HEIGHT, PAGE_USED_HEIGHT)); + lhcPeriodsOverviewModel.pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount( + TABLEROW_HEIGHT, + PAGE_USED_HEIGHT, + )); return h('', [ - h( - '.flex-row.header-container.pv2', - filtersPanelPopover(lhcPeriodsOverviewModel, lhcPeriodsActiveColumns), - ), - warningComponent(lhcPeriodsOverviewModel), + h('.flex-row.header-container.pv2', filtersPanelPopover(lhcPeriodsOverviewModel, lhcPeriodsActiveColumns)), h('.w-100.flex-column', [ - table(items, lhcPeriodsActiveColumns, { classes: '.table-sm' }, null, { sort: sortModel }), - paginationComponent(pagination), + table( + lhcPeriodsOverviewModel.items, + lhcPeriodsActiveColumns, + { classes: '.table-sm' }, + null, + { sort: lhcPeriodsOverviewModel.sortModel }, + ), + paginationComponent(lhcPeriodsOverviewModel.pagination), ]), ]); }; diff --git a/lib/server/Loggers/FilterLogger.js b/lib/server/Loggers/FilterLogger.js deleted file mode 100644 index 0ae19af9bf..0000000000 --- a/lib/server/Loggers/FilterLogger.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ -const { LogManager, LogLevel } = require('@aliceo2/web-ui'); -const { isInTestMode } = require('../../utilities/env-utils'); - -/** - * Logger dedicated to filter-related endpoint access events. - */ -class FilterLogger { - /** - * Creates an instance of FilterLogger. - */ - constructor(silent = isInTestMode()) { - LogManager.configure({ infologger: true }); - this._logger = LogManager.getLogger('FILTERING'); - this._logLevel = LogLevel.OPERATIONS; - this._silent = silent; - } - - /** - * Logs an informational message about endpoint access and applied filters. - * - * @param {object} request the request received at any given endpoint. - * @param {string} endpoint the endpoint that was accessed. - * @param {string|number} id identifier of the user accessing the endpoint. - * @param {Object} [filters={}] filters applied to the request. - * @returns {void} - */ - infoMessage({ path, session: { id } = {}, query = {} }) { - if (this._silent) { - return; - } - - const filters = query.filter ?? {}; - - let message = `Endpoint ${path} was accessed by `; - message += id ? `user ${id} ` : 'an unauthenticated user '; - - if (!Object.keys(filters).length) { - message += 'without filters'; - } else { - message += 'with the following filters:\n'; - message += JSON.stringify(filters); - } - - this._logger.infoMessage(message, { level: this._logLevel }); - } -} - -module.exports = new FilterLogger(); diff --git a/lib/server/controllers/dataPasses.controller.js b/lib/server/controllers/dataPasses.controller.js index 116673beaf..81e2de5d6b 100644 --- a/lib/server/controllers/dataPasses.controller.js +++ b/lib/server/controllers/dataPasses.controller.js @@ -21,10 +21,7 @@ const { dtoValidator } = require('../utilities/dtoValidator.js'); const { countedItemsToHttpView } = require('../utilities/countedItemsToHttpView.js'); const { updateExpressResponseFromNativeError } = require('../express/updateExpressResponseFromNativeError'); const PaginationDto = require('../../domain/dtos/PaginationDto.js'); -const { - NON_PHYSICS_PRODUCTIONS_NAMES_WORDS, - NON_PHYSICS_PRODUCTIONS_NAMES_TOTAL_LENGTH, -} = require('../../domain/enums/NonPhysicsProductionsNamesWords.js'); +const { NON_PHYSICS_PRODUCTIONS_NAMES_WORDS } = require('../../domain/enums/NonPhysicsProductionsNamesWords.js'); /** * List All DataPasses with statistics @@ -37,14 +34,17 @@ const listDataPassesHandler = async (req, res) => { lhcPeriodIds: Joi.array().items(Joi.number()), ids: Joi.array().items(Joi.number()), names: Joi.array().items(Joi.string()), - permittedNonPhysicsNames: Joi.string().max(NON_PHYSICS_PRODUCTIONS_NAMES_TOTAL_LENGTH).custom((value, helper) => { - const nameTokens = value.split(','); + include: Joi.object({ byName: Joi.string().custom((value, helper) => { + if (value.length > 10) { + return helper.error('byName cannot have more than 10 characters'); + } + const nameTokens = value?.split(','); const allTokensCorrect = nameTokens.every((token) => NON_PHYSICS_PRODUCTIONS_NAMES_WORDS.includes(token)); if (!allTokensCorrect) { - return helper.error(`All permittedNonPhysicsNames must comma delimited list of ${NON_PHYSICS_PRODUCTIONS_NAMES_WORDS}`); + return helper.error(`All byName must comma delimited list of ${NON_PHYSICS_PRODUCTIONS_NAMES_WORDS}`); } return nameTokens; - }), + }) }), }, page: PaginationDto, sort: DtoFactory.order(['id', 'name']), diff --git a/lib/server/controllers/lhcPeriodStatistics.controller.js b/lib/server/controllers/lhcPeriodStatistics.controller.js index 8784e3871e..c70b04b67c 100644 --- a/lib/server/controllers/lhcPeriodStatistics.controller.js +++ b/lib/server/controllers/lhcPeriodStatistics.controller.js @@ -42,7 +42,7 @@ const listLhcPeriodStatisticsHandler = async (req, res) => { ); if (validatedDTO) { try { - const { filter, page: { limit = ApiConfig.pagination.limit, offset } = {}, sort = { id: 'DESC' } } = validatedDTO.query; + const { filter, page: { limit = ApiConfig.pagination.limit, offset } = {}, sort = { name: 'DESC' } } = validatedDTO.query; const { count, rows: items } = await lhcPeriodStatisticsService.getAllForPhysicsRuns({ filter, limit, diff --git a/lib/server/middleware/InfoLoggerListener.middleware.js b/lib/server/middleware/InfoLoggerListener.middleware.js deleted file mode 100644 index 858b8a805d..0000000000 --- a/lib/server/middleware/InfoLoggerListener.middleware.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -/** - * Logger based middleware generator - * - * @param {Class} logger class that exposes an infoMessage function that recceives the request and then sends specific data to InfoLogger - * @return {(function(*, *, *): void)} the infoLoggerListener middleware - */ -exports.infoLoggerListenerMiddleware = (logger) => (request, _response, next) => { - logger.infoMessage(request); - next(); -}; diff --git a/lib/server/routers/dataPasses.router.js b/lib/server/routers/dataPasses.router.js index 89e0e7cd58..34f97d2547 100644 --- a/lib/server/routers/dataPasses.router.js +++ b/lib/server/routers/dataPasses.router.js @@ -14,13 +14,11 @@ const { DataPassesController } = require('../controllers/dataPasses.controller.js'); const { rbacMiddleware } = require('../middleware/rbac.middleware.js'); const { BkpRoles } = require('../../domain/enums/BkpRoles.js'); -const FilterLogger = require('../Loggers/FilterLogger.js'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware.js'); exports.dataPassesRouter = { path: '/dataPasses', method: 'get', - controller: [infoLoggerListenerMiddleware(FilterLogger), DataPassesController.listDataPassesHandler], + controller: DataPassesController.listDataPassesHandler, children: [ { diff --git a/lib/server/routers/environments.router.js b/lib/server/routers/environments.router.js index 1c0769bc68..56d4066e73 100644 --- a/lib/server/routers/environments.router.js +++ b/lib/server/routers/environments.router.js @@ -12,13 +12,11 @@ */ const { EnvironmentsController } = require('../controllers'); -const FilterLogger = require('../Loggers/FilterLogger'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware'); module.exports = { method: 'get', path: '/environments', - controller: [infoLoggerListenerMiddleware(FilterLogger), EnvironmentsController.getAllEnvironments], + controller: EnvironmentsController.getAllEnvironments, children: [ { method: 'post', diff --git a/lib/server/routers/lhcFills.router.js b/lib/server/routers/lhcFills.router.js index 73bc50433e..2b33cedb8c 100644 --- a/lib/server/routers/lhcFills.router.js +++ b/lib/server/routers/lhcFills.router.js @@ -12,15 +12,13 @@ */ const { LhcFillsController } = require('../controllers'); -const FilterLogger = require('../Loggers/FilterLogger'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware'); module.exports = { path: '/lhcFills', children: [ { method: 'get', - controller: [infoLoggerListenerMiddleware(FilterLogger), LhcFillsController.listLhcFills], + controller: LhcFillsController.listLhcFills, }, { method: 'post', diff --git a/lib/server/routers/lhcPeriodsStatistics.router.js b/lib/server/routers/lhcPeriodsStatistics.router.js index feb0f4b058..073288903c 100644 --- a/lib/server/routers/lhcPeriodsStatistics.router.js +++ b/lib/server/routers/lhcPeriodsStatistics.router.js @@ -12,15 +12,13 @@ */ const { LhcPeriodStatisticsController } = require('../controllers/lhcPeriodStatistics.controller.js'); -const FilterLogger = require('../Loggers/FilterLogger.js'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware.js'); exports.lhcPeriodsRouter = { path: '/lhcPeriodsStatistics', children: [ { method: 'get', - controller: [infoLoggerListenerMiddleware(FilterLogger), LhcPeriodStatisticsController.listLhcPeriodStatisticsHandler], + controller: LhcPeriodStatisticsController.listLhcPeriodStatisticsHandler, }, { method: 'get', diff --git a/lib/server/routers/logs.router.js b/lib/server/routers/logs.router.js index 9c115f855e..d4381dc170 100644 --- a/lib/server/routers/logs.router.js +++ b/lib/server/routers/logs.router.js @@ -12,14 +12,12 @@ */ const { LogsController } = require('../controllers'); -const FilterLogger = require('../Loggers/FilterLogger'); const { multerMiddleware: { attachmentMiddleware } } = require('../middleware'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware'); module.exports = { method: 'get', path: '/logs', - controller: [infoLoggerListenerMiddleware(FilterLogger), LogsController.listLogs], + controller: LogsController.listLogs, children: [ { method: 'get', diff --git a/lib/server/routers/qcFlag.router.js b/lib/server/routers/qcFlag.router.js index f97a565c86..569a6802ec 100644 --- a/lib/server/routers/qcFlag.router.js +++ b/lib/server/routers/qcFlag.router.js @@ -13,8 +13,6 @@ const { BkpRoles } = require('../../domain/enums/BkpRoles.js'); const { QcFlagController } = require('../controllers/qcFlag.controller.js'); -const FilterLogger = require('../Loggers/FilterLogger.js'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware.js'); const { rbacMiddleware } = require('../middleware/rbac.middleware.js'); exports.qcFlagsRouter = { @@ -23,7 +21,7 @@ exports.qcFlagsRouter = { { path: 'gaq', method: 'get', - controller: [infoLoggerListenerMiddleware(FilterLogger), QcFlagController.getGaqQcFlagsHandler], + controller: QcFlagController.getGaqQcFlagsHandler, }, { path: 'summary', diff --git a/lib/server/routers/runs.router.js b/lib/server/routers/runs.router.js index 59d453d9bc..cfff057864 100644 --- a/lib/server/routers/runs.router.js +++ b/lib/server/routers/runs.router.js @@ -12,8 +12,6 @@ */ const { RunsController } = require('../controllers'); -const FilterLogger = require('../Loggers/FilterLogger'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware'); module.exports = { children: [ @@ -32,7 +30,7 @@ module.exports = { }, { method: 'get', - controller: [infoLoggerListenerMiddleware(FilterLogger), RunsController.listRuns], + controller: RunsController.listRuns, }, { method: 'get', diff --git a/lib/server/routers/simulationPasses.router.js b/lib/server/routers/simulationPasses.router.js index ccd7ba18c1..057a914fbc 100644 --- a/lib/server/routers/simulationPasses.router.js +++ b/lib/server/routers/simulationPasses.router.js @@ -12,8 +12,6 @@ */ const { SimulationPassesController } = require('../controllers/simulationPasses.controller.js'); -const FilterLogger = require('../Loggers/FilterLogger.js'); -const { infoLoggerListenerMiddleware } = require('../middleware/InfoLoggerListener.middleware.js'); exports.simulationPassesRouter = { path: '/simulationPasses', @@ -25,7 +23,7 @@ exports.simulationPassesRouter = { }, { method: 'get', - controller: [infoLoggerListenerMiddleware(FilterLogger), SimulationPassesController.listSimulationPassesHandler], + controller: SimulationPassesController.listSimulationPassesHandler, }, ], }; diff --git a/lib/server/services/dataPasses/DataPassService.js b/lib/server/services/dataPasses/DataPassService.js index df29634c9c..617aa9c7e4 100644 --- a/lib/server/services/dataPasses/DataPassService.js +++ b/lib/server/services/dataPasses/DataPassService.js @@ -88,25 +88,13 @@ class DataPassService { * @returns {Promise>} result */ async getAll({ - filter = {}, + filter, limit, offset, sort, } = {}) { const queryBuilder = this.prepareQueryBuilder(); - /** - * @typedef - * @property {object} filter - * @property {number[]} [filter.lhcPeriodIds] lhcPeriod identifier to filter with - * @property {number[]} [filter.simulationPassIds] simulationPass identifier to filter with - * @property {number[]} [filter.ids] data passes identifier to filter with - * @property {string[]} [filter.names] data passes names to filter with - * @property {string[]} [filter.permittedNonPhysicsNames] list of tokens in data passes names which indicate - * a given data pass should not be excluded, possible tokens are 'test', 'debug'. - */ - const { ids, names, permittedNonPhysicsNames = [], lhcPeriodIds, simulationPassIds } = filter; - if (sort) { for (const property in sort) { queryBuilder.orderBy(property, sort[property]); @@ -120,24 +108,37 @@ class DataPassService { queryBuilder.offset(offset); } - if (lhcPeriodIds) { - queryBuilder.where('lhcPeriodId').oneOf(...lhcPeriodIds); - } - if (simulationPassIds) { - queryBuilder.whereAssociation('anchoredSimulationPasses', 'id').oneOf(...simulationPassIds); - } - if (ids) { - queryBuilder.where('id').oneOf(...ids); - } - if (names) { - queryBuilder.where('name').oneOf(...names); + if (filter) { + /** + * @typedef + * @property {object} filter + * @property {number[]} [filter.lhcPeriodIds] lhcPeriod identifier to filter with + * @property {number[]} [filter.simulationPassIds] simulationPass identifier to filter with + * @property {number[]} [filter.ids] data passes identifier to filter with + * @property {string[]} [filter.names] data passes names to filter with + * @property {boolean} [filter.include.byName] list of tokens in data passes names which indicate + * a given data pass should not be excluded, possible tokens are 'test', 'debug'. + */ + const { ids, names, lhcPeriodIds, simulationPassIds } = filter; + if (lhcPeriodIds) { + queryBuilder.where('lhcPeriodId').oneOf(...lhcPeriodIds); + } + if (simulationPassIds) { + queryBuilder.whereAssociation('anchoredSimulationPasses', 'id').oneOf(...simulationPassIds); + } + if (ids) { + queryBuilder.where('id').oneOf(...ids); + } + if (names) { + queryBuilder.where('name').oneOf(...names); + } } - if (!permittedNonPhysicsNames.includes(NonPhysicsProductionsNamesWords.TEST)) { + const byName = filter?.include?.byName ?? []; + if (!byName.includes(NonPhysicsProductionsNamesWords.TEST)) { queryBuilder.where('name').not().substring(`\\_${NonPhysicsProductionsNamesWords.TEST}`); } - - if (!permittedNonPhysicsNames.includes(NonPhysicsProductionsNamesWords.DEBUG)) { + if (!byName.includes(NonPhysicsProductionsNamesWords.DEBUG)) { queryBuilder.where('name').not().substring(`\\_${NonPhysicsProductionsNamesWords.DEBUG}`); } diff --git a/lib/server/services/lhcPeriod/LhcPeriodStatisticsService.js b/lib/server/services/lhcPeriod/LhcPeriodStatisticsService.js index a64c5566ee..4e92ae675e 100644 --- a/lib/server/services/lhcPeriod/LhcPeriodStatisticsService.js +++ b/lib/server/services/lhcPeriod/LhcPeriodStatisticsService.js @@ -21,12 +21,6 @@ const { NotFoundError } = require('../../errors/NotFoundError'); const { RunDefinition } = require('../../../domain/enums/RunDefinition.js'); const { NonPhysicsProductionsNamesWords } = require('../../../domain/enums/NonPhysicsProductionsNamesWords.js'); -const sortExpressionMap = { - name: (sequelize) => sequelize.col('`lhcPeriod`.`name`'), - year: (sequelize) => sequelize.literal('SUBSTRING(lhcPeriod.name, 4, 2)'), - pdpBeamTypes: (sequelize) => sequelize.literal('pdpBeamTypes'), -}; - /** * @typedef LhcPeriodIdentifier object to uniquely identify a lhc period * @property {string} [name] the lhc period name @@ -91,9 +85,22 @@ class LhcPeriodStatisticsService { sort, } = {}) { const queryBuilder = this.prepareQueryBuilder(); + if (sort) { for (const property in sort) { - const expression = sortExpressionMap[property]; + let expression; + switch (property) { + case 'name': + expression = (sequelize) => sequelize.col('`lhcPeriod`.`name`'); + break; + case 'year': + expression = (sequelize) => sequelize.literal('SUBSTRING(lhcPeriod.name, 4, 2)'); + break; + case 'pdpBeamTypes': + expression = (sequelize) => sequelize.literal('pdpBeamTypes'); + break; + } + queryBuilder.orderBy(expression ?? property, sort[property]); } } diff --git a/lib/usecases/environment/GetAllEnvironmentsUseCase.js b/lib/usecases/environment/GetAllEnvironmentsUseCase.js index 14923a63ca..c742c53b62 100644 --- a/lib/usecases/environment/GetAllEnvironmentsUseCase.js +++ b/lib/usecases/environment/GetAllEnvironmentsUseCase.js @@ -23,7 +23,6 @@ const { dataSource } = require('../../database/DataSource.js'); const { statusAcronyms } = require('../../domain/enums/StatusAcronyms.js'); const { unpackNumberRange } = require('../../utilities/rangeUtils.js'); const { splitStringToStringsTrimmed } = require('../../utilities/stringUtils.js'); -const { setTimeRangeQuery } = require('../../utilities/setTimeRangeQuery.js'); /** * Subquery to select the latest history item for each environment. @@ -70,11 +69,18 @@ class GetAllEnvironmentsUseCase { const { filter, page = {} } = query; const { limit = ApiConfig.pagination.limit, offset = 0 } = page; - const queryBuilder = dataSource.createQueryBuilder() + /** + * Prepare a query builder with ordering, limit and offset + * + * @return {QueryBuilder} the created query builder + */ + const prepareQueryBuilder = () => dataSource.createQueryBuilder() .orderBy('updatedAt', 'desc') .limit(limit) .offset(offset); + const fetchQueryBuilder = prepareQueryBuilder(); + if (filter) { const { ids: idsExpression, @@ -84,8 +90,12 @@ class GetAllEnvironmentsUseCase { created, } = filter; + const filterQueryBuilder = prepareQueryBuilder(); + if (created) { - setTimeRangeQuery(created, 'createdAt', queryBuilder); + const from = created.from !== undefined ? created.from : 0; + const to = created.to !== undefined ? created.to : Date.now(); + filterQueryBuilder.where('createdAt').between(from, to); } if (idsExpression) { @@ -93,12 +103,12 @@ class GetAllEnvironmentsUseCase { // Filter should be like with only one filter if (filters.length === 1) { - queryBuilder.where('id').substring(filters[0]); + filterQueryBuilder.where('id').substring(filters[0]); } // Filters should be exact with more than one filter if (filters.length > 1) { - queryBuilder.andWhere({ id: { [Op.in]: filters } }); + filterQueryBuilder.andWhere({ id: { [Op.in]: filters } }); } } @@ -106,12 +116,12 @@ class GetAllEnvironmentsUseCase { const filters = currentStatusExpression.split(',').map((status) => status.trim()); // Filter the environments by current status using the subquery - queryBuilder.literalWhere( + filterQueryBuilder.literalWhere( `${ENVIRONMENT_LATEST_HISTORY_ITEM_SUBQUERY} IN (:filters)`, { filters }, ); - queryBuilder.includeAttribute({ + filterQueryBuilder.includeAttribute({ query: ENVIRONMENT_LATEST_HISTORY_ITEM_SUBQUERY, alias: 'currentStatus', }); @@ -147,7 +157,7 @@ class GetAllEnvironmentsUseCase { * Use OR condition to match subsequences ending with either DESTROYED or DONE * Filter the environments by using LIKE for subsequence matching */ - queryBuilder.literalWhere( + filterQueryBuilder.literalWhere( `(${ENVIRONMENT_STATUS_HISTORY_SUBQUERY} LIKE :statusFiltersWithDestroyed OR ` + `${ENVIRONMENT_STATUS_HISTORY_SUBQUERY} LIKE :statusFiltersWithDone)`, { @@ -156,17 +166,17 @@ class GetAllEnvironmentsUseCase { }, ); - queryBuilder.includeAttribute({ + filterQueryBuilder.includeAttribute({ query: ENVIRONMENT_STATUS_HISTORY_SUBQUERY, alias: 'statusHistory', }); } else { - queryBuilder.literalWhere( + filterQueryBuilder.literalWhere( `${ENVIRONMENT_STATUS_HISTORY_SUBQUERY} LIKE :statusFilters`, { statusFilters: `%${statusFilters.join(',')}%` }, ); - queryBuilder.includeAttribute({ + filterQueryBuilder.includeAttribute({ query: ENVIRONMENT_STATUS_HISTORY_SUBQUERY, alias: 'statusHistory', }); @@ -180,7 +190,7 @@ class GetAllEnvironmentsUseCase { // Check that the final run numbers list contains at least one valid run number if (finalRunNumberList.length > 0) { - queryBuilder.include({ + filterQueryBuilder.include({ association: 'runs', where: { // Filter should be like with only one filter and exact with more than one filter @@ -188,12 +198,22 @@ class GetAllEnvironmentsUseCase { }, }); } + }; + + const filteredEnvironmentsIds = (await EnvironmentRepository.findAll(filterQueryBuilder)).map(({ id }) => id); + // If no environments match the filter, return an empty result + if (filteredEnvironmentsIds.length === 0) { + return { + count: 0, + environments: [], + }; } + fetchQueryBuilder.where('id').oneOf(filteredEnvironmentsIds); } - queryBuilder.include({ association: 'runs' }); - queryBuilder.include({ association: 'historyItems' }); - const { count, rows } = await EnvironmentRepository.findAndCountAll(queryBuilder); + fetchQueryBuilder.include({ association: 'runs' }); + fetchQueryBuilder.include({ association: 'historyItems' }); + const { count, rows } = await EnvironmentRepository.findAndCountAll(fetchQueryBuilder); return { count, environments: rows.map((environment) => environmentAdapter.toEntity(environment)), diff --git a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js index f69ed2de34..4315cf9e1a 100644 --- a/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js +++ b/lib/usecases/lhcFill/GetAllLhcFillsUseCase.js @@ -24,7 +24,6 @@ const { ApiConfig } = require('../../config/index.js'); const { RunDefinition } = require('../../domain/enums/RunDefinition.js'); const { unpackNumberRange } = require('../../utilities/rangeUtils.js'); const { splitStringToStringsTrimmed } = require('../../utilities/stringUtils.js'); -const { setTimeRangeQuery } = require('../../utilities/setTimeRangeQuery.js'); /** * GetAllLhcFillsUseCase @@ -55,11 +54,15 @@ class GetAllLhcFillsUseCase { } if (stableBeamsStart) { - setTimeRangeQuery(stableBeamsStart, 'stableBeamsStart', queryBuilder); + const from = stableBeamsStart.from !== undefined ? stableBeamsStart.from : 0; + const to = stableBeamsStart.to !== undefined ? stableBeamsStart.to : new Date().getTime(); + queryBuilder.where('stableBeamsStart').between(from, to); } if (stableBeamsEnd) { - setTimeRangeQuery(stableBeamsEnd, 'stableBeamsEnd', queryBuilder); + const from = stableBeamsEnd.from !== undefined ? stableBeamsEnd.from : 0; + const to = stableBeamsEnd.to !== undefined ? stableBeamsEnd.to : new Date().getTime(); + queryBuilder.where('stableBeamsEnd').between(from, to); } if (fillNumbers) { diff --git a/lib/usecases/log/GetAllLogsUseCase.js b/lib/usecases/log/GetAllLogsUseCase.js index c70fabfb31..b1f7ea72b5 100644 --- a/lib/usecases/log/GetAllLogsUseCase.js +++ b/lib/usecases/log/GetAllLogsUseCase.js @@ -29,7 +29,6 @@ const { ApiConfig } = require('../../config/index.js'); const { Op } = require('sequelize'); const { dataSource } = require('../../database/DataSource.js'); const { checkForFilterExclusion } = require('../common/checkForFilterExclusion.js'); -const { setTimeRangeQuery } = require('../../utilities/setTimeRangeQuery.js'); /** * Apply the given filter on the given query builder @@ -40,19 +39,7 @@ const { setTimeRangeQuery } = require('../../utilities/setTimeRangeQuery.js'); * @return {Promise} resolves once the filter has been applied */ const applyFilter = async (dataSource, queryBuilder, filter) => { - const { - title, - content, - author, - created, - origin, - parentLog, - rootLog, - rootOnly, - runNumbers, - environmentIds, - fillNumbers, - } = filter; + const { title, content, author, created, origin, parentLog, rootLog, rootOnly } = filter; if (title) { queryBuilder.where('title').substring(title); @@ -86,7 +73,9 @@ const applyFilter = async (dataSource, queryBuilder, filter) => { } if (created) { - setTimeRangeQuery(created, 'createdAt', queryBuilder); + const from = created.from !== undefined ? created.from : 0; + const to = created.to !== undefined ? created.to : new Date().getTime(); + queryBuilder.where('createdAt').between(from, to); } if (origin) { @@ -123,47 +112,74 @@ const applyFilter = async (dataSource, queryBuilder, filter) => { queryBuilder.where('id').oneOf(...logIds); } - if (runNumbers) { + if (filter.run?.values?.length > 0) { const runQueryBuilder = dataSource.createQueryBuilder(); runQueryBuilder.include({ association: 'run', - where: { runNumber: { [Op.in]: runNumbers } }, + where: { runNumber: { [Op.in]: filter.run.values } }, }).orderBy('logId', 'asc'); - let logRuns = await LogRunsRepository.findAllAndGroup(runQueryBuilder); - logRuns = logRuns.filter((logRun) => runNumbers.every((runNumber) => logRun.runNumbers.includes(runNumber))); + let logRuns; + switch (filter.run.operation) { + case 'and': + logRuns = await LogRunsRepository + .findAllAndGroup(runQueryBuilder); + logRuns = logRuns + .filter((logRun) => filter.run.values.every((runNumber) => logRun.runNumbers.includes(runNumber))); + break; + case 'or': + logRuns = await LogRunsRepository + .findAll(runQueryBuilder); + break; + } const logIds = logRuns.map((logRun) => logRun.logId); queryBuilder.where('id').oneOf(...logIds); } - if (fillNumbers) { + if (filter.lhcFills?.values?.length > 0) { const logLhcFillQueryBuilder = dataSource.createQueryBuilder(); logLhcFillQueryBuilder.include({ association: 'lhcFill', - where: { fill_number: { [Op.in]: fillNumbers } }, + where: { fill_number: { [Op.in]: filter.lhcFills.values } }, }).orderBy('logId', 'asc'); - let logLhcFills = await LogLhcFillsRepository.findAllAndGroup(logLhcFillQueryBuilder); - logLhcFills = logLhcFills.filter((logLhcFill) => - fillNumbers.every((fillNumber) => logLhcFill.fillNumbers.includes(fillNumber))); + let logLhcFills; + switch (filter.lhcFills.operation) { + case 'and': + logLhcFills = await LogLhcFillsRepository.findAllAndGroup(logLhcFillQueryBuilder); + logLhcFills = logLhcFills + .filter((logLhcFill) => filter.lhcFills.values.every((fillNumber) => logLhcFill.fillNumbers.includes(fillNumber))); + break; + case 'or': + logLhcFills = await LogLhcFillsRepository.findAll(logLhcFillQueryBuilder); + break; + } const logIds = logLhcFills.map((logLhcFill) => logLhcFill.logId); queryBuilder.where('id').oneOf(...logIds); } - if (environmentIds) { - const validEnvironments = await EnvironmentRepository.findAll({ where: { id: { [Op.in]: environmentIds } } }); + if (filter.environments?.values?.length > 0) { + const validEnvironments = await EnvironmentRepository.findAll({ where: { id: { [Op.in]: filter.environments.values } } }); const logEnvironmentQueryBuilder = dataSource.createQueryBuilder() .where('environmentId') .oneOf(...validEnvironments.map(({ id }) => id)) .orderBy('logId', 'asc'); - const logIds = groupByProperty(await LogEnvironmentsRepository.findAll(logEnvironmentQueryBuilder), 'logId') - .filter(({ values }) => validEnvironments.every((env) => values.some((item) => item.environmentId === env.id))) - .map(({ index }) => index); + let logIds; + switch (filter.environments.operation) { + case 'and': + logIds = groupByProperty(await LogEnvironmentsRepository.findAll(logEnvironmentQueryBuilder), 'logId') + .filter(({ values }) => validEnvironments.every((env) => values.some((item) => item.environmentId === env.id))) + .map(({ index }) => index); + break; + case 'or': + logIds = (await LogEnvironmentsRepository.findAll(logEnvironmentQueryBuilder)).map(({ logId }) => logId); + break; + } queryBuilder.where('id').oneOf(...logIds); } diff --git a/lib/usecases/run/GetAllRunsUseCase.js b/lib/usecases/run/GetAllRunsUseCase.js index ae0b14d071..df1b5f7f5b 100644 --- a/lib/usecases/run/GetAllRunsUseCase.js +++ b/lib/usecases/run/GetAllRunsUseCase.js @@ -25,7 +25,6 @@ const { qcFlagSummaryService } = require('../../server/services/qualityControlFl const { DetectorType } = require('../../domain/enums/DetectorTypes.js'); const { unpackNumberRange } = require('../../utilities/rangeUtils.js'); const { splitStringToStringsTrimmed } = require('../../utilities/stringUtils.js'); -const { setTimeRangeQuery } = require('../../utilities/setTimeRangeQuery.js'); /** * GetAllRunsUseCase @@ -82,7 +81,7 @@ class GetAllRunsUseCase { inelasticInteractionRateAtMid, inelasticInteractionRateAtEnd, gaq, - detectorsQcNotBadFraction, + detectorsQc, beamModes, } = filter; @@ -152,15 +151,21 @@ class GetAllRunsUseCase { } if (o2start) { - setTimeRangeQuery(o2start, 'timeO2Start', filteringQueryBuilder); + const from = o2start.from !== undefined ? o2start.from : 0; + const to = o2start.to !== undefined ? o2start.to : new Date().getTime(); + filteringQueryBuilder.where('timeO2Start').between(from, to); } if (o2end) { - setTimeRangeQuery(o2end, 'timeO2End', filteringQueryBuilder); + const from = o2end.from !== undefined ? o2end.from : 0; + const to = o2end.to !== undefined ? o2end.to : new Date().getTime(); + filteringQueryBuilder.where('timeO2End').between(from, to); } if (updatedAt) { - setTimeRangeQuery(updatedAt, 'updatedAt', filteringQueryBuilder); + const from = updatedAt.from ?? 0; + const to = updatedAt.to ?? new Date().getTime(); + filteringQueryBuilder.where('updatedAt').between(from, to); } if (triggerValues) { @@ -340,16 +345,13 @@ class GetAllRunsUseCase { } if (dataPassIds) { - const whereDataPassIds = dataPassIds.length === 1 - ? { id: { [Op.eq]: dataPassIds[0] } } - : { id: { [Op.in]: dataPassIds } }; const runNumbers = (await RunRepository.findAll({ attributes: ['runNumber'], raw: true, include: [ { association: 'dataPass', - where: whereDataPassIds, + where: { id: { [Op.in]: dataPassIds } }, }, ], })).map(({ runNumber }) => runNumber); @@ -389,21 +391,28 @@ class GetAllRunsUseCase { } } - if (detectorsQcNotBadFraction) { + if (detectorsQc) { const [dataPassId] = dataPassIds ?? []; const [simulationPassId] = simulationPassIds ?? []; const [lhcPeriodId] = lhcPeriodIds ?? []; - const { mcReproducibleAsNotBad } = detectorsQcNotBadFraction; - delete detectorsQcNotBadFraction.mcReproducibleAsNotBad; + const { mcReproducibleAsNotBad } = detectorsQc; + delete detectorsQc.mcReproducibleAsNotBad; - const dplDetectorIds = Object.keys(detectorsQcNotBadFraction).map((id) => parseInt(id.slice(1), 10)); + const dplDetectorIds = Object.keys(detectorsQc).map((id) => parseInt(id.slice(1), 10)); if (dplDetectorIds.length > 0) { - const scope = { dataPassId, simulationPassId, lhcPeriodId, dplDetectorIds }; - const qcSummary = await qcFlagSummaryService.getSummary(scope, { mcReproducibleAsNotBad }); + const qcSummary = await qcFlagSummaryService.getSummary( + { + dataPassId, + simulationPassId, + lhcPeriodId, + dplDetectorIds, + }, + { mcReproducibleAsNotBad }, + ); const runNumbers = Object.entries(qcSummary) .filter(([_, runSummary]) => { - const mask = Object.entries(detectorsQcNotBadFraction).map(([prefixedDetectorId, { operator, limit }]) => { + const mask = Object.entries(detectorsQc).map(([prefixedDetectorId, { notBadFraction: { operator, limit } }]) => { const dplDetectorId = parseInt(prefixedDetectorId.slice(1), 10); if (!(dplDetectorId in runSummary)) { return false; @@ -525,17 +534,15 @@ class GetAllRunsUseCase { const qcFlagsAssociationDef = { association: 'qcFlags', required: false, - separate: true, - order: [['from', 'ASC']], where: { [Op.and]: [ { deleted: false }, sequelize.literal(`( - \`detector\`.\`type\` not in (${detectorTypesOfNoneExportableAnonymousFlagsEscaped}) - OR \`createdBy\`.\`name\` != 'Anonymous' + \`qcFlags->detector\`.\`type\` not in (${detectorTypesOfNoneExportableAnonymousFlagsEscaped}) + OR \`qcFlags->createdBy\`.\`name\` != 'Anonymous' )`), ] }, include: [ - { association: 'effectivePeriods', required: true, separate: true }, + { association: 'effectivePeriods', required: true }, { association: 'flagType' }, { association: 'detector', required: true }, { association: 'createdBy' }, @@ -551,7 +558,13 @@ class GetAllRunsUseCase { } else { qcFlagsAssociationDef.include.push({ association: 'dataPasses', required: false }); qcFlagsAssociationDef.include.push({ association: 'simulationPasses', required: false }); - qcFlagsAssociationDef.where[Op.and].push(sequelize.literal('(`dataPasses`.`id` IS NULL AND `simulationPasses`.`id` IS NULL)')); + qcFlagsAssociationDef.where[Op.or] = [ + { '$qcFlags.id$': null }, + { + '$qcFlags.dataPasses.id$': null, + '$qcFlags.simulationPasses.id$': null, + }, + ]; fetchQueryBuilder.include(qcFlagsAssociationDef); } diff --git a/lib/utilities/setTimeRangeQuery.js b/lib/utilities/setTimeRangeQuery.js deleted file mode 100644 index ced721ce0f..0000000000 --- a/lib/utilities/setTimeRangeQuery.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -/** - * Function that sets a time range in a QueryBuilder. - * - * @param {object} timerange an object that defines a time range to add to the query - * @param {number} timerange.from the lower bound of the time range - * @param {number} timerange.to the upper bound of the time range - * @param {string} attribute the model attribute for which the range will be set - * @param {QueryBuilder} queryBuilder queryBuider instance in which the time range will be set. - * @returns {void} - */ -exports.setTimeRangeQuery = ({ from = 0, to = Date.now() }, attribute, queryBuilder) => - queryBuilder.where(attribute).between(from, to); diff --git a/package-lock.json b/package-lock.json index ba5c5640fb..14d73124ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aliceo2/bookkeeping", - "version": "1.18.1", + "version": "1.17.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aliceo2/bookkeeping", - "version": "1.18.1", + "version": "1.17.1", "bundleDependencies": [ "@aliceo2/web-ui", "@grpc/grpc-js", @@ -19,43 +19,45 @@ "mariadb", "multer", "node-fetch", + "protobufjs", "sequelize", "umzug" ], "dependencies": { - "@aliceo2/web-ui": "2.11.0", - "@grpc/grpc-js": "1.14.4", + "@aliceo2/web-ui": "2.9.0", + "@grpc/grpc-js": "1.14.0", "@grpc/proto-loader": "0.8.0", "cls-hooked": "4.2.2", - "d3": "7.9.0", + "d3": "7.8.5", "deepmerge": "4.3.0", - "dotenv": "17.4.2", - "joi": "18.2.1", + "dotenv": "17.2.0", + "joi": "18.0.0", "kafkajs": "2.2.0", "mariadb": "3.0.0", "mkdirp": "3.0.1", - "multer": "2.2.0", + "multer": "2.0.2", "node-fetch": "3.3.1", - "sequelize": "6.37.8", + "protobufjs": "8.0.0", + "sequelize": "6.37.0", "umzug": "3.8.2" }, "devDependencies": { "@eslint/js": "^9.39.1", "@stylistic/eslint-plugin-js": "^4.4.1", - "@types/d3": "7.4.3", + "@types/d3": "7.4.0", "chai": "4.5.0", "date-and-time": "3.6.0", "eslint": "^9.37.0", - "eslint-plugin-jsdoc": "^62.9.0", - "globals": "^17.6.0", - "js-yaml": "4.2.0", + "eslint-plugin-jsdoc": "^62.5.0", + "globals": "^17.3.0", + "js-yaml": "4.1.1", "mocha": "11.7.0", "nodemon": "3.1.3", - "nyc": "18.0.0", - "puppeteer": "25.1.0", + "nyc": "17.1.0", + "puppeteer": "24.37.2", "puppeteer-to-istanbul": "1.4.0", "sequelize-cli": "6.6.0", - "sinon": "22.0.0", + "sinon": "21.0.0", "supertest": "7.2.2" }, "engines": { @@ -72,45 +74,47 @@ } }, "node_modules/@aliceo2/web-ui": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@aliceo2/web-ui/-/web-ui-2.11.0.tgz", - "integrity": "sha512-ISVPe8BqekVsNlIJFTsqrw1c2nhSxDDUVgvOsio2ZKsRRBlV5ndZkQFbAXSjpMabswKi+BCE466TG0Oagc3fuQ==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@aliceo2/web-ui/-/web-ui-2.9.0.tgz", + "integrity": "sha512-bPSpI/xXUPNShKF2muu5IIKgwZSfS37toneKBCCentw1seeTEsZBIo3kavdrb3v5SXOPkGeZiS6n7ixrkRKBEw==", "inBundle": true, "license": "GPL-3.0", "dependencies": { - "express": "4.22.2", - "helmet": "8.1.0", - "jsonwebtoken": "9.0.3", - "kafkajs": "2.2.4", + "express": "^4.22.1", + "helmet": "^8.1.0", + "jsonwebtoken": "^9.0.0", + "kafkajs": "^2.2.0", "mithril": "1.1.7", - "openid-client": "5.6.5", - "protobufjs": "8.4.2", + "mysql": "^2.18.1", + "openid-client": "^5.6.0", + "protobufjs": "^7.5.0", "winston": "3.19.0", - "ws": "8.21.0" + "ws": "^8.19.0" }, "engines": { "node": ">= 22.x" } }, - "node_modules/@aliceo2/web-ui/node_modules/kafkajs": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", - "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aliceo2/web-ui/node_modules/protobufjs": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.2.tgz", - "integrity": "sha512-64rfNzkWOZAIazXzpBFPWq6F9up6gMvTzjE2oWIzApx2N/dqVUEE7+bCn2+40780dFVtKOUab8QfxJ6KJDWbqA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", "hasInstallScript": true, "inBundle": true, "license": "BSD-3-Clause", "dependencies": { - "long": "^5.3.2" + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" }, "engines": { "node": ">=12.0.0" @@ -546,17 +550,17 @@ } }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.86.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.86.0.tgz", - "integrity": "sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw==", + "version": "0.83.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.83.0.tgz", + "integrity": "sha512-e1MHSEPJ4m35zkBvNT6kcdeH1SvMaJDsPC3Xhfseg3hvF50FUE3f46Yn36jgbrPYYXezlWUQnevv23c+lx2MCA==", "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.8", - "@typescript-eslint/types": "^8.58.0", - "comment-parser": "1.4.6", + "@typescript-eslint/types": "^8.53.1", + "comment-parser": "1.4.5", "esquery": "^1.7.0", - "jsdoc-type-pratt-parser": "~7.2.0" + "jsdoc-type-pratt-parser": "~7.1.0" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -762,9 +766,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", - "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.0.tgz", + "integrity": "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==", "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -1383,28 +1387,25 @@ "inBundle": true }, "node_modules/@puppeteer/browsers": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.4.tgz", - "integrity": "sha512-HGM8iAmGTf+Y7t0373szVbTmt3d7vPkYL/1bpOkOFO0YUYLgSeuYBCzESklogNPvOBnZ/MRD5f07OkpqH1trtA==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.12.0.tgz", + "integrity": "sha512-Xuq42yxcQJ54ti8ZHNzF5snFvtpgXzNToJ1bXUGQRaiO8t+B6UM8sTUJfvV+AJnqtkJU/7hdy6nbKyA12aHtRw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "modern-tar": "^0.7.6", + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.3", + "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { - "browsers": "lib/main-cli.js" + "browsers": "lib/cjs/main-cli.js" }, "engines": { - "node": ">=22.12.0" - }, - "peerDependencies": { - "proxy-agent": ">=8.0.1" - }, - "peerDependenciesMeta": { - "proxy-agent": { - "optional": true - } + "node": ">=18" } }, "node_modules/@puppeteer/browsers/node_modules/cliui": { @@ -1422,6 +1423,44 @@ "node": ">=12" } }, + "node_modules/@puppeteer/browsers/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@puppeteer/browsers/node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -1507,9 +1546,9 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", - "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1517,13 +1556,14 @@ } }, "node_modules/@sinonjs/samsam": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-10.0.2.tgz", - "integrity": "sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", "type-detect": "^4.1.0" } }, @@ -1539,9 +1579,9 @@ } }, "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "inBundle": true, "license": "MIT" }, @@ -1573,6 +1613,13 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/argparse": { "version": "1.0.38", "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", @@ -1586,11 +1633,10 @@ "dev": true }, "node_modules/@types/d3": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", - "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.0.tgz", + "integrity": "sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==", "dev": true, - "license": "MIT", "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", @@ -1892,10 +1938,21 @@ "integrity": "sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==", "inBundle": true }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/types": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", - "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", "dev": true, "license": "MIT", "engines": { @@ -1927,11 +1984,10 @@ } }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1948,12 +2004,21 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "dev": true, "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -1963,11 +2028,10 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2022,9 +2086,8 @@ "node_modules/archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", - "dev": true, - "license": "MIT" + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true }, "node_modules/are-docs-informative": { "version": "0.0.2", @@ -2067,6 +2130,19 @@ "node": "*" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -2102,12 +2178,143 @@ "node": ">= 4.0.0" } }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.3.tgz", + "integrity": "sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz", + "integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "inBundle": true, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2124,9 +2331,9 @@ "dev": true }, "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "inBundle": true, "license": "MIT", "dependencies": { @@ -2138,7 +2345,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.15.1", + "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -2209,12 +2416,21 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "inBundle": true, - "license": "BSD-3-Clause" + "inBundle": true }, "node_modules/buffer-from": { "version": "1.1.2", @@ -2459,18 +2675,15 @@ } }, "node_modules/chromium-bidi": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz", - "integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-13.1.1.tgz", + "integrity": "sha512-zB9MpoPd7VJwjowQqiW3FKOvQwffFMjQ8Iejp5ZW+sJaKLRhZX1sTxzl3Zt22TDB4zP0OOqs8lRoY7eAW5geyQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, - "engines": { - "node": ">=20.19.0 <22.0.0 || >=22.12.0" - }, "peerDependencies": { "devtools-protocol": "*" } @@ -2480,7 +2693,6 @@ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -2614,9 +2826,9 @@ "dev": true }, "node_modules/comment-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.6.tgz", - "integrity": "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.5.tgz", + "integrity": "sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==", "dev": true, "license": "MIT", "engines": { @@ -2760,6 +2972,38 @@ "dev": true, "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "inBundle": true + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2786,11 +3030,10 @@ } }, "node_modules/d3": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", "inBundle": true, - "license": "ISC", "dependencies": { "d3-array": "3", "d3-axis": "3", @@ -3282,6 +3525,21 @@ "node": ">=8" } }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/delaunator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", @@ -3332,9 +3590,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1624250", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1624250.tgz", - "integrity": "sha512-YFAat/lOiIk0ARmBweG+ygrEcbZrq5B9urRyUoeQKp53MlidHXE2TmTbxKcaXoQj7u/aX+jebDO4BW55rs0WwA==", + "version": "0.0.1566079", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz", + "integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==", "dev": true, "license": "BSD-3-Clause" }, @@ -3360,9 +3618,9 @@ } }, "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", + "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -3404,7 +3662,6 @@ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "inBundle": true, - "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" } @@ -3497,6 +3754,40 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-ex/node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3618,6 +3909,28 @@ "inBundle": true, "license": "MIT" }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "9.39.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", @@ -3679,24 +3992,24 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "62.9.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.9.0.tgz", - "integrity": "sha512-PY7/X4jrVgoIDncUmITlUqK546Ltmx/Pd4Hdsu4CvSjryQZJI2mEV4vrdMufyTetMiZ5taNSqvK//BTgVUlNkA==", + "version": "62.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.5.0.tgz", + "integrity": "sha512-D+1haMVDzW/ZMoPwOnsbXCK07rJtsq98Z1v+ApvDKxSzYTTcPgmFc/nyUDCGmxm2cP7g7hszyjYHO7Zodl/43w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@es-joy/jsdoccomment": "~0.86.0", + "@es-joy/jsdoccomment": "~0.83.0", "@es-joy/resolve.exports": "1.2.0", "are-docs-informative": "^0.0.2", - "comment-parser": "1.4.6", + "comment-parser": "1.4.5", "debug": "^4.4.3", "escape-string-regexp": "^4.0.0", - "espree": "^11.2.0", + "espree": "^11.1.0", "esquery": "^1.7.0", "html-entities": "^2.6.0", "object-deep-merge": "^2.0.0", "parse-imports-exports": "^0.2.4", - "semver": "^7.7.4", + "semver": "^7.7.3", "spdx-expression-parse": "^4.0.0", "to-valid-identifier": "^1.0.0" }, @@ -3704,7 +4017,7 @@ "node": "^20.19.0 || ^22.13.0 || >=24" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, "node_modules/eslint-plugin-jsdoc/node_modules/debug": { @@ -3738,9 +4051,9 @@ } }, "node_modules/eslint-plugin-jsdoc/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3751,15 +4064,15 @@ } }, "node_modules/eslint-plugin-jsdoc/node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.0.tgz", + "integrity": "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.16.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -3776,9 +4089,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-jsdoc/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -4016,16 +4329,26 @@ "es5-ext": "~0.10.14" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "inBundle": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "~1.20.5", + "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", @@ -4044,7 +4367,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.15.1", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -4099,12 +4422,65 @@ "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", "dev": true }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -4149,6 +4525,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -4278,11 +4664,10 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true }, "node_modules/fn.name": { "version": "1.1.0", @@ -4296,7 +4681,6 @@ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", "dev": true, - "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^3.0.2" @@ -4306,17 +4690,17 @@ } }, "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" + "hasown": "^2.0.2", + "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" @@ -4512,106 +4896,108 @@ "node": ">= 0.4" } }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" + "pump": "^3.0.0" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "inBundle": true, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, - "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, "license": "MIT", "engines": { - "node": "18 || 20 || >=22" + "node": ">= 14" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "node_modules/get-uri/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" + "ms": "^2.1.3" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/glob/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "node_modules/get-uri/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } + "license": "MIT" }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": "18 || 20 || >=22" + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "inBundle": true, "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" + "is-glob": "^4.0.1" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 6" } }, "node_modules/globals": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", - "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", "dev": true, "license": "MIT", "engines": { @@ -4704,9 +5090,9 @@ } }, "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "inBundle": true, "license": "MIT", "dependencies": { @@ -4779,6 +5165,84 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4837,7 +5301,6 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -4882,6 +5345,16 @@ "node": ">=12" } }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -5002,11 +5475,16 @@ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "inBundle": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5063,21 +5541,21 @@ } }, "node_modules/istanbul-lib-processinfo": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-3.0.0.tgz", - "integrity": "sha512-P7nLXRRlo7Sqinty6lNa7+4o9jBUYGpqtejqCOZKfgXlRoxY/QArflcB86YO500Ahj4pDJEG34JjMRbQgePLnQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", "dev": true, - "license": "ISC", "dependencies": { "archy": "^1.0.0", - "cross-spawn": "^7.0.3", - "istanbul-lib-coverage": "^3.2.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", "p-map": "^3.0.0", - "rimraf": "^6.1.3", - "uuid": "^8.3.2" + "rimraf": "^3.0.0", + "uuid": "^3.3.3" }, "engines": { - "node": "20 || >=22" + "node": ">=8" } }, "node_modules/istanbul-lib-report": { @@ -5184,9 +5662,9 @@ } }, "node_modules/joi": { - "version": "18.2.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz", - "integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.0.tgz", + "integrity": "sha512-fpbpXN/TD04Xz1/cCXzUR3ghDkhyiHjbzTILx3wNyKXIzQJ55uYAkUGWwhX72uHge/6MdFA/kp1ZUh35DlYmaA==", "inBundle": true, "license": "BSD-3-Clause", "dependencies": { @@ -5196,18 +5674,17 @@ "@hapi/pinpoint": "^2.0.1", "@hapi/tlds": "^1.1.1", "@hapi/topo": "^6.0.2", - "@standard-schema/spec": "^1.1.0" + "@standard-schema/spec": "^1.0.0" }, "engines": { "node": ">= 20" } }, "node_modules/jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", "inBundle": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } @@ -5262,11 +5739,10 @@ } }, "node_modules/js-beautify/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -5297,20 +5773,10 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -5326,9 +5792,9 @@ "dev": true }, "node_modules/jsdoc-type-pratt-parser": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.2.0.tgz", - "integrity": "sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.1.0.tgz", + "integrity": "sha512-SX7q7XyCwzM/MEDCYz0l8GgGbJAACGFII9+WfNYr5SLEKukHWRy2Jk3iWRe7P+lpYJNs7oQ+OSei4JtKGUjd7A==", "dev": true, "license": "MIT", "engines": { @@ -5353,6 +5819,12 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5390,22 +5862,15 @@ } }, "node_modules/jsonwebtoken": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", + "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", "inBundle": true, - "license": "MIT", "dependencies": { - "jws": "^4.0.1", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", + "jws": "^3.2.2", + "lodash": "^4.17.21", "ms": "^2.1.1", - "semver": "^7.5.4" + "semver": "^7.3.8" }, "engines": { "node": ">=12", @@ -5416,15 +5881,16 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "inBundle": true, - "license": "MIT" + "inBundle": true }, "node_modules/jsonwebtoken/node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "inBundle": true, - "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { "semver": "bin/semver.js" }, @@ -5433,25 +5899,23 @@ } }, "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", "inBundle": true, - "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "^1.0.1", + "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "inBundle": true, - "license": "MIT", "dependencies": { - "jwa": "^2.0.1", + "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, @@ -5494,18 +5958,11 @@ "node": ">= 0.8.0" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true }, "node_modules/locate-path": { "version": "6.0.0", @@ -5540,46 +5997,12 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "inBundle": true, - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "inBundle": true, - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "inBundle": true, - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "inBundle": true, - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "inBundle": true, - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "inBundle": true, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { @@ -5588,13 +6011,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "inBundle": true, - "license": "MIT" - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -5838,11 +6254,10 @@ } }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5850,12 +6265,22 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "inBundle": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } @@ -6052,13 +6477,13 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^2.0.1" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -6154,16 +6579,6 @@ "node": ">=12" } }, - "node_modules/modern-tar": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz", - "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -6193,23 +6608,50 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", - "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", "inBundle": true, "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", - "type-is": "^1.6.18" + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" }, "engines": { "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mysql": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", + "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", + "inBundle": true, + "dependencies": { + "bignumber.js": "9.0.0", + "readable-stream": "2.3.7", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/natural-compare": { @@ -6228,6 +6670,16 @@ "node": ">= 0.6" } }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -6380,9 +6832,9 @@ } }, "node_modules/nyc": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-18.0.0.tgz", - "integrity": "sha512-G5UyHinFkB1BxqGTrmZdB6uIYH0+v7ZnVssuflUDi+J+RhKWyAhRT1RCehBSI6jLFLuUUgFDyLt49mUtdO1XeQ==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz", + "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==", "dev": true, "license": "ISC", "dependencies": { @@ -6395,11 +6847,11 @@ "find-up": "^4.1.0", "foreground-child": "^3.3.0", "get-package-type": "^0.1.0", - "glob": "^13.0.6", + "glob": "^7.1.6", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-hook": "^3.0.0", "istanbul-lib-instrument": "^6.0.2", - "istanbul-lib-processinfo": "^3.0.0", + "istanbul-lib-processinfo": "^2.0.2", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.0.2", @@ -6408,17 +6860,17 @@ "p-map": "^3.0.0", "process-on-spawn": "^1.0.0", "resolve-from": "^5.0.0", - "rimraf": "^6.1.3", + "rimraf": "^3.0.0", "signal-exit": "^3.0.2", - "spawn-wrap": "^3.0.0", - "test-exclude": "^8.0.0", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", "yargs": "^15.0.2" }, "bin": { "nyc": "bin/nyc.js" }, "engines": { - "node": "20 || >=22" + "node": ">=18" } }, "node_modules/nyc/node_modules/ansi-styles": { @@ -6606,6 +7058,15 @@ "node": ">=6" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "inBundle": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-deep-merge": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/object-deep-merge/-/object-deep-merge-2.0.0.tgz", @@ -6618,7 +7079,6 @@ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", "inBundle": true, - "license": "MIT", "engines": { "node": ">= 6" } @@ -6637,11 +7097,10 @@ } }, "node_modules/oidc-token-hash": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz", - "integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", "inBundle": true, - "license": "MIT", "engines": { "node": "^10.13.0 || >=12.0.0" } @@ -6679,13 +7138,12 @@ } }, "node_modules/openid-client": { - "version": "5.6.5", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", - "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz", + "integrity": "sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==", "inBundle": true, - "license": "MIT", "dependencies": { - "jose": "^4.15.5", + "jose": "^4.15.1", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" @@ -6746,7 +7204,6 @@ "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", "dev": true, - "license": "MIT", "dependencies": { "aggregate-error": "^3.0.0" }, @@ -6763,6 +7220,65 @@ "node": ">=6" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/package-hash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", @@ -6806,6 +7322,24 @@ "parse-statements": "1.0.11" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-statements": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", @@ -6831,6 +7365,15 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6871,9 +7414,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "inBundle": true, "license": "MIT" }, @@ -6886,6 +7429,13 @@ "node": "*" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/pg-connection-string": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", @@ -7002,6 +7552,12 @@ "node": ">= 0.8.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "inBundle": true + }, "node_modules/process-on-spawn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", @@ -7014,12 +7570,47 @@ "node": ">=8" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "dev": true }, + "node_modules/protobufjs": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.0.0.tgz", + "integrity": "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==", + "hasInstallScript": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7034,6 +7625,68 @@ "node": ">= 0.10" } }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -7046,6 +7699,17 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7056,45 +7720,71 @@ } }, "node_modules/puppeteer": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.1.0.tgz", - "integrity": "sha512-7L6/0JM7XStK99lIL4xQySyNEXNfII6pk0BxkI5kKBTOhR7AsoQiv067YTsE/rIXxQiq9ajlO4WcqBjS/FWK1A==", + "version": "24.37.2", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.37.2.tgz", + "integrity": "sha512-FV1W/919ve0y0oiS/3Rp5XY4MUNUokpZOH/5M4MMDfrrvh6T9VbdKvAHrAFHBuCxvluDxhjra20W7Iz6HJUcIQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "3.0.4", - "chromium-bidi": "16.0.1", - "devtools-protocol": "0.0.1624250", - "lilconfig": "^3.1.3", - "puppeteer-core": "25.1.0", - "typed-query-selector": "^2.12.2" + "@puppeteer/browsers": "2.12.0", + "chromium-bidi": "13.1.1", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1566079", + "puppeteer-core": "24.37.2", + "typed-query-selector": "^2.12.0" }, "bin": { - "puppeteer": "lib/puppeteer/node/cli.js" + "puppeteer": "lib/cjs/puppeteer/node/cli.js" }, "engines": { - "node": ">=22.12.0" + "node": ">=18" } }, "node_modules/puppeteer-core": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.1.0.tgz", - "integrity": "sha512-jKzy5y4WG6uNuFbTWgW1D7mqoT9o0nllc/6a1DGF775T1mPmgw3scdFEtEq67yVFikavQmbYq6NLfbTfxHSlqQ==", + "version": "24.37.2", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.37.2.tgz", + "integrity": "sha512-nN8qwE3TGF2vA/+xemPxbesntTuqD9vCGOiZL2uh8HES3pPzLX20MyQjB42dH2rhQ3W3TljZ4ZaKZ0yX/abQuw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "3.0.4", - "chromium-bidi": "16.0.1", - "devtools-protocol": "0.0.1624250", - "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.2", - "ws": "^8.21.0" + "@puppeteer/browsers": "2.12.0", + "chromium-bidi": "13.1.1", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1566079", + "typed-query-selector": "^2.12.0", + "webdriver-bidi-protocol": "0.4.0", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" }, "engines": { - "node": ">=22.12.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, + "node_modules/puppeteer-core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/puppeteer-to-istanbul": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/puppeteer-to-istanbul/-/puppeteer-to-istanbul-1.4.0.tgz", @@ -7265,9 +7955,9 @@ } }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "inBundle": true, "license": "BSD-3-Clause", "dependencies": { @@ -7335,6 +8025,21 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "inBundle": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -7430,20 +8135,15 @@ } }, "node_modules/rimraf": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", - "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^13.0.3", - "package-json-from-dist": "^1.0.1" + "glob": "^7.1.3" }, "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" + "rimraf": "bin.js" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -7554,9 +8254,9 @@ "license": "MIT" }, "node_modules/sequelize": { - "version": "6.37.8", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz", - "integrity": "sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==", + "version": "6.37.0", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.0.tgz", + "integrity": "sha512-MS6j6aXqWzB3fe9FhmfpQMgVC16bBdYroJCqIqR0l9M2ko8pZdKoi/0PiNWgMyFQDXUHxXyAOG3K07CbnOhteQ==", "funding": [ { "type": "opencollective", @@ -7564,7 +8264,6 @@ } ], "inBundle": true, - "license": "MIT", "dependencies": { "@types/debug": "^4.1.8", "@types/validator": "^13.7.17", @@ -7697,6 +8396,15 @@ "node": ">=10" } }, + "node_modules/sequelize/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "inBundle": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -7878,32 +8586,110 @@ } }, "node_modules/sinon": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-22.0.0.tgz", - "integrity": "sha512-sq/6DpdXOrLyfbKlXLg/Usc7xu8YXPeLkOFZRvA3bNUSA2lhbrZ06yuXbH1fkzBPCbz9O10+7hznzUsjaYNm0Q==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz", + "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^15.4.0", - "@sinonjs/samsam": "^10.0.2", - "diff": "^9.0.0" + "@sinonjs/fake-timers": "^13.0.5", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "supports-color": "^7.2.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/sinon" } }, - "node_modules/sinon/node_modules/diff": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", - "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "BSD-3-Clause", "engines": { - "node": ">=0.3.1" + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -7914,17 +8700,15 @@ } }, "node_modules/spawn-wrap": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-3.0.0.tgz", - "integrity": "sha512-z+s5vv4KzFPJVddGab0xX2n7kQPGMdNUX5l9T8EJqsXdKTWpcxmAqWHpsgHEXoC1taGBCc7b79bi62M5kdbrxQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { - "cross-spawn": "^7.0.6", "foreground-child": "^2.0.0", "is-windows": "^1.0.2", "make-dir": "^3.0.0", - "rimraf": "^6.1.3", + "rimraf": "^3.0.0", "signal-exit": "^3.0.2", "which": "^2.0.1" }, @@ -7960,6 +8744,15 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "inBundle": true }, + "node_modules/sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=", + "inBundle": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stack-chain": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", @@ -7995,6 +8788,18 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -8198,58 +9003,55 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/test-exclude": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", - "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", + "node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^13.0.6", - "minimatch": "^10.2.2" + "pump": "^3.0.0", + "tar-stream": "^3.1.5" }, - "engines": { - "node": "20 || >=22" + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" } }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "dev": true, "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" } }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, - "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=8" } }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "Apache-2.0", "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "b4a": "^1.6.4" } }, "node_modules/text-hex": { @@ -8336,6 +9138,13 @@ "node": ">= 14.0.0" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/type": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", @@ -8387,9 +9196,9 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", "dev": true, "license": "MIT" }, @@ -8519,13 +9328,13 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "inBundle": true, - "license": "MIT", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, "bin": { - "uuid": "dist/bin/uuid" + "uuid": "bin/uuid" } }, "node_modules/v8-to-istanbul": { @@ -8566,9 +9375,9 @@ } }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", - "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.0.tgz", + "integrity": "sha512-U9VIlNRrq94d1xxR9JrCEAx5Gv/2W7ERSv8oWRoNe/QYbfccS0V3h/H6qeNeCRJxXGMhhnkqvwNrvPAYeuP9VA==", "dev": true, "license": "Apache-2.0" }, @@ -8801,9 +9610,9 @@ } }, "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "inBundle": true, "license": "MIT", "engines": { @@ -8822,6 +9631,15 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "inBundle": true, + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", @@ -8909,6 +9727,17 @@ "node": ">=10" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 87e13806bf..1020f607da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aliceo2/bookkeeping", - "version": "1.18.1", + "version": "1.17.1", "author": "ALICEO2", "repository": { "type": "git", @@ -26,20 +26,21 @@ "node": ">= 22.x" }, "dependencies": { - "@aliceo2/web-ui": "2.11.0", - "@grpc/grpc-js": "1.14.4", + "@aliceo2/web-ui": "2.9.0", + "@grpc/grpc-js": "1.14.0", "@grpc/proto-loader": "0.8.0", "cls-hooked": "4.2.2", - "d3": "7.9.0", + "d3": "7.8.5", "deepmerge": "4.3.0", - "dotenv": "17.4.2", - "joi": "18.2.1", + "dotenv": "17.2.0", + "joi": "18.0.0", "kafkajs": "2.2.0", "mariadb": "3.0.0", "mkdirp": "3.0.1", - "multer": "2.2.0", + "multer": "2.0.2", "node-fetch": "3.3.1", - "sequelize": "6.37.8", + "protobufjs": "8.0.0", + "sequelize": "6.37.0", "umzug": "3.8.2" }, "files": [ @@ -50,20 +51,20 @@ "devDependencies": { "@eslint/js": "^9.39.1", "@stylistic/eslint-plugin-js": "^4.4.1", - "@types/d3": "7.4.3", + "@types/d3": "7.4.0", "chai": "4.5.0", "date-and-time": "3.6.0", "eslint": "^9.37.0", - "eslint-plugin-jsdoc": "^62.9.0", - "globals": "^17.6.0", - "js-yaml": "4.2.0", + "eslint-plugin-jsdoc": "^62.5.0", + "globals": "^17.3.0", + "js-yaml": "4.1.1", "mocha": "11.7.0", "nodemon": "3.1.3", - "nyc": "18.0.0", - "puppeteer": "25.1.0", + "nyc": "17.1.0", + "puppeteer": "24.37.2", "puppeteer-to-istanbul": "1.4.0", "sequelize-cli": "6.6.0", - "sinon": "22.0.0", + "sinon": "21.0.0", "supertest": "7.2.2" }, "bundleDependencies": [ @@ -78,6 +79,7 @@ "mariadb", "multer", "node-fetch", + "protobufjs", "sequelize", "umzug" ] diff --git a/test/api/dataPasses.test.js b/test/api/dataPasses.test.js index a092118267..3ce5947435 100644 --- a/test/api/dataPasses.test.js +++ b/test/api/dataPasses.test.js @@ -296,13 +296,13 @@ module.exports = () => { }); }); it('should successfully include TEST productions', async () => { - const response = await request(server).get('/api/dataPasses?filter[lhcPeriodIds][]=2&filter[permittedNonPhysicsNames]=test'); + const response = await request(server).get('/api/dataPasses?filter[lhcPeriodIds][]=2&filter[include][byName]=test'); expect(response.status).to.be.equal(200); const { data } = await response.body; expect(data.map(({ name }) => name)).to.have.all.members(['LHC22b_apass1', 'LHC22b_skimming','LHC22b_apass2_skimmed', 'LHC22b_test']); }); it('should successfully include DEBUG productions', async () => { - const response = await request(server).get('/api/dataPasses?filter[lhcPeriodIds][]=2&filter[permittedNonPhysicsNames]=debug'); + const response = await request(server).get('/api/dataPasses?filter[lhcPeriodIds][]=2&filter[include][byName]=debug'); expect(response.status).to.be.equal(200); const { data } = await response.body; expect(data.map(({ name }) => name)).to.have.all.members(['LHC22b_apass1', 'LHC22b_skimming','LHC22b_apass2_skimmed', 'LHC22b_debug']); diff --git a/test/api/logs.test.js b/test/api/logs.test.js index 9d2f774ad3..ada81f070e 100644 --- a/test/api/logs.test.js +++ b/test/api/logs.test.js @@ -233,7 +233,7 @@ module.exports = () => { }); it('should successfully filter by run number', async () => { - const response = await request(server).get('/api/logs?filter[runNumbers]=1,2'); + const response = await request(server).get('/api/logs?filter[run][values]=1,2&filter[run][operation]=and'); expect(response.status).to.equal(200); expect(response.body.data).to.be.an('array'); @@ -244,30 +244,6 @@ module.exports = () => { } }); - it('should successfully filter by lhcFillNumber', async () => { - const response = await request(server).get('/api/logs?filter[fillNumbers]=1,4,6'); - expect(response.status).to.equal(200); - - expect(response.body.data).to.be.an('array'); - expect(response.body.data).to.lengthOf(1); - for (const { lhcFills } of response.body.data) { - const fillNumbers = lhcFills.map(({ fillNumber }) => fillNumber); - expect([1, 4, 6].every((fillNumber) => fillNumbers.includes(fillNumber))).to.be.true; - } - }); - - it('should successfully filter by EnvironmentIds', async () => { - const response = await request(server).get('/api/logs?filter[environmentIds]=Dxi029djX,eZF99lH6'); - expect(response.status).to.equal(200); - - expect(response.body.data).to.be.an('array'); - expect(response.body.data).to.lengthOf(1); - for (const { environments } of response.body.data) { - const environmentIds = environments.map(({ id }) => id); - expect(["Dxi029djX", "eZF99lH6"].every((environmentId) => environmentIds.includes(environmentId))).to.be.true; - } - }); - it('should successfully filter by content', async () => { const response = await request(server).get('/api/logs?filter[content]=particle'); expect(response.status).to.equal(200); @@ -280,30 +256,6 @@ module.exports = () => { } }); - it('should successfully filter by rootOnly', async () => { - const unfilteredResponse = await request(server).get('/api/logs'); - expect(unfilteredResponse.status).to.equal(200); - - // When a log has no rootLogId the logs adapter will set the row itself as the root log - let hasChildLogs = unfilteredResponse.body.data.some(({ rootLogId, id }) => rootLogId !== id); - expect(hasChildLogs).to.be.true; - - const filteredResponse = await request(server).get('/api/logs?filter[rootOnly]=true'); - expect(filteredResponse.status).to.equal(200); - - hasChildLogs = filteredResponse.body.data.every(({ rootLogId, id }) => rootLogId !== id); - expect(hasChildLogs).to.be.false; - }) - - it('should successfully ignore rootOnly filters if rootLog is provided', async () => { - const response = await request(server).get('/api/logs?filter[rootOnly]=true&filter[rootLog]=1'); - - expect(response.status).to.equal(200); - - expect(response.body.data).to.lengthOf(3); - expect(response.body.data.every(({ rootLogId, id }) => rootLogId !== id)).to.be.true; - }) - it('should return 400 if the author filter is left empty', (done) => { request(server) .get('/api/logs?filter[author]= ') @@ -654,105 +606,6 @@ module.exports = () => { expect(response.body.meta.page.totalCount).to.equal(totalNumber); }); - it('should support sorting, runs DESC', (done) => { - request(server) - .get('/api/logs?sort[runs]=desc') - .expect(200) - .end((err, res) => { - if (err) { - done(err); - return; - } - - const { data } = res.body; - const logsWithRuns = data.filter(({ runs }) => runs.length > 0); - - for (let i = 0; i < logsWithRuns.length - 1; i++) { - const currentId = logsWithRuns[i].runs[0].id; - const nextId = logsWithRuns[i + 1].runs[0].id; - - expect(currentId).to.be.at.least(nextId); - } - - - done(); - }); - }); - - it('should support sorting, runs ASC', (done) => { - request(server) - .get('/api/logs?sort[runs]=asc') - .expect(200) - .end((err, res) => { - if (err) { - done(err); - return; - } - - const { data } = res.body; - const logsWithRuns = data.filter(({ runs }) => runs.length > 0); - for (let i = 0; i < logsWithRuns.length - 1; i++) { - - const currentId = logsWithRuns[i].runs[0].id; - const nextId = logsWithRuns[i + 1].runs[0].id; - - expect(currentId).to.be.at.most(nextId); - } - - - - done(); - }); - }); - - it('should support sorting, environments DESC', (done) => { - request(server) - .get('/api/logs?sort[environments]=desc') - .expect(200) - .end((err, res) => { - if (err) { - done(err); - return; - } - - const { data } = res.body; - const logsWithEnvs = data.filter(({ environments }) => environments.length > 0); - - for (let i = 0; i < logsWithEnvs.length - 1; i++) { - const currentId = logsWithEnvs[i].environments[0].id; - const nextId = logsWithEnvs[i + 1].environments[0].id; - - expect(currentId >= nextId).to.be.true; - } - - done(); - }); - }); - - it('should support sorting, environments ASC', (done) => { - request(server) - .get('/api/logs?sort[environments]=asc') - .expect(200) - .end((err, res) => { - if (err) { - done(err); - return; - } - - const { data } = res.body; - const logsWithEnvs = data.filter(({ environments }) => environments.length > 0); - - for (let i = 0; i < logsWithEnvs.length - 1; i++) { - const currentId = logsWithEnvs[i].environments[0].id; - const nextId = logsWithEnvs[i + 1].environments[0].id; - - expect(currentId <= nextId).to.be.true; - } - - done(); - }); - }); - it('should support sorting, id DESC', (done) => { request(server) .get('/api/logs?sort[id]=desc') diff --git a/test/api/qcFlags.test.js b/test/api/qcFlags.test.js index 24e843d98e..092df4d883 100644 --- a/test/api/qcFlags.test.js +++ b/test/api/qcFlags.test.js @@ -743,8 +743,8 @@ module.exports = () => { const response = await request(server).get(`/api/qcFlags/synchronous?runNumber=${runNumber}&detectorId=${detectorId}`); expect(response.status).to.be.equal(200); const { data: flags, meta } = response.body; - expect(meta).to.be.eql({ page: { totalCount: 3, pageCount: 1 } }); - expect(flags.map(({ id }) => id)).to.have.all.ordered.members([103, 101, 100]); + expect(meta).to.be.eql({ page: { totalCount: 2, pageCount: 1 } }); + expect(flags.map(({ id }) => id)).to.have.all.ordered.members([101, 100]); }); it('should successfully fetch synchronous flags with pagination', async () => { @@ -752,11 +752,11 @@ module.exports = () => { const detectorId = 7; { const response = await request(server) - .get(`/api/qcFlags/synchronous?runNumber=${runNumber}&detectorId=${detectorId}&page[limit]=1&page[offset]=2`); + .get(`/api/qcFlags/synchronous?runNumber=${runNumber}&detectorId=${detectorId}&page[limit]=1&page[offset]=1`); expect(response.status).to.be.equal(200); const { data: flags, meta } = response.body; - expect(meta).to.be.eql({ page: { totalCount: 3, pageCount: 3 } }); + expect(meta).to.be.eql({ page: { totalCount: 2, pageCount: 2 } }); expect(flags).to.be.lengthOf(1); const [flag] = flags; expect(flag.id).to.be.equal(100); @@ -770,7 +770,7 @@ module.exports = () => { { const response = await request(server) .get(`/api/qcFlags/synchronous?runNumber=${runNumber}&detectorId=${detectorId}&filter[createdBy][names]=Jan%20Jansen&filter[createdBy][operator]=or`); - expect(response.body.data).to.be.lengthOf(3); + expect(response.body.data).to.be.lengthOf(2); } { diff --git a/test/api/runs.test.js b/test/api/runs.test.js index e3272b6cef..083771bf02 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -311,24 +311,6 @@ module.exports = () => { expect(data.map(({ runNumber }) => runNumber)).to.have.all.members([1, 2, 55, 49, 54, 56, 105]); }); - it('should return 400 if GAQ notBadFraction is used with multiple dataPassIds', (done) => { - const url = '/api/runs?filter[dataPassIds][]=2&filter[dataPassIds][]=3&filter[gaq][notBadFraction][operator]==&filter[gaq][notBadFraction][limit]=0.5'; - request(server) - .get(url) - .expect(400) - .end((err, res) => { - if (err) { - done(err); - return; - } - - const { errors } = res.body; - expect(errors[0].detail).to.equal('Filtering by GAQ is enabled only when filtering with one dataPassId'); - - done(); - }); - }); - it('should successfully filter on simulation pass id', async () => { const response = await request(server).get('/api/runs?filter[simulationPassIds][]=1'); expect(response.status).to.equal(200); @@ -472,11 +454,11 @@ module.exports = () => { } }); - it('should successfully filter by detectorsQcNotBadFraction', async () => { + it('should successfully filter by detectors notBadFraction', async () => { const dataPassId = 1; { const response = await request(server).get(`/api/runs?filter[dataPassIds][]=${dataPassId}` - + '&filter[detectorsQcNotBadFraction][_1][operator]=>&filter[detectorsQcNotBadFraction][_1][limit]=0.7'); + + '&filter[detectorsQc][_1][notBadFraction][operator]=>&filter[detectorsQc][_1][notBadFraction][limit]=0.7'); expect(response.status).to.equal(200); const { data: runs } = response.body; @@ -486,7 +468,7 @@ module.exports = () => { } { const response = await request(server).get(`/api/runs?filter[dataPassIds][]=${dataPassId}` - + '&filter[detectorsQcNotBadFraction][_1][operator]=<&filter[detectorsQcNotBadFraction][_1][limit]=0.9&filter[detectorsQcNotBadFraction][mcReproducibleAsNotBad]=true'); + + '&filter[detectorsQc][_1][notBadFraction][operator]=<&filter[detectorsQc][_1][notBadFraction][limit]=0.9&filter[detectorsQc][mcReproducibleAsNotBad]=true'); expect(response.status).to.equal(200); const { data: runs } = response.body; @@ -496,8 +478,8 @@ module.exports = () => { } { const response = await request(server).get(`/api/runs?filter[dataPassIds][]=${dataPassId}` - + '&filter[detectorsQcNotBadFraction][_1][operator]=<&filter[detectorsQcNotBadFraction][_1][limit]=0.7' - + '&filter[detectorsQcNotBadFraction][_16][operator]=>&filter[detectorsQcNotBadFraction][_16][limit]=0.9' + + '&filter[detectorsQc][_1][notBadFraction][operator]=<&filter[detectorsQc][_1][notBadFraction][limit]=0.7' + + '&filter[detectorsQc][_16][notBadFraction][operator]=>&filter[detectorsQc][_16][notBadFraction][limit]=0.9' ); expect(response.status).to.equal(200); diff --git a/test/lib/server/services/qualityControlFlag/QcFlagService.test.js b/test/lib/server/services/qualityControlFlag/QcFlagService.test.js index 3aa4300ab4..f4c533444f 100644 --- a/test/lib/server/services/qualityControlFlag/QcFlagService.test.js +++ b/test/lib/server/services/qualityControlFlag/QcFlagService.test.js @@ -142,15 +142,15 @@ module.exports = () => { const detectorId = 7; { const { rows: flags, count } = await qcFlagService.getAllSynchronousPerRunAndDetector({ runNumber, detectorId }); - expect(count).to.be.equal(3); - expect(flags.map(({ id }) => id)).to.have.all.ordered.members([103, 101, 100]); + expect(count).to.be.equal(2); + expect(flags.map(({ id }) => id)).to.have.all.ordered.members([101, 100]); } { const { rows: flags, count } = await qcFlagService.getAllSynchronousPerRunAndDetector( { runNumber, detectorId }, - { limit: 1, offset: 2 }, + { limit: 1, offset: 1 }, ); - expect(count).to.be.equal(3); + expect(count).to.be.equal(2); expect(flags).to.be.lengthOf(1); const [flag] = flags; expect(flag.id).to.be.equal(100); @@ -2124,10 +2124,10 @@ module.exports = () => { }); }); - it('should successfully filter sync flags by created by name', async () => { + it('should successfult fiter sync flags by created by name', async () => { { const { rows } = await qcFlagService.getAllSynchronousPerRunAndDetector({ runNumber: 56, detectorId: 7 }, {}, { createdBy: { names: ['Jan Jansen'], operator: 'or' }}); - expect(rows).to.be.lengthOf(3); + expect(rows).to.be.lengthOf(2); } { @@ -2136,7 +2136,7 @@ module.exports = () => { } }); - it('should successfully filter data pass flags by created by name', async () => { + it('should successfult fiter data pass flags by created by name', async () => { { const { rows } = await qcFlagService.getAllPerDataPassAndRunAndDetector({ dataPassId: 1, runNumber: 107, detectorId: 1 }, {}, { createdBy: { names: ['John Doe'], operator: 'or' }}); expect(rows).to.be.lengthOf(2); @@ -2148,7 +2148,7 @@ module.exports = () => { } }); - it('should successfully filter simulation pass flags by created by name', async () => { + it('should successfult fiter simulation pass flags by created by name', async () => { { const { rows } = await qcFlagService.getAllPerSimulationPassAndRunAndDetector({ simulationPassId: 1, runNumber: 106, detectorId: 1 }, {}, { createdBy: { names: ['Jan Jansen'], operator: 'or' }}); expect(rows).to.be.lengthOf(2); diff --git a/test/lib/usecases/environment/GetAllEnvironmentsUseCase.test.js b/test/lib/usecases/environment/GetAllEnvironmentsUseCase.test.js index 5f1e816571..96b4ee1c11 100644 --- a/test/lib/usecases/environment/GetAllEnvironmentsUseCase.test.js +++ b/test/lib/usecases/environment/GetAllEnvironmentsUseCase.test.js @@ -225,40 +225,4 @@ module.exports = () => { expect(environments).to.be.an('array'); expect(environments.length).to.be.equal(0); // Environments from seeders }); - - it('should return correct total count and all filtered results across pages', async () => { - const totalMatchingFilter = 6; // 'RUNNING, ERROR' matches 6 environments at this point - const limit = 2; - - // First page - getAllEnvsDto.query = { page: { limit, offset: 0 }, filter: { currentStatus: 'RUNNING, ERROR' } }; - const page1 = await new GetAllEnvironmentsUseCase().execute(getAllEnvsDto); - - expect(page1.count).to.be.equal(totalMatchingFilter); - expect(page1.environments).to.be.an('array'); - expect(page1.environments.length).to.be.equal(limit); - - // Second page - getAllEnvsDto.query = { page: { limit, offset: 2 }, filter: { currentStatus: 'RUNNING, ERROR' } }; - const page2 = await new GetAllEnvironmentsUseCase().execute(getAllEnvsDto); - - expect(page2.count).to.be.equal(totalMatchingFilter); - expect(page2.environments).to.be.an('array'); - expect(page2.environments.length).to.be.equal(limit); - - // Third page - getAllEnvsDto.query = { page: { limit, offset: 4 }, filter: { currentStatus: 'RUNNING, ERROR' } }; - const page3 = await new GetAllEnvironmentsUseCase().execute(getAllEnvsDto); - - expect(page3.count).to.be.equal(totalMatchingFilter); - expect(page3.environments).to.be.an('array'); - expect(page3.environments.length).to.be.equal(limit); - - // Collect all environment IDs and verify no duplicates and all present - const allIds = [page1, page2, page3].flatMap(({ environments })=> environments.map(({ id }) => id)); - - expect(allIds.length).to.be.equal(totalMatchingFilter); - expect(new Set(allIds).size).to.be.equal(totalMatchingFilter); - expect(allIds).to.have.members(['SomeId', 'newId', 'CmCvjNbg', 'EIDO13i3D', '8E4aZTjY', 'Dxi029djX']); - }); }; diff --git a/test/lib/usecases/log/GetAllLogsUseCase.test.js b/test/lib/usecases/log/GetAllLogsUseCase.test.js index d4475d2d60..61a402cdb8 100644 --- a/test/lib/usecases/log/GetAllLogsUseCase.test.js +++ b/test/lib/usecases/log/GetAllLogsUseCase.test.js @@ -73,7 +73,7 @@ module.exports = () => { it('should successfully filter on run numbers', async () => { const runNumbers = [1, 2]; - getAllLogsDto.query = { filter: { runNumbers } }; + getAllLogsDto.query = { filter: { run: { operation: 'and', values: runNumbers } } }; { const { logs: filteredResult } = await new GetAllLogsUseCase().execute(getAllLogsDto); @@ -83,6 +83,17 @@ module.exports = () => { expect(runNumbers.every((runNumber) => relatedRunNumbers.includes(runNumber))).to.be.true; } } + + getAllLogsDto.query = { filter: { run: { operation: 'or', values: runNumbers } } }; + + { + const { logs: filteredResult } = await new GetAllLogsUseCase().execute(getAllLogsDto); + expect(filteredResult).to.lengthOf(6); + for (const log of filteredResult) { + const relatedRunNumbers = log.runs.map(({ runNumber }) => runNumber); + expect(runNumbers.some((runNumber) => relatedRunNumbers.includes(runNumber))).to.be.true; + } + } }); it('should successfully filter on log content', async () => { @@ -106,9 +117,9 @@ module.exports = () => { }); it('should successfully filter on lhc fills', async () => { - const fillNumbers = [1, 6]; + const lhcFills = [1, 6]; - getAllLogsDto.query = { filter: { fillNumbers } }; + getAllLogsDto.query = { filter: { lhcFills: { operation: 'and', values: lhcFills } } }; { const { logs: filteredResult } = await new GetAllLogsUseCase().execute(getAllLogsDto); expect(filteredResult).to.have.lengthOf(1); @@ -117,24 +128,47 @@ module.exports = () => { // For each returned log, check at least one of the associated fill numbers was in the filter query expect(fillNumbersPerLog.every((logFillNumbers) => - logFillNumbers.includes(fillNumbers[0]) && logFillNumbers.includes(fillNumbers[1]))).to.be.true; + logFillNumbers.includes(lhcFills[0]) && logFillNumbers.includes(lhcFills[1]))).to.be.true; + } + + getAllLogsDto.query = { filter: { lhcFills: { operation: 'or', values: lhcFills } } }; + { + const { logs: filteredResult } = await new GetAllLogsUseCase().execute(getAllLogsDto); + expect(filteredResult).to.have.lengthOf(3); + + const fillNumbersPerLog = filteredResult.map(({ lhcFills }) => lhcFills.map(({ fillNumber }) => fillNumber)); + + // For each returned log, check at least one of the associated fill numbers was in the filter query + expect(fillNumbersPerLog.every((logFillNumbers) => + logFillNumbers.includes(lhcFills[0]) || logFillNumbers.includes(lhcFills[1]))).to.be.true; } }); it ('should successfully filter on log environment', async () => { - const environmentIds = ['8E4aZTjY', 'eZF99lH6']; - getAllLogsDto.query = { filter: { environmentIds } }; + const environments = ['8E4aZTjY', 'eZF99lH6']; + getAllLogsDto.query = { filter: { environments: { operation: 'and', values: environments } } }; { const { logs: filteredResult } = await new GetAllLogsUseCase().execute(getAllLogsDto); expect(filteredResult).to.lengthOf(2); for (const log of filteredResult) { - const relatedenvironmentIds = log.environments.map(({ id }) => id); - expect(environmentIds.every((env) => relatedenvironmentIds.includes(env))).to.be.true; + const relatedEnvironments = log.environments.map(({ id }) => id); + expect(environments.every((env) => relatedEnvironments.includes(env))).to.be.true; + } + } + + getAllLogsDto.query = { filter: { environments: { operation: 'or', values: environments } } }; + + { + const { logs: filteredResult } = await new GetAllLogsUseCase().execute(getAllLogsDto); + expect(filteredResult).to.lengthOf(5); + for (const log of filteredResult) { + const relatedEnvironments = log.environments.map(({ id }) => id); + expect(environments.some((env) => relatedEnvironments.includes(env))).to.be.true; } } - getAllLogsDto.query = { filter: { environmentIds: ['non-existent-environment'] } }; + getAllLogsDto.query = { filter: { environments: { operation: 'and', values: ['non-existent-environment'] } } }; { const { logs: filteredResult } = await new GetAllLogsUseCase().execute(getAllLogsDto); diff --git a/test/lib/usecases/run/GetAllRunsUseCase.test.js b/test/lib/usecases/run/GetAllRunsUseCase.test.js index febeae02aa..5b080d056c 100644 --- a/test/lib/usecases/run/GetAllRunsUseCase.test.js +++ b/test/lib/usecases/run/GetAllRunsUseCase.test.js @@ -831,7 +831,7 @@ module.exports = () => { query: { filter: { dataPassIds, - detectorsQcNotBadFraction: { '_1': { operator: '<', limit: 0.7 } }, + detectorsQc: { '_1': { notBadFraction: { operator: '<', limit: 0.7 } } }, }, }, }); @@ -843,7 +843,7 @@ module.exports = () => { query: { filter: { dataPassIds, - detectorsQcNotBadFraction: { '_1': { operator: '<', limit: 0.8 } }, + detectorsQc: { '_1': { notBadFraction: { operator: '<', limit: 0.8 } } }, }, }, }); @@ -855,7 +855,7 @@ module.exports = () => { query: { filter: { dataPassIds, - detectorsQcNotBadFraction: { '_1': { operator: '<', limit: 0.9 }, mcReproducibleAsNotBad: true }, + detectorsQc: { '_1': { notBadFraction: { operator: '<', limit: 0.9 } }, mcReproducibleAsNotBad: true }, }, }, }); @@ -867,10 +867,9 @@ module.exports = () => { query: { filter: { dataPassIds, - detectorsQcNotBadFraction: - { - '_2': { operator: '>', limit: 0.8 }, - '_1': { operator: '<', limit: 0.8 }, + detectorsQc: { + '_2': { notBadFraction: { operator: '>', limit: 0.8 } }, + '_1': { notBadFraction: {operator: '<', limit: 0.8 } }, }, }, }, diff --git a/test/public/Filters/FilteringModel.test.js b/test/public/Filters/FilteringModel.test.js deleted file mode 100644 index a00ff7996b..0000000000 --- a/test/public/Filters/FilteringModel.test.js +++ /dev/null @@ -1,157 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const { - defaultBefore, - defaultAfter, - goToPage, - fillInput, - pressElement, - waitForTableTotalRowsCountToEqual, - getPopoverSelector, - getPeriodInputsSelectors, -} = require('../defaults.js'); - -module.exports = () => { - let page; - let browser; - - before(async () => { - [page, browser] = await defaultBefore(); - }); - - // Not all filters for the pages will be checked, as many of them are identical between pages. - // Environments is not checked at all because it has no filter implementations not allready covered by other pages - const runSelectionFiltersChecks = { - 'tags': [{ count: 1, selector: '#tag-dropdown-option-FOOD' }, { count: 0, selector: '#tag-dropdown-option-CTP' }, { count: 1, selector: '#tag-filter-combination-operator-radio-button-or' }], - 'beam mode': [{ count: 1, selector: '#beam-mode-dropdown-option-NO\\ BEAM' }, { count: 2, selector: '#beam-mode-dropdown-option-UNSTABLE\\ BEAMS' }], - 'definitions': [{ count: 1, selector: '#run-definition-checkbox-TECHNICAL' }, { count: 3, selector: '#run-definition-checkbox-SYNTHETIC' }], - 'quality': [{ count: 1, selector: '#checkboxes-checkbox-none' }, { count: 3, selector: '#checkboxes-checkbox-bad' }], - 'detectors': [{ count: 3, selector: '#detector-filter-dropdown-option-ACO' }, { count: 0, selector: '#detector-filter-dropdown-option-FDD' }, { count: 3, selector: '#detector-filter-combination-operator-radio-button-or' }], - 'runTypes': [{ count: 4, selector: '#run-types-dropdown-option-14' }, { count: 5, selector: '#run-types-dropdown-option-2' }], - 'ddFLP': [{ count: 101, selector: '#ddFlpFilterRadioON' }, { count: 8, selector: '#ddFlpFilterRadioOFF' }], - 'magnets': [{ count: 1, selector: '#l3-dipole-current-dropdown-option-20003kA\\/0kA' }, { count: 3, selector: '#l3-dipole-current-dropdown-option-30003kA\\/0kA' }], - }; - - const logSelectionFiltersChecks = { - 'tags': [{ count: 1, selector: '#tag-dropdown-option-DPG' }, { count: 0, selector: '#tag-dropdown-option-FOOD' }, { count: 3, selector: '#tag-filter-combination-operator-radio-button-or' } ], - }; - - const lhcFillsSelectionFiltersChecks = { - 'hasStableBeams': [{ count: 6, selector: '.switch > input' }], - 'beamTypes': [{ count: 1, selector: '#beam-types-checkbox-p-p' }, { count: 2, selector: '#beam-types-checkbox-p-Pb' }] - }; - - const checkSelectionFilters = async (selectionFilterObject, baseRowCount) => { - for (const [_key, checks] of Object.entries(selectionFilterObject)) { - await waitForTableTotalRowsCountToEqual(page, baseRowCount); - - for (const { count, selector } of checks) { - await pressElement(page, selector, true); - await waitForTableTotalRowsCountToEqual(page, count); - } - - for (const { count } of checks.reverse()) { - await waitForTableTotalRowsCountToEqual(page, count); - await page.goBack(); - } - - await waitForTableTotalRowsCountToEqual(page, baseRowCount); - } - }; - - it('should undo filters if the user presses go-back on the runs page', async () => { - await goToPage(page, 'run-overview'); - const baseRowCount = 109; - const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); - - const { fromDateSelector, fromTimeSelector } = getPeriodInputsSelectors(startPopoverSelector); - - await checkSelectionFilters(runSelectionFiltersChecks, baseRowCount); - - // Run duration - await page.select('#duration-operator', '>'); - await fillInput(page, '#duration-operand', 500, ['change']); - await waitForTableTotalRowsCountToEqual(page, 8); - await page.select('#duration-operator', '='); - await waitForTableTotalRowsCountToEqual(page, 0); - await page.goBack(); - await waitForTableTotalRowsCountToEqual(page, 8); - await page.goBack(); - await waitForTableTotalRowsCountToEqual(page, baseRowCount); - - // EorReason filter - await page.select('#eorCategories', 'DETECTORS'); - await waitForTableTotalRowsCountToEqual(page, 3); - await page.select('#eorTitles', 'CPV'); - await waitForTableTotalRowsCountToEqual(page, 2); - await fillInput(page, '#eorDescription', 'some', ['change']); - await waitForTableTotalRowsCountToEqual(page, 1); - await page.goBack(); - await waitForTableTotalRowsCountToEqual(page, 2); - await page.goBack(); - await waitForTableTotalRowsCountToEqual(page, 3); - await page.goBack(); - await waitForTableTotalRowsCountToEqual(page, baseRowCount); - - // O2 Start Filter: - await fillInput(page, fromTimeSelector, '11:11', ['change']); - await fillInput(page, fromDateSelector, '2021-02-03', ['change']); - await waitForTableTotalRowsCountToEqual(page, 1); - await fillInput(page, fromDateSelector, '2020-02-03', ['change']); - await waitForTableTotalRowsCountToEqual(page, 2); - await page.goBack(); - await waitForTableTotalRowsCountToEqual(page, 1); - await page.goBack(); - await waitForTableTotalRowsCountToEqual(page, baseRowCount); - }); - - it('should undo filters if the user presses go-back on the LHC fills page', async () => { - await goToPage(page, 'lhc-fill-overview'); - await checkSelectionFilters(lhcFillsSelectionFiltersChecks, 5) - }); - - it('should undo filters if the user presses go-back on the logs page', async () => { - await goToPage(page, 'log-overview'); - await waitForTableTotalRowsCountToEqual(page, 119); - - // AuthorFilter - await pressElement(page, '.author-filter .switch input', true); - await waitForTableTotalRowsCountToEqual(page, 117); - await fillInput(page, '#authorFilterText', '!Anonymous,John', ['change']); - await waitForTableTotalRowsCountToEqual(page, 5); - await page.goBack(); - await waitForTableTotalRowsCountToEqual(page, 117); - await page.goBack(); - await waitForTableTotalRowsCountToEqual(page, 119); - - await checkSelectionFilters(logSelectionFiltersChecks, 119); - }); - - it('should undo filters if the user presses go-back on the lhc periods page', async () => { - await goToPage(page, 'lhc-period-overview'); - await waitForTableTotalRowsCountToEqual(page, 3); - - // Name - await fillInput(page, '.name-filter input', 'LHC23f'); - await waitForTableTotalRowsCountToEqual(page, 1); - await fillInput(page, '.name-filter input', 'bogus'); - await waitForTableTotalRowsCountToEqual(page, 0); - await page.goBack(); - await waitForTableTotalRowsCountToEqual(page, 1); - await page.goBack(); - await waitForTableTotalRowsCountToEqual(page, 3); - }); - - after(async () => await defaultAfter(page, browser)); -} diff --git a/test/public/Filters/filtersToUrl.test.js b/test/public/Filters/filtersToUrl.test.js deleted file mode 100644 index c89547e244..0000000000 --- a/test/public/Filters/filtersToUrl.test.js +++ /dev/null @@ -1,529 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const { expect } = require('chai'); -const { - defaultBefore, - defaultAfter, - goToPage, - fillInput, - getPopoverSelector, - getPeriodInputsSelectors, - pressElement, - openFilteringPanel, - waitForTableLength, -} = require('../defaults.js'); - -module.exports = () => { - let page; - let browser; - - before(async () => { - [page, browser] = await defaultBefore(); - }); - - const getQueryParameters = (page) => Object.fromEntries(new URL(page.url()).searchParams.entries()); - - it('should set filters from LogsOverview to the URL', async () => { - await goToPage(page, 'log-overview'); - const firstCheckboxId = 'tag-dropdown-option-DPG'; - const popoverTrigger = '.createdAt-filter .popover-trigger'; - - await page.waitForSelector(popoverTrigger); - await openFilteringPanel(page); - - const popOverSelector = await getPopoverSelector(await page.$(popoverTrigger)); - const { fromDateSelector, toDateSelector, fromTimeSelector, toTimeSelector } = getPeriodInputsSelectors(popOverSelector); - - await fillInput(page, '.title-textFilter', 'bogusbogusbogus', ['change']); - await fillInput(page, '#authorFilterText', 'Jane', ['change']); - await fillInput(page, '.content-textFilter', 'particle', ['change']); - await pressElement(page, '.tags-filter .dropdown-trigger'); - await pressElement(page, `#${firstCheckboxId}`, true); - await fillInput(page, '.environments-filter input', '8E4aZTjY', ['change']); - await fillInput(page, '.runNumbers-textFilter', '1,2', ['change']); - await fillInput(page, '.fillNumbers-textFilter', '1, 6', ['change']); - await fillInput(page, fromDateSelector, '2020-02-02', ['change']); - await fillInput(page, toDateSelector, '2020-02-02', ['change']); - await fillInput(page, fromTimeSelector, '11:00', ['change']); - await fillInput(page, toTimeSelector, '12:00', ['change']); - - const queryParameters = getQueryParameters(page); - expect(queryParameters).to.deep.equal({ - "page": "log-overview", - "filter[author]": "Jane", - "filter[title]": "bogusbogusbogus", - "filter[content]": "particle", - "filter[tags][values]": "DPG", - "filter[tags][operation]": "and", - "filter[runNumbers]": "1,2", - "filter[environmentIds]": "8E4aZTjY", - "filter[fillNumbers]": "1, 6", - "filter[created][from]": "1580641200000", - "filter[created][to]": "1580644800000" - }); - }); - - it('should set filters from EnvironmentsOverview to the URL', async () => { - await goToPage(page, 'env-overview'); - const popoverTrigger = '.createdAt-filter .popover-trigger'; - - await page.waitForSelector(popoverTrigger); - await openFilteringPanel(page); - - const createdAtPopoverSelector = await getPopoverSelector(await page.$(popoverTrigger)); - const periodInputsSelectors = getPeriodInputsSelectors(createdAtPopoverSelector); - - await fillInput(page, '.runs-filter input', '10', ['change']); - await fillInput(page, '.id-filter input', 'Dxi029djX, TDI59So3d', ['change']); - await pressElement(page, '#checkboxes-checkbox-DESTROYED'); - await fillInput(page, '.historyItems-filter input', 'C-R-D-X', ['change']); - await fillInput(page, periodInputsSelectors.fromDateSelector, '2019-08-09', ['change']); - await fillInput(page, periodInputsSelectors.toDateSelector, '2019-08-10', ['change']); - await fillInput(page, periodInputsSelectors.fromTimeSelector, '00:00', ['change']); - await fillInput(page, periodInputsSelectors.toTimeSelector, '23:59', ['change']); - - const queryParameters = getQueryParameters(page); - expect(queryParameters).to.deep.equal({ - "page": "env-overview", - "filter[created][from]": "1565308800000", - "filter[created][to]": "1565481540000", - "filter[runNumbers]": "10", - "filter[statusHistory]": "C-R-D-X", - "filter[currentStatus]": "DESTROYED", - "filter[ids]": "Dxi029djX, TDI59So3d" - }); - }); - - it('should set filters from LhcFillsOverview to the URL', async () => { - await goToPage(page, 'lhc-fill-overview'); - await waitForTableLength(page, 5); - const sbEndPopoverTrigger = '.stableBeamsEnd-filter .popover-trigger'; - const sbStartPopoverTrigger = '.stableBeamsStart-filter .popover-trigger'; - const sbStartPopOverSelector = await getPopoverSelector(await page.$(sbStartPopoverTrigger)); - const sbEndPopOverSelector = await getPopoverSelector(await page.$(sbEndPopoverTrigger)); - const filterSchemeNameInputField= '.fillingSchemeName-filter input'; - const { - fromDateSelector: sbStartFromDateSelector, - toDateSelector: sbStartToDateSelector, - fromTimeSelector: sbStartFromTimeSelector, - toTimeSelector: sbStartToTimeSelector - } = getPeriodInputsSelectors(sbStartPopOverSelector); - - const { - fromDateSelector: sbEndFromDateSelector, - toDateSelector: sbEndToDateSelector, - fromTimeSelector: sbEndFromTimeSelector, - toTimeSelector: sbEndToTimeSelector - } = getPeriodInputsSelectors(sbEndPopOverSelector); - - await openFilteringPanel(page); - await fillInput(page, '#beam-duration-filter-operand', '00:01:40', ['change']); - await fillInput(page, '#run-duration-filter-operand', '00:00:00', ['change']); - await pressElement(page, '#beam-types-checkbox-p-Pb'); - await fillInput(page, sbStartFromDateSelector, '2019-08-08', ['change']); - await fillInput(page, sbStartToDateSelector, '2019-08-08', ['change']); - await fillInput(page, sbStartFromTimeSelector, '10:00', ['change']); - await fillInput(page, sbStartToTimeSelector, '12:00', ['change']); - await fillInput(page, sbEndFromDateSelector, '2022-03-22', ['change']); - await fillInput(page, sbEndToDateSelector, '2022-03-22', ['change']); - await fillInput(page, sbEndFromTimeSelector, '01:00', ['change']); - await fillInput(page, sbEndToTimeSelector, '23:59', ['change']); - await fillInput(page, filterSchemeNameInputField, 'Single_12b_8_1024_8_2018', ['change']); - - const queryParameters = getQueryParameters(page); - expect(queryParameters).to.deep.equal({ - "page": "lhc-fill-overview", - "filter[beamDuration][operator]": "=", - "filter[beamDuration][limit]": "00:01:40", - "filter[runDuration][operator]": "=", - "filter[runDuration][limit]": "00:00:00", - "filter[hasStableBeams]": "true", - "filter[stableBeamsEnd][from]": "1647910800000", - "filter[stableBeamsEnd][to]": "1647993540000", - "filter[stableBeamsStart][from]": "1565258400000", - "filter[stableBeamsStart][to]": "1565265600000", - "filter[beamTypes]": "p-Pb", - "filter[schemeName]": "Single_12b_8_1024_8_2018" - }); - }); - - it('should set filters from runsOverview to the URL', async () => { - await goToPage(page, 'run-overview'); - const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); - const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); - const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); - - const { - fromDateSelector: startFromDateSelector, - toDateSelector: startToDateSelector, - fromTimeSelector: startFromTimeSelector, - toTimeSelector: startToTimeSelector - } = getPeriodInputsSelectors(startPopoverSelector); - - const { - fromDateSelector: endFromDateSelector, - toDateSelector: endToDateSelector, - fromTimeSelector: endFromTimeSelector, - toTimeSelector: endToTimeSelector - } = getPeriodInputsSelectors(endPopoverSelector); - - await openFilteringPanel(page); - await pressElement(page, '#detector-filter-dropdown-option-ITS', true); - await pressElement(page, '#tag-dropdown-option-FOOD', true); - await pressElement(page, '#run-definition-checkbox-PHYSICS', true); - await pressElement(page, '.timeO2Start-filter .popover-trigger'); - await fillInput(page, startFromTimeSelector, '11:11', ['change']); - await fillInput(page, startToTimeSelector, '14:00', ['change']); - await fillInput(page, startFromDateSelector, '2021-02-03', ['change']); - await fillInput(page, startToDateSelector, '2021-02-03', ['change']); - await fillInput(page, endFromTimeSelector, '11:11', ['change']); - await fillInput(page, endToTimeSelector, '14:00', ['change']); - await fillInput(page, endFromDateSelector, '2021-02-03', ['change']); - await fillInput(page, endToDateSelector, '2021-02-03', ['change']); - await fillInput(page, '#duration-operand', '1500', ['change']); - await pressElement(page, `${dipolePopoverSelector} .dropdown-option:last-child`, true); - await pressElement(page, '#checkboxes-checkbox-bad'); - await pressElement(page, '#triggerValue-checkbox-OFF'); - await fillInput(page, '#runOverviewFilter .runNumbers-textFilter', '101'); - await fillInput(page, '.fillNumbers-textFilter', '1, 3', ['change']); - await fillInput(page, '.environmentIds-textFilter', 'Dxi029djX, TDI59So3d', ['change']); - await pressElement(page, '#run-types-dropdown-option-2', true); - await pressElement(page, '#beam-mode-dropdown-option-NO\\ BEAM', true); - await fillInput(page, '#nDetectors-operand', '1', ['change']); - await fillInput(page, '#nFlps-operand', '10', ['change']); - await fillInput(page, '#nEpns-operand', '10', ['change']); - await fillInput(page, '#ctfFileCount-operand', '1', ['change']); - await fillInput(page, '#tfFileCount-operand', '1', ['change']); - await fillInput(page, '#otherFileCount-operand', '1', ['change']); - await pressElement(page, '#epnFilterRadioOFF', true); - await page.select('#eorCategories', 'DETECTORS'); - await page.select('#eorTitles', 'CPV'); - await fillInput(page, '#eorDescription', 'some', ['change']); - - const queryParameters = getQueryParameters(page); - expect(queryParameters).to.deep.equal({ - "page": "run-overview", - "filter[runNumbers]": "101", - "filter[detectors][operator]": "and", - "filter[detectors][values]": "ITS", - "filter[tags][values]": "FOOD", - "filter[tags][operation]": "and", - "filter[fillNumbers]": "1, 3", - "filter[o2start][from]": "1612350660000", - "filter[o2start][to]": "1612360800000", - "filter[o2end][from]": "1612350660000", - "filter[o2end][to]": "1612360800000", - "filter[definitions]": "PHYSICS", - "filter[runDuration][operator]": "=", - "filter[runDuration][limit]": "90000000", - "filter[environmentIds]": "Dxi029djX, TDI59So3d", - "filter[runTypes]": "2", - "filter[beamModes]": "NO BEAM", - "filter[runQualities]": "bad", - "filter[nDetectors][operator]": "=", - "filter[nDetectors][limit]": "1", - "filter[nEpns][operator]": "=", - "filter[nEpns][limit]": "10", - "filter[nFlps][operator]": "=", - "filter[nFlps][limit]": "10", - "filter[ctfFileCount][operator]": "=", - "filter[ctfFileCount][limit]": "1", - "filter[tfFileCount][operator]": "=", - "filter[tfFileCount][limit]": "1", - "filter[otherFileCount][operator]": "=", - "filter[otherFileCount][limit]": "1", - "filter[eorReason][category]": "DETECTORS", - "filter[eorReason][title]": "CPV", - "filter[eorReason][description]": "some", - "filter[magnets][l3]": "30003", - "filter[magnets][dipole]": "0", - "filter[epn]": "false", - "filter[triggerValues]": "OFF" - }); - }); - - it('should set filters from lhcPriodOverview to the URL', async () => { - await goToPage(page, 'lhc-period-overview'); - - await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22a'); - await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(2) input[type=text]', '2022'); - await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(3) input[type=text]', 'PbPb'); - const queryParameters = getQueryParameters(page); - expect(queryParameters).to.deep.equal({ - "page": "lhc-period-overview", - "filter[names][]": "LHC22a", - "filter[years][]": "2022", - "filter[pdpBeamTypes][]": "PbPb" - }); - }); - - it('should set filters from qcFlagTypesOverview to the URL', async () => { - await goToPage(page, 'qc-flag-types-overview'); - - await fillInput(page, '.name-filter input[type=text]', 'bad'); - await fillInput(page, '.method-filter input[type=text]', 'bad'); - await pressElement(page, '#badFilterRadioBad', true); - - const queryParameters = getQueryParameters(page); - expect(queryParameters).to.deep.equal({ - "page": "qc-flag-types-overview", - "filter[names][]": "bad", - "filter[methods][]": "bad", - "filter[bad]": "true" - }); - }); - - it('should set filters from runsPerLhcPeriodOverview to the URL', async () => { - await goToPage(page, 'runs-per-lhc-period', { queryParameters: { lhcPeriodId: 2 }}); - const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); - const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); - const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); - - const { - fromDateSelector: startFromDateSelector, - toDateSelector: startToDateSelector, - fromTimeSelector: startFromTimeSelector, - toTimeSelector: startToTimeSelector - } = getPeriodInputsSelectors(startPopoverSelector); - - const { - fromDateSelector: endFromDateSelector, - toDateSelector: endToDateSelector, - fromTimeSelector: endFromTimeSelector, - toTimeSelector: endToTimeSelector - } = getPeriodInputsSelectors(endPopoverSelector); - - await fillInput(page, '#inelasticInteractionRateAvg-operand', '100000', ['change']); - await fillInput(page, '#muInelasticInteractionRate-operand', '100000', ['change']); - await fillInput(page, '#runOverviewFilter .runNumbers-textFilter', '101'); - await fillInput(page, '.fillNumbers-textFilter', '1, 3', ['change']); - - await pressElement(page, `${dipolePopoverSelector} .dropdown-option:last-child`, true); - await fillInput(page, startFromTimeSelector, '11:11', ['change']); - await fillInput(page, startToTimeSelector, '14:00', ['change']); - await fillInput(page, startFromDateSelector, '2021-02-03', ['change']); - await fillInput(page, startToDateSelector, '2021-02-03', ['change']); - await fillInput(page, endFromTimeSelector, '11:11', ['change']); - await fillInput(page, endToTimeSelector, '14:00', ['change']); - await fillInput(page, endFromDateSelector, '2021-02-03', ['change']); - await fillInput(page, endToDateSelector, '2021-02-03', ['change']); - - - const queryParameters = getQueryParameters(page); - expect(queryParameters).to.deep.equal({ - "page": "runs-per-lhc-period", - "lhcPeriodId": "2", - "filter[runNumbers]": "101", - "filter[fillNumbers]": "1, 3", - "filter[o2end][from]": "1612350660000", - "filter[o2end][to]": "1612360800000", - "filter[o2start][from]": "1612350660000", - "filter[o2start][to]": "1612360800000", - "filter[magnets][l3]": "30003", - "filter[magnets][dipole]": "0", - "filter[muInelasticInteractionRate][operator]": "=", - "filter[muInelasticInteractionRate][limit]": "100000", - "filter[inelasticInteractionRateAvg][operator]": "=", - "filter[inelasticInteractionRateAvg][limit]": "100000", - "filter[detectorsQcNotBadFraction][mcReproducibleAsNotBad]": "false" - }); - }); - - it('should set filters from DataPassesPerLhcPeriodOverview to the URL', async () => { - await goToPage(page, 'data-passes-per-lhc-period-overview', { queryParameters: { lhcPeriodId: 2 }}); - - await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1', ['input']); - await pressElement(page, '#checkboxes-checkbox-test', true); - - - const queryParameters = getQueryParameters(page); - expect(queryParameters).to.deep.equal({ - "page": "data-passes-per-lhc-period-overview", - "lhcPeriodId": "2", - "filter[names][]": "LHC22b_apass1", - "filter[permittedNonPhysicsNames]": "test" - }); - }); - - it('should set filters from DataPassesPerSimulationPassOverview to the URL', async () => { - await goToPage(page, 'data-passes-per-simulation-pass-overview', { queryParameters: { simulationPassId: 1 }}); - - await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1', ['input']); - await pressElement(page, '#checkboxes-checkbox-test', true); - - const queryParameters = getQueryParameters(page); - expect(queryParameters).to.deep.equal({ - "page": "data-passes-per-simulation-pass-overview", - "simulationPassId": "1", - "filter[names][]": "LHC22b_apass1", - "filter[permittedNonPhysicsNames]": "test" - }); - }); - - it('should set filters from AnchoredSimulationPassesOverview to the URL', async () => { - await goToPage(page, 'anchored-simulation-passes-overview', { queryParameters: { dataPassId: 1 }}); - - await fillInput(page, '.name-filter input', 'LHC23k6c', ['input']); - - const queryParameters = getQueryParameters(page); - expect(queryParameters).to.deep.equal({ - "page": "anchored-simulation-passes-overview", - "dataPassId": "1", - "filter[names][]": "LHC23k6c" - }); - }); - - it('should set filters from RunsPerSimulationPass to the URL', async () => { - await goToPage(page, 'runs-per-simulation-pass', { queryParameters: { simulationPassId: 2 }}); - - const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); - const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); - const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); - - const { - fromDateSelector: startFromDateSelector, - toDateSelector: startToDateSelector, - fromTimeSelector: startFromTimeSelector, - toTimeSelector: startToTimeSelector - } = getPeriodInputsSelectors(startPopoverSelector); - - const { - fromDateSelector: endFromDateSelector, - toDateSelector: endToDateSelector, - fromTimeSelector: endFromTimeSelector, - toTimeSelector: endToTimeSelector - } = getPeriodInputsSelectors(endPopoverSelector); - - await openFilteringPanel(page); - await pressElement(page, '.timeO2Start-filter .popover-trigger'); - await fillInput(page, startFromTimeSelector, '11:11', ['change']); - await fillInput(page, startToTimeSelector, '14:00', ['change']); - await fillInput(page, startFromDateSelector, '2021-02-03', ['change']); - await fillInput(page, startToDateSelector, '2021-02-03', ['change']); - await fillInput(page, endFromTimeSelector, '11:11', ['change']); - await fillInput(page, endToTimeSelector, '14:00', ['change']); - await fillInput(page, endFromDateSelector, '2021-02-03', ['change']); - await fillInput(page, endToDateSelector, '2021-02-03', ['change']); - await fillInput(page, '.inelasticInteractionRateAtMid-filter input', '1', ['change']); - await fillInput(page, '.inelasticInteractionRateAtEnd-filter input', '1', ['change']); - await fillInput(page, '.inelasticInteractionRateAtStart-filter input', '1', ['change']); - await pressElement(page, `${dipolePopoverSelector} .dropdown-option:last-child`, true); - await pressElement(page, '#mcReproducibleAsNotBadToggle', true); - - // These two are detectorQCNotBadFraction[_id] filters. There are a dozen more, but they are all identical hence why only these were tested - await fillInput(page, '.QC-SPECIFIC-filter input', '1', ['change']); - await fillInput(page, '.ACO-filter input', '1', ['change']); - - const queryParameters = getQueryParameters(page); - expect(queryParameters).to.deep.equal({ - "page": "runs-per-simulation-pass", - "simulationPassId": "2", - "filter[o2end][from]": "1612350660000", - "filter[o2start][from]": "1612350660000", - "filter[o2end][to]": "1612360800000", - "filter[o2start][to]": "1612360800000", - "filter[magnets][l3]": "30003", - "filter[magnets][dipole]": "0", - "filter[inelasticInteractionRateAtStart][operator]": "=", - "filter[inelasticInteractionRateAtStart][limit]": "1", - "filter[inelasticInteractionRateAtMid][operator]": "=", - "filter[inelasticInteractionRateAtMid][limit]": "1", - "filter[inelasticInteractionRateAtEnd][operator]": "=", - "filter[inelasticInteractionRateAtEnd][limit]": "1", - "filter[detectorsQcNotBadFraction][mcReproducibleAsNotBad]": "true", - "filter[detectorsQcNotBadFraction][_20][operator]": "=", - "filter[detectorsQcNotBadFraction][_20][limit]": "0.01", - "filter[detectorsQcNotBadFraction][_17][operator]": "=", - "filter[detectorsQcNotBadFraction][_17][limit]": "0.01" - }); - }); - - it('should set filters from RunsPerSimulationPass to the URL', async () => { - await goToPage(page, 'runs-per-data-pass', { queryParameters: { dataPassId: 1 }}); - - const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); - const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); - const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); - - const { - fromDateSelector: startFromDateSelector, - toDateSelector: startToDateSelector, - fromTimeSelector: startFromTimeSelector, - toTimeSelector: startToTimeSelector - } = getPeriodInputsSelectors(startPopoverSelector); - - const { - fromDateSelector: endFromDateSelector, - toDateSelector: endToDateSelector, - fromTimeSelector: endFromTimeSelector, - toTimeSelector: endToTimeSelector - } = getPeriodInputsSelectors(endPopoverSelector); - - await openFilteringPanel(page); - await pressElement(page, '#detector-filter-dropdown-option-ITS', true); - await pressElement(page, '#tag-dropdown-option-FOOD', true); - await pressElement(page, '.timeO2Start-filter .popover-trigger'); - await fillInput(page, startFromTimeSelector, '11:11', ['change']); - await fillInput(page, startToTimeSelector, '14:00', ['change']); - await fillInput(page, startFromDateSelector, '2021-02-03', ['change']); - await fillInput(page, startToDateSelector, '2021-02-03', ['change']); - await fillInput(page, endFromTimeSelector, '11:11', ['change']); - await fillInput(page, endToTimeSelector, '14:00', ['change']); - await fillInput(page, endFromDateSelector, '2021-02-03', ['change']); - await fillInput(page, endToDateSelector, '2021-02-03', ['change']); - await fillInput(page, '#duration-operand', '1500', ['change']); - await fillInput(page, '.muInelasticInteractionRate-filter input', '1', ['change']); - await fillInput(page, '.inelasticInteractionRateAvg-filter input', '1', ['change']); - await fillInput(page, '.globalAggregatedQuality-filter input', '1', ['change']); - - await pressElement(page, `${dipolePopoverSelector} .dropdown-option:last-child`, true); - await pressElement(page, '#mcReproducibleAsNotBadToggle', true); - - // These two are detectorQCNotBadFraction[_id] filters. There are a dozen more, but they are all identical hence why only these were tested - await fillInput(page, '.QC-SPECIFIC-filter input', '1', ['change']); - await fillInput(page, '.ACO-filter input', '1', ['change']); - - const queryParameters = getQueryParameters(page); - expect(queryParameters).to.deep.equal({ - "page": "runs-per-data-pass", - "dataPassId": "1", - "filter[detectors][operator]": "and", - "filter[detectors][values]": "ITS", - "filter[tags][values]": "FOOD", - "filter[tags][operation]": "and", - "filter[o2end][from]": "1612350660000", - "filter[o2end][to]": "1612360800000", - "filter[o2start][from]": "1612350660000", - "filter[o2start][to]": "1612360800000", - "filter[runDuration][limit]": "90000000", - "filter[runDuration][operator]": "=", - "filter[magnets][l3]": "30003", - "filter[magnets][dipole]": "0", - "filter[muInelasticInteractionRate][operator]": "=", - "filter[muInelasticInteractionRate][limit]": "1", - "filter[inelasticInteractionRateAvg][operator]": "=", - "filter[inelasticInteractionRateAvg][limit]": "1", - "filter[detectorsQcNotBadFraction][mcReproducibleAsNotBad]": "true", - "filter[detectorsQcNotBadFraction][_20][operator]": "=", - "filter[detectorsQcNotBadFraction][_20][limit]": "0.01", - "filter[detectorsQcNotBadFraction][_17][operator]": "=", - "filter[detectorsQcNotBadFraction][_17][limit]": "0.01", - "filter[gaq][notBadFraction][operator]": "=", - "filter[gaq][notBadFraction][limit]": "0.01", - "filter[gaq][mcReproducibleAsNotBad]": "true" - }); - }); - - after(async () => await defaultAfter(page, browser)); -} diff --git a/test/public/Filters/urlToFilter.test.js b/test/public/Filters/urlToFilter.test.js deleted file mode 100644 index 06eb280039..0000000000 --- a/test/public/Filters/urlToFilter.test.js +++ /dev/null @@ -1,372 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const { - defaultBefore, - defaultAfter, - fillInput, - getPopoverSelector, - getPeriodInputsSelectors, - pressElement, - openFilteringPanel, - expectInputValue, -} = require('../defaults.js'); - -module.exports = () => { - let page; - let browser; - - before(async () => { - [page, browser] = await defaultBefore(); - }); - - it('should apply filters from url in logsOverviewPage', async () => { - const url = 'http://localhost:4000/?page=log-overview&filter[author]=Jane&filter[title]=bogusbogusbogus&filter[content]=particle'+ - '&filter[tags][values]=DPG&filter[tags][operation]=and&filter[runNumbers]=1%2C2&filter[environmentIds]=8E4aZTjY'+ - '&filter[fillNumbers]=1%2C%206&filter[created][from]=1580637600000&filter[created][to]=1580641200000'; - - - await page.goto(url, { waitUntil: 'load' }); - - const firstCheckboxId = 'tag-dropdown-option-DPG'; - const popoverTrigger = '.createdAt-filter .popover-trigger'; - - await page.waitForSelector(popoverTrigger); - await openFilteringPanel(page); - - const popOverSelector = await getPopoverSelector(await page.$(popoverTrigger)); - const { fromDateSelector, toDateSelector, fromTimeSelector, toTimeSelector } = getPeriodInputsSelectors(popOverSelector); - - await expectInputValue(page, '.title-textFilter', 'bogusbogusbogus'); - await expectInputValue(page, '#authorFilterText', 'Jane'); - await expectInputValue(page, '.content-textFilter', 'particle'); - await pressElement(page, '.tags-filter .dropdown-trigger'); - await page.waitForSelector(`#${firstCheckboxId}:checked`); - await expectInputValue(page, '.environments-filter input', '8E4aZTjY'); - await expectInputValue(page, '.runNumbers-textFilter', '1,2'); - await expectInputValue(page, '.fillNumbers-textFilter', '1, 6'); - await expectInputValue(page, fromDateSelector, '2020-02-02'); - await expectInputValue(page, toDateSelector, '2020-02-02'); - - await expectInputValue(page, fromTimeSelector, '10:00'); - await expectInputValue(page, toTimeSelector, '11:00'); - }); - - it('should set filters from EnvironmentsOverview to the URL', async () => { - const url = 'http://localhost:4000/?page=env-overview&filter[created][from]=1565301600000&filter[created][to]=1565474340000' + - '&filter[runNumbers]=10&filter[statusHistory]=C-R-D-X&filter[currentStatus]=DESTROYED&filter[ids]=Dxi029djX%2C%20TDI59So3d'; - await page.goto(url, { waitUntil: 'load' }); - await openFilteringPanel(page); - - const popoverTrigger = '.createdAt-filter .popover-trigger'; - const createdAtPopoverSelector = await getPopoverSelector(await page.$(popoverTrigger)); - const periodInputsSelectors = getPeriodInputsSelectors(createdAtPopoverSelector); - - await expectInputValue(page, '.runs-filter input', '10'); - await expectInputValue(page, '.id-filter input', 'Dxi029djX, TDI59So3d'); - await page.waitForSelector('#checkboxes-checkbox-DESTROYED:checked'); - await expectInputValue(page, '.historyItems-filter input', 'C-R-D-X'); - await expectInputValue(page, periodInputsSelectors.fromDateSelector, '2019-08-08'); - await expectInputValue(page, periodInputsSelectors.toDateSelector, '2019-08-10'); - await expectInputValue(page, periodInputsSelectors.fromTimeSelector, '22:00'); - await expectInputValue(page, periodInputsSelectors.toTimeSelector, '21:59'); - }); - - it('should set filters from LhcFillsOverview to the URL', async () => { - const url = 'http://localhost:4000/?page=lhc-fill-overview&filter[beamDuration][operator]=%3D&filter[beamDuration][limit]=00%3A01%3A40&' + - 'filter[runDuration][operator]=%3D&filter[runDuration][limit]=00%3A00%3A00&filter[hasStableBeams]=true&filter[stableBeamsStart][from]=1565251200000&' + - 'filter[stableBeamsStart][to]=1565258400000&filter[stableBeamsEnd][from]=1647907200000&filter[stableBeamsEnd][to]=1647989940000&filter[beamTypes]=p-Pb&filter[schemeName]=Single_12b_8_1024_8_2018'; - - await page.goto(url, { waitUntil: 'load' }); - - const sbEndPopoverTrigger = '.stableBeamsEnd-filter .popover-trigger'; - const sbStartPopoverTrigger = '.stableBeamsStart-filter .popover-trigger'; - const sbStartPopOverSelector = await getPopoverSelector(await page.$(sbStartPopoverTrigger)); - const sbEndPopOverSelector = await getPopoverSelector(await page.$(sbEndPopoverTrigger)); - const filterSchemeNameInputField= '.fillingSchemeName-filter input'; - const { - fromDateSelector: sbStartFromDateSelector, - toDateSelector: sbStartToDateSelector, - fromTimeSelector: sbStartFromTimeSelector, - toTimeSelector: sbStartToTimeSelector - } = getPeriodInputsSelectors(sbStartPopOverSelector); - - const { - fromDateSelector: sbEndFromDateSelector, - toDateSelector: sbEndToDateSelector, - fromTimeSelector: sbEndFromTimeSelector, - toTimeSelector: sbEndToTimeSelector - } = getPeriodInputsSelectors(sbEndPopOverSelector); - - await openFilteringPanel(page); - await expectInputValue(page, '#beam-duration-filter-operand', '00:01:40'); - await expectInputValue(page, '#run-duration-filter-operand', '00:00:00'); - await expectInputValue(page, sbStartFromDateSelector, '2019-08-08'); - await expectInputValue(page, sbStartToDateSelector, '2019-08-08'); - await expectInputValue(page, sbStartFromTimeSelector, '08:00'); - await expectInputValue(page, sbStartToTimeSelector, '10:00'); - await expectInputValue(page, sbEndFromDateSelector, '2022-03-22'); - await expectInputValue(page, sbEndToDateSelector, '2022-03-22'); - await expectInputValue(page, sbEndFromTimeSelector, '00:00'); - await expectInputValue(page, sbEndToTimeSelector, '22:59'); - await expectInputValue(page, filterSchemeNameInputField, 'Single_12b_8_1024_8_2018'); - await page.waitForSelector('#beam-types-checkbox-p-Pb:checked'); - }); - - it('should set filters from runsOverview to the URL', async () => { - const url = 'http://localhost:4000/?page=run-overview&filter[runNumbers]=101&filter[detectors][operator]=and&filter[detectors][values]=ITS&filter[tags][values]=FOOD&' + - 'filter[tags][operation]=and&filter[fillNumbers]=1%2C%203&filter[o2start][from]=1612347060000&filter[o2start][to]=1612357200000&filter[o2end][from]=1612347060000&' + - 'filter[o2end][to]=1612357200000&filter[definitions]=PHYSICS&filter[runDuration][operator]=%3D&filter[runDuration][limit]=90000000' + - '&filter[environmentIds]=Dxi029djX%2C%20TDI59So3d&filter[runTypes]=2&filter[beamModes]=NO%20BEAM&filter[runQualities]=bad&filter[nDetectors][operator]=%3D&' + - 'filter[nDetectors][limit]=1&filter[nEpns][operator]=%3D&filter[nEpns][limit]=10&filter[nFlps][operator]=%3D&filter[nFlps][limit]=10&filter[ctfFileCount][operator]=%3D&' + - 'filter[ctfFileCount][limit]=1&filter[tfFileCount][operator]=%3D&filter[tfFileCount][limit]=1&filter[otherFileCount][operator]=%3D&filter[otherFileCount][limit]=1&' + - 'filter[eorReason][category]=DETECTORS&filter[eorReason][title]=CPV&filter[eorReason][description]=some&filter[magnets][l3]=30003&filter[magnets][dipole]=0&filter[epn]=false&filter[triggerValues]=OFF'; - - await page.goto(url, { waitUntil: 'load' }); - - const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); - const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); - const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); - - const { - fromDateSelector: startFromDateSelector, - toDateSelector: startToDateSelector, - fromTimeSelector: startFromTimeSelector, - toTimeSelector: startToTimeSelector - } = getPeriodInputsSelectors(startPopoverSelector); - - const { - fromDateSelector: endFromDateSelector, - toDateSelector: endToDateSelector, - fromTimeSelector: endFromTimeSelector, - toTimeSelector: endToTimeSelector - } = getPeriodInputsSelectors(endPopoverSelector); - - await openFilteringPanel(page); - await page.waitForSelector('#detector-filter-dropdown-option-ITS:checked'); - await page.waitForSelector('#run-types-dropdown-option-2:checked'); - await page.waitForSelector('#beam-mode-dropdown-option-NO\\ BEAM:checked'); - await page.waitForSelector('#tag-dropdown-option-FOOD:checked'); - await page.waitForSelector('#run-definition-checkbox-PHYSICS:checked'); - await page.waitForSelector('#epnFilterRadioOFF:checked'); - await pressElement(page, '.timeO2Start-filter .popover-trigger'); - await page.waitForSelector('#checkboxes-checkbox-bad:checked'); - await page.waitForSelector('#triggerValue-checkbox-OFF:checked'); - await page.waitForSelector(`${dipolePopoverSelector} .dropdown-option:last-child input:checked`); - await expectInputValue(page, '#duration-operand', '1500'); - await expectInputValue(page, '#runOverviewFilter .runNumbers-textFilter', '101'); - await expectInputValue(page, '.fillNumbers-textFilter', '1, 3'); - await expectInputValue(page, '.environmentIds-textFilter', 'Dxi029djX, TDI59So3d'); - await expectInputValue(page, '#nDetectors-operand', '1'); - await expectInputValue(page, '#nFlps-operand', '10'); - await expectInputValue(page, '#nEpns-operand', '10'); - await expectInputValue(page, '#ctfFileCount-operand', '1'); - await expectInputValue(page, '#tfFileCount-operand', '1'); - await expectInputValue(page, '#otherFileCount-operand', '1'); - await expectInputValue(page, '#eorDescription', 'some'); - await expectInputValue(page, '#eorTitles', 'CPV'); - await expectInputValue(page, '#eorCategories', 'DETECTORS'); - await expectInputValue(page, startFromTimeSelector, '10:11'); - await expectInputValue(page, startToTimeSelector, '13:00'); - await expectInputValue(page, startFromDateSelector, '2021-02-03'); - await expectInputValue(page, startToDateSelector, '2021-02-03'); - await expectInputValue(page, endFromTimeSelector, '10:11'); - await expectInputValue(page, endToTimeSelector, '13:00'); - await expectInputValue(page, endFromDateSelector, '2021-02-03'); - await expectInputValue(page, endToDateSelector, '2021-02-03'); - }); - - it('should set filters from lhcPriodOverview to the URL', async () => { - const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=LHC22a&filter[years][]=2022&filter[pdpBeamTypes][]=PbPb'; - await page.goto(url, { waitUntil: 'load' }); - - await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22a'); - await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(2) input[type=text]', '2022'); - await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(3) input[type=text]', 'PbPb'); - }); - - it('should set filters from qcFlagTypesOverview to the URL', async () => { - const url = 'http://localhost:4000/?page=qc-flag-types-overview&filter[names][]=bad&filter[methods][]=bad&filter[bad]=true'; - await page.goto(url, { waitUntil: 'load' }); - - await expectInputValue(page, '.name-filter input[type=text]', 'bad'); - await expectInputValue(page, '.method-filter input[type=text]', 'bad'); - await page.waitForSelector('#badFilterRadioBad:checked'); - }); - - it('should set filters from runsPerLhcPeriodOverview to the URL', async () => { - const url = 'http://localhost:4000/?page=runs-per-lhc-period&lhcPeriodId=2&filter[runNumbers]=101&filter[fillNumbers]=1%2C%203&filter[o2start][from]=1612347060000&' + - 'filter[o2start][to]=1612357200000&filter[o2end][from]=1612347060000&filter[o2end][to]=1612357200000&filter[magnets][l3]=30003&filter[magnets][dipole]=0&' + - 'filter[muInelasticInteractionRate][operator]=%3D&filter[muInelasticInteractionRate][limit]=100000&filter[inelasticInteractionRateAvg][operator]=%3D&filter[inelasticInteractionRateAvg][limit]=100000'; - await page.goto(url, { waitUntil: 'load' }); - - const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); - const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); - const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); - - const { - fromDateSelector: startFromDateSelector, - toDateSelector: startToDateSelector, - fromTimeSelector: startFromTimeSelector, - toTimeSelector: startToTimeSelector - } = getPeriodInputsSelectors(startPopoverSelector); - - const { - fromDateSelector: endFromDateSelector, - toDateSelector: endToDateSelector, - fromTimeSelector: endFromTimeSelector, - toTimeSelector: endToTimeSelector - } = getPeriodInputsSelectors(endPopoverSelector); - - await expectInputValue(page, '#inelasticInteractionRateAvg-operand', '100000'); - await expectInputValue(page, '#muInelasticInteractionRate-operand', '100000'); - await expectInputValue(page, '#runOverviewFilter .runNumbers-textFilter', '101'); - await expectInputValue(page, '.fillNumbers-textFilter', '1, 3'); - await expectInputValue(page, startFromTimeSelector, '10:11'); - await expectInputValue(page, startToTimeSelector, '13:00'); - await expectInputValue(page, startFromDateSelector, '2021-02-03'); - await expectInputValue(page, startToDateSelector, '2021-02-03'); - await expectInputValue(page, endFromTimeSelector, '10:11'); - await expectInputValue(page, endToTimeSelector, '13:00'); - await expectInputValue(page, endFromDateSelector, '2021-02-03'); - await expectInputValue(page, endToDateSelector, '2021-02-03'); - await page.waitForSelector(`${dipolePopoverSelector} .dropdown-option:last-child input:checked`); - }); - - it('should set filters from DataPassesPerLhcPeriodOverview to the URL', async () => { - const url = 'http://localhost:4000/?page=data-passes-per-lhc-period-overview&lhcPeriodId=2&filter[names][]=LHC22b_apass1&filter[permittedNonPhysicsNames]=test'; - await page.goto(url, { waitUntil: 'load' }); - - await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1'); - await page.waitForSelector('#checkboxes-checkbox-test:checked'); - }); - - it('should set filters from DataPassesPerSimulationPassOverview to the URL', async () => { - const url = 'http://localhost:4000/?page=data-passes-per-simulation-pass-overview&simulationPassId=1&filter[names][]=LHC22b_apass1&filter[permittedNonPhysicsNames]=test'; - await page.goto(url, { waitUntil: 'load' }); - - await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1'); - await page.waitForSelector('#checkboxes-checkbox-test:checked'); - }); - - it('should set filters from AnchoredSimulationPassesOverview to the URL', async () => { - const url = 'http://localhost:4000/?page=anchored-simulation-passes-overview&dataPassId=1&filter[names][]=LHC23k6c'; - await page.goto(url, { waitUntil: 'load' }); - - await expectInputValue(page, '.name-filter input', 'LHC23k6c'); - }); - - it('should set filters from RunsPerSimulationPass to the URL', async () => { - const url = 'http://localhost:4000/?page=runs-per-simulation-pass&simulationPassId=2&filter[o2start][from]=1612347060000&' + - 'filter[o2start][to]=1612357200000&filter[o2end][from]=1612347060000&filter[o2end][to]=1612357200000&' + - 'filter[magnets][l3]=30003&filter[magnets][dipole]=0&filter[inelasticInteractionRateAtStart][operator]=%3D&' + - 'filter[inelasticInteractionRateAtStart][limit]=1&filter[inelasticInteractionRateAtMid][operator]=%3D&' + - 'filter[inelasticInteractionRateAtMid][limit]=1&filter[inelasticInteractionRateAtEnd][operator]=%3D&' + - 'filter[inelasticInteractionRateAtEnd][limit]=1&filter[detectorsQcNotBadFraction][mcReproducibleAsNotBad]=true&' + - 'filter[detectorsQcNotBadFraction][_20][operator]=%3D&filter[detectorsQcNotBadFraction][_20][limit]=0.01&' + - 'filter[detectorsQcNotBadFraction][_17][operator]=%3D&filter[detectorsQcNotBadFraction][_17][limit]=0.01'; - - await page.goto(url, { waitUntil: 'load' }); - - const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); - const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); - const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); - - const { - fromDateSelector: startFromDateSelector, - toDateSelector: startToDateSelector, - fromTimeSelector: startFromTimeSelector, - toTimeSelector: startToTimeSelector - } = getPeriodInputsSelectors(startPopoverSelector); - - const { - fromDateSelector: endFromDateSelector, - toDateSelector: endToDateSelector, - fromTimeSelector: endFromTimeSelector, - toTimeSelector: endToTimeSelector - } = getPeriodInputsSelectors(endPopoverSelector); - - await openFilteringPanel(page); - await expectInputValue(page, '.inelasticInteractionRateAtMid-filter input', '1'); - await expectInputValue(page, '.inelasticInteractionRateAtEnd-filter input', '1'); - await expectInputValue(page, '.inelasticInteractionRateAtStart-filter input', '1'); - await expectInputValue(page, startFromTimeSelector, '10:11'); - await expectInputValue(page, startToTimeSelector, '13:00'); - await expectInputValue(page, startFromDateSelector, '2021-02-03'); - await expectInputValue(page, startToDateSelector, '2021-02-03'); - await expectInputValue(page, endFromTimeSelector, '10:11'); - await expectInputValue(page, endToTimeSelector, '13:00'); - await expectInputValue(page, endFromDateSelector, '2021-02-03'); - await expectInputValue(page, endToDateSelector, '2021-02-03'); - await page.waitForSelector(`${dipolePopoverSelector} .dropdown-option:last-child input:checked`); - await page.waitForSelector('#mcReproducibleAsNotBadToggle input:checked'); - - - // These two are detectorQCNotBadFraction[_id] filters. There are a dozen more, but they are all identical hence why only these were tested - await expectInputValue(page, '.QC-SPECIFIC-filter input', '1'); - await expectInputValue(page, '.ACO-filter input', '1'); - }); - - it('should set filters from RunsPerSimulationPass to the URL', async () => { - const url = 'http://localhost:4000/?page=runs-per-data-pass&dataPassId=1&filter[detectors][operator]=and&filter[detectors][values]=ITS&' + - 'filter[tags][values]=FOOD&filter[tags][operation]=and&filter[o2start][from]=1612347060000&filter[o2start][to]=1612357200000&' + - 'filter[o2end][from]=1612347060000&filter[o2end][to]=1612357200000&filter[runDuration][operator]=%3D&filter[runDuration][limit]=90000000&' + - 'filter[magnets][l3]=30003&filter[magnets][dipole]=0&filter[muInelasticInteractionRate][operator]=%3D&filter[muInelasticInteractionRate][limit]=1&' + - 'filter[inelasticInteractionRateAvg][operator]=%3D&filter[inelasticInteractionRateAvg][limit]=1&filter[detectorsQcNotBadFraction][mcReproducibleAsNotBad]=true&' + - 'filter[detectorsQcNotBadFraction][_20][operator]=%3D&filter[detectorsQcNotBadFraction][_20][limit]=0.01&filter[detectorsQcNotBadFraction][_17][operator]=%3D&' + - 'filter[detectorsQcNotBadFraction][_17][limit]=0.01&filter[gaq][notBadFraction][operator]=%3D&filter[gaq][notBadFraction][limit]=0.01&filter[gaq][mcReproducibleAsNotBad]=true'; - - await page.goto(url, { waitUntil: 'load' }); - - const dipolePopoverSelector = await getPopoverSelector(await page.$('.aliceL3AndDipoleCurrent-filter .popover-trigger')); - const startPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); - const endPopoverSelector = await getPopoverSelector(await page.$('.timeO2End-filter .popover-trigger')); - - const { - fromDateSelector: startFromDateSelector, - toDateSelector: startToDateSelector, - fromTimeSelector: startFromTimeSelector, - toTimeSelector: startToTimeSelector - } = getPeriodInputsSelectors(startPopoverSelector); - - const { - fromDateSelector: endFromDateSelector, - toDateSelector: endToDateSelector, - fromTimeSelector: endFromTimeSelector, - toTimeSelector: endToTimeSelector - } = getPeriodInputsSelectors(endPopoverSelector); - - await openFilteringPanel(page); - await expectInputValue(page, startFromTimeSelector, '10:11'); - await expectInputValue(page, startToTimeSelector, '13:00'); - await expectInputValue(page, startFromDateSelector, '2021-02-03'); - await expectInputValue(page, startToDateSelector, '2021-02-03'); - await expectInputValue(page, endFromTimeSelector, '10:11'); - await expectInputValue(page, endToTimeSelector, '13:00'); - await expectInputValue(page, endFromDateSelector, '2021-02-03'); - await expectInputValue(page, endToDateSelector, '2021-02-03'); - await expectInputValue(page, '#duration-operand', '1500'); - await expectInputValue(page, '.muInelasticInteractionRate-filter input', '1'); - await expectInputValue(page, '.inelasticInteractionRateAvg-filter input', '1'); - await expectInputValue(page, '.globalAggregatedQuality-filter input', '1'); - await fillInput(page, '.ACO-filter input', '1', ['change']); - await fillInput(page, '.QC-SPECIFIC-filter input', '1', ['change']); - - await page.waitForSelector('#detector-filter-dropdown-option-ITS'); - await page.waitForSelector('#tag-dropdown-option-FOOD'); - await page.waitForSelector(`${dipolePopoverSelector} .dropdown-option:last-child input:checked`); - await page.waitForSelector('#mcReproducibleAsNotBadToggle input:checked'); - }); - - after(async () => await defaultAfter(page, browser)); -} diff --git a/test/public/components/filtersPopoverPanel.test.js b/test/public/components/filtersPopoverPanel.test.js deleted file mode 100644 index 53df9e8ee3..0000000000 --- a/test/public/components/filtersPopoverPanel.test.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const { expect } = require('chai'); -const { defaultBefore, defaultAfter, pressElement, takeScreenshot, expectInputValue } = require('../defaults.js'); - -module.exports = () => { - let page; - let browser; - let context; - let url; - - before(async () => { - [page, browser, url] = await defaultBefore(page, browser); - context = browser.defaultBrowserContext(); - context.overridePermissions(url, ['clipboard-read', 'clipboard-write', 'clipboard-sanitized-write']); - }); - - it('Should copy url when clicking filer copy button', async () => { - const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb'; - await page.goto(url, { waitUntil: 'load' }); - await takeScreenshot(page, 'test'); - await pressElement(page, '#copy-filters', true); - - const clipboardContents = await page.evaluate(async () => decodeURI(await navigator.clipboard.readText())); - expect(clipboardContents).to.equal(url); - }); - - it('Should set filters when pressing paste active filters button', async () => { - const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb'; - - await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); - await pressElement(page, '#paste-filters', true); - - const actualUrl = page.url(); - expect(actualUrl).to.equal(url); - - await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'name'); - await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(2) input[type=text]', '100'); - await expectInputValue(page, 'div.flex-row.items-baseline:nth-of-type(3) input[type=text]', 'PbPb'); - }); - - it('Should reset filters when pressing the reset all filters button', async () => { - const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name&filter[years][]=100&filter[pdpBeamTypes][]=PbPb'; - - await page.goto(url, { waitUntil: 'load' }); - - await pressElement(page, '.dropdown #reset-filters', true); - const actualUrl = page.url(); - expect(actualUrl).to.equal('http://localhost:4000/?page=lhc-period-overview'); - - await expectInputValue(page, '.name-filter input', ''); - await expectInputValue(page, '.year-filter input', ''); - await expectInputValue(page, '.pdpBeamTypes-filter input', ''); - }); - - after(async () => { - await defaultAfter(page, browser); - }); -}; diff --git a/test/public/components/index.js b/test/public/components/index.js index 794ae79252..5e06743c62 100644 --- a/test/public/components/index.js +++ b/test/public/components/index.js @@ -12,11 +12,7 @@ */ const NavBarSuite = require('./navBar.test') -const WarningSuite = require('./warnings.test') -const FiltersPanelSuite = require('./filtersPopoverPanel.test') module.exports = () => { describe('Navbar component', NavBarSuite); - describe('Warning component', WarningSuite) - describe('FiltersPanelPopover component', FiltersPanelSuite) }; diff --git a/test/public/components/warnings.test.js b/test/public/components/warnings.test.js deleted file mode 100644 index 5fb32457f3..0000000000 --- a/test/public/components/warnings.test.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const { expect } = require('chai'); -const { - defaultBefore, - defaultAfter, - getInnerText, - pressElement, - goToPage, -} = require('../defaults.js'); - -module.exports = () => { - let page; - let browser; - let url; - let context; - - before(async () => { - [page, browser, url] = await defaultBefore(page, browser); - context = browser.defaultBrowserContext(); - context.overridePermissions(url, ['clipboard-read', 'clipboard-write', 'clipboard-sanitized-write']); - }); - - it('Should show warning when a filter in the url is not recognised', async () => { - await page.goto('http://localhost:4000/?page=log-overview&filter[fake]=fake', { waitUntil: 'load' }); - const warningText = await getInnerText(await page.waitForSelector('.alert-warning > ul')); - - expect(warningText).to.equal('Unknown Filters:\nThe filters: [\'fake\']; are not reccognised. Check if they are spelled correctly.'); - }); - - it('Should remove warnings entry after clicking the x icon', async () => { - await pressElement(page, '.alert-warning .btn', true); - const warning = await page.$('.alert-warning'); - - expect(warning).to.be.null; - }); - - it('Should show warning when a url filter cannot be parsed/normalized', async () => { - await page.goto('http://localhost:4000/?page=run-overview&filter[detectors][operator]=or&filter[detecttors][values]=CTP&filter[tagss][values]=CPV&filter[tags][operation]=or', { waitUntil: 'load' }); - const unparsableWarningText = await getInnerText(await page.waitForSelector('.alert-warning > ul > li:nth-of-type(1)')); - const unknownFilterWarningText = await getInnerText(await page.waitForSelector('.alert-warning > ul > li:nth-of-type(2)')); - - // The tags and detectors filters will fail if it has no value. - // However, if the url also contains its operator, it will still attempt to set the filters, which would fail, hence the warning - expect(unparsableWarningText).to.equal('Unparsable Filters:\nThe following filter-value pairs could not be parsed: [detectors[operator]=or, tags[operation]=or]'); - expect(unknownFilterWarningText).to.equal('Unknown Filters:\nThe filters: [\'detecttors\', \'tagss\']; are not reccognised. Check if they are spelled correctly.'); - }); - - it('Should show warning if an unparsable filter url is pasted', async () => { - const url = 'unparsable url'; - await goToPage(page, 'log-overview'); - - await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); - await pressElement(page, '.dropdown #paste-filters', true); - - const warningText = await getInnerText(await page.waitForSelector('.alert-warning > ul')); - expect(warningText).to.equal('Unparseable URL:\nURL could not be parsed. URL: unparsable url'); - }); - - it('Should show warning if filter url is pasted on the wong page', async () => { - const url = 'http://localhost:4000/?page=lhc-period-overview&filter[names][]=name'; - await goToPage(page, 'log-overview'); - - await page.evaluate(async (url) => await navigator.clipboard.writeText(url), url); - await pressElement(page, '.dropdown #paste-filters', true); - - const warningText = await getInnerText(await page.waitForSelector('.alert-warning > ul')); - expect(warningText).to.equal('Page-Filter mismatch:\nThe filters provided were meant for lhc-period-overview'); - }); - - after(async () => { - await defaultAfter(page, browser); - }); -}; diff --git a/test/public/dataPasses/overviewPerLhcPeriod.test.js b/test/public/dataPasses/overviewPerLhcPeriod.test.js index a6215dd989..4ab08e8de0 100644 --- a/test/public/dataPasses/overviewPerLhcPeriod.test.js +++ b/test/public/dataPasses/overviewPerLhcPeriod.test.js @@ -164,7 +164,7 @@ module.exports = () => { it('should successfully apply data pass name filter', async () => { await pressElement(page, '#openFilterToggle'); - await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1'); + await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1', ['change']); await expectColumnValues(page, 'name', ['deleted\nLHC22b_apass1\nSkimmable']); diff --git a/test/public/dataPasses/overviewPerSimulationPass.test.js b/test/public/dataPasses/overviewPerSimulationPass.test.js index 188ec17dc2..27b6c2d2c9 100644 --- a/test/public/dataPasses/overviewPerSimulationPass.test.js +++ b/test/public/dataPasses/overviewPerSimulationPass.test.js @@ -113,7 +113,7 @@ module.exports = () => { it('should successfully apply data pass name filter', async () => { await pressElement(page, '#openFilterToggle'); - await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1'); + await fillInput(page, 'div.flex-row.items-baseline:nth-of-type(1) input[type=text]', 'LHC22b_apass1', ['change']); await expectColumnValues(page, 'name', ['deleted\nLHC22b_apass1\nSkimmable']); await pressElement(page, '#reset-filters', true); diff --git a/test/public/defaults.js b/test/public/defaults.js index 9dac35f2bb..d841c4bc05 100644 --- a/test/public/defaults.js +++ b/test/public/defaults.js @@ -198,15 +198,14 @@ module.exports.waitForTableLength = waitForTableToLength; * Wait for the total number of elements to be the expected one * * @param {puppeteer.Page} page The puppeteer page where the table is located - * @param {number} amount the expected amount of items. If amount is 0 it is converted to undefined, as empty tables don't display a row count + * @param {number} amount the expected amount of items * @return {Promise} resolves once the expected amount is present */ module.exports.waitForTableTotalRowsCountToEqual = async (page, amount) => { try { - amount = amount === 0 ? undefined : `${amount}`; await page.waitForSelector('#totalRowsCount'); await page.waitForFunction( - (amount) => document.querySelector('#totalRowsCount')?.innerText === amount, + (amount) => document.querySelector('#totalRowsCount').innerText === `${amount}`, {}, amount, ); @@ -276,26 +275,12 @@ exports.waitForNavigation = waitForNavigation; * @returns {Promise} Whether the element was clickable or not. */ module.exports.pressElement = async (page, selector, jsClick = false) => { - await page.waitForFunction( - (sel, isJsClick) => { - const element = document.querySelector(sel); - - if (!element) { - return false; - } - // Moving the click to outside the function causes it to fail for unknown reasons - if (isJsClick) { - element.click(); - } + const elementHandler = await page.waitForSelector(selector); - return true; - }, - {}, - selector, jsClick - ); - - if (!jsClick) { - await page.click(selector); + if (jsClick) { + await elementHandler.evaluate((element) => element.click()); + } else { + await elementHandler.click(selector); } }; @@ -668,24 +653,14 @@ module.exports.checkColumnBalloon = async (page, rowIndex, columnIndex) => { * @return {Promise} resolves once the value has been typed */ module.exports.fillInput = async (page, inputSelector, value, events = ['input']) => { - await page.waitForFunction((inputSelector, value, events) => { + await page.waitForSelector(inputSelector); + await page.evaluate((inputSelector, value, events) => { const element = document.querySelector(inputSelector); - - if (!element) { - return false; - } - element.value = value; - for (const eventKey of events) { element.dispatchEvent(new Event(eventKey, { bubbles: true })); } - - return true; - }, - {}, - inputSelector, value, events - ); + }, inputSelector, value, events); }; /** @@ -880,10 +855,10 @@ module.exports.testTableSortingByColumn = async (page, columnId) => { * @return {Promise} resolve once data was successfully validated */ module.exports.validateTableData = async (page, validators) => { + await page.waitForSelector('table tbody'); for (const [columnId, validator] of validators) { - await page.waitForSelector(`table tbody .column-${columnId}`); - const columnData = await getColumnCellsInnerTexts(page, columnId); + expect(columnData, `Too few values for column ${columnId} or there is no such column`).to.be.length.greaterThan(0); expect( columnData.every((cellData) => validator(cellData)), `Invalid data in column ${columnId}: (${columnData})`, @@ -1002,14 +977,3 @@ module.exports.resetFilters = async (page) => { { timeout: 5000 }, ); }; - -/** - * Fuction that waits for a button to become active - * @param {puppeteer.page} page page handler - * @param {string} selector Css selector for the button. - */ -module.exports.waitForButtonToBecomeActive = async (page, selector) => await page.waitForFunction((sel) => { - const button = document.querySelector(sel); - return button && !button.disabled; - }, {}, selector); - diff --git a/test/public/index.js b/test/public/index.js index 293d9a9e94..8ebdc23e68 100644 --- a/test/public/index.js +++ b/test/public/index.js @@ -27,11 +27,9 @@ const ComponentsSuite = require('./components'); const SimulationPassesSuite = require('./simulationPasses'); const QcFlagTypesSuite = require('./qcFlagTypes'); const QcFlagsSuite = require('./qcFlags'); -const FilterSuite = require('./Filters'); module.exports = () => { describe('Components', ComponentsSuite); - describe('Filters', FilterSuite); describe('LhcPeriods', LhcPeriodsSuite); describe('LhcFills', LhcFillsSuite); describe('Logs', LogsSuite); diff --git a/test/public/logs/overview.test.js b/test/public/logs/overview.test.js index f2b0d14f89..39119d7ef1 100644 --- a/test/public/logs/overview.test.js +++ b/test/public/logs/overview.test.js @@ -34,9 +34,6 @@ const { waitForEmptyTable, waitForTableTotalRowsCountToEqual, waitForTableFirstRowIndexToEqual, - resetFilters, - getPeriodInputsSelectors, - openFilteringPanel, } = require('../defaults.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -91,8 +88,184 @@ module.exports = () => { await checkColumnBalloon(page, 1, 5); }); + it('can filter by log title', async () => { + await waitForTableLength(page, 10); + + await pressElement(page, '#openFilterToggle'); + await page.waitForSelector('#titleFilterText'); + + await fillInput(page, '#titleFilterText', 'first'); + await waitForTableLength(page, 1); + + await fillInput(page, '#titleFilterText', 'bogusbogusbogus'); + await waitForEmptyTable(page); + + await pressElement(page, '#reset-filters'); + }); + + it('should successfully provide an input to filter on log content', async () => { + await waitForTableLength(page, 10); + + await fillInput(page, '#contentFilterText', 'particle'); + await waitForTableLength(page, 2); + + await fillInput(page, '#titleFilterText', 'this-content-do-not-exists-anywhere'); + await waitForEmptyTable(page); + + await pressElement(page, '#reset-filters'); + }); + + it('can filter by log author', async () => { + await waitForTableLength(page, 10); + + await fillInput(page, '#authorFilterText', 'Jane'); + await waitForEmptyTable(page); + + await pressElement(page, '#reset-filters'); + + await waitForTableLength(page, 10); + + await fillInput(page, '#authorFilterText', 'John'); + await waitForTableLength(page, 5); + + await pressElement(page, '#reset-filters'); + }); + + it('should successfully provide an easy-to-access button to filter in/out anonymous logs', async () => { + // Close the filter panel + await pressElement(page, '#openFilterToggle'); + await waitForTableTotalRowsCountToEqual(page, 119); + + const authors = await getColumnCellsInnerTexts(page, 'author'); + expect(authors.some((author) => author === 'Anonymous')).to.be.true; + + await pressElement(page, '#main-action-bar > div:nth-child(1) .switch'); + await waitForTableTotalRowsCountToEqual(page, 117); + + await checkColumnValuesWithRegex(page, 'author', '^Anonymous$', { + negation: true, + }); + + await pressElement(page, '#main-action-bar > div:nth-child(1) .switch'); + await waitForTableTotalRowsCountToEqual(page, 119); + await checkColumnValuesWithRegex(page, 'author', '^Anonymous$', { + valuesCheckingMode: 'some', + }); + }); + + it('can filter by creation date', async () => { + await pressElement(page, '#openFilterToggle'); + + await waitForTableTotalRowsCountToEqual(page, 119); + + // Insert a minimum date into the filter + const limit = '2020-02-02'; + await fillInput(page, '#createdFilterFrom', limit); + await fillInput(page, '#createdFilterTo', limit); + await waitForTableLength(page, 1); + + await pressElement(page, '#reset-filters'); + }); + + it('can filter by tags', async () => { + await waitForTableTotalRowsCountToEqual(page, 119); + + await pressElement(page, '.tags-filter .dropdown-trigger'); + + // Select the second available filter and wait for the changes to be processed + const firstCheckboxId = 'tag-dropdown-option-DPG'; + await pressElement(page, `#${firstCheckboxId}`, true); + await waitForTableLength(page, 1); + + // Deselect the filter and wait for the changes to process + await pressElement(page, `#${firstCheckboxId}`, true); + await waitForTableLength(page, 10); + + // Select the first available filter and the second one at once + const secondCheckboxId = 'tag-dropdown-option-FOOD'; + await pressElement(page, `#${firstCheckboxId}`, true); + await pressElement(page, `#${secondCheckboxId}`, true); + await waitForEmptyTable(page); + + // Set the filter operation to "OR" + await pressElement(page, '#tag-filter-combination-operator-radio-button-or', true); + await waitForTableLength(page, 3); + + await pressElement(page, '#reset-filters'); + }); + + it('can filter by environments', async () => { + await waitForTableLength(page, 10); + + await fillInput(page, '.environments-filter input', '8E4aZTjY'); + await waitForTableLength(page, 3); + + await pressElement(page, '#reset-filters'); + await waitForTableLength(page, 10); + + await fillInput(page, '.environments-filter input', 'abcdefgh'); + await waitForEmptyTable(page); + + await pressElement(page, '#reset-filters'); + }); + + it('can search for tag in the dropdown', async () => { + await pressElement(page, '.tags-filter .dropdown-trigger'); + + { + await fillInput(page, '#tag-dropdown-search-input', 'food'); + const popoverTrigger = await page.$('.tags-filter .popover-trigger'); + const popoverSelector = await getPopoverSelector(popoverTrigger); + await page.waitForSelector(`${popoverSelector} .dropdown-option:nth-child(2)`, { hidden: true }); + const options = await page.$$(`${popoverSelector} .dropdown-option`); + expect(await options[0].evaluate((option) => option.innerText)).to.equal('FOOD'); + } + { + await fillInput(page, '#tag-dropdown-search-input', 'fOoD'); + const popoverTrigger = await page.$('.tags-filter .popover-trigger'); + const popoverSelector = await getPopoverSelector(popoverTrigger); + await page.waitForSelector(`${popoverSelector} .dropdown-option:nth-child(2)`, { hidden: true }); + const options = await page.$$(`${popoverSelector} .dropdown-option`); + expect(await options[0].evaluate((option) => option.innerText)).to.equal('FOOD'); + } + }); + + it('can filter by run number', async () => { + await waitForTableLength(page, 10); + + // Insert some text into the filter + await fillInput(page, '#runsFilterText', '1, 2'); + await waitForTableLength(page, 2); + + await pressElement(page, '#reset-filters'); + await waitForTableLength(page, 10); + + await fillInput(page, '#runsFilterText', '1234567890'); + await waitForEmptyTable(page); + + await pressElement(page, '#reset-filters'); + }); + + it('can filter by lhc fill number', async () => { + await waitForTableLength(page, 10); + + await fillInput(page, '#lhcFillsFilter', '1, 6'); + await waitForTableLength(page, 1); + + await pressElement(page, '#reset-filters'); + await waitForTableLength(page, 10); + + await fillInput(page, '#lhcFillsFilter', '1234567890'); + await waitForEmptyTable(page); + + await pressElement(page, '#reset-filters'); + }); + it('can sort by columns in ascending and descending manners', async () => { + await waitForTableLength(page, 10); + // Close the filter panel + await pressElement(page, '#openFilterToggle'); await waitForFirstRowToHaveId(page, 'row119'); await page.waitForSelector('th#title'); @@ -352,158 +525,4 @@ module.exports = () => { await waitForNavigation(page, () => pressElement(page, `${popoverSelector} a`)) expectUrlParams(page, { page: 'run-detail', runNumber: 2 }) }); - - describe('Filters', () => { - before(async () => { - await goToPage(page, 'log-overview'); - }) - - beforeEach(async () => { - await resetFilters(page); - await waitForTableLength(page, 10); - }) - - it('can filter by log title', async () => { - await fillInput(page, '.title-textFilter', 'first', ['change']); - await waitForTableLength(page, 1); - - await fillInput(page, '.title-textFilter', 'bogusbogusbogus', ['change']); - await waitForEmptyTable(page); - }); - - it('can filter by log author', async () => { - await fillInput(page, '#authorFilterText', 'Jane', ['change']); - await waitForEmptyTable(page); - - await resetFilters(page); - - await waitForTableLength(page, 10); - - await fillInput(page, '#authorFilterText', 'John', ['change']); - await waitForTableLength(page, 5); - }); - - it('should successfully provide an input to filter on log content', async () => { - await fillInput(page, '.content-textFilter', 'particle', ['change']); - await waitForTableLength(page, 2); - - await fillInput(page, '.title-textFilter', 'this-content-do-not-exists-anywhere', ['change']); - await waitForEmptyTable(page); - }); - - it('should successfully provide an easy-to-access button to filter in/out anonymous logs', async () => { - await waitForTableTotalRowsCountToEqual(page, 119); - const authors = await getColumnCellsInnerTexts(page, 'author'); - - expect(authors.some((author) => author === 'Anonymous')).to.be.true; - - await pressElement(page, '#main-action-bar > div:nth-child(1) .switch'); - await waitForTableTotalRowsCountToEqual(page, 117); - await checkColumnValuesWithRegex(page, 'author', '^Anonymous$', { - negation: true, - }); - - await pressElement(page, '#main-action-bar > div:nth-child(1) .switch'); - await waitForTableTotalRowsCountToEqual(page, 119); - await checkColumnValuesWithRegex(page, 'author', '^Anonymous$', { - valuesCheckingMode: 'some', - }); - }); - - it('can filter by creation date', async () => { - const popoverTrigger = '.createdAt-filter .popover-trigger'; - const popOverSelector = await getPopoverSelector(await page.$(popoverTrigger)); - - await waitForTableTotalRowsCountToEqual(page, 119); - - const { fromDateSelector, toDateSelector, fromTimeSelector, toTimeSelector } = getPeriodInputsSelectors(popOverSelector); - - const limit = '2020-02-02'; - - await fillInput(page, fromDateSelector, limit, ['change']); - await fillInput(page, toDateSelector, limit, ['change']); - await fillInput(page, fromTimeSelector, '11:00', ['change']); - await fillInput(page, toTimeSelector, '12:00', ['change']); - - await waitForTableLength(page, 1); - }); - - it('can filter by tags', async () => { - await openFilteringPanel(page); - await pressElement(page, '.tags-filter .dropdown-trigger'); - - // Select the second available filter and wait for the changes to be processed - const firstCheckboxId = 'tag-dropdown-option-DPG'; - await pressElement(page, `#${firstCheckboxId}`, true); - await waitForTableLength(page, 1); - - // Deselect the filter and wait for the changes to process - await pressElement(page, `#${firstCheckboxId}`, true); - await waitForTableLength(page, 10); - - // Select the first available filter and the second one at once - const secondCheckboxId = 'tag-dropdown-option-FOOD'; - await pressElement(page, `#${firstCheckboxId}`, true); - await pressElement(page, `#${secondCheckboxId}`, true); - await waitForEmptyTable(page); - - // Set the filter operation to "OR" - await pressElement(page, '#tag-filter-combination-operator-radio-button-or', true); - await waitForTableLength(page, 3); - }); - - it('can filter by environments', async () => { - await fillInput(page, '.environments-filter input', '8E4aZTjY', ['change']); - await waitForTableLength(page, 3); - await resetFilters(page); - await waitForTableLength(page, 10); - - await fillInput(page, '.environments-filter input', 'abcdefgh', ['change']); - await waitForEmptyTable(page); - }); - - it('can search for tag in the dropdown', async () => { - await pressElement(page, '.tags-filter .dropdown-trigger'); - - { - await fillInput(page, '#tag-dropdown-search-input', 'food'); - const popoverTrigger = await page.$('.tags-filter .popover-trigger'); - const popoverSelector = await getPopoverSelector(popoverTrigger); - await page.waitForSelector(`${popoverSelector} .dropdown-option:nth-child(2)`, { hidden: true }); - const options = await page.$$(`${popoverSelector} .dropdown-option`); - expect(await options[0].evaluate((option) => option.innerText)).to.equal('FOOD'); - } - { - await fillInput(page, '#tag-dropdown-search-input', 'fOoD'); - const popoverTrigger = await page.$('.tags-filter .popover-trigger'); - const popoverSelector = await getPopoverSelector(popoverTrigger); - await page.waitForSelector(`${popoverSelector} .dropdown-option:nth-child(2)`, { hidden: true }); - const options = await page.$$(`${popoverSelector} .dropdown-option`); - expect(await options[0].evaluate((option) => option.innerText)).to.equal('FOOD'); - } - }); - - it('can filter by run number', async () => { - // Insert some text into the filter - await fillInput(page, '.runNumbers-textFilter', '1, 2', ['change']); - await waitForTableLength(page, 2); - await resetFilters(page); - - await waitForTableLength(page, 10); - - await fillInput(page, '.runNumbers-textFilter', '1234567890', ['change']); - await waitForEmptyTable(page); - }); - - it('can filter by lhc fill number', async () => { - await fillInput(page, '.fillNumbers-textFilter', '1, 6', ['change']); - await waitForTableLength(page, 1); - await resetFilters(page); - - await waitForTableLength(page, 10); - - await fillInput(page, '.fillNumbers-textFilter', '1234567890', ['change']); - await waitForEmptyTable(page); - }); - }) }; diff --git a/test/public/qcFlagTypes/overview.test.js b/test/public/qcFlagTypes/overview.test.js index 77b4fe656b..0bf4d519cc 100644 --- a/test/public/qcFlagTypes/overview.test.js +++ b/test/public/qcFlagTypes/overview.test.js @@ -112,7 +112,7 @@ module.exports = () => { it('should successfully apply QC flag type bad filter', async () => { await waitForTableLength(page, 7); - await pressElement(page, '#badFilterRadioBad', true); + await pressElement(page, '.bad-filter input[type=checkbox]', true); await checkColumnValuesWithRegex(page, 'bad', '^Yes$'); await pressElement(page, '#reset-filters', true); diff --git a/test/public/qcFlags/synchronousOverview.test.js b/test/public/qcFlags/synchronousOverview.test.js index 16c2900904..e72c4eca91 100644 --- a/test/public/qcFlags/synchronousOverview.test.js +++ b/test/public/qcFlags/synchronousOverview.test.js @@ -22,7 +22,6 @@ const { expectUrlParams, waitForNavigation, getColumnCellsInnerTexts, - getPopoverContent, } = require('../defaults.js'); const { expect } = chai; @@ -60,21 +59,14 @@ module.exports = () => { it('shows correct datatypes in respective columns', async () => { // eslint-disable-next-line require-jsdoc - const validateDate = (date) => date === '-' || !isNaN(dateAndTime.parse(date, 'DD/MM/YYYY, hh:mm:ss')); + const validateDate = (date) => date === '-' || !isNaN(dateAndTime.parse(date, 'DD/MM/YYYY hh:mm:ss')); const tableDataValidators = { flagType: (flagType) => flagType && flagType !== '-', - from: (cellContent) => { - const match = cellContent.match(/^From:\s*(.+)\nTo:\s*(.+)$/); - if (!match) return false; - const [, from, to] = match; - return (['Whole run coverage', 'Since run start'].includes(from) || validateDate(from)) - && (['Whole run coverage', 'Until run end'].includes(to) || validateDate(to)); - }, - deleted: (value) => value === 'Yes' || value === 'No', - createdBy: (cellContent) => { - const match = cellContent.match(/^By:\s*(.+)\nAt:\s*(.+)$/); - return match && match[1] !== '-' && validateDate(match[2]); - }, + createdBy: (userName) => userName && userName !== '-', + from: (timestamp) => timestamp === 'Whole run coverage' || timestamp === 'Since run start' || validateDate(timestamp), + to: (timestamp) => timestamp === 'Whole run coverage' || timestamp === 'Until run end' || validateDate(timestamp), + createdAt: validateDate, + updatedAt: validateDate, }; await validateTableData(page, new Map(Object.entries(tableDataValidators))); @@ -84,34 +76,8 @@ module.exports = () => { it('Should display the correct items counter at the bottom of the page', async () => { await expectInnerText(page, '#firstRowIndex', '1'); - await expectInnerText(page, '#lastRowIndex', '3'); - await expectInnerText(page, '#totalRowsCount', '3'); - }); - - it('should display Comment tooltip with full information', async () => { - let popoverTrigger = await page.$(`#row100-comment .popover-trigger`); - expect(popoverTrigger).to.not.be.null; - - const popoverContent = await getPopoverContent(popoverTrigger); - expect(popoverContent).to.equal('first part good'); - }); - - it('should display CreatedBy tooltip with full information', async () => { - let popoverTrigger = await page.$(`#row100-createdBy .popover-trigger`); - expect(popoverTrigger).to.not.be.null; - - const popoverContent = await getPopoverContent(popoverTrigger); - expect(popoverContent).to.equal('By: Jan JansenAt: 12/08/2024, 12:00:00'); - }); - - it('should display correct Deleted text colour', async () => { - const deletedCell = await page.$('#row103-deleted-text:nth-child(1)'); - - const deletedCellText = await page.evaluate(cell => cell.textContent.trim(), deletedCell); - expect(deletedCellText).to.equal('Yes'); - - const deletedCellFirstChildClass = await page.evaluate(cell => cell.firstElementChild.className, deletedCell); - expect(deletedCellFirstChildClass).to.include('danger'); + await expectInnerText(page, '#lastRowIndex', '2'); + await expectInnerText(page, '#totalRowsCount', '2'); }); it('can navigate to run details page from breadcrumbs link', async () => { diff --git a/test/public/runs/detail.test.js b/test/public/runs/detail.test.js index 515f36d8d8..fa94143746 100644 --- a/test/public/runs/detail.test.js +++ b/test/public/runs/detail.test.js @@ -54,7 +54,7 @@ const banIconPath = */ const goToRunDetails = async (page, runNumber) => { await waitForNavigation(page, () => pressElement(page, '#run-overview')); - await fillInput(page, '.runNumbers-textFilter', `${runNumber},${runNumber}`, ['change']); + await fillInput(page, '.run-numbers-filter', `${runNumber},${runNumber}`, ['change']); await waitForTableLength(page, 1); return waitForNavigation(page, () => pressElement(page, `a[href="?page=run-detail&runNumber=${runNumber}"]`)); }; @@ -208,10 +208,10 @@ module.exports = () => { expect(eorReasons).to.lengthOf(2); expect(await eorReasons[0].evaluate((element) => element.innerText)) - .to.equal('DETECTORS - TPC - Some Reason other than selected plus one\nAnonymous'); + .to.equal('DETECTORS - TPC - Some Reason other than selected plus one'); expect(await eorReasons[1].evaluate((element) => element.innerText)) - .to.equal('DETECTORS - CPV - A new EOR reason\nAnonymous'); + .to.equal('DETECTORS - CPV - A new EOR reason'); }); it('should successfully revert the update end of run reasons', async () => { @@ -234,19 +234,10 @@ module.exports = () => { expect(eorReasons).to.lengthOf(2); expect(await eorReasons[0].evaluate((element) => element.innerText)) - .to.equal('DETECTORS - TPC - Some Reason other than selected plus one\nAnonymous'); + .to.equal('DETECTORS - TPC - Some Reason other than selected plus one'); expect(await eorReasons[1].evaluate((element) => element.innerText)) - .to.equal('DETECTORS - CPV - A new EOR reason\nAnonymous'); - }); - - it('should display lastEditedName tooltip with "Last edited by" on formatRunEorReason', async () => { - const eorReasonElement = await page.$('#eor-reasons .eor-reason'); - const popoverTrigger = await eorReasonElement.$('.popover-trigger'); - expect(popoverTrigger).to.not.be.null; - - const popoverContent = await getPopoverContent(popoverTrigger); - expect(popoverContent).to.equal('Last edited by'); + .to.equal('DETECTORS - CPV - A new EOR reason'); }); it('should successfully update inelasticInteractionRate values of PbPb run', async () => { diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 66333186d8..807b821ffc 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -40,7 +40,6 @@ const { getColumnCellsInnerTexts, resetFilters, openFilteringPanel, - waitForButtonToBecomeActive, } = require('../defaults.js'); const { RUN_QUALITIES, RunQualities } = require('../../../lib/domain/enums/RunQualities.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -601,7 +600,7 @@ module.exports = () => { it('Should successfully filter runs by their trigger value', async () => { await navigateToRunsOverview(page); - const filterInputSelectorPrefix = '#triggerValue-checkbox-'; + const filterInputSelectorPrefix = '#triggerValueCheckbox'; const offFilterSelector = `${filterInputSelectorPrefix}OFF`; const ltuFilterSelector = `${filterInputSelectorPrefix}LTU`; @@ -671,7 +670,7 @@ module.exports = () => { }; // First filter validation on the main page. - await filterOnRun('#runOverviewFilter .runNumbers-textFilter'); + await filterOnRun('#runOverviewFilter .run-numbers-filter'); // Validate if the filter tab value is equal to the main page value. await expectInputValue(page, filterPanelRunNumbersInputSelector, inputValue); await resetFilters(page); @@ -698,7 +697,7 @@ module.exports = () => { await expectColumnValues(page, 'runNumber', ['10']); }; - await filterOnRun('#runOverviewFilter .runNumbers-textFilter'); + await filterOnRun('#runOverviewFilter .run-numbers-filter'); await expectInputValue(page, filterPanelRunNumbersInputSelector, inputValue); await resetFilters(page); await filterOnRun(filterPanelRunNumbersInputSelector); @@ -706,7 +705,7 @@ module.exports = () => { it('should successfully filter on a list of fill numbers and inform the user about it', async () => { await page.evaluate(() => window.model.disableInputDebounce()); - const filterInputSelector = '.fillNumbers-textFilter'; + const filterInputSelector = '.fill-numbers-filter'; expect(await page.$eval(filterInputSelector, (input) => input.placeholder)).to.equal('e.g. 7966, 7954, 7948...'); await fillInput(page, filterInputSelector, '1, 3', ['change']); @@ -714,7 +713,7 @@ module.exports = () => { }); it('should successfully filter on a list of environment ids and inform the user about it', async () => { - const filterInputSelector = '.environmentIds-textFilter'; + const filterInputSelector = '.environment-ids-filter'; expect(await page.$eval(filterInputSelector, (input) => input.placeholder)).to.equal('e.g. Dxi029djX, TDI59So3d...'); await fillInput(page, filterInputSelector, 'Dxi029djX, TDI59So3d', ['change']); @@ -886,7 +885,6 @@ module.exports = () => { let exportModal = await page.$('#export-data-modal'); expect(exportModal).to.be.null; - await waitForButtonToBecomeActive(page, EXPORT_RUNS_TRIGGER_SELECTOR); await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); await page.waitForSelector('#export-data-modal', { timeout: 5000 }); exportModal = await page.$('#export-data-modal'); @@ -895,7 +893,6 @@ module.exports = () => { }); it('should successfully display information when export will be truncated', async () => { - await waitForButtonToBecomeActive(page, EXPORT_RUNS_TRIGGER_SELECTOR); await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR, true); const truncatedExportWarning = await page.waitForSelector('#export-data-modal #truncated-export-warning'); @@ -915,7 +912,6 @@ module.exports = () => { }); it('should successfully export filtered runs', async () => { - await waitForButtonToBecomeActive(page, EXPORT_RUNS_TRIGGER_SELECTOR); const targetFileName = 'data.json'; // First export @@ -954,9 +950,9 @@ module.exports = () => { await page.waitForSelector(badFilterSelector); await page.$eval(badFilterSelector, (element) => element.click()); await page.waitForSelector('tbody tr:nth-child(2)'); + await page.waitForSelector(EXPORT_RUNS_TRIGGER_SELECTOR); ///// Download - await waitForButtonToBecomeActive(page, EXPORT_RUNS_TRIGGER_SELECTOR); await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); await page.waitForSelector('#export-data-modal', { timeout: 5000 }); diff --git a/test/public/runs/runsPerDataPass.overview.test.js b/test/public/runs/runsPerDataPass.overview.test.js index 4d1edbb4d6..f45d004e55 100644 --- a/test/public/runs/runsPerDataPass.overview.test.js +++ b/test/public/runs/runsPerDataPass.overview.test.js @@ -152,17 +152,6 @@ module.exports = () => { .to.be.equal('Missing 3 verifications'); }); - it('should display detector columns in RCT order (AOT/MUON after physical)', async () => { - const headers = await page.$$eval( - 'table thead th', - (ths) => ths.map((th) => th.id).filter(Boolean), - ); - - // See DetectorOrders.RCT in detectorOrders.js - expect(headers.indexOf('VTX')).to.be.greaterThan(headers.indexOf('ZDC')); - expect(headers.indexOf('MUD')).to.be.greaterThan(headers.indexOf('ZDC')); - }); - it('should ignore QC flags created by services in QC summaries of AOT and MUON ', async () => { await navigateToRunsPerDataPass(page, 2, 1, 3); // apass await expectInnerText(page, '#row106-VTX-text', '100'); @@ -405,10 +394,10 @@ module.exports = () => { const exportContent = fs.readFileSync(path.resolve(downloadPath, targetFileName)).toString(); expect(exportContent.trim()).to.be.eql([ - 'runNumber;CPV;VTX', + 'runNumber;VTX;CPV', '108;"";""', - '107;"Limited Acceptance MC Reproducible (from: 1565269140000 to: 1565290800000) | Good (from: 1565290800000 to: 1565359260000)";""', - '106;"Limited Acceptance MC Reproducible (from: 1565304200000 to: 1565324200000) | Limited acceptance (from: 1565329200000 to: 1565334200000) | Bad (from: 1565339200000 to: 1565344200000)";"Good (from: 1565269200000 to: 1565304200000) | Good (from: 1565324200000 to: 1565359200000)"', + '107;"";"Good (from: 1565290800000 to: 1565359260000) | Limited Acceptance MC Reproducible (from: 1565269140000 to: 1565290800000)"', + '106;"Good (from: 1565269200000 to: 1565304200000) | Good (from: 1565324200000 to: 1565359200000)";"Limited Acceptance MC Reproducible (from: 1565304200000 to: 1565324200000) | Limited acceptance (from: 1565329200000 to: 1565334200000) | Bad (from: 1565339200000 to: 1565344200000)"', ].join('\r\n')); fs.unlinkSync(path.resolve(downloadPath, targetFileName)); }); @@ -423,6 +412,7 @@ module.exports = () => { await waitForTableLength(page, 2); await expectColumnValues(page, 'runNumber', ['108', '107']); + await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters'); await waitForTableLength(page, 3); await expectColumnValues(page, 'runNumber', ['108', '107', '106']); @@ -437,6 +427,7 @@ module.exports = () => { await pressElement(page, '#detector-filter-dropdown-option-CPV', true); await expectColumnValues(page, 'runNumber', ['2', '1']); + await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters'); await expectColumnValues(page, 'runNumber', ['55', '2', '1']); }); @@ -452,6 +443,8 @@ module.exports = () => { await expectColumnValues(page, 'runNumber', ['106']); + await page.waitForSelector('#openFilterToggle'); + await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters'); await expectColumnValues(page, 'runNumber', ['108', '107', '106']); }); @@ -473,6 +466,7 @@ module.exports = () => { await expectColumnValues(page, 'runNumber', ['55', '1']); + await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters'); await expectColumnValues(page, 'runNumber', ['55', '2', '1']); }); @@ -486,6 +480,7 @@ module.exports = () => { await expectColumnValues(page, 'runNumber', ['54']); + await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters'); await expectColumnValues(page, 'runNumber', ['105', '56', '54', '49']); }); @@ -508,6 +503,7 @@ module.exports = () => { await fillInput(page, `#${property}-operand`, value, ['change']); await expectColumnValues(page, 'runNumber', expectedRuns); + await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters', true); await expectColumnValues(page, 'runNumber', ['105', '56', '54', '49']); }); @@ -516,6 +512,8 @@ module.exports = () => { it('should successfully apply gaqNotBadFraction filters', async () => { await navigateToRunsPerDataPass(page, 2, 1, 3); + await pressElement(page, '#openFilterToggle', true); + await page.waitForSelector('#gaqNotBadFraction-operator'); await page.select('#gaqNotBadFraction-operator', '<='); await fillInput(page, '#gaqNotBadFraction-operand', '80', ['change']); @@ -524,6 +522,7 @@ module.exports = () => { await pressElement(page, '#mcReproducibleAsNotBadToggle input', true); await expectColumnValues(page, 'runNumber', []); + await pressElement(page, '#openFilterToggle', true); await pressElement(page, '#reset-filters', true); await expectColumnValues(page, 'runNumber', ['108', '107', '106']); }); @@ -532,8 +531,12 @@ module.exports = () => { await page.waitForSelector('#detectorsQc-for-1-notBadFraction-operator'); await page.select('#detectorsQc-for-1-notBadFraction-operator', '<='); await fillInput(page, '#detectorsQc-for-1-notBadFraction-operand', '90', ['change']); + await expectColumnValues(page, 'runNumber', ['106']); + + await pressElement(page, '#mcReproducibleAsNotBadToggle input', true); await expectColumnValues(page, 'runNumber', ['107', '106']); + await pressElement(page, '#openFilterToggle', true); await pressElement(page, '#reset-filters', true); await expectColumnValues(page, 'runNumber', ['108', '107', '106']); }); @@ -547,6 +550,7 @@ module.exports = () => { await fillInput(page, '#muInelasticInteractionRate-operand', 0.03, ['change']); await expectColumnValues(page, 'runNumber', ['106']); + await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters', true); await expectColumnValues(page, 'runNumber', ['108', '107', '106']); }); @@ -605,6 +609,7 @@ module.exports = () => { it('should successfully disable QC flag creation when data pass is frozen', async () => { await waitForTableLength(page, 3); await page.waitForSelector('.select-multi-flag', { hidden: true }); + await pressElement(page, '#actions-dropdown-button .popover-trigger'); await page.waitForSelector('#set-qc-flags-trigger[disabled]'); await page.waitForSelector('#row107-ACO-text button[disabled]'); }); @@ -618,10 +623,16 @@ module.exports = () => { it('should successfully enable QC flag creation when data pass is un-frozen', async () => { await waitForTableLength(page, 3); - await page.waitForSelector('#set-qc-flags-trigger[disabled]'); + await pressElement(page, '.select-multi-flag'); + await pressElement(page, '#actions-dropdown-button .popover-trigger'); + await page.waitForSelector('#set-qc-flags-trigger[disabled]', { hidden: true }); await page.waitForSelector('#set-qc-flags-trigger'); await page.waitForSelector('#row107-ACO-text a'); }); + + after(async () => { + await pressElement(page, '#actions-dropdown-button .popover-trigger', true); + }); }); it('should successfully not display button to discard all QC flags for the data pass', async () => { @@ -643,8 +654,8 @@ module.exports = () => { // Press again actions dropdown to re-trigger render await pressElement(page, '#actions-dropdown-button .popover-trigger', true); setConfirmationDialogToBeAccepted(page); - const oldTable = await page.waitForSelector('table').then((table) => table.evaluate((t) => t.innerHTML)); await pressElement(page, `${popoverSelector} button:nth-child(4)`, true); + const oldTable = await page.waitForSelector('table').then((table) => table.evaluate((t) => t.innerHTML)); await pressElement(page, '#actions-dropdown-button .popover-trigger', true); await waitForTableLength(page, 3, undefined, oldTable); // Processing of data might take a bit of time, but then expect QC flag button to be there diff --git a/test/public/runs/runsPerLhcPeriod.overview.test.js b/test/public/runs/runsPerLhcPeriod.overview.test.js index 77d1ec4a24..f38dc635a9 100644 --- a/test/public/runs/runsPerLhcPeriod.overview.test.js +++ b/test/public/runs/runsPerLhcPeriod.overview.test.js @@ -32,7 +32,6 @@ const { expectColumnValues, openFilteringPanel, resetFilters, - waitForButtonToBecomeActive } = require('../defaults.js'); const { RUN_QUALITIES, RunQualities } = require('../../../lib/domain/enums/RunQualities.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -76,7 +75,6 @@ module.exports = () => { after(async () => { [page, browser] = await defaultAfter(page, browser); }); - const EXPORT_RUNS_TRIGGER_SELECTOR = '#export-data-trigger'; it('loads the page successfully', async () => { const response = await goToPage(page, 'runs-per-lhc-period', { queryParameters: { lhcPeriodId: 1 } }); @@ -132,17 +130,6 @@ module.exports = () => { await expectInnerText(page, '#row56-FT0', '83'); }); - it('should display detector columns in RCT order (AOT/MUON after physical) for synchronous flags', async () => { - // Note test starts already on synchronous flags tab - const headers = await page.$$eval( - 'table thead th', - (ths) => ths.map((th) => th.id).filter(Boolean), - ); - - // See DetectorOrders.RCT in detectorOrders.js - expect(headers.indexOf('MUD')).to.be.greaterThan(headers.indexOf('ZDC')); - }); - it('should successfully sort by runNumber in ascending and descending manners', async () => { await testTableSortingByColumn(page, 'runNumber'); }); @@ -201,19 +188,25 @@ module.exports = () => { // Revert changes for next test await page.evaluate(() => { // eslint-disable-next-line no-undef - model.runs.perLhcPeriodOverviewModel.pagination.itemsPerPage = 2; + model.runs.perLhcPeriodOverviewModel.pagination.itemsPerPage = 10; }); - await waitForTableLength(page, 2); + await waitForTableLength(page, 4); }); + const EXPORT_RUNS_TRIGGER_SELECTOR = '#export-data-trigger'; it('should successfully export all runs per lhc Period', async () => { + await page.evaluate(() => { + // eslint-disable-next-line no-undef + model.runs.perLhcPeriodOverviewModel.pagination.itemsPerPage = 2; + }); + const targetFileName = 'data.json'; - await waitForButtonToBecomeActive(page, EXPORT_RUNS_TRIGGER_SELECTOR); + // First export await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR, true); - await page.waitForSelector('select.form-control'); - await page.waitForSelector('option[value=runNumber]'); + await page.waitForSelector('select.form-control', { timeout: 200 }); + await page.waitForSelector('option[value=runNumber]', { timeout: 200 }); await page.select('select.form-control', 'runQuality', 'runNumber', 'definition', 'lhcPeriod'); await expectInnerText(page, '#send:enabled', 'Export'); @@ -282,9 +275,9 @@ module.exports = () => { await navigateToRunsPerLhcPeriod(page, 1, 4); const targetFileName = 'data.csv'; - await waitForButtonToBecomeActive(page, EXPORT_RUNS_TRIGGER_SELECTOR); + // Export - await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR); + await pressElement(page, '#export-data-trigger'); await page.waitForSelector('#export-data-modal'); await page.waitForSelector('#send:disabled'); await page.waitForSelector('.form-control'); diff --git a/test/public/runs/runsPerSimulationPass.overview.test.js b/test/public/runs/runsPerSimulationPass.overview.test.js index f3c2d47316..b7b1c725fd 100644 --- a/test/public/runs/runsPerSimulationPass.overview.test.js +++ b/test/public/runs/runsPerSimulationPass.overview.test.js @@ -31,7 +31,6 @@ const { testTableSortingByColumn, waitForTableLength, expectColumnValues, - waitForButtonToBecomeActive, } = require('../defaults.js'); const { expect } = chai; @@ -75,8 +74,6 @@ module.exports = () => { [page, browser] = await defaultAfter(page, browser); }); - const EXPORT_RUNS_TRIGGER_SELECTOR = '#export-data-trigger'; - it('loads the page successfully', async () => { const response = await goToPage(page, 'runs-per-simulation-pass', { queryParameters: { simulationPassId: 2 } }); @@ -140,17 +137,6 @@ module.exports = () => { await qcFlagService.delete(tmpQcFlag.id); }); - it('should display detector columns in RCT order (AOT/MUON after physical)', async () => { - const headers = await page.$$eval( - 'table thead th', - (ths) => ths.map((th) => th.id).filter(Boolean), - ); - - // See DetectorOrders.RCT in detectorOrders.js - expect(headers.indexOf('VTX')).to.be.greaterThan(headers.indexOf('ZDC')); - expect(headers.indexOf('MUD')).to.be.greaterThan(headers.indexOf('ZDC')); - }); - it('should successfully sort by runNumber in ascending and descending manners', async () => { await testTableSortingByColumn(page, 'runNumber'); }); @@ -217,6 +203,7 @@ module.exports = () => { await fillInput(page, '#detectorsQc-for-1-notBadFraction-operand', '90', ['change']); await expectColumnValues(page, 'runNumber', ['106']); + await pressElement(page, '#openFilterToggle', true); await pressElement(page, '#reset-filters', true); await expectColumnValues(page, 'runNumber', ['107', '106', '105']); }); @@ -230,16 +217,18 @@ module.exports = () => { await fillInput(page, '#detectorsQc-for-1-notBadFraction-operand', '90', ['change']); await expectColumnValues(page, 'runNumber', ['106']); + await pressElement(page, '#openFilterToggle', true); await pressElement(page, '#reset-filters', true); await expectColumnValues(page, 'runNumber', ['107', '106', '105']); }); it('should successfully export runs', async () => { await navigateToRunsPerSimulationPass(page, 1, 2, 3); + const EXPORT_RUNS_TRIGGER_SELECTOR = '#export-data-trigger'; + const targetFileName = 'data.json'; // Export - await waitForButtonToBecomeActive(page, EXPORT_RUNS_TRIGGER_SELECTOR); await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR); await page.waitForSelector('#export-data-modal'); await page.waitForSelector('#send:disabled'); @@ -270,8 +259,7 @@ module.exports = () => { const targetFileName = 'data.csv'; // Export - await waitForButtonToBecomeActive(page, EXPORT_RUNS_TRIGGER_SELECTOR); - await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR); + await pressElement(page, '#export-data-trigger'); await page.waitForSelector('#export-data-modal'); await page.waitForSelector('#send:disabled'); await page.waitForSelector('.form-control');