From 0d6b17a5f5aa4187e85fd0ac56e971cfafb70b77 Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Thu, 19 Mar 2026 13:14:55 +0100 Subject: [PATCH 1/2] feat: separate toolbar rendering from table data rendering on the RunsPerLhcPeriodOverview page --- .../RunsPerLhcPeriodOverviewPage.js | 56 ++++++++++--------- .../runs/runsPerLhcPeriod.overview.test.js | 6 +- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js b/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js index 7526324b35..77da8a6eee 100644 --- a/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js +++ b/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js @@ -95,30 +95,34 @@ export const RunsPerLhcPeriodOverviewPage = ({ runs: { perLhcPeriodOverviewModel { sort: sortModel }, ); - return h( - '.intermediate-flex-column', - mergeRemoteData([remoteLhcPeriodStatistics, remoteRuns]).match({ - NotAsked: () => null, - Failure: (errors) => errorAlert(errors), - Loading: () => spinner(), - Success: ([lhcPeriodStatistics]) => { - const activeColumns = { - ...runsActiveColumns, - ...getInelasticInteractionRateColumns(pdpBeamTypes), + const activeColumns = { + ...runsActiveColumns, + ...getInelasticInteractionRateColumns(pdpBeamTypes), + }; - }; + const lhcPeriodName = remoteLhcPeriodStatistics?.match({ + Success: (lhcPeriodStatistics) => lhcPeriodStatistics.lhcPeriod.name, + Other: () => null, + }); - return [ - h('.flex-row.justify-between.items-center.g2', [ - filtersPanelPopover(perLhcPeriodOverviewModel, activeColumns, { profile: 'runsPerLhcPeriod' }), - h('.pl2#runOverviewFilter', runNumbersFilter(perLhcPeriodOverviewModel.filteringModel.get('runNumbers'))), - h('h2', `Good, physics runs of ${lhcPeriodStatistics.lhcPeriod.name}`), - mcReproducibleAsNotBadToggle( - mcReproducibleAsNotBad, - () => perLhcPeriodOverviewModel.setMcReproducibleAsNotBad(!mcReproducibleAsNotBad), - ), - exportTriggerAndModal(perLhcPeriodOverviewModel.exportModel, modalModel), - ]), + return [ + h('.flex-row.justify-between.items-center.g2', [ + filtersPanelPopover(perLhcPeriodOverviewModel, activeColumns, { profile: 'runsPerLhcPeriod' }), + h('.pl2#runOverviewFilter', runNumbersFilter(perLhcPeriodOverviewModel.filteringModel.get('runNumbers'))), + h('h2', `Good, physics runs of ${lhcPeriodName}`), + mcReproducibleAsNotBadToggle( + mcReproducibleAsNotBad, + () => perLhcPeriodOverviewModel.setMcReproducibleAsNotBad(!mcReproducibleAsNotBad), + ), + exportTriggerAndModal(perLhcPeriodOverviewModel.exportModel, modalModel), + ]), + h( + '.intermediate-flex-column', + remoteRuns.match({ + NotAsked: () => null, + Failure: (errors) => errorAlert(errors), + Loading: () => spinner(), + Success: () => [ ...tabbedPanelComponent( tabbedPanelModel, { @@ -153,9 +157,7 @@ export const RunsPerLhcPeriodOverviewPage = ({ runs: { perLhcPeriodOverviewModel { panelClass: ['scroll-auto'] }, ), paginationComponent(perLhcPeriodOverviewModel.pagination), - ]; - }, - }), - - ); + ] }), + ), + ]; }; diff --git a/test/public/runs/runsPerLhcPeriod.overview.test.js b/test/public/runs/runsPerLhcPeriod.overview.test.js index 75b5eb978c..e82b47862c 100644 --- a/test/public/runs/runsPerLhcPeriod.overview.test.js +++ b/test/public/runs/runsPerLhcPeriod.overview.test.js @@ -215,9 +215,9 @@ module.exports = () => { const targetFileName = 'data.json'; // First export - await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR, true); - await page.waitForSelector('select.form-control', { timeout: 200 }); - await page.waitForSelector('option[value=runNumber]', { timeout: 200 }); + await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR); + await page.waitForSelector('select.form-control'); + await page.waitForSelector('option[value=runNumber]'); await page.select('select.form-control', 'runQuality', 'runNumber', 'definition', 'lhcPeriod'); await expectInnerText(page, '#send:enabled', 'Export'); From 6efa56e61722a61ef38ca61fc6c0c2ce6b20feea Mon Sep 17 00:00:00 2001 From: GuustMetz Date: Thu, 19 Mar 2026 13:17:26 +0100 Subject: [PATCH 2/2] feat: separate toolbar rendering from table data rendering on the RunsPerDataPassOverview page --- .../RunsPerDataPassOverviewPage.js | 354 ++++++++++-------- .../runs/runsPerDataPass.overview.test.js | 17 +- 2 files changed, 192 insertions(+), 179 deletions(-) diff --git a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js index 8f63fb608b..ce4c77fe05 100644 --- a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js +++ b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js @@ -117,177 +117,205 @@ export const RunsPerDataPassOverviewPage = ({ const commonTitle = h('h2#breadcrumb-header', { style: 'white-space: nowrap;' }, 'Physics Runs'); const runDetectorsSelectionIsEmpty = perDataPassOverviewModel.runDetectorsSelectionModel.selectedQueryString.length === 0; + const dataPass = remoteDataPass.match({ Other: () => null, Success: (data) => data }); + const detectors = remoteDetectors.match({ Other: () => null, Success: (data) => data }); + const qcSummary = remoteQcSummary.match({ Other: () => null, Success: (data) => data }); - return h( - '.intermediate-flex-column', - { onremove: () => { - perDataPassOverviewModel._abortGaqFetches(); - } }, - mergeRemoteData([remoteDataPass, remoteRuns, remoteDetectors, remoteQcSummary]).match({ - NotAsked: () => null, - Failure: (errors) => errorAlert(errors), - Success: ([dataPass, runs, detectors, qcSummary]) => { - const activeColumns = { - ...runsActiveColumns, - ...getInelasticInteractionRateColumns(pdpBeamTypes), - ...dataPass.skimmingStage === SkimmingStage.SKIMMABLE - ? { - readyForSkimming: { - name: 'Ready for skimming', - visible: true, - format: (_, { runNumber }) => remoteSkimmableRuns.match({ - Success: (skimmableRuns) => switchInput( - skimmableRuns[runNumber], - () => perDataPassOverviewModel.changeReadyForSkimmingFlagForRun({ - runNumber, - readyForSkimming: !skimmableRuns[runNumber], - }), - { - labelAfter: skimmableRuns[runNumber] - ? badge('ready', { color: Color.GREEN }) - : badge('not ready', { color: Color.WARNING_DARKER }), - }, + /* + * The table drawing can be done without using mergeRemoteData, but that will redraw it + * each independent update to dataPass, detectors, or qcSummary. + * While this wouldn't necessarly be noticable for users, it would would detach nodes from + * the document, which would make writing integration test difficult and unreliable. + */ + const fullPageData = mergeRemoteData([remoteRuns, remoteDataPass, remoteDetectors, remoteQcSummary]); + + const activeColumns = { + ...runsActiveColumns, + ...getInelasticInteractionRateColumns(pdpBeamTypes), + + globalAggregatedQuality: { + name: 'GAQ', + information: h( + '', + h('', 'Global aggregated flag based on critical detectors.'), + h('', 'Default detectors: FT0, ITS, TPC (and ZDC for heavy-ion runs)'), + ), + visible: true, + + format: (_, { runNumber }) => { + const runGaqSummary = remoteGaqSummary[runNumber]; + const spinnerEl = h('.flex-row.items-center.justify-center.black', spinner({ size: 2, absolute: false })); + + return runGaqSummary.match({ + Success: (gaqSummary) => { + const gaqDisplay = + gaqSummary?.undefinedQualityPeriodsCount === 0 + ? getQcSummaryDisplay(gaqSummary) + : h('button.btn.btn-primary.w-100', 'GAQ'); + + return frontLink(gaqDisplay, 'gaq-flags', { dataPassId, runNumber }); + }, + Loading: () => tooltip(spinnerEl), + NotAsked: () => tooltip(spinnerEl), + Failure: () => + frontLink( + h('button.btn.btn-primary.w-100', [ + 'GAQ', + h( + '.d-inline-block.va-t-bottom', + tooltip( + h('.f7', iconWarning()), + 'GAQ Summary failed, please click to view GAQ flags', ), - Loading: () => h('.mh3.ph4', '... ...'), - Failure: () => tooltip(iconWarning(), 'Error occurred'), - NotAsked: () => tooltip(iconWarning(), 'Not asked for data'), - }), - profiles: ['runsPerDataPass'], - }, - } - : {}, - globalAggregatedQuality: { - name: 'GAQ', - information: h( - '', - h('', 'Global aggregated flag based on critical detectors.'), - h('', 'Default detectors: FT0, ITS, TPC (and ZDC for heavy-ion runs)'), + ), + ]), + 'gaq-flags', + { dataPassId, runNumber }, ), - visible: true, - format: (_, { runNumber }) => { - const gaqLoadingSpinner = h('.flex-row.items-center.justify-center.black', spinner({ size: 2, absolute: false })); - const runGaqSummary = remoteGaqSummary[runNumber]; + }); + }, - return runGaqSummary.match({ - Success: (gaqSummary) => { - const gaqDisplay = gaqSummary?.undefinedQualityPeriodsCount === 0 - ? getQcSummaryDisplay(gaqSummary) - : h('button.btn.btn-primary.w-100', 'GAQ'); + filter: ({ filteringModel }) => + numericalComparisonFilter( + filteringModel.get('gaq[notBadFraction]'), + { step: 0.1, selectorPrefix: 'gaqNotBadFraction' }, + ), - return frontLink(gaqDisplay, 'gaq-flags', { dataPassId, runNumber }); - }, - Loading: () => tooltip(gaqLoadingSpinner, 'Loading GAQ summary...'), - NotAsked: () => tooltip(gaqLoadingSpinner, 'Loading GAQ summary...'), - Failure: () => { - const gaqDisplay = h('button.btn.btn-primary.w-100', [ - 'GAQ', - h( - '.d-inline-block.va-t-bottom', - tooltip(h('.f7', iconWarning()), 'GAQ Summary failed, please click to view GAQ flags'), - ), - ]); - return frontLink(gaqDisplay, 'gaq-flags', { dataPassId, runNumber }); + filterTooltip: 'not-bad fraction expressed as a percentage', + profiles: ['runsPerDataPass'], + }, + ...dataPass?.skimmingStage === SkimmingStage.SKIMMABLE && { + readyForSkimming: { + name: 'Ready for skimming', + visible: true, + + format: (_, { runNumber }) => + remoteSkimmableRuns.match({ + Success: (skimmableRuns) => + switchInput( + skimmableRuns[runNumber], + () => + perDataPassOverviewModel.changeReadyForSkimmingFlagForRun({ + runNumber, + readyForSkimming: !skimmableRuns[runNumber], + }), + { + labelAfter: skimmableRuns[runNumber] + ? badge('ready', { color: Color.GREEN }) + : badge('not ready', { color: Color.WARNING_DARKER }), }, - }); + ), + + Loading: () => h('.mh3.ph4', '... ...'), + Failure: () => tooltip(iconWarning(), 'Error occurred'), + NotAsked: () => tooltip(iconWarning(), 'Not asked for data'), + }), + + profiles: ['runsPerDataPass'], + }, + }, + }; + + detectors && dataPass && Object.assign( + activeColumns, + createRunDetectorsAsyncQcActiveColumns( + perDataPassOverviewModel.runDetectorsSelectionModel, + detectors, + remoteDplDetectorsUserHasAccessTo, + { dataPass }, + { + profile: 'runsPerDataPass', + qcSummary, + mcReproducibleAsNotBad, + }, + ), + ); + + return [ + h('.flex-row.justify-between.items-center.g2', [ + filtersPanelPopover(perDataPassOverviewModel, activeColumns, { profile: 'runsPerDataPass' }), + h('.pl2#runOverviewFilter', runNumbersFilter(perDataPassOverviewModel.filteringModel.get('runNumbers'))), + h( + '.flex-row.g1.items-center', + h('.flex-row.items-center.g1', [ + breadcrumbs([commonTitle, h('h2#breadcrumb-data-pass-name', dataPass?.name)]), + h('#skimmableControl', dataPass && skimmableControl( + dataPass, + () => { + if (confirm('The data pass is going to be set as skimmable. Do you want to continue?')) { + perDataPassOverviewModel.markDataPassAsSkimmable(); + } }, - filter: ({ filteringModel }) => numericalComparisonFilter( - filteringModel.get('gaq[notBadFraction]'), - { step: 0.1, selectorPrefix: 'gaqNotBadFraction' }, - ), - filterTooltip: 'not-bad fraction expressed as a percentage', - profiles: ['runsPerDataPass'], - }, - ...createRunDetectorsAsyncQcActiveColumns( - perDataPassOverviewModel.runDetectorsSelectionModel, - detectors, - remoteDplDetectorsUserHasAccessTo, - { dataPass }, + markAsSkimmableRequestResult, + )), + ]), + ), + mcReproducibleAsNotBadToggle( + mcReproducibleAsNotBad, + () => perDataPassOverviewModel.setMcReproducibleAsNotBad(!mcReproducibleAsNotBad), + ), + h('.mlauto', qcSummaryLegendTooltip()), + h('#actions-dropdown-button', DropdownComponent( + h('button.btn.btn-primary', h('.flex-row.g2', ['Actions', iconCaretBottom()])), + h('.flex-column.p2.g2', [ + exportTriggerAndModal(perDataPassOverviewModel.exportModel, modalModel, { autoMarginLeft: false }), + frontLink( + h('button.btn.btn-primary.w-100.h2}#set-qc-flags-trigger', { + disabled: runDetectorsSelectionIsEmpty, + }, 'Set QC Flags'), + 'qc-flag-creation-for-data-pass', { - profile: 'runsPerDataPass', - qcSummary, - mcReproducibleAsNotBad, + runNumberDetectorsMap: perDataPassOverviewModel.runDetectorsSelectionModel.selectedQueryString, + dataPassId, }, ), - }; - - return [ - h('.flex-row.justify-between.items-center.g2', [ - filtersPanelPopover(perDataPassOverviewModel, activeColumns, { profile: 'runsPerDataPass' }), - h('.pl2#runOverviewFilter', runNumbersFilter(perDataPassOverviewModel.filteringModel.get('runNumbers'))), + sessionService.hasAccess([BkpRoles.DPG_ASYNC_QC_ADMIN]) && [ h( - '.flex-row.g1.items-center', - h('.flex-row.items-center.g1', [ - breadcrumbs([commonTitle, h('h2#breadcrumb-data-pass-name', dataPass.name)]), - h('#skimmableControl', skimmableControl( - dataPass, - () => { - if (confirm('The data pass is going to be set as skimmable. Do you want to continue?')) { - perDataPassOverviewModel.markDataPassAsSkimmable(); - } - }, - markAsSkimmableRequestResult, - )), - ]), + 'button.btn.btn-danger', + { + ...freezeOrUnfreezeActionState.match({ + Loading: () => ({ + disabled: true, + title: 'Loading', + }), + Other: () => ({}), + }), + onclick: () => dataPass?.isFrozen + ? perDataPassOverviewModel.unfreezeDataPass() + : perDataPassOverviewModel.freezeDataPass(), + }, + `${dataPass?.isFrozen ? 'Unfreeze' : 'Freeze'} the data pass`, ), - mcReproducibleAsNotBadToggle( - mcReproducibleAsNotBad, - () => perDataPassOverviewModel.setMcReproducibleAsNotBad(!mcReproducibleAsNotBad), + h( + 'button.btn.btn-danger', + { + ...discardAllQcFlagsActionState.match({ + Loading: () => ({ + disabled: true, + title: 'Loading', + }), + Other: () => ({}), + }), + onclick: () => { + if (confirm('Are you sure you want to delete ALL the QC flags for this data pass?')) { + perDataPassOverviewModel.discardAllQcFlags(); + } + }, + }, + 'Delete ALL QC flags', ), - h('.mlauto', qcSummaryLegendTooltip()), - h('#actions-dropdown-button', DropdownComponent( - h('button.btn.btn-primary', h('.flex-row.g2', ['Actions', iconCaretBottom()])), - h('.flex-column.p2.g2', [ - exportTriggerAndModal(perDataPassOverviewModel.exportModel, modalModel, { autoMarginLeft: false }), - frontLink( - h('button.btn.btn-primary.w-100.h2}#set-qc-flags-trigger', { - disabled: runDetectorsSelectionIsEmpty, - }, 'Set QC Flags'), - 'qc-flag-creation-for-data-pass', - { - runNumberDetectorsMap: perDataPassOverviewModel.runDetectorsSelectionModel.selectedQueryString, - dataPassId, - }, - ), - sessionService.hasAccess([BkpRoles.DPG_ASYNC_QC_ADMIN]) && [ - h( - 'button.btn.btn-danger', - { - ...freezeOrUnfreezeActionState.match({ - Loading: () => ({ - disabled: true, - title: 'Loading', - }), - Other: () => ({}), - }), - onclick: () => dataPass.isFrozen - ? perDataPassOverviewModel.unfreezeDataPass() - : perDataPassOverviewModel.freezeDataPass(), - }, - `${dataPass.isFrozen ? 'Unfreeze' : 'Freeze'} the data pass`, - ), - h( - 'button.btn.btn-danger', - { - ...discardAllQcFlagsActionState.match({ - Loading: () => ({ - disabled: true, - title: 'Loading', - }), - Other: () => ({}), - }), - onclick: () => { - if (confirm('Are you sure you want to delete ALL the QC flags for this data pass?')) { - perDataPassOverviewModel.discardAllQcFlags(); - } - }, - }, - 'Delete ALL QC flags', - ), - ], - ]), - { alignment: 'right' }, - )), - ]), + ], + ]), + { alignment: 'right' }, + )), + ]), + h( + '.intermediate-flex-column', + { onremove: () => perDataPassOverviewModel._abortGaqFetches() }, + fullPageData.match({ + NotAsked: () => null, + Failure: (errors) => errorAlert(errors), + Success: ([runs]) => [ markAsSkimmableRequestResult.match({ Failure: (errors) => errorAlert(errors), Other: () => null, @@ -311,9 +339,9 @@ export const RunsPerDataPassOverviewPage = ({ { sort: sortModel }, ), paginationComponent(perDataPassOverviewModel.pagination), - ]; - }, - Loading: () => spinner(), - }), - ); + ], + Loading: () => spinner(), + }), + ), + ]; }; diff --git a/test/public/runs/runsPerDataPass.overview.test.js b/test/public/runs/runsPerDataPass.overview.test.js index 5eeef9c018..513bb072b3 100644 --- a/test/public/runs/runsPerDataPass.overview.test.js +++ b/test/public/runs/runsPerDataPass.overview.test.js @@ -423,7 +423,6 @@ module.exports = () => { await waitForTableLength(page, 2); await expectColumnValues(page, 'runNumber', ['108', '107']); - await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters'); await waitForTableLength(page, 3); await expectColumnValues(page, 'runNumber', ['108', '107', '106']); @@ -438,7 +437,6 @@ module.exports = () => { await pressElement(page, '#detector-filter-dropdown-option-CPV', true); await expectColumnValues(page, 'runNumber', ['2', '1']); - await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters'); await expectColumnValues(page, 'runNumber', ['55', '2', '1']); }); @@ -454,8 +452,6 @@ module.exports = () => { await expectColumnValues(page, 'runNumber', ['106']); - await page.waitForSelector('#openFilterToggle'); - await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters'); await expectColumnValues(page, 'runNumber', ['108', '107', '106']); }); @@ -477,7 +473,6 @@ module.exports = () => { await expectColumnValues(page, 'runNumber', ['55', '1']); - await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters'); await expectColumnValues(page, 'runNumber', ['55', '2', '1']); }); @@ -491,7 +486,6 @@ module.exports = () => { await expectColumnValues(page, 'runNumber', ['54']); - await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters'); await expectColumnValues(page, 'runNumber', ['105', '56', '54', '49']); }); @@ -514,7 +508,6 @@ module.exports = () => { await fillInput(page, `#${property}-operand`, value, ['change']); await expectColumnValues(page, 'runNumber', expectedRuns); - await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters', true); await expectColumnValues(page, 'runNumber', ['105', '56', '54', '49']); }); @@ -561,7 +554,6 @@ module.exports = () => { await fillInput(page, '#muInelasticInteractionRate-operand', 0.03, ['change']); await expectColumnValues(page, 'runNumber', ['106']); - await pressElement(page, '#openFilterToggle'); await pressElement(page, '#reset-filters', true); await expectColumnValues(page, 'runNumber', ['108', '107', '106']); }); @@ -620,7 +612,6 @@ module.exports = () => { it('should successfully disable QC flag creation when data pass is frozen', async () => { await waitForTableLength(page, 3); await page.waitForSelector('.select-multi-flag', { hidden: true }); - await pressElement(page, '#actions-dropdown-button .popover-trigger'); await page.waitForSelector('#set-qc-flags-trigger[disabled]'); await page.waitForSelector('#row107-ACO-text button[disabled]'); }); @@ -634,16 +625,10 @@ module.exports = () => { it('should successfully enable QC flag creation when data pass is un-frozen', async () => { await waitForTableLength(page, 3); - await pressElement(page, '.select-multi-flag'); - await pressElement(page, '#actions-dropdown-button .popover-trigger'); - await page.waitForSelector('#set-qc-flags-trigger[disabled]', { hidden: true }); + await page.waitForSelector('#set-qc-flags-trigger[disabled]'); await page.waitForSelector('#set-qc-flags-trigger'); await page.waitForSelector('#row107-ACO-text a'); }); - - after(async () => { - await pressElement(page, '#actions-dropdown-button .popover-trigger', true); - }); }); it('should successfully not display button to discard all QC flags for the data pass', async () => {