diff --git a/lib/database/adapters/SimulationPassAdapter.js b/lib/database/adapters/SimulationPassAdapter.js index 6b09fd1356..64b372820f 100644 --- a/lib/database/adapters/SimulationPassAdapter.js +++ b/lib/database/adapters/SimulationPassAdapter.js @@ -43,6 +43,7 @@ class SimulationPassAdapter { const dataPassesCount = databaseObject.get('dataPassesCount'); const runsCount = databaseObject.get('runsCount'); + const logsCount = databaseObject.get('logsCount'); const pdpBeamTypes = databaseObject.get('pdpBeamTypes')?.split(',') || []; return { @@ -56,6 +57,7 @@ class SimulationPassAdapter { outputSize, runsCount, dataPassesCount, + logsCount, pdpBeamTypes, }; } diff --git a/lib/database/models/typedefs/SequelizeSimulationPass.js b/lib/database/models/typedefs/SequelizeSimulationPass.js index 29b0de55bb..90da6d9715 100644 --- a/lib/database/models/typedefs/SequelizeSimulationPass.js +++ b/lib/database/models/typedefs/SequelizeSimulationPass.js @@ -23,4 +23,5 @@ * @property {number} outputSize * @property {number} dataPassesCount * @property {number} runsCount + * @property {number} logsCount */ diff --git a/lib/domain/dtos/GetAllLogsDto.js b/lib/domain/dtos/GetAllLogsDto.js index 4018cdffa7..d483bf20c1 100644 --- a/lib/domain/dtos/GetAllLogsDto.js +++ b/lib/domain/dtos/GetAllLogsDto.js @@ -38,6 +38,7 @@ const FilterDto = Joi.object({ rootOnly: Joi.boolean(), environmentIds: EnvironmentsFilterDto, dataPasses: StringsFilterDto, + simulationPasses: StringsFilterDto, lhcPeriods: StringsFilterDto, }); diff --git a/lib/domain/entities/SimulationPass.js b/lib/domain/entities/SimulationPass.js index 4af583e24c..055fce7086 100644 --- a/lib/domain/entities/SimulationPass.js +++ b/lib/domain/entities/SimulationPass.js @@ -23,4 +23,5 @@ * @property {number} outputSize * @property {number} dataPassesCount * @property {number} runsCount + * @property {number} logsCount */ diff --git a/lib/public/components/Log/logDisplayComponent.js b/lib/public/components/Log/logDisplayComponent.js index f28e919838..ca6105bda7 100644 --- a/lib/public/components/Log/logDisplayComponent.js +++ b/lib/public/components/Log/logDisplayComponent.js @@ -71,6 +71,14 @@ const logFields = () => ({ parameters: { dataPassId: id }, })), }, + simulationPasses: { + name: 'Simulation passes', + format: (simulationPasses) => detailsFrontLinks(simulationPasses, ({ id, name }) => ({ + content: name, + page: 'runs-per-simulation-pass', + parameters: { simulationPassId: id }, + })), + }, attachments: { name: 'Attachments', visible: true, @@ -121,7 +129,7 @@ const displayLogText = (log, options) => { ); }; -const expandedLogFields = ['runs', 'environments', 'lhcFills', 'dataPasses', 'attachments']; +const expandedLogFields = ['runs', 'environments', 'lhcFills', 'dataPasses', 'simulationPasses', 'attachments']; /** * Returns a card for the given log diff --git a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js index 7379d9d21e..fe2d8ad5e1 100644 --- a/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js +++ b/lib/public/views/Logs/ActiveColumns/logsActiveColumns.js @@ -23,6 +23,7 @@ import { formatRunsList } from '../../Runs/format/formatRunsList.js'; import { profiles } from '../../../components/common/table/profiles.js'; import { formatLhcFillsList } from '../../LhcFills/format/formatLhcFillsList.js'; import { formatDataPassesList } from '../../DataPasses/format/formatDataPassesList.js'; +import { formatSimulationPassesList } from '../../SimulationPasses/format/formatSimulationPassesList.js'; import { formatTagsList } from '../../Tags/format/formatTagsList.js'; import { timeRangeFilter } from '../../../components/Filters/common/filters/timeRangeFilter.js'; import { textInputFilter } from '../../../components/Filters/common/filters/textInputFilter.js'; @@ -68,7 +69,7 @@ export const logsActiveColumns = { name: 'Title', visible: true, sortable: true, - size: 'w-30', + size: 'w-20', /** * Title filter component @@ -243,6 +244,24 @@ export const logsActiveColumns = { balloon: true, profiles: [profiles.none, 'embeded'], }, + simulationPasses: { + name: 'Simulation passes', + visible: true, + sortable: false, + size: 'w-10', + format: formatSimulationPassesList, + + /** + * Simulation passes filter component + * + * @param {object} logOverviewModel the overview model + * @param {FilteringModel} logOverviewModel.filteringModel filtering model + * @return {Component} the filter component + */ + filter: ({ filteringModel }) => textInputFilter(filteringModel, 'simulationPasses', 'e.g. LHC23k6c, LHC23k6b'), + balloon: true, + profiles: [profiles.none, 'embeded'], + }, lhcPeriods: { name: 'LHC Periods', visible: false, diff --git a/lib/public/views/Logs/Overview/LogsOverviewModel.js b/lib/public/views/Logs/Overview/LogsOverviewModel.js index 263404fb6f..93e5ad4d84 100644 --- a/lib/public/views/Logs/Overview/LogsOverviewModel.js +++ b/lib/public/views/Logs/Overview/LogsOverviewModel.js @@ -44,6 +44,7 @@ export class LogsOverviewModel extends FilterableOverviewPageModel { environmentIds: new RawTextFilterModel(), fillNumbers: new RawTextFilterModel(), dataPasses: new RawTextFilterModel(), + simulationPasses: new RawTextFilterModel(), lhcPeriods: new RawTextFilterModel(), created: new TimeRangeInputModel(), }, diff --git a/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js b/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js index 3085772121..6b454c2647 100644 --- a/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js +++ b/lib/public/views/SimulationPasses/ActiveColumns/simulationPassesActiveColumns.js @@ -63,6 +63,20 @@ export const simulationPassesActiveColumns = { classes: 'w-10 f6', }, + associatedLogs: { + name: 'Logs', + visible: true, + format: (_, { name, logsCount }) => + logsCount === 0 + ? 'No logs' + : frontLink( + badge(logsCount), + 'log-overview', + { 'filter[simulationPasses]': name }, + ), + classes: 'w-10 f6', + }, + description: { name: 'Description', visible: true, diff --git a/lib/public/views/SimulationPasses/format/formatSimulationPassesList.js b/lib/public/views/SimulationPasses/format/formatSimulationPassesList.js new file mode 100644 index 0000000000..242b10a515 --- /dev/null +++ b/lib/public/views/SimulationPasses/format/formatSimulationPassesList.js @@ -0,0 +1,26 @@ +/** + * @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 { frontLinks } from '../../../components/common/navigation/frontLinks.js'; + +/** + * Format a given simulation passes list as a list of hyperlink to their runs page + * + * @param {{id: number, name: string}[]} simulationPasses the list of simulation passes to display + * @return {Component} the list of simulation passes + */ +export const formatSimulationPassesList = (simulationPasses) => frontLinks(simulationPasses, ({ id, name }) => ({ + content: name, + page: 'runs-per-simulation-pass', + parameters: { simulationPassId: id }, +})); diff --git a/lib/server/services/log/LogService.js b/lib/server/services/log/LogService.js index b7896972af..883b08e3d4 100644 --- a/lib/server/services/log/LogService.js +++ b/lib/server/services/log/LogService.js @@ -207,6 +207,7 @@ class LogService { .include({ association: 'runs', attributes: ['id', 'runNumber'] }) .include({ association: 'lhcFills', attributes: ['fillNumber'] }) .include({ association: 'dataPasses', attributes: ['id', 'name'] }) + .include({ association: 'simulationPasses', attributes: ['id', 'name'] }) .include({ association: 'environments', attributes: ['id'] }) .include({ association: 'user', attributes: ['name'] }) .include('tags') diff --git a/lib/server/services/simulationPasses/SimulationPassService.js b/lib/server/services/simulationPasses/SimulationPassService.js index 957a7ff3a5..23f9976303 100644 --- a/lib/server/services/simulationPasses/SimulationPassService.js +++ b/lib/server/services/simulationPasses/SimulationPassService.js @@ -140,6 +140,11 @@ class SimulationPassService { query: (sequelize) => sequelize.fn('count', sequelize.fn('distinct', sequelize.col('dataPasses.id'))), alias: 'dataPassesCount', }) + // Use a subquery to not multiply the joined rows by the number of logs + .includeAttribute({ + query: '(SELECT COUNT(*) FROM log_simulation_passes WHERE log_simulation_passes.simulation_pass_id = `SimulationPass`.id)', + alias: 'logsCount', + }) .includeAttribute({ query: (sequelize) => sequelize.fn('GROUP_CONCAT', sequelize.fn('DISTINCT', sequelize.col('runs.pdp_beam_type'))), alias: 'pdpBeamTypes', diff --git a/lib/usecases/log/GetAllLogsUseCase.js b/lib/usecases/log/GetAllLogsUseCase.js index 3384674a16..9ba516bc8b 100644 --- a/lib/usecases/log/GetAllLogsUseCase.js +++ b/lib/usecases/log/GetAllLogsUseCase.js @@ -21,6 +21,7 @@ const { LogLhcFillsRepository, LogEnvironmentsRepository, LogDataPassesRepository, + LogSimulationPassesRepository, }, } = require('../../database'); const { getTagsByText } = require('../../server/services/tag/getTagsByText.js'); @@ -54,6 +55,7 @@ const applyFilter = async (dataSource, queryBuilder, filter) => { environmentIds, fillNumbers, dataPasses, + simulationPasses, lhcPeriods, } = filter; @@ -164,6 +166,14 @@ const applyFilter = async (dataSource, queryBuilder, filter) => { queryBuilder.where('id').oneOf(...new Set(logDataPasses.map(({ logId }) => logId))); } + if (simulationPasses) { + // The log must be linked to at least one of the given simulation passes + const logSimulationPasses = await LogSimulationPassesRepository.findAll(dataSource.createQueryBuilder() + .include({ association: 'simulationPass', attributes: [], required: true, where: { name: { [Op.in]: simulationPasses } } })); + + queryBuilder.where('id').oneOf(...new Set(logSimulationPasses.map(({ logId }) => logId))); + } + if (lhcPeriods) { // Logs are not directly linked to LHC periods, use the period of their linked data passes. At least one period must match const logDataPasses = await LogDataPassesRepository.findAll(dataSource.createQueryBuilder() @@ -253,6 +263,7 @@ class GetAllLogsUseCase { .include({ association: 'runs', attributes: ['id', 'runNumber'] }) .include({ association: 'lhcFills', attributes: ['fillNumber'] }) .include({ association: 'dataPasses', attributes: ['id', 'name'] }) + .include({ association: 'simulationPasses', attributes: ['id', 'name'] }) .include('attachments') .include({ association: 'environments', attributes: ['id'] }); diff --git a/test/api/environments.test.js b/test/api/environments.test.js index 4c573c3e1b..bd6bfd3535 100644 --- a/test/api/environments.test.js +++ b/test/api/environments.test.js @@ -349,6 +349,7 @@ module.exports = () => { expect(log.lhcFills.map(({ fillNumber }) => fillNumber)).to.have.members([5, 6]); expect(log.environments.map(({ id }) => id)).to.have.members(['8E4aZTjY', 'eZF99lH6']); expect(log.dataPasses.map(({ name }) => name)).to.have.members(['LHC22b_apass1', 'LHC22b_skimming']); + expect(log.simulationPasses.map(({ name }) => name)).to.have.members(['LHC23k6c', 'LHC23k6b']); expect(log.attachments).to.be.an('array'); }); }); diff --git a/test/api/lhcFills.test.js b/test/api/lhcFills.test.js index adb661ae45..65214e52db 100644 --- a/test/api/lhcFills.test.js +++ b/test/api/lhcFills.test.js @@ -964,6 +964,7 @@ module.exports = () => { expect(log.lhcFills.map(({ fillNumber }) => fillNumber)).to.have.members([5, 6]); expect(log.environments.map(({ id }) => id)).to.have.members(['8E4aZTjY', 'eZF99lH6']); expect(log.dataPasses.map(({ name }) => name)).to.have.members(['LHC22b_apass1', 'LHC22b_skimming']); + expect(log.simulationPasses.map(({ name }) => name)).to.have.members(['LHC23k6c', 'LHC23k6b']); expect(log.attachments).to.be.an('array'); }); }); diff --git a/test/api/logs.test.js b/test/api/logs.test.js index 1801f5785a..b79ec63a9d 100644 --- a/test/api/logs.test.js +++ b/test/api/logs.test.js @@ -256,6 +256,17 @@ module.exports = () => { } }); + it('should return the simulation passes linked to each log', async () => { + const response = await request(server).get('/api/logs?sort[id]=asc'); + expect(response.status).to.equal(200); + + const log1 = response.body.data.find(({ id }) => id === 1); + expect(log1.simulationPasses.map(({ name }) => name).sort()).to.deep.equal(['LHC23k6b', 'LHC23k6c']); + for (const { simulationPasses } of response.body.data) { + expect(simulationPasses).to.be.an('array'); + } + }); + it('should return the data passes linked to each log', async () => { const response = await request(server).get('/api/logs?sort[id]=asc'); expect(response.status).to.equal(200); @@ -278,6 +289,17 @@ module.exports = () => { expect(singleResponse.body.data.map(({ id }) => id).sort((a, b) => a - b)).to.deep.equal([1, 119]); }); + it('should successfully filter by simulation pass names', async () => { + // At least one of the simulation passes must be linked to the log + const response = await request(server).get('/api/logs?filter[simulationPasses]=LHC23k6b,LHC23k6a'); + expect(response.status).to.equal(200); + expect(response.body.data.map(({ id }) => id).sort((a, b) => a - b)).to.deep.equal([1, 2]); + + const singleResponse = await request(server).get('/api/logs?filter[simulationPasses]=LHC23k6c'); + expect(singleResponse.status).to.equal(200); + expect(singleResponse.body.data.map(({ id }) => id).sort((a, b) => a - b)).to.deep.equal([1, 119]); + }); + it('should successfully filter by LHC period names', async () => { const response = await request(server).get('/api/logs?filter[lhcPeriods]=LHC22a'); expect(response.status).to.equal(200); @@ -288,8 +310,8 @@ module.exports = () => { expect(multipleResponse.body.data.map(({ id }) => id).sort((a, b) => a - b)).to.deep.equal([1, 2, 119]); }); - it('should return an empty list when filtering by unknown data pass or LHC period', async () => { - for (const [filterKey, value] of [['dataPasses', 'NOT_A_DATA_PASS'], ['lhcPeriods', 'NOT_A_PERIOD']]) { + it('should return an empty list when filtering by unknown data pass, simulation pass or LHC period', async () => { + for (const [filterKey, value] of [['dataPasses', 'NOT_A_DATA_PASS'], ['simulationPasses', 'NOT_A_SIMULATION_PASS'], ['lhcPeriods', 'NOT_A_PERIOD']]) { const response = await request(server).get(`/api/logs?filter[${filterKey}]=${value}`); expect(response.status).to.equal(200); expect(response.body.data).to.lengthOf(0); diff --git a/test/api/runs.test.js b/test/api/runs.test.js index 67747b9b99..17f74ed5d2 100644 --- a/test/api/runs.test.js +++ b/test/api/runs.test.js @@ -1071,6 +1071,7 @@ module.exports = () => { expect(log.lhcFills.map(({ fillNumber }) => fillNumber)).to.have.members([5, 6]); expect(log.environments.map(({ id }) => id)).to.have.members(['8E4aZTjY', 'eZF99lH6']); expect(log.dataPasses.map(({ name }) => name)).to.have.members(['LHC22b_apass1', 'LHC22b_skimming']); + expect(log.simulationPasses.map(({ name }) => name)).to.have.members(['LHC23k6c', 'LHC23k6b']); expect(log.attachments).to.be.an('array'); // Tags must be complete to be displayed with their color and archived state expect(log.tags).to.not.be.empty; diff --git a/test/api/simulationPasses.test.js b/test/api/simulationPasses.test.js index c4c154b212..6d68ddeced 100644 --- a/test/api/simulationPasses.test.js +++ b/test/api/simulationPasses.test.js @@ -28,6 +28,7 @@ const LHC23k6c = { outputSize: 14013600611699, dataPassesCount: 2, runsCount: 3, + logsCount: 2, }; const LHC23k6b = { @@ -42,6 +43,7 @@ const LHC23k6b = { outputSize: 157000310748, dataPassesCount: 1, runsCount: 3, + logsCount: 1, }; const LHC23k6a = { @@ -56,6 +58,7 @@ const LHC23k6a = { outputSize: 147000310748, dataPassesCount: 1, runsCount: 1, + logsCount: 1, }; module.exports = () => { diff --git a/test/api/tags.test.js b/test/api/tags.test.js index 7cd52b2837..073da4a64d 100644 --- a/test/api/tags.test.js +++ b/test/api/tags.test.js @@ -484,6 +484,7 @@ module.exports = () => { expect(log.lhcFills.map(({ fillNumber }) => fillNumber)).to.have.members([5, 6]); expect(log.environments.map(({ id }) => id)).to.have.members(['8E4aZTjY', 'eZF99lH6']); expect(log.dataPasses.map(({ name }) => name)).to.have.members(['LHC22b_apass1', 'LHC22b_skimming']); + expect(log.simulationPasses.map(({ name }) => name)).to.have.members(['LHC23k6c', 'LHC23k6b']); expect(log.attachments).to.be.an('array'); }); }); diff --git a/test/lib/server/services/log/LogService.test.js b/test/lib/server/services/log/LogService.test.js index 3d272cdf02..8648802c18 100644 --- a/test/lib/server/services/log/LogService.test.js +++ b/test/lib/server/services/log/LogService.test.js @@ -127,6 +127,7 @@ module.exports = () => { expect(log.lhcFills.map(({ fillNumber }) => fillNumber)).to.have.members([5, 6]); expect(log.environments.map(({ id }) => id)).to.have.members(['8E4aZTjY', 'eZF99lH6']); expect(log.dataPasses.map(({ name }) => name)).to.have.members(['LHC22b_apass1', 'LHC22b_skimming']); + expect(log.simulationPasses.map(({ name }) => name)).to.have.members(['LHC23k6c', 'LHC23k6b']); // Complete tags (with their color and archived state) are required to display the logs, the author is only exposed by name expect(log.tags).to.not.be.empty; expect(log.tags.every(({ archived, updatedAt }) => typeof archived === 'boolean' && typeof updatedAt === 'number')).to.be.true; diff --git a/test/lib/server/services/simulationPasses/SimulationPassesService.test.js b/test/lib/server/services/simulationPasses/SimulationPassesService.test.js index 01a1ed321b..cf4971dfd3 100644 --- a/test/lib/server/services/simulationPasses/SimulationPassesService.test.js +++ b/test/lib/server/services/simulationPasses/SimulationPassesService.test.js @@ -29,6 +29,7 @@ const LHC23k6c = { outputSize: 14013600611699, dataPassesCount: 2, runsCount: 3, + logsCount: 2, }; const LHC23k6b = { @@ -43,6 +44,7 @@ const LHC23k6b = { outputSize: 157000310748, dataPassesCount: 1, runsCount: 3, + logsCount: 1, }; const LHC23k6a = { @@ -57,6 +59,7 @@ const LHC23k6a = { outputSize: 147000310748, dataPassesCount: 1, runsCount: 1, + logsCount: 1, }; module.exports = () => { diff --git a/test/lib/usecases/log/GetAllLogsUseCase.test.js b/test/lib/usecases/log/GetAllLogsUseCase.test.js index ea07a40fa6..ea2db55cbc 100644 --- a/test/lib/usecases/log/GetAllLogsUseCase.test.js +++ b/test/lib/usecases/log/GetAllLogsUseCase.test.js @@ -39,6 +39,13 @@ module.exports = () => { expect(logs.find(({ id }) => id === 119).dataPasses.map(({ name }) => name)).to.deep.equal(['LHC22b_apass1']); }); + it('should return the simulation passes of each log', async () => { + const { logs } = await new GetAllLogsUseCase().execute(); + + expect(logs.every(({ simulationPasses }) => Array.isArray(simulationPasses))).to.be.true; + expect(logs.find(({ id }) => id === 119).simulationPasses.map(({ name }) => name)).to.deep.equal(['LHC23k6c']); + }); + it('should return an array, only containing human originated logs', async () => { getAllLogsDto.query = { filter: { origin: 'human' } }; const { logs } = await new GetAllLogsUseCase() @@ -156,6 +163,45 @@ module.exports = () => { expect(logs.map(({ id }) => id)).to.include(2); }); + it('should successfully filter on simulation passes, requiring at least one of them', async () => { + // Other tests of the suite may have created logs linked to simulation passes, so only check seeded logs and the filter semantic + const getSimulationPassNames = (log) => log.simulationPasses.map(({ name }) => name); + + getAllLogsDto.query = { filter: { simulationPasses: ['LHC23k6c'] } }; + let { logs } = await new GetAllLogsUseCase().execute(getAllLogsDto); + expect(logs.map(({ id }) => id)).to.include.members([1, 119]); + expect(logs.map(({ id }) => id)).to.not.include(2); + expect(logs.every((log) => getSimulationPassNames(log).includes('LHC23k6c'))).to.be.true; + + getAllLogsDto.query = { filter: { simulationPasses: ['LHC23k6b', 'LHC23k6a'] } }; + ({ logs } = await new GetAllLogsUseCase().execute(getAllLogsDto)); + expect(logs.map(({ id }) => id)).to.include.members([1, 2]); + expect(logs.map(({ id }) => id)).to.not.include(119); + expect(logs.every((log) => ['LHC23k6b', 'LHC23k6a'].some((name) => getSimulationPassNames(log).includes(name)))).to.be.true; + // Linked simulation passes are all returned, not only the filtered ones + expect(getSimulationPassNames(logs.find(({ id }) => id === 1)).sort()).to.deep.equal(['LHC23k6b', 'LHC23k6c']); + + getAllLogsDto.query = { filter: { simulationPasses: ['NOT_A_SIMULATION_PASS'] } }; + ({ logs } = await new GetAllLogsUseCase().execute(getAllLogsDto)); + expect(logs).to.lengthOf(0); + + // Unknown simulation passes are ignored when at least another one matches + getAllLogsDto.query = { filter: { simulationPasses: ['LHC23k6a', 'NOT_A_SIMULATION_PASS'] } }; + ({ logs } = await new GetAllLogsUseCase().execute(getAllLogsDto)); + expect(logs.map(({ id }) => id)).to.include(2); + }); + + it('should successfully combine the data passes and simulation passes filters', async () => { + getAllLogsDto.query = { filter: { dataPasses: ['LHC22b_skimming'], simulationPasses: ['LHC23k6b'] } }; + let { logs } = await new GetAllLogsUseCase().execute(getAllLogsDto); + expect(logs.map(({ id }) => id)).to.include(1); + expect(logs.map(({ id }) => id)).to.not.include(119); + + getAllLogsDto.query = { filter: { dataPasses: ['LHC22b_skimming'], simulationPasses: ['LHC23k6a'] } }; + ({ logs } = await new GetAllLogsUseCase().execute(getAllLogsDto)); + expect(logs).to.lengthOf(0); + }); + it('should successfully filter on the LHC periods of the linked data passes', async () => { const getPeriodsDataPassNames = (log) => log.dataPasses.map(({ name }) => name.split('_')[0]); diff --git a/test/lib/usecases/log/GetLogUseCase.test.js b/test/lib/usecases/log/GetLogUseCase.test.js index b750787728..6c56fd93de 100644 --- a/test/lib/usecases/log/GetLogUseCase.test.js +++ b/test/lib/usecases/log/GetLogUseCase.test.js @@ -36,6 +36,12 @@ module.exports = () => { expect(result.id).to.equal(1); }); + it('should return the simulation passes linked to the log', async () => { + const result = await new GetLogUseCase().execute(getLogDto); + + expect(result.simulationPasses.map(({ name }) => name).sort()).to.deep.equal(['LHC23k6b', 'LHC23k6c']); + }); + it('should return the data passes linked to the log', async () => { const result = await new GetLogUseCase().execute(getLogDto); diff --git a/test/public/logs/detail.test.js b/test/public/logs/detail.test.js index 0ba3aad20d..c4d7e59612 100644 --- a/test/public/logs/detail.test.js +++ b/test/public/logs/detail.test.js @@ -164,6 +164,17 @@ module.exports = () => { expectUrlParams(page, { page: 'runs-per-data-pass', dataPassId: '1' }); }); + it('allows navigating to an associated simulation pass', async () => { + const logId = 119; + + await goToPage(page, 'log-detail', { queryParameters: { id: logId } }); + + await expectInnerText(page, `#log-${logId}-simulationPasses`, 'Simulation passes:\nLHC23k6c'); + + await waitForNavigation(page, () => pressElement(page, `#log-${logId}-simulationPasses a`)); + expectUrlParams(page, { page: 'runs-per-simulation-pass', simulationPassId: '1' }); + }); + it('should have a button to reply on a entry', async () => { const parentLogId = 2; await goToPage(page, 'log-detail', { queryParameters: { id: parentLogId } }); diff --git a/test/public/logs/overview.test.js b/test/public/logs/overview.test.js index ae63b31f76..c50e49cb8c 100644 --- a/test/public/logs/overview.test.js +++ b/test/public/logs/overview.test.js @@ -352,6 +352,14 @@ module.exports = () => { expectUrlParams(page, { page: 'runs-per-data-pass', dataPassId: '1' }); }); + it('should successfully display the list of related simulation passes as hyperlinks to their runs page', async () => { + await goToPage(page, 'log-overview'); + await page.evaluate(() => window.model.disableInputDebounce()); + + await waitForNavigation(page, () => pressElement(page, '#row119-simulationPasses a')); + expectUrlParams(page, { page: 'runs-per-simulation-pass', simulationPassId: '1' }); + }); + it('should successfult navigate to run details via popover', async () => { await goToPage(page, 'log-overview'); await waitForFirstRowToHaveId(page, 'row119'); @@ -453,6 +461,19 @@ module.exports = () => { await waitForEmptyTable(page); }); + it('can filter by simulation passes', async () => { + await openFilteringPanel(page); + await fillInput(page, '.simulationPasses-textFilter', 'LHC23k6c', ['change']); + await waitForTableLength(page, 2); + + // At least one of the simulation passes must be linked to the log entry + await fillInput(page, '.simulationPasses-textFilter', 'LHC23k6b, LHC23k6a', ['change']); + await waitForTableLength(page, 2); + + await fillInput(page, '.simulationPasses-textFilter', 'NOT_A_SIMULATION_PASS', ['change']); + await waitForEmptyTable(page); + }); + it('can filter by LHC periods inherited from the data passes and explain it with an info tooltip', async () => { await openFilteringPanel(page); diff --git a/test/public/simulationPasses/overviewAnchoredSimulationPasses.test.js b/test/public/simulationPasses/overviewAnchoredSimulationPasses.test.js index 0c801f9c00..f88e951286 100644 --- a/test/public/simulationPasses/overviewAnchoredSimulationPasses.test.js +++ b/test/public/simulationPasses/overviewAnchoredSimulationPasses.test.js @@ -61,6 +61,7 @@ module.exports = () => { name: (name) => periodNameRegex.test(name), associatedRuns: (display) => /(No runs)|(\d+)/.test(display), associatedDataPasses: (display) => /(No anchorage)|(\d+)/.test(display), + associatedLogs: (display) => /(No logs)|(\d+)/.test(display), pwg: (pwg) => /PWG.+/.test(pwg), jiraId: (jiraId) => /[A-Z][A-Z0-9]+-[0-9]+/.test(jiraId), requestedEventsCount: (requestedEventsCount) => !isNaN(requestedEventsCount.replace(/,/g, '')), diff --git a/test/public/simulationPasses/overviewPerLhcPeriod.test.js b/test/public/simulationPasses/overviewPerLhcPeriod.test.js index bcad3fab5b..71e8371c9e 100644 --- a/test/public/simulationPasses/overviewPerLhcPeriod.test.js +++ b/test/public/simulationPasses/overviewPerLhcPeriod.test.js @@ -22,6 +22,12 @@ const { expectColumnValues, validateTableData, expectInnerText, + getTableDataSlice, + waitForTableLength, + waitForNavigation, + openFilteringPanel, + expectInputValue, + expectUrlParams, } = require('../defaults.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -62,6 +68,7 @@ module.exports = () => { name: (name) => periodNameRegex.test(name), associatedRuns: (display) => /(No runs)|(\d+)/.test(display), associatedDataPasses: (display) => /(No anchorage)|(\d+)/.test(display), + associatedLogs: (display) => /(No logs)|(\d+)/.test(display), pwg: (pwg) => /PWG.+/.test(pwg), jiraId: (jiraId) => /[A-Z]+[A-Z0-9]+-\d+/.test(jiraId), requestedEventsCount: (requestedEventsCount) => !isNaN(requestedEventsCount.replace(/,/g, '')), @@ -75,6 +82,30 @@ module.exports = () => { await validateTableData(page, new Map(Object.entries(headerDatatypes))); }); + it('shows the amount of logs linked to each simulation pass', async () => { + await goToPage(page, 'simulation-passes-per-lhc-period-overview', { queryParameters: { lhcPeriodId: 1 } }); + await waitForTableLength(page, 2); + + const tableSlice = await getTableDataSlice(page, ['name', 'associatedLogs']); + expect(tableSlice.map(({ name, associatedLogs }) => ({ name, logsCount: Number(associatedLogs.split('\n')[0]) || 0 }))) + .to.have.all.deep.members([ + { name: 'LHC23k6b', logsCount: 1 }, + { name: 'LHC23k6a', logsCount: 1 }, + ]); + }); + + it('can navigate to the logs overview filtered by simulation pass', async () => { + await goToPage(page, 'simulation-passes-per-lhc-period-overview', { queryParameters: { lhcPeriodId: 1 } }); + await waitForTableLength(page, 2); + const [{ name }] = await getTableDataSlice(page, ['name']); + + await waitForNavigation(page, () => pressElement(page, 'tbody tr:nth-of-type(1) td:nth-of-type(4) a')); + expectUrlParams(page, { page: 'log-overview' }, ['filter[simulationPasses]', 'filter%5BsimulationPasses%5D']); + expect(decodeURIComponent(page.url())).to.include(`filter[simulationPasses]=${name}`); + await openFilteringPanel(page); + await expectInputValue(page, '.simulationPasses-textFilter', name); + }); + 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 } });