diff --git a/lib/database/seeders/20240404100811-qc-flags.js b/lib/database/seeders/20240404100811-qc-flags.js index b66ca15bce..560cb644bc 100644 --- a/lib/database/seeders/20240404100811-qc-flags.js +++ b/lib/database/seeders/20240404100811-qc-flags.js @@ -281,6 +281,21 @@ module.exports = { created_at: '2024-08-12 12:00:10', updated_at: '2024-08-12 12:00:10', }, + { + id: 103, + deleted: true, + from: null, + to: '2019-08-08 20:50:00', + comment: 'deleted flag', + + run_number: 56, + flag_type_id: 13, // Bad + created_by_id: 2, + detector_id: 7, // FT0 + + created_at: '2024-08-12 12:00:15', + updated_at: '2024-08-12 12:00:15', + }, // Run : 56, ITS { @@ -394,6 +409,12 @@ module.exports = { from: '2019-08-08 20:50:00', to: null, }, + { + id: 103, + flag_id: 103, + from: null, + to: '2019-08-08 20:50:00', + }, // Run : 56, ITS { diff --git a/lib/public/views/QcFlags/ActiveColumns/synchronousQcFlagsActiveColumns.js b/lib/public/views/QcFlags/ActiveColumns/synchronousQcFlagsActiveColumns.js new file mode 100644 index 0000000000..e087e2b780 --- /dev/null +++ b/lib/public/views/QcFlags/ActiveColumns/synchronousQcFlagsActiveColumns.js @@ -0,0 +1,61 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +import { h } from '/js/src/index.js'; +import { qcFlagsActiveColumns } from './qcFlagsActiveColumns.js'; +import { formatQcFlagStart } from '../format/formatQcFlagStart.js'; +import { formatQcFlagEnd } from '../format/formatQcFlagEnd.js'; +import { formatQcFlagCreatedBy } from '../format/formatQcFlagCreatedBy.js'; +import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.js'; + +/** + * Active columns configuration for synchronous QC flags table + */ +export const synchronousQcFlagsActiveColumns = { + id: { + name: 'Id', + visible: false, + }, + flagType: { + ...qcFlagsActiveColumns.flagType, + classes: 'w-15', + }, + from: { + name: 'From/To', + visible: true, + format: (_, qcFlag) => h('', [ + h('.flex-row', ['From: ', formatQcFlagStart(qcFlag, true)]), + h('.flex-row', ['To: ', formatQcFlagEnd(qcFlag, true)]), + ]), + classes: 'w-15', + }, + comment: { + ...qcFlagsActiveColumns.comment, + balloon: true, + }, + deleted: { + name: 'Deleted', + visible: true, + classes: 'w-5', + format: (deleted) => deleted ? h('.danger', 'Yes') : 'No', + }, + createdBy: { + name: 'Created', + visible: true, + balloon: true, + format: (_, qcFlag) => h('', [ + h('.flex-row', ['By: ', formatQcFlagCreatedBy(qcFlag)]), + h('.flex-row', ['At: ', formatTimestamp(qcFlag.createdAt)]), + ]), + }, +}; diff --git a/lib/public/views/QcFlags/Synchronous/SynchronousQcFlagsOverviewPage.js b/lib/public/views/QcFlags/Synchronous/SynchronousQcFlagsOverviewPage.js index b8937c51ba..8fa5058b25 100644 --- a/lib/public/views/QcFlags/Synchronous/SynchronousQcFlagsOverviewPage.js +++ b/lib/public/views/QcFlags/Synchronous/SynchronousQcFlagsOverviewPage.js @@ -16,7 +16,7 @@ import { h } from '/js/src/index.js'; import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; import { table } from '../../../components/common/table/table.js'; import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; -import { qcFlagsActiveColumns } from '../ActiveColumns/qcFlagsActiveColumns.js'; +import { synchronousQcFlagsActiveColumns } from '../ActiveColumns/synchronousQcFlagsActiveColumns.js'; import { qcFlagsBreadcrumbs } from '../../../components/qcFlags/qcFlagsBreadcrumbs.js'; import { mergeRemoteData } from '../../../utilities/mergeRemoteData.js'; import errorAlert from '../../../components/common/errorAlert.js'; @@ -46,16 +46,6 @@ export const SynchronousQcFlagsOverviewPage = ({ qcFlags: { synchronousOverviewM PAGE_USED_HEIGHT, )); - const activeColumns = { - qcFlagId: { - name: 'Id', - visible: false, - classes: 'w-5', - }, - ...qcFlagsActiveColumns, - }; - delete activeColumns.verified; - return h( '', { onremove: () => synchronousOverviewModel.reset() }, @@ -70,8 +60,8 @@ export const SynchronousQcFlagsOverviewPage = ({ qcFlags: { synchronousOverviewM h('.w-100.flex-column', [ table( qcFlags, - activeColumns, - { classes: '.table-sm' }, + synchronousQcFlagsActiveColumns, + { classes: '.table-sm.f6' }, null, { sort: sortModel }, ), diff --git a/lib/public/views/QcFlags/format/formatQcFlagEnd.js b/lib/public/views/QcFlags/format/formatQcFlagEnd.js index dac1426802..9cb2a7857d 100644 --- a/lib/public/views/QcFlags/format/formatQcFlagEnd.js +++ b/lib/public/views/QcFlags/format/formatQcFlagEnd.js @@ -17,11 +17,12 @@ import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.j * Format QC flag `to` timestamp * * @param {QcFlag} qcFlag QC flag + * @param {boolean} inline if true, date and time are on a single line * @return {Component} formatted `to` timestamp */ -export const formatQcFlagEnd = ({ from, to }) => { +export const formatQcFlagEnd = ({ from, to }, inline = false) => { if (to) { - return formatTimestamp(to, false); + return formatTimestamp(to, inline); } else { return from ? 'Until run end' diff --git a/lib/public/views/QcFlags/format/formatQcFlagStart.js b/lib/public/views/QcFlags/format/formatQcFlagStart.js index b5a11b9b6d..bf9e8ccae5 100644 --- a/lib/public/views/QcFlags/format/formatQcFlagStart.js +++ b/lib/public/views/QcFlags/format/formatQcFlagStart.js @@ -17,11 +17,12 @@ import { formatTimestamp } from '../../../utilities/formatting/formatTimestamp.j * Format QC flag `from` timestamp * * @param {QcFlag} qcFlag QC flag + * @param {boolean} inline if true, date and time are on a single line * @return {Component} formatted `from` timestamp */ -export const formatQcFlagStart = ({ from, to }) => { +export const formatQcFlagStart = ({ from, to }, inline = false) => { if (from) { - return formatTimestamp(from, false); + return formatTimestamp(from, inline); } else { return to ? 'Since run start' diff --git a/test/api/qcFlags.test.js b/test/api/qcFlags.test.js index ee67961439..ece5d54c54 100644 --- a/test/api/qcFlags.test.js +++ b/test/api/qcFlags.test.js @@ -743,8 +743,8 @@ module.exports = () => { const response = await request(server).get(`/api/qcFlags/synchronous?runNumber=${runNumber}&detectorId=${detectorId}`); expect(response.status).to.be.equal(200); const { data: flags, meta } = response.body; - expect(meta).to.be.eql({ page: { totalCount: 2, pageCount: 1 } }); - expect(flags.map(({ id }) => id)).to.have.all.ordered.members([101, 100]); + expect(meta).to.be.eql({ page: { totalCount: 3, pageCount: 1 } }); + expect(flags.map(({ id }) => id)).to.have.all.ordered.members([103, 101, 100]); }); it('should successfully fetch synchronous flags with pagination', async () => { @@ -752,11 +752,11 @@ module.exports = () => { const detectorId = 7; { const response = await request(server) - .get(`/api/qcFlags/synchronous?runNumber=${runNumber}&detectorId=${detectorId}&page[limit]=1&page[offset]=1`); + .get(`/api/qcFlags/synchronous?runNumber=${runNumber}&detectorId=${detectorId}&page[limit]=1&page[offset]=2`); expect(response.status).to.be.equal(200); const { data: flags, meta } = response.body; - expect(meta).to.be.eql({ page: { totalCount: 2, pageCount: 2 } }); + expect(meta).to.be.eql({ page: { totalCount: 3, pageCount: 3 } }); expect(flags).to.be.lengthOf(1); const [flag] = flags; expect(flag.id).to.be.equal(100); @@ -770,7 +770,7 @@ module.exports = () => { { const response = await request(server) .get(`/api/qcFlags/synchronous?runNumber=${runNumber}&detectorId=${detectorId}&filter[createdBy][names]=Jan%20Jansen&filter[createdBy][operator]=or`); - expect(response.body.data).to.be.lengthOf(2); + expect(response.body.data).to.be.lengthOf(3); } { diff --git a/test/lib/server/services/qualityControlFlag/QcFlagService.test.js b/test/lib/server/services/qualityControlFlag/QcFlagService.test.js index f4c533444f..3aa4300ab4 100644 --- a/test/lib/server/services/qualityControlFlag/QcFlagService.test.js +++ b/test/lib/server/services/qualityControlFlag/QcFlagService.test.js @@ -142,15 +142,15 @@ module.exports = () => { const detectorId = 7; { const { rows: flags, count } = await qcFlagService.getAllSynchronousPerRunAndDetector({ runNumber, detectorId }); - expect(count).to.be.equal(2); - expect(flags.map(({ id }) => id)).to.have.all.ordered.members([101, 100]); + expect(count).to.be.equal(3); + expect(flags.map(({ id }) => id)).to.have.all.ordered.members([103, 101, 100]); } { const { rows: flags, count } = await qcFlagService.getAllSynchronousPerRunAndDetector( { runNumber, detectorId }, - { limit: 1, offset: 1 }, + { limit: 1, offset: 2 }, ); - expect(count).to.be.equal(2); + expect(count).to.be.equal(3); expect(flags).to.be.lengthOf(1); const [flag] = flags; expect(flag.id).to.be.equal(100); @@ -2124,10 +2124,10 @@ module.exports = () => { }); }); - it('should successfult fiter sync flags by created by name', async () => { + it('should successfully filter sync flags by created by name', async () => { { const { rows } = await qcFlagService.getAllSynchronousPerRunAndDetector({ runNumber: 56, detectorId: 7 }, {}, { createdBy: { names: ['Jan Jansen'], operator: 'or' }}); - expect(rows).to.be.lengthOf(2); + expect(rows).to.be.lengthOf(3); } { @@ -2136,7 +2136,7 @@ module.exports = () => { } }); - it('should successfult fiter data pass flags by created by name', async () => { + it('should successfully filter data pass flags by created by name', async () => { { const { rows } = await qcFlagService.getAllPerDataPassAndRunAndDetector({ dataPassId: 1, runNumber: 107, detectorId: 1 }, {}, { createdBy: { names: ['John Doe'], operator: 'or' }}); expect(rows).to.be.lengthOf(2); @@ -2148,7 +2148,7 @@ module.exports = () => { } }); - it('should successfult fiter simulation pass flags by created by name', async () => { + it('should successfully filter simulation pass flags by created by name', async () => { { const { rows } = await qcFlagService.getAllPerSimulationPassAndRunAndDetector({ simulationPassId: 1, runNumber: 106, detectorId: 1 }, {}, { createdBy: { names: ['Jan Jansen'], operator: 'or' }}); expect(rows).to.be.lengthOf(2); diff --git a/test/public/qcFlags/synchronousOverview.test.js b/test/public/qcFlags/synchronousOverview.test.js index e72c4eca91..16c2900904 100644 --- a/test/public/qcFlags/synchronousOverview.test.js +++ b/test/public/qcFlags/synchronousOverview.test.js @@ -22,6 +22,7 @@ const { expectUrlParams, waitForNavigation, getColumnCellsInnerTexts, + getPopoverContent, } = require('../defaults.js'); const { expect } = chai; @@ -59,14 +60,21 @@ module.exports = () => { it('shows correct datatypes in respective columns', async () => { // eslint-disable-next-line require-jsdoc - const validateDate = (date) => date === '-' || !isNaN(dateAndTime.parse(date, 'DD/MM/YYYY hh:mm:ss')); + const validateDate = (date) => date === '-' || !isNaN(dateAndTime.parse(date, 'DD/MM/YYYY, hh:mm:ss')); const tableDataValidators = { flagType: (flagType) => flagType && flagType !== '-', - createdBy: (userName) => userName && userName !== '-', - from: (timestamp) => timestamp === 'Whole run coverage' || timestamp === 'Since run start' || validateDate(timestamp), - to: (timestamp) => timestamp === 'Whole run coverage' || timestamp === 'Until run end' || validateDate(timestamp), - createdAt: validateDate, - updatedAt: validateDate, + from: (cellContent) => { + const match = cellContent.match(/^From:\s*(.+)\nTo:\s*(.+)$/); + if (!match) return false; + const [, from, to] = match; + return (['Whole run coverage', 'Since run start'].includes(from) || validateDate(from)) + && (['Whole run coverage', 'Until run end'].includes(to) || validateDate(to)); + }, + deleted: (value) => value === 'Yes' || value === 'No', + createdBy: (cellContent) => { + const match = cellContent.match(/^By:\s*(.+)\nAt:\s*(.+)$/); + return match && match[1] !== '-' && validateDate(match[2]); + }, }; await validateTableData(page, new Map(Object.entries(tableDataValidators))); @@ -76,8 +84,34 @@ module.exports = () => { it('Should display the correct items counter at the bottom of the page', async () => { await expectInnerText(page, '#firstRowIndex', '1'); - await expectInnerText(page, '#lastRowIndex', '2'); - await expectInnerText(page, '#totalRowsCount', '2'); + await expectInnerText(page, '#lastRowIndex', '3'); + await expectInnerText(page, '#totalRowsCount', '3'); + }); + + it('should display Comment tooltip with full information', async () => { + let popoverTrigger = await page.$(`#row100-comment .popover-trigger`); + expect(popoverTrigger).to.not.be.null; + + const popoverContent = await getPopoverContent(popoverTrigger); + expect(popoverContent).to.equal('first part good'); + }); + + it('should display CreatedBy tooltip with full information', async () => { + let popoverTrigger = await page.$(`#row100-createdBy .popover-trigger`); + expect(popoverTrigger).to.not.be.null; + + const popoverContent = await getPopoverContent(popoverTrigger); + expect(popoverContent).to.equal('By: Jan JansenAt: 12/08/2024, 12:00:00'); + }); + + it('should display correct Deleted text colour', async () => { + const deletedCell = await page.$('#row103-deleted-text:nth-child(1)'); + + const deletedCellText = await page.evaluate(cell => cell.textContent.trim(), deletedCell); + expect(deletedCellText).to.equal('Yes'); + + const deletedCellFirstChildClass = await page.evaluate(cell => cell.firstElementChild.className, deletedCell); + expect(deletedCellFirstChildClass).to.include('danger'); }); it('can navigate to run details page from breadcrumbs link', async () => {