From e13000eec3870302f32197b6279dc3fe7764e8ed Mon Sep 17 00:00:00 2001 From: xsalonx <65893715+xsalonx@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:24:07 +0100 Subject: [PATCH] [O2B-1133] Implement Simulation Passes per LHC Period view (#1367) * add simulation passes migration * add anchored_passes * add sim passes runs * correct name * add sim passes runs * add model * add associations * expose model * extend MonALISACLient * add env vars * feed synchronizer constructor (fetching works) * ad dfiltering * test WIP * add test data * expose test data * test * test WIP * test * t * use joi * add upsert * add output size, renmae * add/rename columns * typdefs * rename * fix * docs * add repo * add timestamps * inerting works * add cleaup * test WIP * test WIP * test WIP * rename * reorder code * test completed * typo * typo * add stuff * typo * amend QueryBuilder * filter by lhc period * filtering * amend test * typo * test WIP * seeder * seeder * seeders completed * test completed * ad controller * api completed * amend test| * test * test completed * add model and view * view works * expose view * cleanup * use common styling * test wip, change strability * change sortability * extend test * extend tests * test completed * amend test * extract jiraId * rename * change types * fix * amend * add jira links * rename * rename * one migration file * add comment * rename * one migration file * association def * asso as plural * remove queryBuilder * amend test * remove filed specifier * enhance test * rename asso * separate * restore queryBuilder * add balloon * correct docs * change error * revoke * add \n * sync in scheduler * cleanup * cleanup * prettify * rename * rename * restore * add one more sim pass * cleanup * typo * refactor * update expected length * revoke changes * extend test * add get one sim pass endpoint * amend test * add fetching period * add title * greater gap * docs * cleanup * change regex * remove slash * typo * typo * docs * simplify function * docs * rename MonALISA to MonAlisa * rename * rename * rename * refactor * docs * update docs * correct * hide stuff * amend test * rename ALISA to Alisa * rename files * add counts: * update test * update test * use breadcrumbs * rename * rename * docs * cleanup * test WIP * test ok * add breadcumbs checking * revoke * tmp test amend * extend seeder * amend test * amend * merge main * t WIP * waitForTableReaload * refactor * extract function * typo{ * typo * apply comments * docs * restyle * refactor * refactor * typo * refactor * refactor breadcrumbs * tesfactor * change unit display to use () * change unit display to use () * extract only needed information * Update lib/public/utilities/formatting/formatItemsCount.js Co-authored-by: Martin Boulais <31805063+martinboulais@users.noreply.github.com> * Update lib/public/components/common/navigation/breadcrumbs.js Co-authored-by: Martin Boulais <31805063+martinboulais@users.noreply.github.com> * linter * refactor * refactor{ * fix * simplify error message * cleanup --------- Co-authored-by: Martin Boulais <31805063+martinboulais@users.noreply.github.com> --- lib/public/Model.js | 7 + .../common/navigation/breadcrumbs.js | 22 ++ .../utilities/formatting/formatItemsCount.js | 19 ++ .../utilities/formatting/formatSizeInBytes.js | 24 ++ lib/public/view.js | 3 + .../ActiveColumns/dataPassesActiveColumns.js | 2 +- .../Runs/ActiveColumns/runsActiveColumns.js | 4 +- .../simulationPassesActiveColumns.js | 82 +++++++ ...mulationPassesPerLhcPeriodOverviewModel.js | 152 ++++++++++++ ...imulationPassesPerLhcPeriodOverviewPage.js | 71 ++++++ .../SimulationPasses/SimulationPassesModel.js | 59 +++++ .../ActiveColumns/lhcPeriodsActiveColumns.js | 14 +- test/public/defaults.js | 11 + test/public/index.js | 2 + test/public/lhcPeriods/overview.test.js | 1 + test/public/simulationPasses/index.js | 18 ++ .../overviewPerLhcPeriod.test.js | 221 ++++++++++++++++++ 17 files changed, 707 insertions(+), 5 deletions(-) create mode 100644 lib/public/components/common/navigation/breadcrumbs.js create mode 100644 lib/public/utilities/formatting/formatItemsCount.js create mode 100644 lib/public/utilities/formatting/formatSizeInBytes.js create mode 100644 lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js create mode 100644 lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js create mode 100644 lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewPage.js create mode 100644 lib/public/views/SimulationPasses/SimulationPassesModel.js create mode 100644 test/public/simulationPasses/index.js create mode 100644 test/public/simulationPasses/overviewPerLhcPeriod.test.js diff --git a/lib/public/Model.js b/lib/public/Model.js index 14f5b34ca6..195255e8af 100644 --- a/lib/public/Model.js +++ b/lib/public/Model.js @@ -31,6 +31,7 @@ import { StatisticsPageModel } from './views/Statistics/StatisticsPageModel.js'; import { LhcPeriodsModel } from './views/lhcPeriods/LhcPeriodsModel.js'; import { HomePageModel } from './views/Home/Overview/HomePageModel.js'; import { DataPassesModel } from './views/DataPasses/DataPassesModel.js'; +import { SimulationPassesModel } from './views/SimulationPasses/SimulationPassesModel.js'; /** * Root of model tree @@ -77,6 +78,9 @@ export default class Model extends Observable { this.dataPasses = new DataPassesModel(this); this.dataPasses.bubbleTo(this); + this.simulationPasses = new SimulationPassesModel(this); + this.simulationPasses.bubbleTo(this); + this.logs = new LogsModel(this); this.logs.bubbleTo(this); @@ -164,6 +168,9 @@ export default class Model extends Observable { case 'data-passes-per-lhc-period-overview': this.dataPasses.loadPerLhcPeriodOverview(this.router.params); break; + case 'simulation-passes-per-lhc-period-overview': + this.simulationPasses.loadPerLhcPeriodOverview(this.router.params); + break; case 'env-overview': this.envs.loadOverview(); break; diff --git a/lib/public/components/common/navigation/breadcrumbs.js b/lib/public/components/common/navigation/breadcrumbs.js new file mode 100644 index 0000000000..3d66e1e93a --- /dev/null +++ b/lib/public/components/common/navigation/breadcrumbs.js @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { h, iconChevronRight } from '/js/src/index.js'; + +/** + * Create Breadcrumbs out of provided items which are separated with rightChevron Icon + * @param {Component[]} items items in breadcrumbs + * @return {Component} breadcrumbs + */ +export const breadcrumbs = (items) => h('.flex-row.g1.items-center', items.flatMap((item) => [iconChevronRight(), item]).slice(1)); diff --git a/lib/public/utilities/formatting/formatItemsCount.js b/lib/public/utilities/formatting/formatItemsCount.js new file mode 100644 index 0000000000..e7a5fe1d41 --- /dev/null +++ b/lib/public/utilities/formatting/formatItemsCount.js @@ -0,0 +1,19 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +/** + * Format number to locale string + * @param {number} itemsCount number to be formatted + * @return {string} formatted number + */ +export const formatItemsCount = (itemsCount) => itemsCount.toLocaleString(); diff --git a/lib/public/utilities/formatting/formatSizeInBytes.js b/lib/public/utilities/formatting/formatSizeInBytes.js new file mode 100644 index 0000000000..ce441698f1 --- /dev/null +++ b/lib/public/utilities/formatting/formatSizeInBytes.js @@ -0,0 +1,24 @@ +/** + * @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 DATA_SIZE_UNITS = ['B', 'kB', 'MB', 'GB', 'TB']; + +/** + * Format size in bytes as human readibly + * @param {number} size number of bytes to be formatted + * @return {string} formatted number + */ +export const formatSizeInBytes = (size) => { + const exponent = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); + return `${Number((size / Math.pow(1024, exponent)).toFixed(2))} ${DATA_SIZE_UNITS[exponent]}`; +}; diff --git a/lib/public/view.js b/lib/public/view.js index c5c8571b2c..6040fe6584 100644 --- a/lib/public/view.js +++ b/lib/public/view.js @@ -37,6 +37,8 @@ import { LhcPeriodsOverviewPage } from './views/lhcPeriods/Overview/LhcPeriodsOv import { RunsPerLhcPeriodOverviewPage } from './views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js'; import { HomePage } from './views/Home/Overview/HomePage.js'; import { DataPassesPerLhcPeriodOverviewPage } from './views/DataPasses/PerLhcPeriodOverview/DataPassesPerLhcPeriodOverviewView.js'; +import { SimulationPassesPerLhcPeriodOverviewPage } + from './views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewPage.js'; import { RunsPerDataPassOverviewPage } from './views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js'; import { LogReplyPage } from './views/Logs/Create/LogReplyPage.js'; @@ -52,6 +54,7 @@ export default (model) => { 'lhc-period-overview': LhcPeriodsOverviewPage, 'data-passes-per-lhc-period-overview': DataPassesPerLhcPeriodOverviewPage, + 'simulation-passes-per-lhc-period-overview': SimulationPassesPerLhcPeriodOverviewPage, 'log-overview': LogsOverview, 'log-detail': LogTreeViewPage, diff --git a/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js b/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js index b2e1d0fb1d..dd1eb176cf 100644 --- a/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js +++ b/lib/public/views/DataPasses/ActiveColumns/dataPassesActiveColumns.js @@ -60,7 +60,7 @@ export const dataPassesActiveColumns = { }, outputSize: { - name: 'Output Size [B]', + name: 'Output Size (B)', visible: true, format: (outputSize) => outputSize ? outputSize.toLocaleString('en-US') : '-', sortable: true, diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index c44df43bef..3266433d7c 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -423,13 +423,13 @@ export const runsActiveColumns = { visible: false, }, aliceL3Current: { - name: 'L3 [A]', + name: 'L3 (A)', visible: true, format: (_, run) => formatAliceCurrent(run.aliceL3Polarity, run.aliceL3Current), profiles: ['runsPerLhcPeriod', 'runsPerDataPass'], }, dipoleCurrent: { - name: 'Dipole [A]', + name: 'Dipole (A)', visible: true, format: (_, run) => formatAliceCurrent(run.aliceDipolePolarity, run.aliceDipoleCurrent), profiles: ['runsPerLhcPeriod', 'runsPerDataPass'], diff --git a/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js b/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js new file mode 100644 index 0000000000..853f95cb94 --- /dev/null +++ b/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js @@ -0,0 +1,82 @@ +/** + * @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 { textFilter } from '../../../components/Filters/common/filters/textFilter.js'; +import { absoluteFrontLink } from '../../../components/common/navigation/absoluteFrontLink.js'; +import { externalLinks } from '../../../components/common/navigation/externalLinks.js'; +import { formatItemsCount } from '../../../utilities/formatting/formatItemsCount.js'; +import { formatSizeInBytes } from '../../../utilities/formatting/formatSizeInBytes.js'; + +/** + * List of active columns for a generic simulation passes table + */ +export const simulationPassesActiveColumns = { + name: { + name: 'Name', + visible: true, + sortable: true, + filter: ({ namesFilterModel }) => textFilter( + namesFilterModel, + { class: 'w-75 mt1', placeholder: 'e.g. LHC23k5, ...' }, + ), + classes: 'w-15 f6', + }, + + description: { + name: 'Description', + visible: true, + sortable: false, + balloon: true, + }, + + jiraId: { + name: 'Jira', + visible: true, + format: (jiraId) => absoluteFrontLink(jiraId, `${externalLinks.ALICE_JIRA}/browse/${jiraId}`, { target: '_blank' }) ?? '-', + sortable: true, + classes: 'w-5 f6', + }, + + pwg: { + name: 'PWG', + format: (pwg) => pwg ?? '-', + visible: true, + sortable: true, + classes: 'w-5 f6', + }, + + requestedEventsCount: { + name: 'Requested Events', + format: (requestedEventsCount) => requestedEventsCount ? formatItemsCount(requestedEventsCount) : '-', + visible: true, + sortable: true, + classes: 'w-10 f6', + }, + + generatedEventsCount: { + name: 'Generated Events', + format: (generatedEventsCount) => generatedEventsCount ? formatItemsCount(generatedEventsCount) : '-', + visible: true, + sortable: true, + classes: 'w-10 f6', + }, + + outputSize: { + name: 'Output Size (B)', + visible: true, + format: (outputSize) => outputSize ? formatSizeInBytes(outputSize) : '-', + sortable: true, + classes: 'w-10 f6', + }, + +}; diff --git a/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js new file mode 100644 index 0000000000..13e0451166 --- /dev/null +++ b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js @@ -0,0 +1,152 @@ +/** + * @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 '../../../utilities/fetch/buildUrl.js'; +import { SortModel } from '../../../components/common/table/SortModel.js'; +import { TextTokensFilterModel } from '../../../components/Filters/common/filters/TextTokensFilterModel.js'; +import { OverviewPageModel } from '../../../models/OverviewModel.js'; +import { RemoteData } from '/js/src/index.js'; +import { ObservableData } from '../../../utilities/ObservableData.js'; +import { getRemoteData } from '../../../utilities/fetch/getRemoteData.js'; + +/** + * Simulation Passes Per LHC Period overview model + */ +export class SimulationPassesPerLhcPeriodOverviewModel extends OverviewPageModel { + /** + * Constructor + */ + constructor() { + super(); + this._sortModel = new SortModel(); + this._sortModel.observe(() => { + this._pagination.silentlySetCurrentPage(1); + this.load(); + }); + this._sortModel.visualChange$.bubbleTo(this); + + this._namesFilterModel = new TextTokensFilterModel(); + this._registerFilter(this._namesFilterModel); + + this._lhcPeriod = new ObservableData(RemoteData.notAsked()); + this._lhcPeriod.bubbleTo(this); + + this._lhcPeriodId = null; + } + + /** + * Fetch LHC Period data which simulation passes are fetched + * @return {Promise} promise + */ + async _fetchLhcPeriod() { + this._lhcPeriod.setCurrent(RemoteData.loading()); + try { + const { data: lhcPeriodStatistics } = await getRemoteData(`/api/lhcPeriodsStatistics/${this._lhcPeriodId}`); + this._lhcPeriod.setCurrent(RemoteData.success(lhcPeriodStatistics.lhcPeriod)); + } catch (error) { + this._lhcPeriod.setCurrent(RemoteData.failure(error)); + } + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritdoc + */ + async load() { + this._fetchLhcPeriod(); + super.load(); + } + + // eslint-disable-next-line valid-jsdoc + /** + * @inheritdoc + */ + getRootEndpoint() { + const params = { + filter: { + names: this._namesFilterModel.normalized, + lhcPeriodIds: [this._lhcPeriodId], + }, + }; + + const { appliedOn: sortOn, appliedDirection: sortDirection } = this._sortModel; + if (sortOn && sortDirection) { + params[`sort[${sortOn}]`] = sortDirection; + } + + return buildUrl('/api/simulationPasses', params); + } + + /** + * Reset this model to its default + * + * @returns {void} + */ + reset() { + this._namesFilterModel.reset(); + super.reset(); + } + + /** + * Set id of LHC Period which simulation passes are to be fetched + * @param {number} lhcPeriodId id of LHC Period + */ + set lhcPeriodId(lhcPeriodId) { + this._lhcPeriodId = lhcPeriodId; + } + + /** + * Get current lhc period which simulation passes are fetched + */ + get lhcPeriod() { + return this._lhcPeriod.getCurrent(); + } + + /** + * Returns the model handling the overview page table sort + * + * @return {SortModel} the sort model + */ + get sortModel() { + return this._sortModel; + } + + /** + * 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 new file mode 100644 index 0000000000..998bac243b --- /dev/null +++ b/lib/public/views/SimulationPasses/PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewPage.js @@ -0,0 +1,71 @@ +/** + * @license + * Copyright 2019-2020 CERN and copyright holders of ALICE O2. + * See http://alice-o2.web.cern.ch/copyright for details of the copyright holders. + * All rights not expressly granted are reserved. + * + * This software is distributed under the terms of the GNU General Public + * License v3 (GPL Version 3), copied verbatim in the file "COPYING". + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { h, iconWarning } from '/js/src/index.js'; +import { table } from '../../../components/common/table/table.js'; +import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; +import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; +import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; +import { simulationPassesActiveColumns } from '../ActiveColumns/simulationPassesActiveColumns.js'; +import spinner from '../../../components/common/spinner.js'; +import { tooltip } from '../../../components/common/popover/tooltip.js'; +import { breadcrumbs } from '../../../components/common/navigation/breadcrumbs.js'; + +const TABLEROW_HEIGHT = 42; +// Estimate of the navbar and pagination elements height total; Needs to be updated in case of changes; +const PAGE_USED_HEIGHT = 215; + +/** + * Render Simulation Passes overview page + * @param {Model} model The overall model object. + * @returns {Component} The overview screen + */ +export const SimulationPassesPerLhcPeriodOverviewPage = ({ simulationPasses: { + perLhcPeriodOverviewModel: simulationPassesPerLhcPeriodOverviewModel } }) => { + simulationPassesPerLhcPeriodOverviewModel.pagination.provideDefaultItemsPerPage(estimateDisplayableRowsCount( + TABLEROW_HEIGHT, + PAGE_USED_HEIGHT, + )); + + const { items: simulationPasses, lhcPeriod } = simulationPassesPerLhcPeriodOverviewModel; + + const commonTitle = h('h2', { style: 'white-space: nowrap;' }, 'Monte Carlo'); + + return h('', { + onremove: () => simulationPassesPerLhcPeriodOverviewModel.reset(), + }, [ + h('.flex-row.items-center.g2', [ + filtersPanelPopover(simulationPassesPerLhcPeriodOverviewModel, simulationPassesActiveColumns), + h( + '.flex-row.g1.items-center', + lhcPeriod.match({ + Success: (payload) => breadcrumbs([commonTitle, h('h2', payload.name)]), + Failure: () => [commonTitle, tooltip(h('.f3', iconWarning()), 'Not able to load LHC Period info')], + Loading: () => [commonTitle, spinner({ size: 2, absolute: false })], + NotAsked: () => [commonTitle, tooltip(h('.f3', iconWarning()), 'No data was asked for')], + }), + ), + ]), + h('.w-100.flex-column', [ + 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 new file mode 100644 index 0000000000..99c206bcb5 --- /dev/null +++ b/lib/public/views/SimulationPasses/SimulationPassesModel.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. + */ + +import { Observable } from '/js/src/index.js'; +import { SimulationPassesPerLhcPeriodOverviewModel } from './PerLhcPeriodOverview/SimulationPassesPerLhcPeriodOverviewModel.js'; + +/** + * Simulation Passes model + */ +export class SimulationPassesModel extends Observable { + /** + * The constructor of the model + */ + constructor() { + super(); + + this._perLhcPeriodOverviewModel = new SimulationPassesPerLhcPeriodOverviewModel(); + this._perLhcPeriodOverviewModel.bubbleTo(this); + } + + /** + * Load the overview page model + * @param {string} [params.lhcPeriodId] lhc period id which Simulation Passes should be fetched + * @returns {void} + */ + loadPerLhcPeriodOverview({ lhcPeriodId }) { + if (!this._perLhcPeriodOverviewModel.pagination.isInfiniteScrollEnabled) { + this._perLhcPeriodOverviewModel.lhcPeriodId = lhcPeriodId; + this._perLhcPeriodOverviewModel.load(); + } + } + + /** + * Reset the overview page model to its default state + * @returns {void} + */ + clearPerLhcPeriodOverview() { + this._perLhcPeriodOverviewModel.reset(); + } + + /** + * Returns the model for the overview page + * + * @return {SimulationPassesPerLhcPeriodOverviewModel} the overview model + */ + get perLhcPeriodOverviewModel() { + return this._perLhcPeriodOverviewModel; + } +} diff --git a/lib/public/views/lhcPeriods/ActiveColumns/lhcPeriodsActiveColumns.js b/lib/public/views/lhcPeriods/ActiveColumns/lhcPeriodsActiveColumns.js index afcff73029..884bfd77bc 100644 --- a/lib/public/views/lhcPeriods/ActiveColumns/lhcPeriodsActiveColumns.js +++ b/lib/public/views/lhcPeriods/ActiveColumns/lhcPeriodsActiveColumns.js @@ -60,8 +60,18 @@ export const lhcPeriodsActiveColumns = { classes: 'w-20', }, + associatedSimulationPasses: { + name: 'MC', + visible: true, + format: (_, { id, simulationPassesCount }) => simulationPassesCount === 0 + ? 'No MC' + : frontLink('MC', 'simulation-passes-per-lhc-period-overview', { lhcPeriodId: id }, { class: 'mh1' }), + classes: 'w-10', + + }, + avgCenterOfMassEnergy: { - name: h('.flex-wrap', ['Avg ', h('img', { src: '/assets/center-of-mass-energy.svg' }), '[GeV]']), + name: h('.flex-wrap', ['Avg ', h('img', { src: '/assets/center-of-mass-energy.svg' }), '(GeV)']), visible: true, sortable: true, format: (avgCenterOfMassEnergy) => avgCenterOfMassEnergy ? `${Number(avgCenterOfMassEnergy).toFixed(2)}` : '-', @@ -93,7 +103,7 @@ export const lhcPeriodsActiveColumns = { }, distinctEnergies: { - name: ['Distinct Beam Energies [GeV]'], + name: ['Distinct Beam Energies (GeV)'], visible: true, sortable: false, balloon: true, diff --git a/test/public/defaults.js b/test/public/defaults.js index 9cd98fdd1b..2549360584 100644 --- a/test/public/defaults.js +++ b/test/public/defaults.js @@ -439,3 +439,14 @@ module.exports.checkMismatchingUrlParam = async (page, expectedUrlParameters) => } return ret; }; + +/** + * Call a trigger function, wait for the table to display a loading spinner then wait for the loading spinner to be removed. + * @param {puppeteer.Page} page the puppeteer page + * @param {function} triggerFunction function called to trigger table data loading + * @return {Promise} promise + */ +module.exports.waitForTableDataReload = (page, triggerFunction) => Promise.all([ + page.waitForSelector('table .atom-spinner'), + triggerFunction(), +]).then(() => page.waitForSelector('table .atom-spinner', { hidden: true })); diff --git a/test/public/index.js b/test/public/index.js index edee1387ae..2d68d728ec 100644 --- a/test/public/index.js +++ b/test/public/index.js @@ -23,6 +23,7 @@ const EnvsSuite = require('./envs'); const EosReportSuite = require('./eosReport'); const LhcPeriodsSuite = require('./lhcPeriods'); const DataPassesSuite = require('./dataPasses'); +const SimulationPassesSuite = require('./simulationPasses'); module.exports = () => { describe('LhcPeriods', LhcPeriodsSuite); @@ -37,4 +38,5 @@ module.exports = () => { describe('About', AboutSuite); describe('EosReport', EosReportSuite); describe('DataPasses', DataPassesSuite); + describe('SimulationPasses', SimulationPassesSuite); }; diff --git a/test/public/lhcPeriods/overview.test.js b/test/public/lhcPeriods/overview.test.js index 8dd15df561..62cabdf116 100644 --- a/test/public/lhcPeriods/overview.test.js +++ b/test/public/lhcPeriods/overview.test.js @@ -55,6 +55,7 @@ module.exports = () => { name: (name) => periodNameRegex.test(name), associatedRuns: (display) => /(No runs)|(\d+\nRuns)/.test(display), associatedDataPasses: (display) => /(No data passes)|(\d+\nData Passes)/.test(display), + associatedSimulationPasses: (display) => /(No MC)|(MC)/.test(display), year: (year) => !isNaN(year), beamTypes: (beamTypes) => beamTypes.split(',').every((type) => allowedBeamTypesDisplayes.has(type)), avgCenterOfMassEnergy: (avgCenterOfMassEnergy) => !isNaN(avgCenterOfMassEnergy), diff --git a/test/public/simulationPasses/index.js b/test/public/simulationPasses/index.js new file mode 100644 index 0000000000..4fc1374123 --- /dev/null +++ b/test/public/simulationPasses/index.js @@ -0,0 +1,18 @@ +/** + * @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 PerLhcPeriodOverviewSuite = require('./overviewPerLhcPeriod.test'); + +module.exports = () => { + describe('Per LHC Period Overview Page', PerLhcPeriodOverviewSuite); +}; diff --git a/test/public/simulationPasses/overviewPerLhcPeriod.test.js b/test/public/simulationPasses/overviewPerLhcPeriod.test.js new file mode 100644 index 0000000000..26c13c4156 --- /dev/null +++ b/test/public/simulationPasses/overviewPerLhcPeriod.test.js @@ -0,0 +1,221 @@ +/** + * @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 chai = require('chai'); +const { + defaultBefore, + defaultAfter, + goToPage, + getAllDataFields, + fillInput, + waitForTableDataReload, +} = require('../defaults'); + +const { expect } = chai; + +const periodNameRegex = /LHC\d\d[a-zA-Z]+/; + +module.exports = () => { + let page; + let browser; + + before(async () => { + [page, browser] = await defaultBefore(page, browser); + }); + + after(async () => { + [page, browser] = await defaultAfter(page, browser); + }); + + it('loads page - simulation passes per LHC Period successfully', async () => { + const response = await goToPage(page, 'simulation-passes-per-lhc-period-overview', { queryParameters: { lhcPeriodId: 1 } }); + + // We expect the page to return the correct status code, making sure the server is running properly + expect(response.status()).to.equal(200); + + // We expect the page to return the correct title, making sure there isn't another server running on this port + const title = await page.title(); + expect(title).to.equal('AliceO2 Bookkeeping'); + const headerBreadcrumbs = await page.$$('h2'); + expect(await headerBreadcrumbs[0].evaluate((element) => element.innerText)).to.be.equal('Monte Carlo'); + expect(await headerBreadcrumbs[1].evaluate((element) => element.innerText)).to.be.equal('LHC22a'); + }); + + it('shows correct datatypes in respective columns', async () => { + // Expectations of header texts being of a certain datatype + const headerDatatypes = { + name: (name) => periodNameRegex.test(name), + year: (year) => !isNaN(year), + pwg: (pwg) => /PWG.+/.test(pwg), + requestedEventsCount: (requestedEventsCount) => !isNaN(requestedEventsCount), + generatedEventsCount: (generatedEventsCount) => !isNaN(generatedEventsCount), + outpuSize: (outpuSize) => !isNaN(outpuSize), + }; + + // We find the headers matching the datatype keys + await page.waitForSelector('th'); + const headers = await page.$$('th'); + const headerIndices = {}; + for (const [index, header] of headers.entries()) { + const headerContent = await page.evaluate((element) => element.id, header); + const matchingDatatype = Object.keys(headerDatatypes).find((key) => headerContent === key); + if (matchingDatatype !== undefined) { + headerIndices[index] = matchingDatatype; + } + } + + // We expect every value of a header matching a datatype key to actually be of that datatype + + // Use the third row because it is where statistics are present + const firstRowCells = await page.$$('tr:nth-of-type(3) td'); + for (const [index, cell] of firstRowCells.entries()) { + if (index in headerIndices) { + const cellContent = await page.evaluate((element) => element.innerText, cell); + const expectedDatatype = headerDatatypes[headerIndices[index]](cellContent); + expect(expectedDatatype).to.be.true; + } + } + }); + + it('Should display the correct items counter at the bottom of the page', async () => { + await goToPage(page, 'simulation-passes-per-lhc-period-overview', { queryParameters: { lhcPeriodId: 1 } }); + await page.waitForSelector('#firstRowIndex'); + + expect(await page.$eval('#firstRowIndex', (element) => parseInt(element.innerText, 10))).to.equal(1); + expect(await page.$eval('#lastRowIndex', (element) => parseInt(element.innerText, 10))).to.equal(2); + expect(await page.$eval('#totalRowsCount', (element) => parseInt(element.innerText, 10))).to.equal(2); + }); + + it('can set how many simulation passes is available per page', async () => { + await goToPage(page, 'simulation-passes-per-lhc-period-overview', { queryParameters: { lhcPeriodId: 1 } }); + + // Expect the amount selector to currently be set to 10 (because of the defined page height) + await page.waitForSelector('.dropup button'); + const amountSelectorButton = await page.$('.dropup button'); + const amountSelectorButtonText = await amountSelectorButton.evaluate((element) => element.innerText); + expect(amountSelectorButtonText.trim().endsWith('9')).to.be.true; + + // Expect the dropdown options to be visible when it is selected + await amountSelectorButton.evaluate((button) => button.click()); + await page.waitForSelector('.dropup'); + const amountSelectorDropdown = await page.$('.dropup'); + expect(Boolean(amountSelectorDropdown)).to.be.true; + + // Expect the amount of visible simulationPasses to reduce when the first option (5) is selected + const menuItem = await page.$('.dropup .menu-item'); + await menuItem.evaluate((button) => button.click()); + + // Expect the custom per page input to have red border and text color if wrong value typed + const customPerPageInput = await page.$('.dropup input[type=number]'); + await customPerPageInput.evaluate((input) => input.focus()); + + await page.$eval('.dropup input[type=number]', (element) => { + element.value = '1111'; + element.dispatchEvent(new Event('input')); + }); + await page.waitForSelector('.dropup'); + + expect(Boolean(await page.$('.dropup input:invalid'))).to.be.true; + }); + + it('can sort by name column in ascending and descending manners', async () => { + await goToPage(page, 'simulation-passes-per-lhc-period-overview', { queryParameters: { lhcPeriodId: 1 } }); + // Expect a sorting preview to appear when hovering over a column header + await page.waitForSelector('th#name'); + await page.hover('th#name'); + const sortingPreviewIndicator = await page.$('#name-sort-preview'); + expect(Boolean(sortingPreviewIndicator)).to.be.true; + + // Sort by name in an ascending manner + const nameHeader = await page.$('th#name'); + await waitForTableDataReload(page, () => nameHeader.evaluate((button) => button.click())); + + // Expect the names to be in alphabetical order + const firstNames = await getAllDataFields(page, 'name'); + expect(firstNames).to.have.all.deep.ordered.members(firstNames.sort()); + }); + + it('can sort by requestedEventsCount column in ascending and descending manners', async () => { + await goToPage(page, 'simulation-passes-per-lhc-period-overview', { queryParameters: { lhcPeriodId: 1 } }); + // Expect a sorting preview to appear when hovering over a column header + await page.waitForSelector('th#requestedEventsCount'); + await page.hover('th#requestedEventsCount'); + const sortingPreviewIndicator = await page.$('#requestedEventsCount-sort-preview'); + expect(Boolean(sortingPreviewIndicator)).to.be.true; + + // Sort by year in an ascending manner + const requestedEventsCountHeader = await page.$('th#requestedEventsCount'); + + await waitForTableDataReload(page, () => requestedEventsCountHeader.evaluate((button) => button.click())); + + // Expect the year to be in order + const firstReconstructedEventsCounts = await getAllDataFields(page, 'requestedEventsCount'); + expect(firstReconstructedEventsCounts).to.have.all.deep.ordered.members(firstReconstructedEventsCounts.sort()); + }); + + it('can sort by generatedEventsCount column in ascending and descending manners', async () => { + await goToPage(page, 'simulation-passes-per-lhc-period-overview', { queryParameters: { lhcPeriodId: 1 } }); + // Expect a sorting preview to appear when hovering over a column header + await page.waitForSelector('th#generatedEventsCount'); + await page.hover('th#generatedEventsCount'); + const sortingPreviewIndicator = await page.$('#generatedEventsCount-sort-preview'); + expect(Boolean(sortingPreviewIndicator)).to.be.true; + + // Sort by year in an ascending manner + const generatedEventsCountHeader = await page.$('th#generatedEventsCount'); + + await waitForTableDataReload(page, () => generatedEventsCountHeader.evaluate((button) => button.click())); + + // Expect the year to be in order + const firstReconstructedEventsCounts = await getAllDataFields(page, 'generatedEventsCount'); + expect(firstReconstructedEventsCounts).to.have.all.deep.ordered.members(firstReconstructedEventsCounts.sort()); + }); + + it('can sort by outputSize column in ascending and descending manners', async () => { + await goToPage(page, 'simulation-passes-per-lhc-period-overview', { queryParameters: { lhcPeriodId: 1 } }); + // Expect a sorting preview to appear when hovering over a column header + await page.waitForSelector('th#outputSize'); + await page.hover('th#outputSize'); + const sortingPreviewIndicator = await page.$('#outputSize-sort-preview'); + expect(Boolean(sortingPreviewIndicator)).to.be.true; + + // Sort by avgCenterOfMassEnergy in an ascending manner + const outputSizeHeader = await page.$('th#outputSize'); + + await waitForTableDataReload(page, () => outputSizeHeader.evaluate((button) => button.click())); + + // Expect the avgCenterOfMassEnergy to be in order + const firstOutputSize = await getAllDataFields(page, 'outputSize'); + expect(firstOutputSize).to.have.all.deep.ordered.members(firstOutputSize.sort()); + }); + + it('should successfuly apply simulation passes name filter', async () => { + await goToPage(page, 'simulation-passes-per-lhc-period-overview', { queryParameters: { lhcPeriodId: 1 } }); + await page.waitForSelector('#openFilterToggle'); + const filterToggleButton = await page.$('#openFilterToggle'); + expect(filterToggleButton).to.not.be.null; + + await filterToggleButton.evaluate((button) => button.click()); + + await waitForTableDataReload(page, () => fillInput(page, 'div.flex-row.items-baseline:nth-of-type(2) input[type=text]', 'LHC23k6a')); + + let allDataPassesNames = await getAllDataFields(page, 'name'); + expect(allDataPassesNames).to.has.all.deep.members(['LHC23k6a']); + + await waitForTableDataReload(page, () => + fillInput(page, 'div.flex-row.items-baseline:nth-of-type(2) input[type=text]', 'LHC23k6a, LHC23k6b')); + + allDataPassesNames = await getAllDataFields(page, 'name'); + expect(allDataPassesNames).to.has.all.deep.members(['LHC23k6a', 'LHC23k6b']); + }); +};