diff --git a/lib/public/components/Filters/common/filters/TimeRangeInputModel.js b/lib/public/components/Filters/common/filters/TimeRangeInputModel.js index 66a4481847..d8050e6104 100644 --- a/lib/public/components/Filters/common/filters/TimeRangeInputModel.js +++ b/lib/public/components/Filters/common/filters/TimeRangeInputModel.js @@ -35,11 +35,11 @@ export class TimeRangeInputModel extends FilterModel { constructor(value, periodLabel, configuration) { super(); - const { required = false, seconds } = configuration || {}; + const { required = false, seconds, milliseconds } = configuration || {}; this._required = required; - this._fromTimeInputModel = new DateTimeInputModel({ seconds }); - this._toTimeInputModel = new DateTimeInputModel({ seconds }); + this._fromTimeInputModel = new DateTimeInputModel({ seconds, milliseconds }); + this._toTimeInputModel = new DateTimeInputModel({ seconds, milliseconds }); // eslint-disable-next-line no-return-assign this._fromTimeInputModel.observe(() => this._periodLabel = null); diff --git a/lib/public/components/common/form/inputs/DateTimeInputComponent.js b/lib/public/components/common/form/inputs/DateTimeInputComponent.js index 9a8e7a0ada..683eb62272 100644 --- a/lib/public/components/common/form/inputs/DateTimeInputComponent.js +++ b/lib/public/components/common/form/inputs/DateTimeInputComponent.js @@ -26,6 +26,7 @@ import { formatTimestampForDateTimeInput } from '../../../../utilities/formattin * @property {function} onChange function called with the new value when it changes * @property {Partial} [defaults] the default raw values to use when partially updating inputs * @property {boolean} [seconds=false] states if the input has granularity up to seconds (if not, granularity is minutes) + * @property {boolean} [milliseconds=false] states if the input has granularity up to milliseconds * @property {boolean} [required] states if the inputs can be omitted * @property {number} [min] the timestamp of the minimum date allowed by the input (included) * @property {number} [max] the timestamp of the maximum date allowed by the input (excluded) @@ -94,7 +95,7 @@ export class DateTimeInputComponent extends StatefulComponent { type: 'time', required: this._required, value: this._value.time, - step: this._seconds ? 1 : undefined, + step: this._milliseconds ? 0.001 : this._seconds ? 1 : undefined, onchange: (e) => this._patchValue({ time: e.target.value }), // Mithril do not remove min/max if previously set... min: inputsMin?.time ?? '', @@ -117,13 +118,14 @@ export class DateTimeInputComponent extends StatefulComponent { * @private */ _updateAttrs(attrs) { - const { value, onChange, seconds, required = false, defaults = {}, min = null, max = null } = attrs; + const { value, onChange, seconds, milliseconds, required = false, defaults = {}, min = null, max = null } = attrs; const { date: defaultDate = '', time: defaultTime = '' } = defaults; this._value = value; this._onChange = onChange; this._seconds = seconds; + this._milliseconds = milliseconds; this._required = required; @@ -179,7 +181,7 @@ export class DateTimeInputComponent extends StatefulComponent { const rawDate = raw.date || null; const rawTime = raw.time || null; - const minDateAndTime = formatTimestampForDateTimeInput(minTimestamp, this._seconds); + const minDateAndTime = formatTimestampForDateTimeInput(minTimestamp, this._seconds || this._milliseconds, this._milliseconds); const inputsMin = {}; if (rawDate !== null && rawDate === minDateAndTime.date) { @@ -187,7 +189,11 @@ export class DateTimeInputComponent extends StatefulComponent { } if (rawTime !== null && rawTime < minDateAndTime.time) { - inputsMin.date = formatTimestampForDateTimeInput(minTimestamp + MILLISECONDS_IN_ONE_DAY, this._seconds).date; + inputsMin.date = formatTimestampForDateTimeInput( + minTimestamp + MILLISECONDS_IN_ONE_DAY, + this._seconds || this._milliseconds, + this._milliseconds, + ).date; } else { inputsMin.date = minDateAndTime.date; } @@ -210,14 +216,14 @@ export class DateTimeInputComponent extends StatefulComponent { const rawDate = raw.date || null; const rawTime = raw.time || null; - const maxDateAndTime = formatTimestampForDateTimeInput(maxTimestamp, this._seconds); + const maxDateAndTime = formatTimestampForDateTimeInput(maxTimestamp, this._seconds, this._milliseconds); const inputsMax = {}; if (rawDate !== null && rawDate === maxDateAndTime.date) { inputsMax.time = maxDateAndTime.time; } if (rawTime !== null && rawTime > maxDateAndTime.time) { - inputsMax.date = formatTimestampForDateTimeInput(maxTimestamp - MILLISECONDS_IN_ONE_DAY, this._seconds).date; + inputsMax.date = formatTimestampForDateTimeInput(maxTimestamp - MILLISECONDS_IN_ONE_DAY, this._seconds, this._milliseconds).date; } else { inputsMax.date = maxDateAndTime.date; } diff --git a/lib/public/components/common/form/inputs/DateTimeInputModel.js b/lib/public/components/common/form/inputs/DateTimeInputModel.js index 69456fd95d..91604692f7 100644 --- a/lib/public/components/common/form/inputs/DateTimeInputModel.js +++ b/lib/public/components/common/form/inputs/DateTimeInputModel.js @@ -37,12 +37,14 @@ export class DateTimeInputModel extends Observable { * Constructor * @param {object} [configuration] the model configuration * @param {boolean} [configuration.seconds=false] states if the date time input granularity is seconds (else, it is minutes) + * @param {boolean} [configuration.milliseconds=false] states if the date time input granularity is milliseconds */ constructor(configuration) { super(); - const { seconds } = configuration || {}; + const { seconds = false, milliseconds = false } = configuration || {}; this._seconds = seconds; + this._milliseconds = milliseconds; /** * @type {DateTimeInputRawData} @@ -129,7 +131,7 @@ export class DateTimeInputModel extends Observable { this._value = value; this._raw = value !== null - ? formatTimestampForDateTimeInput(value, this._seconds) + ? formatTimestampForDateTimeInput(value, this._seconds || this._milliseconds, this._milliseconds) : { date: '', time: '' }; !silent && this.notify(); diff --git a/lib/public/components/common/form/inputs/TimeRangeInputComponent.js b/lib/public/components/common/form/inputs/TimeRangeInputComponent.js index 3bb6f05f68..3ec854f548 100644 --- a/lib/public/components/common/form/inputs/TimeRangeInputComponent.js +++ b/lib/public/components/common/form/inputs/TimeRangeInputComponent.js @@ -34,7 +34,10 @@ import { DateTimeInputComponent } from '../../../common/form/inputs/DateTimeInpu * @param {TimeRangeInputComponentOptions} options inputs options * @return {Component} time range input component */ -export const timeRangeInput = (timeRangeInputModel, { seconds, min, max, fromInputConfiguration, toInputConfiguration } = {}) => { +export const timeRangeInput = ( + timeRangeInputModel, + { milliseconds, seconds, min, max, fromInputConfiguration, toInputConfiguration } = {}, +) => { const { label: fromLabel = h('', 'From') } = fromInputConfiguration || {}; const { label: toLabel = h('', 'To') } = toInputConfiguration || {}; @@ -55,7 +58,7 @@ export const timeRangeInput = (timeRangeInputModel, { seconds, min, max, fromInp toMin = fromValue; } - const timeStep = seconds ? 1000 : 60 * 1000; + const timeStep = milliseconds ? 1 : seconds ? 1000 : 60 * 1000; return h('.flex-column.g3', [ h('.flex-column.g1', [ h('label.flex-row.g2', fromLabel), @@ -64,6 +67,7 @@ export const timeRangeInput = (timeRangeInputModel, { seconds, min, max, fromInp onChange: (value) => timeRangeInputModel.fromTimeInputModel.update(value), required: timeRangeInputModel.isRequired(), seconds, + milliseconds, min, max: fromMax ? fromMax - timeStep : null, }), @@ -75,6 +79,7 @@ export const timeRangeInput = (timeRangeInputModel, { seconds, min, max, fromInp onChange: (value) => timeRangeInputModel.toTimeInputModel.update(value), required: timeRangeInputModel.isRequired(), seconds, + milliseconds, min: toMin ? toMin + timeStep : null, max, }), diff --git a/lib/public/utilities/formatting/dateTimeInputFormatters.mjs b/lib/public/utilities/formatting/dateTimeInputFormatters.mjs index 1a7c236900..f26350c065 100644 --- a/lib/public/utilities/formatting/dateTimeInputFormatters.mjs +++ b/lib/public/utilities/formatting/dateTimeInputFormatters.mjs @@ -16,9 +16,10 @@ * * @param {number} timestamp the timestamp (ms) to format * @param {boolean} enableSeconds states if the time input granularity is seconds (else, it is minutes) + * @param {boolean} enableMilliseconds states if the time input granularity is milliseconds * @return {DateTimeInputRawData} the date expression to use as HTML input values */ -export const formatTimestampForDateTimeInput = (timestamp, enableSeconds) => { +export const formatTimestampForDateTimeInput = (timestamp, enableSeconds, enableMilliseconds) => { const date = new Date(timestamp); /** @@ -40,6 +41,10 @@ export const formatTimestampForDateTimeInput = (timestamp, enableSeconds) => { const seconds = pad(date.getSeconds()); timeExpression = `${timeExpression}:${seconds}`; } + if (enableMilliseconds) { + const milliseconds = `${date.getMilliseconds()}`.padStart(3, '0'); + timeExpression = `${timeExpression}.${milliseconds}`; + } return { date: dateExpression, time: timeExpression }; }; @@ -51,10 +56,13 @@ export const formatTimestampForDateTimeInput = (timestamp, enableSeconds) => { * @return {number} the resulting timestamp */ export const extractTimestampFromDateTimeInput = ({ date, time }) => { - // Add seconds if they are not already here if (time.length === 5) { - time = `${time}:00`; + // HH:MM -> HH:MM:SS.mmm + time = `${time}:00.000`; + } else if (time.length === 8) { + // HH:MM:SS -> HH:MM:SS.mmm + time = `${time}.000`; } - return Date.parse(`${date}T${time}.000`); + return Date.parse(`${date}T${time}`); }; diff --git a/lib/public/views/QcFlags/Create/QcFlagCreationModel.js b/lib/public/views/QcFlags/Create/QcFlagCreationModel.js index f68d8acbca..9246caf79a 100644 --- a/lib/public/views/QcFlags/Create/QcFlagCreationModel.js +++ b/lib/public/views/QcFlags/Create/QcFlagCreationModel.js @@ -45,7 +45,7 @@ export class QcFlagCreationModel extends Observable { this._startTime = startTime; this._endTime = endTime; - this._timeRangeModel = new TimeRangeInputModel(null, null, { required: true, seconds: true }); + this._timeRangeModel = new TimeRangeInputModel(null, null, { required: true, seconds: true, milliseconds: true }); this._timeRangeModel.fromTimeInputModel.setValue(startTime); this._timeRangeModel.toTimeInputModel.setValue(endTime); this._timeRangeModel.bubbleTo(this); @@ -140,9 +140,9 @@ export class QcFlagCreationModel extends Observable { for (const run of runs) { const { firstTfTimestamp, timeTrgStart, timeO2Start, lastTfTimestamp, timeTrgEnd, timeO2End } = run; - const startTime = Math.ceil((firstTfTimestamp ?? timeTrgStart ?? timeO2Start ?? null) / 1000) * 1000; // Round to next second + const startTime = firstTfTimestamp ?? timeTrgStart ?? timeO2Start ?? null; // Do not use run's endTime because it's automatically coalesced to `now` - const endTime = Math.floor(lastTfTimestamp ?? timeTrgEnd ?? timeO2End ?? null); // Round to previous second + const endTime = lastTfTimestamp ?? timeTrgEnd ?? timeO2End ?? null; // Round to previous second if (!startTime) { timings.startTime = null; diff --git a/lib/public/views/QcFlags/Create/qcFlagCreationComponent.js b/lib/public/views/QcFlags/Create/qcFlagCreationComponent.js index bb2d75b78f..e20db9f114 100644 --- a/lib/public/views/QcFlags/Create/qcFlagCreationComponent.js +++ b/lib/public/views/QcFlags/Create/qcFlagCreationComponent.js @@ -47,8 +47,8 @@ const selectedRunsAndDetectorsTable = (runsWithDetectors) => h( h('tbody', runsWithDetectors.map(({ run, detectors }) => h('tr', [ h('td', frontLink(run.runNumber, 'run-detail', { runNumber: run.runNumber })), h('td', detectors.map((detector) => detector.name).join(', ')), - h('td', formatRunStart(run, { qc: true })), - h('td', formatRunEnd(run, { qc: true })), + h('td', formatRunStart(run, { qc: true, displayMilliseconds: true })), + h('td', formatRunEnd(run, { qc: true, displayMilliseconds: true })), ]))), ]), ], @@ -74,7 +74,7 @@ const qcFlagCreationForm = (creationModel) => { } else if (startTime > endTime) { content = h('em', 'The selected runs don\'t have overlapping start/stop times'); } else if (isTimeBasedQcFlag) { - content = timeRangeInput(timeRangeModel, { seconds: true, min: startTime, max: endTime }); + content = timeRangeInput(timeRangeModel, { seconds: true, milliseconds: true, min: startTime, max: endTime }); } else { content = h('em', 'The flag will be applied on the full run'); } diff --git a/lib/public/views/Runs/format/formatRunEnd.js b/lib/public/views/Runs/format/formatRunEnd.js index dee5ed06cc..4ca87b5df2 100644 --- a/lib/public/views/Runs/format/formatRunEnd.js +++ b/lib/public/views/Runs/format/formatRunEnd.js @@ -13,7 +13,7 @@ import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; import { h, iconWarning } from '/js/src/index.js'; -import { getLocaleDateAndTime } from '../../../utilities/dateUtils.mjs'; +import { getLocaleDateAndTime, getLocaleDateAndTimeWithMilliseconds } from '../../../utilities/dateUtils.mjs'; import { tooltip } from '../../../components/common/popover/tooltip.js'; import { TriggerValue } from '../../../domain/enums/TriggerValue.js'; @@ -26,10 +26,11 @@ const MISSING_TRIGGER_STOP_WARNING = 'O2 stop is displayed because trigger stop * @param {object} [configuration] eventual display configuration * @param {boolean} [configuration.inline] true if the date must be inlined * @param {boolean} [configuration.qc] true if QC run end must be displayed, i.e. if last TF timestamp must be used if available + * @param {boolean} [configuration.milliseconds] true if the date must be displayed with milliseconds * @return {Component} the formatted end date */ export const formatRunEnd = (run, configuration) => { - const { inline = false, qc = false } = configuration || {}; + const { inline = false, qc = false, displayMilliseconds = false } = configuration || {}; const { timeTrgEnd, timeO2End, lastTfTimestamp, triggerValue } = run; let runEnd = timeTrgEnd || timeO2End; @@ -38,19 +39,21 @@ export const formatRunEnd = (run, configuration) => { } if (timeTrgEnd || triggerValue === TriggerValue.Off) { - return formatTimestamp(runEnd, inline); + return formatTimestamp(runEnd, inline, displayMilliseconds); } if (timeO2End) { if (inline) { return h('span', [ h('.flex-row.items-center.g2', [ - formatTimestamp(runEnd, inline), + formatTimestamp(runEnd, inline, displayMilliseconds), tooltip(iconWarning(), MISSING_TRIGGER_STOP_WARNING), ]), ]); } else { - const { date, time } = getLocaleDateAndTime(runEnd); + const { date, time } = displayMilliseconds + ? getLocaleDateAndTimeWithMilliseconds(runEnd) + : getLocaleDateAndTime(runEnd); return h('', [ h('', date), h('.flex-row.g2.items-center', [h('', time), tooltip(iconWarning(), MISSING_TRIGGER_STOP_WARNING)]), diff --git a/lib/public/views/Runs/format/formatRunStart.js b/lib/public/views/Runs/format/formatRunStart.js index eb30fa3901..035d0282be 100644 --- a/lib/public/views/Runs/format/formatRunStart.js +++ b/lib/public/views/Runs/format/formatRunStart.js @@ -13,7 +13,7 @@ import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; import { h, iconWarning } from '/js/src/index.js'; -import { getLocaleDateAndTime } from '../../../utilities/dateUtils.mjs'; +import { getLocaleDateAndTime, getLocaleDateAndTimeWithMilliseconds } from '../../../utilities/dateUtils.mjs'; import { tooltip } from '../../../components/common/popover/tooltip.js'; import { TriggerValue } from '../../../domain/enums/TriggerValue.js'; @@ -26,31 +26,33 @@ const MISSING_TRIGGER_START_WARNING = 'O2 start is displayed because trigger sto * @param {object} [configuration] eventual display configuration * @param {boolean} [configuration.inline] true if the date must be inlined * @param {boolean} [configuration.qc] true if QC run start must be displayed, i.e. if first TF timestamp must be used if available + * @param {boolean} [configuration.displayMilliseconds] true if the date must be displayed with milliseconds * @return {Component} the formatted start date */ export const formatRunStart = (run, configuration) => { - const { inline = false, qc = false } = configuration || {}; + const { inline = false, qc = false, displayMilliseconds = false } = configuration || {}; const { timeO2Start, timeTrgStart, firstTfTimestamp, triggerValue } = run; - let runStart = timeTrgStart || timeO2Start; if (qc) { runStart = firstTfTimestamp || runStart; } if (timeTrgStart || triggerValue === TriggerValue.Off) { - return formatTimestamp(runStart, inline); + return formatTimestamp(runStart, inline, displayMilliseconds); } if (timeO2Start) { if (inline) { return h('span', [ h('.flex-row.items-center.g2', [ - formatTimestamp(runStart, inline), + formatTimestamp(runStart, inline, displayMilliseconds), tooltip(iconWarning(), MISSING_TRIGGER_START_WARNING), ]), ]); } else { - const { date, time } = getLocaleDateAndTime(runStart); + const { date, time } = displayMilliseconds + ? getLocaleDateAndTimeWithMilliseconds(runStart) + : getLocaleDateAndTime(runStart); return h('', [ h('', date), h('.flex-row.g2.items-center', [h('', time), tooltip(iconWarning(), MISSING_TRIGGER_START_WARNING)]), diff --git a/test/public/qcFlags/forDataPassCreation.test.js b/test/public/qcFlags/forDataPassCreation.test.js index f154cc337e..fdd2c12abb 100644 --- a/test/public/qcFlags/forDataPassCreation.test.js +++ b/test/public/qcFlags/forDataPassCreation.test.js @@ -102,8 +102,8 @@ module.exports = () => { }); await page.waitForSelector('button#submit[disabled]'); - await expectInnerText(page, 'table > tbody > tr > td:nth-child(3) > div', '08/08/2019\n13:00:00'); - await expectInnerText(page, 'table > tbody > tr > td:nth-child(4) > div', '09/08/2019\n14:00:00'); + await expectInnerText(page, 'table > tbody > tr > td:nth-child(3) > div', '08/08/2019\n13:00:00.000'); + await expectInnerText(page, 'table > tbody > tr > td:nth-child(4) > div', '09/08/2019\n14:00:00.000'); await page.waitForSelector('input[type="time"]', { hidden: true, timeout: 250 }); await pressElement(page, '#flag-type-panel .popover-trigger'); @@ -134,8 +134,8 @@ module.exports = () => { }); await page.waitForSelector('button#submit[disabled]'); - await expectInnerText(page, 'table > tbody > tr > td:nth-child(3) > div', '08/08/2019\n13:00:00'); - await expectInnerText(page, 'table > tbody > tr > td:nth-child(4) > div', '09/08/2019\n14:00:00'); + await expectInnerText(page, 'table > tbody > tr > td:nth-child(3) > div', '08/08/2019\n13:00:00.000'); + await expectInnerText(page, 'table > tbody > tr > td:nth-child(4) > div', '09/08/2019\n14:00:00.000'); await page.waitForSelector('input[type="time"]', { hidden: true }); await pressElement(page, '#flag-type-panel .popover-trigger'); @@ -144,8 +144,8 @@ module.exports = () => { await page.waitForSelector('button#submit[disabled]', { hidden: true }); await pressElement(page, '#time-based-toggle', true); - await fillInput(page, 'div:nth-child(1) > div > input:nth-child(2)', '13:01:01', ['change']); - await fillInput(page, 'div:nth-child(2) > div > input:nth-child(2)', '13:50:59', ['change']); + await fillInput(page, 'div:nth-child(1) > div > input:nth-child(2)', '13:01:01.123', ['change']); + await fillInput(page, 'div:nth-child(2) > div > input:nth-child(2)', '13:50:59.456', ['change']); await waitForNavigation(page, () => pressElement(page, 'button#submit')); expectUrlParams(page, { @@ -158,8 +158,8 @@ module.exports = () => { await expectRowValues(page, 1, { flagType: 'Limited acceptance', - from: '08/08/2019,\n13:01:01.000', - to: '09/08/2019,\n13:50:59.000', + from: '08/08/2019,\n13:01:01.123', + to: '09/08/2019,\n13:50:59.456', }); }); diff --git a/test/public/qcFlags/forSimulationPassCreation.test.js b/test/public/qcFlags/forSimulationPassCreation.test.js index a3625983d1..799a3c936b 100644 --- a/test/public/qcFlags/forSimulationPassCreation.test.js +++ b/test/public/qcFlags/forSimulationPassCreation.test.js @@ -98,8 +98,8 @@ module.exports = () => { }); await page.waitForSelector('button#submit[disabled]'); - await expectInnerText(page, 'table > tbody > tr > td:nth-child(3) > div', '08/08/2019\n13:00:00'); - await expectInnerText(page, 'table > tbody > tr > td:nth-child(4) > div', '09/08/2019\n14:00:00'); + await expectInnerText(page, 'table > tbody > tr > td:nth-child(3) > div', '08/08/2019\n13:00:00.000'); + await expectInnerText(page, 'table > tbody > tr > td:nth-child(4) > div', '09/08/2019\n14:00:00.000'); await page.waitForSelector('input[type="time"]', { hidden: true, timeout: 250 }); await pressElement(page, '#flag-type-panel .popover-trigger'); @@ -130,8 +130,8 @@ module.exports = () => { }); await page.waitForSelector('button#submit[disabled]'); - await expectInnerText(page, 'table > tbody > tr > td:nth-child(3) > div', '08/08/2019\n13:00:00'); - await expectInnerText(page, 'table > tbody > tr > td:nth-child(4) > div', '09/08/2019\n14:00:00'); + await expectInnerText(page, 'table > tbody > tr > td:nth-child(3) > div', '08/08/2019\n13:00:00.000'); + await expectInnerText(page, 'table > tbody > tr > td:nth-child(4) > div', '09/08/2019\n14:00:00.000'); await page.waitForSelector('input[type="time"]', { hidden: true, timeout: 250 }); await pressElement(page, '#flag-type-panel .popover-trigger'); @@ -140,8 +140,8 @@ module.exports = () => { await page.waitForSelector('button#submit[disabled]', { hidden: true, timeout: 250 }); await pressElement(page, '#time-based-toggle', true); - await fillInput(page, '.flex-column.g1:nth-of-type(1) > div input[type="time"]', '13:01:01', ['change']); - await fillInput(page, '.flex-column.g1:nth-of-type(2) > div input[type="time"]', '13:50:59', ['change']); + await fillInput(page, '.flex-column.g1:nth-of-type(1) > div input[type="time"]', '13:01:01.123', ['change']); + await fillInput(page, '.flex-column.g1:nth-of-type(2) > div input[type="time"]', '13:50:59.456', ['change']); await waitForNavigation(page, () => pressElement(page, 'button#submit')); expectUrlParams(page, { @@ -154,8 +154,8 @@ module.exports = () => { await expectRowValues(page, 1, { flagType: 'Limited acceptance', - from: '08/08/2019,\n13:01:01.000', - to: '09/08/2019,\n13:50:59.000', + from: '08/08/2019,\n13:01:01.123', + to: '09/08/2019,\n13:50:59.456', }); });