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/adapters/GaqSummaryAdapter.js b/lib/database/adapters/GaqSummaryAdapter.js new file mode 100644 index 0000000000..53b6baf61a --- /dev/null +++ b/lib/database/adapters/GaqSummaryAdapter.js @@ -0,0 +1,62 @@ +/** + * @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, + badRunCoverage, + explicitlyNotBadRunCoverage, + mcReproducibleCoverage, + missingVerificationsCount, + undefinedQualityPeriodsCount, + notComputable, + invalidatedAt, + createdAt, + updatedAt, + } = databaseObject; + + return { + dataPassId, + runNumber, + badRunCoverage, + explicitlyNotBadRunCoverage, + mcReproducibleCoverage, + missingVerificationsCount, + undefinedQualityPeriodsCount, + notComputable, + invalidatedAt, + createdAt, + updatedAt, + }; + } +} + +module.exports = { GaqSummaryAdapter }; diff --git a/lib/database/adapters/index.js b/lib/database/adapters/index.js index 5ff6404b3d..9c858e1bc2 100644 --- a/lib/database/adapters/index.js +++ b/lib/database/adapters/index.js @@ -27,6 +27,7 @@ const EorReasonAdapter = require('./EorReasonAdapter'); const FlpRoleAdapter = require('./FlpRoleAdapter'); const { HostAdapter } = require('./HostAdapter.js'); const { GaqDetectorAdapter } = require('./GaqDetectorAdapter.js'); +const { GaqSummaryAdapter } = require('./GaqSummaryAdapter.js'); const { LhcFillAdapter } = require('./LhcFillAdapter.js'); const { LhcFillStatisticsAdapter } = require('./LhcFillStatisticsAdapter.js'); const LhcPeriodAdapter = require('./LhcPeriodAdapter'); @@ -63,6 +64,7 @@ const environmentHistoryItemAdapter = new EnvironmentHistoryItemAdapter(); const eorReasonAdapter = new EorReasonAdapter(); const flpRoleAdapter = new FlpRoleAdapter(); const gaqDetectorAdapter = new GaqDetectorAdapter(); +const gaqSummaryAdapter = new GaqSummaryAdapter(); const hostAdapter = new HostAdapter(); const lhcFillAdapter = new LhcFillAdapter(); const lhcFillStatisticsAdapter = new LhcFillStatisticsAdapter(); @@ -159,6 +161,7 @@ module.exports = { eorReasonAdapter, flpRoleAdapter, gaqDetectorAdapter, + gaqSummaryAdapter, 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..639db07c5c --- /dev/null +++ b/lib/database/migrations/v1/20260223120000-create-gaq-summary-tables.js @@ -0,0 +1,71 @@ +'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_run_coverage: { + type: Sequelize.FLOAT, + }, + explicitly_not_bad_run_coverage: { + type: Sequelize.FLOAT, + }, + mc_reproducible_coverage: { + type: Sequelize.FLOAT, + }, + missing_verifications_count: { + type: Sequelize.INTEGER, + }, + undefined_quality_periods_count: { + type: Sequelize.INTEGER, + }, + not_computable: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + invalidated_at: { + type: Sequelize.DATE(3), + allowNull: true, + defaultValue: null, + }, + 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 }); + + await queryInterface.addIndex('gaq_summaries', { + name: 'gaq_summaries_invalidated_at_idx', + fields: ['invalidated_at'], + }, { transaction }); + }), + + down: async (queryInterface) => queryInterface.sequelize.transaction(async (transaction) => { + await queryInterface.dropTable('gaq_summaries', { transaction }); + }), +}; 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/models/gaqSummary.js b/lib/database/models/gaqSummary.js new file mode 100644 index 0000000000..ca2a1f69b8 --- /dev/null +++ b/lib/database/models/gaqSummary.js @@ -0,0 +1,59 @@ +/** + * @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, + primaryKey: true, + }, + runNumber: { + type: Sequelize.INTEGER, + primaryKey: true, + }, + badRunCoverage: { + type: Sequelize.FLOAT, + }, + explicitlyNotBadRunCoverage: { + type: Sequelize.FLOAT, + }, + mcReproducibleCoverage: { + type: Sequelize.FLOAT, + }, + missingVerificationsCount: { + type: Sequelize.INTEGER, + }, + undefinedQualityPeriodsCount: { + type: Sequelize.INTEGER, + }, + notComputable: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + invalidatedAt: { + 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/index.js b/lib/database/models/index.js index 87d793fac3..2549209c5b 100644 --- a/lib/database/models/index.js +++ b/lib/database/models/index.js @@ -27,6 +27,7 @@ const EorReason = require('./eorreason'); const EpnRoleSession = require('./epnrolesession'); const FlpRole = require('./flprole'); const GaqDetector = require('./gaqDetector.js'); +const GaqSummary = require('./gaqSummary.js'); const Host = require('./host.js'); const LhcFill = require('./lhcFill'); const LhcFillStatistics = require('./lhcFillStatistics.js'); @@ -66,6 +67,7 @@ module.exports = (sequelize) => { EpnRoleSessionkey: EpnRoleSession(sequelize), FlpRole: FlpRole(sequelize), GaqDetector: GaqDetector(sequelize), + GaqSummary: GaqSummary(sequelize), Host: Host(sequelize), LhcFill: LhcFill(sequelize), LhcFillStatistics: LhcFillStatistics(sequelize), diff --git a/lib/database/repositories/GaqSummaryRepository.js b/lib/database/repositories/GaqSummaryRepository.js new file mode 100644 index 0000000000..81463d31c3 --- /dev/null +++ b/lib/database/repositories/GaqSummaryRepository.js @@ -0,0 +1,55 @@ +/** + * @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); + } + + /** + * 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/database/repositories/index.js b/lib/database/repositories/index.js index 0c79279752..4be7600145 100644 --- a/lib/database/repositories/index.js +++ b/lib/database/repositories/index.js @@ -27,6 +27,7 @@ const EnvironmentRepository = require('./EnvironmentRepository'); const EorReasonRepository = require('./EorReasonRepository'); const FlpRoleRepository = require('./FlpRoleRepository'); const GaqDetectorRepository = require('./GaqDetectorRepository.js'); +const GaqSummaryRepository = require('./GaqSummaryRepository.js'); const HostRepository = require('./HostRepository.js'); const LhcFillRepository = require('./LhcFillRepository'); const LhcFillStatisticsRepository = require('./LhcFillStatisticsRepository.js'); @@ -70,6 +71,7 @@ module.exports = { EorReasonRepository, FlpRoleRepository, GaqDetectorRepository, + GaqSummaryRepository, HostRepository, LhcFillRepository, LhcFillStatisticsRepository, 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/gaq/GaqDetectorsService.js b/lib/server/services/gaq/GaqDetectorsService.js index 1ac2a0dc7e..4f6d466503 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, GaqSummaryRepository } = require('../../../database/repositories'); const { BadParameterError } = require('../../errors/BadParameterError'); const { dataSource } = require('../../../database/DataSource.js'); const { Op } = require('sequelize'); @@ -57,6 +57,9 @@ class GaqDetectorService { .flatMap((runNumber) => detectorIds .map((detectorId) => ({ dataPassId, runNumber, detectorId }))); const createdEntries = await GaqDetectorRepository.insertAll(gaqEntries); + + await GaqSummaryRepository.invalidateMany(runNumbers.map((runNumber) => ({ dataPassId, runNumber }))); + return createdEntries.map(gaqDetectorAdapter.toEntity); }); } @@ -101,6 +104,9 @@ class GaqDetectorService { .flatMap(({ runNumber, detectors }) => detectors .map(({ id: detectorId }) => ({ dataPassId, runNumber, detectorId }))); const createdEntries = await GaqDetectorRepository.insertAll(gaqEntries); + + await GaqSummaryRepository.invalidateMany(runNumbers.map((runNumber) => ({ dataPassId, runNumber }))); + return createdEntries.map(gaqDetectorAdapter.toEntity); }, { transaction }); } 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/server/services/qualityControlFlag/QcFlagService.js b/lib/server/services/qualityControlFlag/QcFlagService.js index 3d34e1446b..b6a647290a 100644 --- a/lib/server/services/qualityControlFlag/QcFlagService.js +++ b/lib/server/services/qualityControlFlag/QcFlagService.js @@ -20,6 +20,7 @@ const { RunRepository, QcFlagVerificationRepository, QcFlagEffectivePeriodRepository, + GaqSummaryRepository, }, } = require('../../../database/index.js'); const { dataSource } = require('../../../database/DataSource.js'); @@ -209,6 +210,14 @@ class QcFlagService { ], }); + /** + * 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)); } catch (error) { this._logger.warnMessage(`Failed to create QC flag with properties: ${JSON.stringify(qcFlag)}. Error: ${error}`); @@ -284,6 +293,12 @@ 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 deleted flag + await GaqSummaryRepository.invalidate(dataPassId, runNumber); + } + const qcFlagPropertiesToLog = { id, from, @@ -338,8 +353,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: [], @@ -348,14 +363,21 @@ class QcFlagService { id: dataPassId, }, }, + where: { deleted: false }, raw: true, - })).map(({ id }) => id); + })).map(({ id, runNumber }) => ({ id, runNumber })); + + const qcFlagIds = qcFlagIdsToRunNumbers.map(({ id }) => id); + const runNumbers = new Set(qcFlagIdsToRunNumbers.map(({ runNumber }) => runNumber)); await QcFlagRepository.updateAll( { deleted: true }, { where: { id: qcFlagIds } }, ); + // Invalidate GAQ summary for the dataPass and all runNumbers of the deleted flags + await GaqSummaryRepository.invalidateMany(Array.from(runNumbers, (runNumber) => ({ dataPassId, runNumber }))); + return qcFlagIds.length; }); } @@ -389,7 +411,18 @@ 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 GaqSummaryRepository.invalidate(dataPassId, updatedQcFlag.runNumber); + } + + return updatedQcFlag; }, { transaction }); } diff --git a/lib/server/services/run/updateRun.js b/lib/server/services/run/updateRun.js index 2e8b50048c..f4b9f0e69a 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 GaqSummaryRepository = require('../../../database/repositories/GaqSummaryRepository.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'); @@ -35,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 * @@ -82,7 +98,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 +225,20 @@ exports.updateRun = async (identifier, payload, transaction) => { } } + // 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 }))); + } + } + return runModel; }, { transaction }); }; 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 ece5d54c54..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); } { @@ -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/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/gaq/GaqSummary.test.js b/test/lib/server/services/gaq/GaqSummary.test.js new file mode 100644 index 0000000000..c681b62b93 --- /dev/null +++ b/test/lib/server/services/gaq/GaqSummary.test.js @@ -0,0 +1,140 @@ +/** + * @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: { 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 = () => { + 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 summary = await GaqSummaryRepository.findOne({ + where: { dataPassId: expectedDataPassId, runNumber: expectedRunNumber }, + }); + if (toBeNull) { + expect(summary?.invalidatedAt, `Expected no invalidation for dataPassId=${expectedDataPassId} runNumber=${expectedRunNumber}`).to.be.null; + } else { + 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 GaqSummaryRepository.updateAll({ invalidatedAt: null }, { where: {} }); + }); + + 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 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); + 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); + }); + + 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); + }); + + 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); + }); + }); +}; 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); 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');