From a809bc77e8352e80e655dbbdb1ab9bd9d2331c03 Mon Sep 17 00:00:00 2001 From: Martin Boulais <31805063+martinboulais@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:07:57 +0100 Subject: [PATCH] [O2B-532] Use time range filter for run start stop (#1482) * [O2B-532] Use time range filter for run start stop First attempt at fixing tests Fix more tests Fix min/max Implement review comments Renaming ret Fix broken imports Fix tests * Fix linter and tests * Revert merge issues * Fix more tests * Remove spurious tests * Fix tests * Fix linter * Fix behavior when filtering runs "after" or "before" a given date * Fix typo --- .../Filters/RunsFilter/o2StartFilter.js | 22 +++ .../Filters/RunsFilter/o2StopFilter.js | 21 ++ .../components/Filters/RunsFilter/o2start.js | 92 --------- .../components/Filters/RunsFilter/o2stop.js | 89 --------- .../common/filters/TimeRangeInputModel.js | 4 +- .../common/chart/rendering/ChartRenderer.js | 2 +- .../form/inputs/DateTimeInputComponent.js | 68 +++---- .../common/form/inputs/DateTimeInputModel.js | 13 +- .../common/formatting/formatTimeRange.js | 3 +- .../Runs/ActiveColumns/runsActiveColumns.js | 8 +- .../views/Runs/Overview/RunsOverviewModel.js | 180 +++++------------- test/public/defaults.js | 50 ++++- test/public/logs/create.test.js | 17 +- test/public/runs/overview.test.js | 147 ++++---------- .../runs/runsPerDataPass.overview.test.js | 22 +-- 15 files changed, 243 insertions(+), 495 deletions(-) create mode 100644 lib/public/components/Filters/RunsFilter/o2StartFilter.js create mode 100644 lib/public/components/Filters/RunsFilter/o2StopFilter.js delete mode 100644 lib/public/components/Filters/RunsFilter/o2start.js delete mode 100644 lib/public/components/Filters/RunsFilter/o2stop.js diff --git a/lib/public/components/Filters/RunsFilter/o2StartFilter.js b/lib/public/components/Filters/RunsFilter/o2StartFilter.js new file mode 100644 index 0000000000..ab10978cfa --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/o2StartFilter.js @@ -0,0 +1,22 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { timeRangeFilter } from '../common/filters/timeRangeFilter.js'; + +/** + * Returns a filter to be applied on run start + * + * @param {RunsOverviewModel} runsOverviewModel the run overview model object + * @return {Component} the filter component + */ +export const o2StartFilter = (runsOverviewModel) => timeRangeFilter(runsOverviewModel.o2StartFilterModel); diff --git a/lib/public/components/Filters/RunsFilter/o2StopFilter.js b/lib/public/components/Filters/RunsFilter/o2StopFilter.js new file mode 100644 index 0000000000..74f00d4c39 --- /dev/null +++ b/lib/public/components/Filters/RunsFilter/o2StopFilter.js @@ -0,0 +1,21 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ +import { timeRangeFilter } from '../common/filters/timeRangeFilter.js'; + +/** + * Returns a filter to be applied on run stop + * + * @param {RunsOverviewModel} runsOverviewModel the run overview model object + * @return {Component} the filter component + */ +export const o2StopFilter = (runsOverviewModel) => timeRangeFilter(runsOverviewModel.o2stopFilterModel); diff --git a/lib/public/components/Filters/RunsFilter/o2start.js b/lib/public/components/Filters/RunsFilter/o2start.js deleted file mode 100644 index da13099448..0000000000 --- a/lib/public/components/Filters/RunsFilter/o2start.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { h } from '/js/src/index.js'; - -const DATE_FORMAT = 'YYYY-MM-DD'; -let today = new Date(); -today.setMinutes(today.getMinutes() - today.getTimezoneOffset()); -[today] = today.toISOString().split('T'); - -/** - * Returns the creation date filter components - * @param {RunsOverviewModel} runModel the run model object - * @return {vnode} Two date selection boxes to control the minimum and maximum creation dates for the log filters - */ -const o2startFilter = (runModel) => { - const date = new Date(); - const o2from = runModel.getO2startFilterFrom(); - const o2to = runModel.getO2startFilterTo(); - const o2fromTime = runModel.getO2startFilterFromTime(); - const o2toTime = runModel.getO2startFilterToTime(); - // FIXME No leading 0 in hours leads to no max date defined in the inputs if the date is bellow 9 - const now = `${date.getHours()}:${(date.getMinutes() < 10 ? '0' : '') + date.getMinutes()}`; - - return h('', [ - h('.f6', 'Started From:'), - h('input.w-50.mv1', { - type: 'date', - id: 'o2startFilterFrom', - placeholder: DATE_FORMAT, - max: o2to || today, - value: o2from, - oninput: (e) => runModel.setO2startFilter('From', e.target.value, e.target.validity.valid), - }, ''), - h('input.w-50.mv1', { - type: 'time', - id: 'o2startFilterFromTime', - max: today == o2from - ? now - : o2from == o2to ? o2toTime : '23:59', - value: o2fromTime, - oninput: (e) => { - const time = e.target.value ? e.target.value : '00:00'; - if (!o2from) { - runModel.setO2startFilter('From', today, true); - runModel.setO2startFilter('FromTime', time, e.target.value <= now); - } else { - runModel.setO2startFilter('FromTime', time, e.target.validity.valid); - } - }, - - }, ''), - h('.f6', 'Started Till:'), - h('input.w-50.mv1', { - type: 'date', - id: 'o2startFilterTo', - placeholder: DATE_FORMAT, - min: o2from, - max: today, - value: o2to, - oninput: (e) => runModel.setO2startFilter('To', e.target.value, e.target.validity.valid), - }, ''), - h('input.w-50.mv1', { - type: 'time', - id: 'o2startFilterToTime', - min: o2from == o2to ? o2fromTime : '00:00', - // Fixme I suppose that here it should be today == o2to ? - max: today == o2from ? now : '23:59', - value: o2toTime, - oninput: (e) => { - const time = e.target.value ? e.target.value : - today == o2from ? now : '23:59'; - if (!o2to) { - runModel.setO2startFilter('To', today, true); - } - runModel.setO2startFilter('ToTime', time, e.target.validity.valid); - }, - }, ''), - ]); -}; - -export default o2startFilter; diff --git a/lib/public/components/Filters/RunsFilter/o2stop.js b/lib/public/components/Filters/RunsFilter/o2stop.js deleted file mode 100644 index 9ea8175933..0000000000 --- a/lib/public/components/Filters/RunsFilter/o2stop.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -import { h } from '/js/src/index.js'; - -const DATE_FORMAT = 'YYYY-MM-DD'; - -let today = new Date(); -today.setMinutes(today.getMinutes() - today.getTimezoneOffset()); -[today] = today.toISOString().split('T'); - -/** - * Returns the creation date filter components - * @param {RunsOverviewModel} runsOverviewModel the run model object - * @return {vnode} Two date selection boxes to control the minimum and maximum creation dates for the log filters - */ -const o2endFilter = (runsOverviewModel) => { - const date = new Date(); - const o2from = runsOverviewModel.getO2endFilterFrom(); - const o2to = runsOverviewModel.getO2endFilterTo(); - const o2fromTime = runsOverviewModel.getO2endFilterFromTime(); - const o2toTime = runsOverviewModel.getO2endFilterToTime(); - const now = `${date.getHours()}:${(date.getMinutes() < 10 ? '0' : '') + date.getMinutes()}`; - return h('', [ - h('.f6', 'Ended from:'), - h('input.w-50.mv1', { - type: 'date', - id: 'o2endFilterFrom', - placeholder: DATE_FORMAT, - max: o2to || today, - value: o2from, - oninput: (e) => runsOverviewModel.setO2endFilter('From', e.target.value, e.target.validity.valid), - }, ''), - h('input.w-50.mv1', { - type: 'time', - id: 'o2endFilterFromTime', - max: today == o2from ? - now : - o2from == o2to ? o2toTime : '23:59', - value: o2fromTime, - oninput: (e) => { - const time = e.target.value ? e.target.value : '00:00'; - if (!o2from) { - runsOverviewModel.setO2endFilter('From', today, true); - runsOverviewModel.setO2endFilter('FromTime', time, e.target.value <= now); - } else { - runsOverviewModel.setO2endFilter('FromTime', time, e.target.validity.valid); - } - }, - }, ''), - h('.f6', 'Ended Till:'), - h('input.w-50.mv1', { - type: 'date', - id: 'o2endFilterTo', - min: o2from, - max: today, - value: o2to, - oninput: (e) => runsOverviewModel.setO2endFilter('To', e.target.value, e.target.validity.valid), - }, ''), - - h('input.w-50.mv1', { - type: 'time', - id: 'o2endFilterToTime', - min: o2from == o2to ? o2fromTime : '00:00', - max: '23:59', - value: o2toTime, - oninput: (e) => { - const time = e.target.value ? e.target.value : - today == o2from ? now : '23:59'; - if (!o2to) { - runsOverviewModel.setO2endFilter('To', today, true); - } - runsOverviewModel.setO2endFilter('ToTime', time, e.target.validity.valid); - }, - }, ''), - ]); -}; - -export default o2endFilter; diff --git a/lib/public/components/Filters/common/filters/TimeRangeInputModel.js b/lib/public/components/Filters/common/filters/TimeRangeInputModel.js index 0aa78df8f5..99a7587168 100644 --- a/lib/public/components/Filters/common/filters/TimeRangeInputModel.js +++ b/lib/public/components/Filters/common/filters/TimeRangeInputModel.js @@ -52,7 +52,7 @@ export class TimeRangeInputModel extends FilterModel { this._toTimeInputModel.observe(() => this._periodLabel = null); this._toTimeInputModel.bubbleTo(this); - this.setValue(value, periodLabel, false); + this.setValue(value, periodLabel, true); } /** @@ -132,7 +132,7 @@ export class TimeRangeInputModel extends FilterModel { * @override */ get isEmpty() { - return this._fromTimeInputModel.value === null && this._toTimeInputModel.value === null; + return this._fromTimeInputModel.isEmpty && this._toTimeInputModel.isEmpty; } // eslint-disable-next-line valid-jsdoc diff --git a/lib/public/components/common/chart/rendering/ChartRenderer.js b/lib/public/components/common/chart/rendering/ChartRenderer.js index 4a57856dcb..12753c5378 100644 --- a/lib/public/components/common/chart/rendering/ChartRenderer.js +++ b/lib/public/components/common/chart/rendering/ChartRenderer.js @@ -60,7 +60,7 @@ export class ChartRenderer { * - and if index axis is 'y', 'x' must contain an array in the same manner */ constructor(configuration, data) { - if (!data.length) { + if (!data?.length) { throw new Error('The data list can not be empty'); } this._data = data; diff --git a/lib/public/components/common/form/inputs/DateTimeInputComponent.js b/lib/public/components/common/form/inputs/DateTimeInputComponent.js index fac8acab2e..0f900d3a5b 100644 --- a/lib/public/components/common/form/inputs/DateTimeInputComponent.js +++ b/lib/public/components/common/form/inputs/DateTimeInputComponent.js @@ -68,8 +68,8 @@ export class DateTimeInputComponent extends StatefulComponent { * @return {Component} the component */ view() { - const inputsMin = this._getInputsMin(this._min, this._value); - const inputsMax = this._getInputsMax(this._max, this._value); + const inputsMin = this._getInputsMin(this._minTimestamp, this._value); + const inputsMax = this._getInputsMax(this._maxTimestamp, this._value); return h('.flex-row.items-center.g2', [ h( @@ -81,8 +81,9 @@ export class DateTimeInputComponent extends StatefulComponent { required: this._required, value: this._value.date, onchange: (e) => this._patchValue({ date: e.target.value }), - min: inputsMin ? inputsMin.date : undefined, - max: inputsMax ? inputsMax.date : undefined, + // Mithril do not remove min/max if previously set... + min: inputsMin?.date ?? '', + max: inputsMax?.date ?? '', }, ), h( @@ -93,8 +94,9 @@ export class DateTimeInputComponent extends StatefulComponent { value: this._value.time, step: this._seconds ? 1 : undefined, onchange: (e) => this._patchValue({ time: e.target.value }), - min: inputsMin ? inputsMin.time : undefined, - max: inputsMax ? inputsMax.time : undefined, + // Mithril do not remove min/max if previously set... + min: inputsMin?.time ?? '', + max: inputsMax?.time ?? '', }, ), h( @@ -132,8 +134,8 @@ export class DateTimeInputComponent extends StatefulComponent { time: defaultTime, }; - this._min = min; - this._max = max; + this._minTimestamp = min; + this._maxTimestamp = max; } /** @@ -163,59 +165,61 @@ export class DateTimeInputComponent extends StatefulComponent { /** * Returns the min values to apply to the inputs * - * @param {number|null} min the minimal timestamp to represent in the inputs + * @param {number|null} minTimestamp the minimal timestamp to represent in the inputs * @param {DateTimeInputRawData} raw the current raw values * @return {(Partial<{date: string, time: string}>|null)} the min values to apply to date and time inputs */ - _getInputsMin(min, raw) { - if (min === null) { + _getInputsMin(minTimestamp, raw) { + if (minTimestamp === null) { return null; } - const minDateDayAfter = new Date(min + MILLISECONDS_IN_ONE_DAY); + const rawDate = raw.date || null; + const rawTime = raw.time || null; - const minDateAndTime = formatTimestampForDateTimeInput(min, this._seconds); - const ret = {}; + const minDateAndTime = formatTimestampForDateTimeInput(minTimestamp, this._seconds); + const inputsMin = {}; - if (raw.date !== null && raw.date === minDateAndTime.date) { - ret.time = minDateAndTime.time; + if (rawDate !== null && rawDate === minDateAndTime.date) { + inputsMin.time = minDateAndTime.time; } - if (raw.time !== null && raw.time < minDateAndTime.time) { - ret.date = formatTimestampForDateTimeInput(minDateDayAfter.getTime(), this._seconds).date; + if (rawTime !== null && rawTime < minDateAndTime.time) { + inputsMin.date = formatTimestampForDateTimeInput(minTimestamp + MILLISECONDS_IN_ONE_DAY, this._seconds).date; } else { - ret.date = minDateAndTime.date; + inputsMin.date = minDateAndTime.date; } - return ret; + return inputsMin; } /** * Returns the max values to apply to the inputs * - * @param {number|null} max the maximal timestamp to represent in the inputs + * @param {number|null} maxTimestamp the maximal timestamp to represent in the inputs * @param {DateTimeInputRawData} raw the current raw values * @return {(Partial<{date: string, time: string}>|null)} the max values */ - _getInputsMax(max, raw) { - if (max === null) { + _getInputsMax(maxTimestamp, raw) { + if (maxTimestamp === null) { return null; } - const maxDateDayBefore = new Date(max - MILLISECONDS_IN_ONE_DAY); + const rawDate = raw.date || null; + const rawTime = raw.time || null; - const maxDateAndTime = formatTimestampForDateTimeInput(max, this._seconds); - const ret = {}; + const maxDateAndTime = formatTimestampForDateTimeInput(maxTimestamp, this._seconds); + const inputsMax = {}; - if (raw.date !== null && raw.date === maxDateAndTime.date) { - ret.time = maxDateAndTime.time; + if (rawDate !== null && rawDate === maxDateAndTime.date) { + inputsMax.time = maxDateAndTime.time; } - if (raw.time !== null && raw.time > maxDateAndTime.time) { - ret.date = formatTimestampForDateTimeInput(maxDateDayBefore.getTime(), this._seconds).date; + if (rawTime !== null && rawTime > maxDateAndTime.time) { + inputsMax.date = formatTimestampForDateTimeInput(maxTimestamp - MILLISECONDS_IN_ONE_DAY, this._seconds).date; } else { - ret.date = maxDateAndTime.date; + inputsMax.date = maxDateAndTime.date; } - return ret; + return inputsMax; } } diff --git a/lib/public/components/common/form/inputs/DateTimeInputModel.js b/lib/public/components/common/form/inputs/DateTimeInputModel.js index 0ff6aeef8f..51113ba2f0 100644 --- a/lib/public/components/common/form/inputs/DateTimeInputModel.js +++ b/lib/public/components/common/form/inputs/DateTimeInputModel.js @@ -79,9 +79,16 @@ export class DateTimeInputModel extends Observable { * @return {void} */ clear() { - this._raw = { date: '', time: '' }; - this._value = null; - this.notify(); + this.setValue(null, true); + } + + /** + * States if the input model is empty (has no defined value) + * + * @return {boolean} true if the model is empty + */ + get isEmpty() { + return this._value === null; } /** diff --git a/lib/public/components/common/formatting/formatTimeRange.js b/lib/public/components/common/formatting/formatTimeRange.js index a9e7c86456..103254153e 100644 --- a/lib/public/components/common/formatting/formatTimeRange.js +++ b/lib/public/components/common/formatting/formatTimeRange.js @@ -12,6 +12,7 @@ */ import { formatTimestamp as defaultFormatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; +import { h } from '/js/src/index.js'; /** * Format the current timestamp range display @@ -34,7 +35,7 @@ export const formatTimeRange = ({ from, to }, configuration) => { let parts = []; if (from === undefined && to === undefined) { - parts = ['-']; + parts = [h('.badge', '-')]; } else if (from === undefined) { parts = [formatText('Before'), formatTimestamp(to)]; } else if (to === undefined) { diff --git a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js index 1c47037a75..0ec18fb4b0 100644 --- a/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js +++ b/lib/public/views/Runs/ActiveColumns/runsActiveColumns.js @@ -13,8 +13,6 @@ import { h } from '/js/src/index.js'; import runNumberFilter from '../../../components/Filters/RunsFilter/runNumber.js'; -import o2startFilter from '../../../components/Filters/RunsFilter/o2start.js'; -import o2endFilter from '../../../components/Filters/RunsFilter/o2stop.js'; import environmentIdFilter from '../../../components/Filters/RunsFilter/environmentId.js'; import nDetectorsFilter from '../../../components/Filters/RunsFilter/nDetectors.js'; import nFlpsFilter from '../../../components/Filters/RunsFilter/nFlps.js'; @@ -54,6 +52,8 @@ import { CopyToClipboardComponent } from '../../../components/common/selection/i import { infologgerLinksComponents } from '../../../components/common/externalLinks/infologgerLinksComponents.js'; import { RunQualities } from '../../../domain/enums/RunQualities.js'; import { qcGuiLinkComponent } from '../../../components/common/externalLinks/qcGuiLinkComponent.js'; +import { o2StartFilter } from '../../../components/Filters/RunsFilter/o2StartFilter.js'; +import { o2StopFilter } from '../../../components/Filters/RunsFilter/o2StopFilter.js'; import { isRunConsideredRunning } from '../../../services/run/isRunConsideredRunning.js'; import { aliEcsEnvironmentLinkComponent } from '../../../components/common/externalLinks/aliEcsEnvironmentLinkComponent.js'; @@ -174,7 +174,7 @@ export const runsActiveColumns = { noEllipsis: true, format: (_, run) => formatRunStart(run, false), exportFormat: (timestamp) => formatTimestamp(timestamp), - filter: o2startFilter, + filter: o2StartFilter, profiles: { lhcFill: true, environment: true, @@ -200,7 +200,7 @@ export const runsActiveColumns = { noEllipsis: true, format: (_, run) => formatRunEnd(run, false), exportFormat: (timestamp) => formatTimestamp(timestamp), - filter: o2endFilter, + filter: o2StopFilter, profiles: { lhcFill: true, environment: true, diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index eaeb86ec54..25084a524e 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -22,9 +22,10 @@ import pick from '../../../utilities/pick.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; import { getRemoteDataSlice } from '../../../utilities/fetch/getRemoteDataSlice.js'; import { CombinationOperator } from '../../../components/Filters/common/CombinationOperatorChoiceModel.js'; -import { AliceL3AndDipoleFilteringModel } from '../../../components/Filters/RunsFilter/AliceL3AndDipoleFilteringModel.js'; -import { detectorsProvider } from '../../../services/detectors/detectorsProvider.js'; +import { TimeRangeInputModel } from '../../../components/Filters/common/filters/TimeRangeInputModel.js'; import { FloatComparisonFilterModel } from '../../../components/Filters/common/filters/FloatComparisonFilterModel.js'; +import { detectorsProvider } from '../../../services/detectors/detectorsProvider.js'; +import { AliceL3AndDipoleFilteringModel } from '../../../components/Filters/RunsFilter/AliceL3AndDipoleFilteringModel.js'; /** * Model representing handlers for runs page @@ -57,7 +58,15 @@ export class RunsOverviewModel extends OverviewPageModel { this._eorReasonsFilterModel = new EorReasonFilterModel(); this._eorReasonsFilterModel.observe(() => this._applyFilters()); - this._eorReasonsFilterModel.visualChange$.observe(() => this.notify()); + this._eorReasonsFilterModel.visualChange$.bubbleTo(this); + + this._o2StartFilterModel = new TimeRangeInputModel(); + this._o2StartFilterModel.observe(() => this._applyFilters(true)); + this._o2StartFilterModel.visualChange$.bubbleTo(this); + + this._o2StopFilterModel = new TimeRangeInputModel(); + this._o2StopFilterModel.observe(() => this._applyFilters(true)); + this._o2StopFilterModel.visualChange$.bubbleTo(this); this._aliceL3AndDipoleCurrentFilter = new AliceL3AndDipoleFilteringModel(); this._aliceL3AndDipoleCurrentFilter.observe(() => this._applyFilters()); @@ -115,7 +124,7 @@ export class RunsOverviewModel extends OverviewPageModel { * Create the export with the variables set in the model, handling errors appropriately * @param {object[]} runs The source content. * @param {string} fileName The name of the file including the output format. - * @param {Object>} exportFormats defines how particular fields of data units will be formatted + * @param {Object>} exportFormats defines how particular fields of data units will be formated * @return {void} */ async createRunsExport(runs, fileName, exportFormats) { @@ -184,20 +193,13 @@ export class RunsOverviewModel extends OverviewPageModel { this._listingTagsFilterModel.reset(); this._listingRunTypesFilterModel.reset(); this._eorReasonsFilterModel.reset(); + this._o2StartFilterModel.reset(); + this._o2StopFilterModel.reset(); + this._aliceL3AndDipoleCurrentFilter.reset(); this._fillNumbersFilter = ''; - this.o2startFilterFrom = ''; - this.o2startFilterTo = ''; - this.o2startFilterFromTime = '00:00'; - this.o2startFilterToTime = '23:59'; - - this.o2endFilterFrom = ''; - this.o2endFilterTo = ''; - this.o2endFilterFromTime = '00:00'; - this.o2endFilterToTime = '23:59'; - this._runDurationFilter = null; this._lhcPeriodsFilter = null; @@ -238,23 +240,16 @@ export class RunsOverviewModel extends OverviewPageModel { * @return {Boolean} If any filter is active */ isAnyFilterActive() { - return ( - this.runFilterValues !== '' + return this.runFilterValues !== '' || this._runDefinitionFilter.length > 0 || !this._eorReasonsFilterModel.isEmpty() + || !this._o2StartFilterModel.isEmpty + || !this._o2StopFilterModel.isEmpty || !this._detectorsFilterModel.isEmpty() || !this._listingTagsFilterModel.isEmpty() || this._listingRunTypesFilterModel.selected.length !== 0 || this._aliceL3AndDipoleCurrentFilter.selected.length !== 0 || this._fillNumbersFilter !== '' - || this.o2startFilterFrom !== '' - || this.o2startFilterTo !== '' - || this.o2startFilterToTime !== '23:59' - || this.o2startFilterFromTime !== '00:00' - || this.o2endFilterFrom !== '' - || this.o2endFilterTo !== '' - || this.o2endFilterToTime !== '23:59' - || this.o2endFilterFromTime !== '00:00' || this._runDurationFilter !== null || this._lhcPeriodsFilter !== null || this.environmentIdsFilter !== '' @@ -271,8 +266,7 @@ export class RunsOverviewModel extends OverviewPageModel { || this._inelasticInteractionRateAvgFilterModel.isEmpty || this._inelasticInteractionRateAtStartFilterModel.isEmpty || this._inelasticInteractionRateAtMidFilterModel.isEmpty - || this._inelasticInteractionRateAtEndFilterModel.isEmpty - ); + || this._inelasticInteractionRateAtEndFilterModel.isEmpty; } /** @@ -392,99 +386,6 @@ export class RunsOverviewModel extends OverviewPageModel { this._applyFilters(); } - /** - * Returns the current minimum creation date - * @return {number} The current minimum creation date - */ - getO2startFilterFrom() { - return this.o2startFilterFrom; - } - - /** - * Returns the current maximum creation date - * @return {number} The current maximum creation date - */ - getO2startFilterTo() { - return this.o2startFilterTo; - } - - /** - * Returns the current minimum creation time - * @return {number} The current minimum creation time - */ - getO2startFilterFromTime() { - return this.o2startFilterFromTime; - } - - /** - * Returns the current maximum creation time - * @return {number} The current maximum creation time - */ - getO2startFilterToTime() { - return this.o2startFilterToTime; - } - - /** - * Set a datetime for the start datetime filter - * - * @param {string} comparisonType comparison type - * @param {string} date The datetime to be applied to the datetime filter - * @param {boolean} valid states whether provided date is valid - * @return {void} - */ - setO2startFilter(comparisonType, date, valid) { - if (valid) { - this[`o2startFilter${comparisonType}`] = date; - this._applyFilters(); - } - } - - /** - * Returns the current minimum creation datetime - * @return {number} The current minimum creation datetime - */ - getO2endFilterFrom() { - return this.o2endFilterFrom; - } - - /** - * Returns the current maximum creation datetime - * @return {number} The current maximum creation datetime - */ - getO2endFilterTo() { - return this.o2endFilterTo; - } - - /** - * Returns the current minimum creation datetime - * @return {number} The current minimum creation datetime - */ - getO2endFilterFromTime() { - return this.o2endFilterFromTime; - } - - /** - * Returns the current maximum creation datetime - * @return {number} The current maximum creation datetime - */ - getO2endFilterToTime() { - return this.o2endFilterToTime; - } - - /** - * Set a datetime for the creation datetime filter - * @param {string} key The filter value to apply the datetime to - * @param {object} date The datetime to be applied to the creation datetime filter - * @param {boolean} valid Whether the inserted date passes validity check - * @return {undefined} - */ - setO2endFilter(key, date, valid) { - if (valid) { - this[`o2endFilter${key}`] = date; - this._applyFilters(); - } - } - /** * Returns the run duration filter (filter is defined in minutes) * @return {{operator: string, limit: (number|null)}|null} The current run duration filter @@ -811,12 +712,30 @@ export class RunsOverviewModel extends OverviewPageModel { /** * Return the model handling the filtering on run types * - * @return {EorReasonsFilterModel} the run type filtering model + * @return {EorReasonFilterModel} the run type filtering model */ get eorReasonsFilterModel() { return this._eorReasonsFilterModel; } + /** + * Returns the model for o2 start filter + * + * @return {TimeRangeInputModel} the filter model + */ + get o2StartFilterModel() { + return this._o2StartFilterModel; + } + + /** + * Returns the model for o2 stop filter + * + * @return {TimeRangeInputModel} the filter model + */ + get o2stopFilterModel() { + return this._o2StopFilterModel; + } + /** * Set the model handling the filtering on run types * @param {EorReasonFilterModel} model the model to set @@ -894,7 +813,6 @@ export class RunsOverviewModel extends OverviewPageModel { inelasticInteractionRateAtMid: this._inelasticInteractionRateAtMidFilterModel, inelasticInteractionRateAtEnd: this._inelasticInteractionRateAtEndFilterModel, }; - return { ...this.runFilterValues && { 'filter[runNumbers]': this.runFilterValues, @@ -914,21 +832,17 @@ export class RunsOverviewModel extends OverviewPageModel { ...this._fillNumbersFilter && { 'filter[fillNumbers]': this._fillNumbersFilter, }, - ...this.o2startFilterFrom && { - 'filter[o2start][from]': - new Date(`${this.o2startFilterFrom.replace(/\//g, '-')}T${this.o2startFilterFromTime}:00.000`).getTime(), + ...!this._o2StartFilterModel.fromTimeInputModel.isEmpty && { + 'filter[o2start][from]': this._o2StartFilterModel.normalized.from, }, - ...this.o2startFilterTo && { - 'filter[o2start][to]': - new Date(`${this.o2startFilterTo.replace(/\//g, '-')}T${this.o2startFilterToTime}:59.999`).getTime(), + ...!this._o2StartFilterModel.toTimeInputModel.isEmpty && { + 'filter[o2start][to]': this._o2StartFilterModel.normalized.to, }, - ...this.o2endFilterFrom && { - 'filter[o2end][from]': - new Date(`${this.o2endFilterFrom.replace(/\//g, '-')}T${this.o2endFilterFromTime}:00.000`).getTime(), + ...!this._o2StopFilterModel.fromTimeInputModel.isEmpty && { + 'filter[o2end][from]': this._o2StopFilterModel.normalized.from, }, - ...this.o2endFilterTo && { - 'filter[o2end][to]': - new Date(`${this.o2endFilterTo.replace(/\//g, '-')}T${this.o2endFilterToTime}:59.999`).getTime(), + ...!this._o2StopFilterModel.toTimeInputModel.isEmpty && { + 'filter[o2end][to]': this._o2StopFilterModel.normalized.to, }, ...this._runDurationFilter && this._runDurationFilter.limit !== null && { 'filter[runDuration][operator]': this._runDurationFilter.operator, diff --git a/test/public/defaults.js b/test/public/defaults.js index 35403816df..04b5d9c8d6 100644 --- a/test/public/defaults.js +++ b/test/public/defaults.js @@ -396,7 +396,7 @@ module.exports.getInnerText = getInnerText; * @return {Promise} resolves once the text has been checked */ module.exports.expectInnerText = async (page, selector, innerText) => { - const element = await page.waitForSelector(selector); + const elementHandle = await page.waitForSelector(selector); try { await page.waitForFunction( (selector, innerText) => document.querySelector(selector).innerText === innerText, @@ -405,8 +405,38 @@ module.exports.expectInnerText = async (page, selector, innerText) => { innerText, ); } catch (_) { - throw new Error(`Expected innerText for ${selector} to be "${innerText}", got "${await getInnerText(element)}"`); + const actualInnerText = await getInnerText(elementHandle); + await elementHandle.dispose(); + throw new Error(`Expected innerText for ${selector} to be "${innerText}", got "${actualInnerText}"`); + } + await elementHandle.dispose(); +}; + +/** + * Expect a given attribute of an element to have a given value + * + * @param {puppeteer.Page} page the puppeteer page + * @param {string} selector the selector of the element to look for attribute + * @param {string} attributeKey the key of the attribute to check + * @param {string} attributeValue the expected value + * @return {Promise} resolves once the attribute has the expected value + */ +module.exports.expectAttributeValue = async (page, selector, attributeKey, attributeValue) => { + const elementHandle = await page.waitForSelector(selector); + try { + await page.waitForFunction( + (selector, attributeKey, attributeValue) => document.querySelector(selector).getAttribute(attributeKey) === attributeValue, + {}, + selector, + attributeKey, + attributeValue, + ); + } catch (_) { + const actualAttributeValue = await elementHandle.evaluate((element, attributeKey) => element.getAttribute(attributeKey), attributeKey); + await elementHandle.dispose(); + throw new Error(`Expect attribute ${attributeKey} to be ${attributeValue}, got ${actualAttributeValue}`); } + await elementHandle.dispose(); }; /** @@ -818,3 +848,19 @@ module.exports.expectLink = async (element, selector, { href, innerText }) => { * @return {boolean} true if format is correct, false otherwise */ module.exports.validateDate = (date, format = 'DD/MM/YYYY hh:mm:ss') => !isNaN(dateAndTime.parse(date, format)); + +/** + * Return the selector for all the inputs composing a period inputs + * + * @param {string} popoverSelector the selector of the period inputs parent + * @return {{fromDateSelector: string, fromTimeSelector: string, toDateSelector: string, toTimeSelector: string}} the selectors + */ +module.exports.getPeriodInputsSelectors = (popoverSelector) => { + const commonInputsAncestor = `${popoverSelector} > div > div > div > div`; + return { + fromDateSelector: `${commonInputsAncestor} > div:nth-child(1) input:nth-child(1)`, + fromTimeSelector: `${commonInputsAncestor} > div:nth-child(1) input:nth-child(2)`, + toDateSelector: `${commonInputsAncestor} > div:nth-child(2) input:nth-child(1)`, + toTimeSelector: `${commonInputsAncestor} > div:nth-child(2) input:nth-child(2)`, + }; +}; diff --git a/test/public/logs/create.test.js b/test/public/logs/create.test.js index 5218bcc9d8..3147896907 100644 --- a/test/public/logs/create.test.js +++ b/test/public/logs/create.test.js @@ -46,6 +46,7 @@ module.exports = () => { let browser; let url; const assetsDir = [__dirname, '../..', 'assets']; + const downloadDir = [__dirname, '../../../..', 'database/storage']; before(async () => { [page, browser, url] = await defaultBefore(); @@ -333,6 +334,13 @@ module.exports = () => { expect(lastLog.attachments).to.lengthOf(2); expect(lastLog.attachments[0].originalName).to.equal(file1); expect(lastLog.attachments[1].originalName).to.equal(file2); + + try { + fs.unlinkSync(path.resolve(...downloadDir, file1)); + fs.unlinkSync(path.resolve(...downloadDir, file2)); + } catch (_) { + // Do not care if file do not exist, this is just cleaning + } }).timeout(12000); it('can clear the file attachment input if at least one is submitted', async () => { @@ -345,7 +353,8 @@ module.exports = () => { // Add a single file attachment to the input field const attachmentsInput = await page.$('#attachments'); - attachmentsInput.uploadFile(path.resolve(...assetsDir, '1200px-CERN_logo.png')); + const fileName = '1200px-CERN_logo.png'; + attachmentsInput.uploadFile(path.resolve(...assetsDir, fileName)); await waitForTimeout(500); // We expect the clear button to appear @@ -360,6 +369,12 @@ module.exports = () => { await waitForTimeout(100); const newUploadedAttachments = await page.evaluate((element) => element.value, attachmentsInput); expect(newUploadedAttachments).to.equal(''); + + try { + fs.unlinkSync(path.resolve(...downloadDir, fileName)); + } catch (_) { + // Do not care if file do not exist, this is just cleaning + } }); it('can create a log with a run number', async () => { diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 20573f18c5..4f574cbbcf 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -22,25 +22,26 @@ const { getFirstRow, goToPage, checkColumnBalloon, - expectLink, - waitForDownload, fillInput, getPopoverContent, getInnerText, - waitForTimeout, getPopoverSelector, + getPeriodInputsSelectors, waitForTableLength, - waitForTableTotalRowsCountToEqual, - waitForEmptyTable, waitForNavigation, + waitForTableTotalRowsCountToEqual, expectInputValue, expectColumnValues, + waitForEmptyTable, + waitForDownload, + expectLink, expectUrlParams, + expectAttributeValue, } = require('../defaults.js'); const { RUN_QUALITIES, RunQualities } = require('../../../lib/domain/enums/RunQualities.js'); -const { runService } = require('../../../lib/server/services/run/RunService.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); const { RunDefinition } = require('../../../lib/domain/enums/RunDefinition.js'); +const { runService } = require('../../../lib/server/services/run/RunService.js'); const { expect } = chai; @@ -53,18 +54,6 @@ module.exports = () => { let table; let firstRowId; const runNumberInputSelector = '.runNumber-filter input'; - const timeFilterSelectors = { - startFrom: '#o2startFilterFromTime', - startTo: '#o2startFilterToTime', - endFrom: '#o2endFilterFromTime', - endTo: '#o2endFilterToTime', - }; - const dateFilterSelectors = { - startFrom: '#o2startFilterFrom', - startTo: '#o2startFilterTo', - endFrom: '#o2endFilterFrom', - endTo: '#o2endFilterTo', - }; before(async () => { [page, browser] = await defaultBefore(page, browser); @@ -282,6 +271,7 @@ module.exports = () => { // Run 106 has detectors and tags that overflow await page.type(runNumberInputSelector, '106'); + await fillInput(page, runNumberInputSelector, '106'); await waitForTableLength(page, 1); await checkColumnBalloon(page, 1, 2); @@ -437,110 +427,39 @@ module.exports = () => { ); }); - it('should update to current date when empty and time is set', async () => { + it('Should correctly set the the min/max of time range picker inputs', async () => { await goToPage(page, 'run-overview'); // Open the filters await pressElement(page, '#openFilterToggle'); - await page.waitForSelector('#o2startFilterFromTime'); - let today = new Date(); - today.setMinutes(today.getMinutes() - today.getTimezoneOffset()); - [today] = today.toISOString().split('T'); - const time = '00:01'; - - for (const selector of Object.values(timeFilterSelectors)) { - await page.type(selector, time); - await waitForTimeout(300); - } - for (const selector of Object.values(dateFilterSelectors)) { - const value = await page.$eval(selector, (element) => element.value); - expect(String(value)).to.equal(today); - } - const date = new Date(); - const now = `${date.getHours()}:${(date.getMinutes() < 10 ? '0' : '') + date.getMinutes()}`; - const firstTill = await page.$eval(timeFilterSelectors.startFrom, (element) => element.getAttribute('max')); - const secondTill = await page.$eval(timeFilterSelectors.endFrom, (element) => element.getAttribute('max')); - expect(String(firstTill)).to.equal(now); - expect(String(secondTill)).to.equal(now); - }); - it('Validates date will not be set again', async () => { - await goToPage(page, 'run-overview'); - const dateString = '03-21-2021'; - const validValue = '2021-03-21'; - // Open the filters - await pressElement(page, '#openFilterToggle'); - await page.waitForSelector('#eorDescription'); - - // Set date - for (const key in dateFilterSelectors) { - await page.focus(dateFilterSelectors[key]); - await page.keyboard.type(dateString); - await waitForTimeout(500); - await page.focus(timeFilterSelectors[key]); - await page.keyboard.type('00-01-AM'); - await waitForTimeout(500); - const value = await page.$eval(dateFilterSelectors[key], (element) => element.value); - expect(value).to.equal(validValue); - } - }); + await pressElement(page, '.timeO2Start-filter .popover-trigger'); - it('The max/min should be the right value when date is set to same day', async () => { - await goToPage(page, 'run-overview'); + const o2StartPopoverSelector = await getPopoverSelector(await page.$('.timeO2Start-filter .popover-trigger')); + const periodInputsSelectors = getPeriodInputsSelectors(o2StartPopoverSelector); - const dateString = '03-02-2021'; - // Open the filters - await pressElement(page, '#openFilterToggle'); - await page.waitForSelector('#eorDescription'); + await fillInput(page, periodInputsSelectors.fromTimeSelector, '11:11', ['change']); + await fillInput(page, periodInputsSelectors.toTimeSelector, '14:00', ['change']); - // Set date to an open day - for (const selector of Object.values(dateFilterSelectors)) { - await page.type(selector, dateString); - await waitForTimeout(300); - } - await page.type(timeFilterSelectors.startFrom, '11:11'); - await page.type(timeFilterSelectors.startTo, '14:00'); - await page.type(timeFilterSelectors.endFrom, '11:11'); - await page.type(timeFilterSelectors.endTo, '14:00'); - await waitForTimeout(500); - - // Validate if the max value is the same as the till values - const startMax = await page.$eval(timeFilterSelectors.startFrom, (element) => element.getAttribute('max')); - const endMax = await page.$eval(timeFilterSelectors.endFrom, (element) => element.getAttribute('max')); - expect(String(startMax)).to.equal(await page.$eval(timeFilterSelectors.startTo, (element) => element.value)); - expect(String(endMax)).to.equal(await page.$eval(timeFilterSelectors.endTo, (element) => element.value)); - - // Validate if the min value is the same as the from values - const startMin = await page.$eval(timeFilterSelectors.startTo, (element) => element.getAttribute('min')); - const endMin = await page.$eval(timeFilterSelectors.endTo, (element) => element.getAttribute('min')); - expect(String(startMin)).to.equal(await page.$eval(timeFilterSelectors.startFrom, (element) => element.value)); - expect(String(endMin)).to.equal(await page.$eval(timeFilterSelectors.endFrom, (element) => element.value)); - }); + // American style input + await fillInput(page, periodInputsSelectors.fromDateSelector, '2021-02-03', ['change']); + await fillInput(page, periodInputsSelectors.toDateSelector, '2021-02-03', ['change']); - it('The max should be the maximum value when having different dates', async () => { - await goToPage(page, 'run-overview'); + // Wait for page to be refreshed + await expectAttributeValue(page, periodInputsSelectors.toTimeSelector, 'min', '11:12'); + await expectAttributeValue(page, periodInputsSelectors.toDateSelector, 'min', '2021-02-03'); - const dateString = '03-20-2021'; - const maxTime = '23:59'; - const minTime = '00:00'; - // Open the filters - await pressElement(page, '#openFilterToggle'); - await page.waitForSelector('#eorDescription'); - // Set date to an open day - for (const selector of Object.values(dateFilterSelectors)) { - await page.type(selector, dateString); - await waitForTimeout(500); - } - const startMax = await page.$eval(timeFilterSelectors.startFrom, (element) => element.getAttribute('max')); - const endMax = await page.$eval(timeFilterSelectors.endFrom, (element) => element.getAttribute('max')); - expect(String(startMax)).to.equal(maxTime); - expect(String(endMax)).to.equal(maxTime); - - // Validate if the min value is the same as the from values - const startMin = await page.$eval(timeFilterSelectors.startTo, (element) => element.getAttribute('min')); - const endMin = await page.$eval(timeFilterSelectors.endTo, (element) => element.getAttribute('min')); - expect(String(startMin)).to.equal(minTime); - expect(String(endMin)).to.equal(minTime); + await expectAttributeValue(page, periodInputsSelectors.fromTimeSelector, 'max', '13:59'); + await expectAttributeValue(page, periodInputsSelectors.fromDateSelector, 'max', '2021-02-03'); + + // Setting different dates, still american style input + await fillInput(page, periodInputsSelectors.toDateSelector, '2021-02-05', ['change']); + + await expectAttributeValue(page, periodInputsSelectors.toTimeSelector, 'min', ''); + await expectAttributeValue(page, periodInputsSelectors.toDateSelector, 'min', '2021-02-03'); + + await expectAttributeValue(page, periodInputsSelectors.fromTimeSelector, 'max', ''); + await expectAttributeValue(page, periodInputsSelectors.fromDateSelector, 'max', '2021-02-05'); }); it('should successfully filter on duration', async () => { @@ -1134,12 +1053,12 @@ module.exports = () => { await expectLink(page, `${popoverSelector} a:nth-of-type(1)`, { href: 'http://localhost:8081/?q={%22partition%22:{%22match%22:%22TDI59So3d%22},' - + '%22run%22:{%22match%22:%22104%22},%22severity%22:{%22in%22:%22W%20E%20F%22}}', + + '%22run%22:{%22match%22:%22104%22},%22severity%22:{%22in%22:%22W%20E%20F%22}}', innerText: 'Infologger FLP', }); await expectLink(page, `${popoverSelector} a:nth-of-type(2)`, { href: 'http://localhost:8082/' + - '?page=layoutShow&runNumber=104&definition=COMMISSIONING&detector=CPV&pdpBeamType=cosmic&runType=COSMICS', + '?page=layoutShow&runNumber=104&definition=COMMISSIONING&detector=CPV&pdpBeamType=cosmic&runType=COSMICS', innerText: 'QCG', }); diff --git a/test/public/runs/runsPerDataPass.overview.test.js b/test/public/runs/runsPerDataPass.overview.test.js index 990ca1d3bf..1906833373 100644 --- a/test/public/runs/runsPerDataPass.overview.test.js +++ b/test/public/runs/runsPerDataPass.overview.test.js @@ -347,28 +347,8 @@ module.exports = () => { await expectColumnValues(page, 'runNumber', ['108', '107', '106']); }); - it('should successfully apply timeStart filter', async () => { - await navigateToRunsPerDataPass(page, { lhcPeriodId: 2, dataPassId: 2 }, { epectedRowsCount: 3 }); - await pressElement(page, '#openFilterToggle'); - - await fillInput(page, '.timeO2Start-filter input[type=date]', '2021-01-01'); - await expectColumnValues(page, 'runNumber', ['1']); - - await pressElement(page, '#reset-filters'); - await expectColumnValues(page, 'runNumber', ['55', '2', '1']); - }); - - it('should successfully apply timeEnd filter', async () => { - await pressElement(page, '#openFilterToggle'); - - await fillInput(page, '.timeO2End-filter input[type=date]', '2021-01-01'); - await expectColumnValues(page, 'runNumber', ['1']); - - await pressElement(page, '#reset-filters', true); - await expectColumnValues(page, 'runNumber', ['55', '2', '1']); - }); - it('should successfully apply duration filter', async () => { + await navigateToRunsPerDataPass(page, { lhcPeriodId: 2, dataPassId: 2 }, { epectedRowsCount: 3 }); await pressElement(page, '#openFilterToggle'); await page.select('.runDuration-filter select', '>=');