From fcb2b3bc0ab806a51c617b3c5a0593cc0ec2e8c3 Mon Sep 17 00:00:00 2001 From: xsalonx Date: Mon, 11 Mar 2024 09:55:25 +0100 Subject: [PATCH 01/18] fix edndpoint constracion --- lib/public/views/Runs/Overview/RunsOverviewModel.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index bd25cf2857..e1f4cf6951 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -957,9 +957,7 @@ export class RunsOverviewModel extends OverviewPageModel { this._allRuns = RemoteData.loading(); this.notify(); - const params = this._getFilterQueryParams(); - - const endpoint = `/api/runs?${new URLSearchParams(params).toString()}`; + const endpoint = this.getRootEndpoint(); try { const { items } = await getRemoteDataSlice(endpoint); From 1776422a2c7a7a5c8a8eb8e0745106a4b2075501 Mon Sep 17 00:00:00 2001 From: xsalonx Date: Mon, 11 Mar 2024 10:04:31 +0100 Subject: [PATCH 02/18] add test --- .../runs/runsPerPeriod.overview.test.js | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/test/public/runs/runsPerPeriod.overview.test.js b/test/public/runs/runsPerPeriod.overview.test.js index c30f72e5fc..f54386e77f 100644 --- a/test/public/runs/runsPerPeriod.overview.test.js +++ b/test/public/runs/runsPerPeriod.overview.test.js @@ -11,6 +11,8 @@ * or submit itself to any jurisdiction. */ +const path = require('path'); +const fs = require('fs'); const chai = require('chai'); const { defaultBefore, @@ -21,8 +23,10 @@ const { goToPage, reloadPage, } = require('../defaults'); -const { RUN_QUALITIES } = require('../../../lib/domain/enums/RunQualities.js'); +const { RUN_QUALITIES, RunQualities } = require('../../../lib/domain/enums/RunQualities.js'); const { waitForTimeout } = require('../defaults.js'); +const { waitForDownload } = require('../../utilities/waitForDownload'); +const { RunDefinition } = require('../../../lib/server/services/run/getRunDefinition'); const { expect } = chai; @@ -199,4 +203,44 @@ module.exports = () => { expect(urlParameters).to.contain('page=run-detail'); expect(urlParameters).to.contain(`runNumber=${expectedRunNumber}`); }); + + const EXPORT_RUNS_TRIGGER_SELECTOR = '#export-runs-trigger'; + + it('should successfully export all runs', async () => { + await goToPage(page, 'runs-per-lhc-period', { queryParameters: { lhcPeriodName: 'LHC22a' } }); + + const downloadPath = path.resolve('./download'); + + // Check accessibility on frontend + const session = await page.target().createCDPSession(); + await session.send('Browser.setDownloadBehavior', { + behavior: 'allow', + downloadPath: downloadPath, + eventsEnabled: true, + }); + + const targetFileName = 'runs.json'; + + // First export + await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR); + await page.waitForSelector('.form-control', { timeout: 200 }); + await page.select('.form-control', 'runQuality', 'runNumber'); + await expectInnerText(page, '#send:enabled', 'Export'); + await pressElement(page, '#send:enabled'); + + await waitForDownload(session); + + // Check download + const downloadFilesNames = fs.readdirSync(downloadPath); + expect(downloadFilesNames.filter((name) => name == targetFileName)).to.be.lengthOf(1); + const runs = JSON.parse(fs.readFileSync(path.resolve(downloadPath, targetFileName))); + + expect(runs).to.be.lengthOf(3); + expect(runs.every(({ runQuality, definition, runNumber, ...otherProps }) => + runQuality === RunQualities.GOOD + && definition === RunDefinition.Physics + && runNumber + && Object.keys(otherProps).length === 0)).to.be.true; + fs.unlinkSync(path.resolve(downloadPath, targetFileName)); + }); }; From 4f6acb076f67a61d280a2112801a1c1628c200a7 Mon Sep 17 00:00:00 2001 From: xsalonx Date: Mon, 11 Mar 2024 10:20:36 +0100 Subject: [PATCH 03/18] amend --- test/public/runs/runsPerPeriod.overview.test.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/public/runs/runsPerPeriod.overview.test.js b/test/public/runs/runsPerPeriod.overview.test.js index f54386e77f..0a2fcca4b1 100644 --- a/test/public/runs/runsPerPeriod.overview.test.js +++ b/test/public/runs/runsPerPeriod.overview.test.js @@ -224,7 +224,7 @@ module.exports = () => { // First export await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR); await page.waitForSelector('.form-control', { timeout: 200 }); - await page.select('.form-control', 'runQuality', 'runNumber'); + await page.select('.form-control', 'runQuality', 'runNumber', 'definition'); await expectInnerText(page, '#send:enabled', 'Export'); await pressElement(page, '#send:enabled'); @@ -236,11 +236,12 @@ module.exports = () => { const runs = JSON.parse(fs.readFileSync(path.resolve(downloadPath, targetFileName))); expect(runs).to.be.lengthOf(3); - expect(runs.every(({ runQuality, definition, runNumber, ...otherProps }) => - runQuality === RunQualities.GOOD - && definition === RunDefinition.Physics - && runNumber - && Object.keys(otherProps).length === 0)).to.be.true; + expect(runs.every(({ runQuality }) => runQuality === RunQualities.GOOD)).to.be.true; + expect(runs.every(({ definition }) => definition === RunDefinition.Physics)).to.be.true; + expect(runs.every(({ runNumber }) => runNumber)).to.be.true; + expect(runs.every(({ runNumber: _, definition: __, runQuality: ___, ...otherProps }) => + Object.entries(otherProps).length === 0)).to.be.true; + fs.unlinkSync(path.resolve(downloadPath, targetFileName)); }); }; From f19a6ddab01e4286c3cf789755314cf182e52a73 Mon Sep 17 00:00:00 2001 From: xsalonx Date: Mon, 11 Mar 2024 10:28:11 +0100 Subject: [PATCH 04/18] t --- test/public/runs/runsPerPeriod.overview.test.js | 2 +- test/utilities/waitForDownload.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/test/public/runs/runsPerPeriod.overview.test.js b/test/public/runs/runsPerPeriod.overview.test.js index 0a2fcca4b1..19b3c4b241 100644 --- a/test/public/runs/runsPerPeriod.overview.test.js +++ b/test/public/runs/runsPerPeriod.overview.test.js @@ -206,7 +206,7 @@ module.exports = () => { const EXPORT_RUNS_TRIGGER_SELECTOR = '#export-runs-trigger'; - it('should successfully export all runs', async () => { + it('should successfully export all runs per lhc Period', async () => { await goToPage(page, 'runs-per-lhc-period', { queryParameters: { lhcPeriodName: 'LHC22a' } }); const downloadPath = path.resolve('./download'); diff --git a/test/utilities/waitForDownload.js b/test/utilities/waitForDownload.js index 174cf59812..c68b4b0569 100644 --- a/test/utilities/waitForDownload.js +++ b/test/utilities/waitForDownload.js @@ -14,10 +14,12 @@ /** * Create promise which is resolved when last initiated download is completed and rejected when canceled * @param {CDPSession} session puppetear CDP session + * @param {object} options options altering behaviour + * @param {number} [options.timeout] timeout to reject if not downloaded * @return {Promise} promise * !!! Downloading requires to set 'Browser.setDownloadBehavior' behaviour on the given CDP session */ -async function waitForDownload(session) { +async function waitForDownload(session, { timeout = 5000 } = {}) { return new Promise((resolve, reject) => { session.on('Browser.downloadProgress', (event) => { if (event.state === 'completed') { @@ -26,6 +28,7 @@ async function waitForDownload(session) { reject('download canceled'); } }); + setTimeout(() => reject('Rejected because of timeout'), timeout); }); } From 4f2a126706bc697f5d1f97b11ae0d8a70f53de33 Mon Sep 17 00:00:00 2001 From: xsalonx Date: Mon, 11 Mar 2024 11:48:17 +0100 Subject: [PATCH 05/18] Refactor --- lib/public/models/OverviewModel.js | 3 ++ .../views/Runs/Overview/RunsOverviewPage.js | 4 +- ...erAndModal.js => exportTriggerAndModal.js} | 51 ++++++++++--------- .../RunsPerDataPassOverviewPage.js | 4 +- .../RunsPerLhcPeriodOverviewPage.js | 4 +- 5 files changed, 35 insertions(+), 31 deletions(-) rename lib/public/views/Runs/Overview/{exportRunsTriggerAndModal.js => exportTriggerAndModal.js} (68%) diff --git a/lib/public/models/OverviewModel.js b/lib/public/models/OverviewModel.js index 3338c8ec56..0d67b5f03c 100644 --- a/lib/public/models/OverviewModel.js +++ b/lib/public/models/OverviewModel.js @@ -51,6 +51,9 @@ export class OverviewPageModel extends Observable { this._observableItems = ObservableData.builder().initialValue(RemoteData.loading()).build(); this._observableItems.bubbleTo(this); + + this._allObservableItems = ObservableData.builder().initialValue(RemoteData.loading()).build(); + this._allObservableItems.bubbleTo(this); } /** diff --git a/lib/public/views/Runs/Overview/RunsOverviewPage.js b/lib/public/views/Runs/Overview/RunsOverviewPage.js index 1658bebfee..b0b8fe96db 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewPage.js +++ b/lib/public/views/Runs/Overview/RunsOverviewPage.js @@ -13,7 +13,7 @@ import { h } from '/js/src/index.js'; import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; -import { exportRunsTriggerAndModal } from './exportRunsTriggerAndModal.js'; +import { exportTriggerAndModal } from './exportTriggerAndModal.js'; import { filtersPanelPopover } from '../../../components/Filters/common/filtersPanelPopover.js'; import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; import { runsActiveColumns } from '../ActiveColumns/runsActiveColumns.js'; @@ -56,7 +56,7 @@ export const RunsOverviewPage = ({ runs: { overviewModel: runsOverviewModel }, m filtersPanelPopover(runsOverviewModel, runsActiveColumns), h('.pl2#runOverviewFilter', runNumberFilter(runsOverviewModel)), togglePhysicsOnlyFilter(runsOverviewModel), - exportRunsTriggerAndModal(runsOverviewModel, modalModel), + exportTriggerAndModal(runsOverviewModel, modalModel, runsActiveColumns), ]), h('.flex-column.w-100', [ table(runsOverviewModel.items, runsActiveColumns), diff --git a/lib/public/views/Runs/Overview/exportRunsTriggerAndModal.js b/lib/public/views/Runs/Overview/exportTriggerAndModal.js similarity index 68% rename from lib/public/views/Runs/Overview/exportRunsTriggerAndModal.js rename to lib/public/views/Runs/Overview/exportTriggerAndModal.js index b7462fb8ab..519746964f 100644 --- a/lib/public/views/Runs/Overview/exportRunsTriggerAndModal.js +++ b/lib/public/views/Runs/Overview/exportTriggerAndModal.js @@ -12,29 +12,28 @@ */ import { h } from '/js/src/index.js'; -import { runsActiveColumns } from '../ActiveColumns/runsActiveColumns.js'; /** * Export form component, containing the fields to export, the export type and the export button * - * @param {RunsOverviewModel} runsOverviewModel the runsOverviewModel - * @param {array} runs the runs to export + * @param {OverviewPageModel} model the runsOverviewModel + * @param {object[]} items the runs to export * @param {ModalHandler} modalHandler The modal handler, used to dismiss modal after export - * + * @param {object} activeColumns active columns * @return {vnode[]} the form component */ -const exportForm = (runsOverviewModel, runs, modalHandler) => { +const exportForm = (model, items, modalHandler, activeColumns) => { const exportTypes = ['JSON', 'CSV']; - const selectedRunsFields = runsOverviewModel.getSelectedRunsFields() || []; - const selectedExportType = runsOverviewModel.getSelectedExportType() || exportTypes[0]; - const runsFields = Object.keys(runsActiveColumns); + const selectedRunsFields = model.getSelectedRunsFields() || []; + const selectedExportType = model.getSelectedExportType() || exportTypes[0]; + const runsFields = Object.keys(activeColumns); const enabled = selectedRunsFields.length > 0; return [ - runsOverviewModel.isAllRunsTruncated + model.isAllRunsTruncated ? h( '#truncated-export-warning.warning', - `The runs export is limited to ${runs.length} entries, only the last runs will be exported (sorted by run number)`, + `The runs export is limited to ${items.length} entries, only the last runs will be exported (sorted by run number)`, ) : null, h('label.form-check-label.f4.mt1', { for: 'run-number' }, 'Fields'), @@ -46,7 +45,7 @@ const exportForm = (runsOverviewModel, runs, modalHandler) => { h('select#fields.form-control', { style: 'min-height: 20rem;', multiple: true, - onchange: ({ target }) => runsOverviewModel.setSelectedRunsFields(target.selectedOptions), + onchange: ({ target }) => model.setSelectedRunsFields(target.selectedOptions), }, [ ...runsFields .filter((name) => !['id', 'actions'].includes(name)) @@ -66,7 +65,7 @@ const exportForm = (runsOverviewModel, runs, modalHandler) => { value: exportType, checked: selectedExportType.length ? selectedExportType.includes(exportType) : false, name: 'runs-export-type', - onclick: () => runsOverviewModel.setSelectedExportType(exportType), + onclick: () => model.setSelectedExportType(exportType), }), h('label.form-check-label', { for: id, @@ -76,14 +75,14 @@ const exportForm = (runsOverviewModel, runs, modalHandler) => { h('button.shadow-level1.btn.btn-success.mt2#send', { disabled: !enabled, onclick: async () => { - await runsOverviewModel.createRunsExport( - runs, + await model.createRunsExport( + items, 'runs', - runsActiveColumns, + activeColumns, ); modalHandler.dismiss(); }, - }, runs ? 'Export' : 'Loading data'), + }, items ? 'Export' : 'Loading data'), ]; }; @@ -92,19 +91,20 @@ const errorDisplay = () => h('.danger', 'Data fetching failed'); /** * A function to construct the exports runs screen - * @param {RunsOverviewModel} runsOverviewModel Pass the model to access the defined functions + * @param {OverviewPageModel} model Pass the model to access the defined functions * @param {ModalHandler} modalHandler The modal handler, used to dismiss modal after export + * @param {object} activeColumns active columns * @return {Component} Return the view of the inputs */ -const exportRunsModal = (runsOverviewModel, modalHandler) => { - const runsRemoteData = runsOverviewModel.allRuns; +const exportModal = (model, modalHandler, activeColumns) => { + const runsRemoteData = model.allRuns; return h('div#export-runs-modal', [ h('h2', 'Export Runs'), runsRemoteData.match({ NotAsked: () => errorDisplay(), - Loading: () => exportForm(runsOverviewModel, null, modalHandler), - Success: (payload) => exportForm(runsOverviewModel, payload, modalHandler), + Loading: () => exportForm(model, null, modalHandler, activeColumns), + Success: (payload) => exportForm(model, payload, modalHandler, activeColumns), Failure: () => errorDisplay(), }), ]); @@ -112,16 +112,17 @@ const exportRunsModal = (runsOverviewModel, modalHandler) => { /** * Builds a button which will open popover for data export - * @param {RunsOverviewModel} runsModel runs overview model + * @param {OverviewPageModel} model runs overview model * @param {ModelModel} modalModel modal model + * @param {object} activeColumns active columns * @returns {Component} button */ -export const exportRunsTriggerAndModal = (runsModel, modalModel) => h('button.btn.btn-primary.w-15.h2.mlauto#export-runs-trigger', { - disabled: runsModel.items.match({ +export const exportTriggerAndModal = (model, modalModel, activeColumns) => h('button.btn.btn-primary.w-15.h2.mlauto#export-runs-trigger', { + disabled: model.items.match({ Success: (payload) => payload.length === 0, NotAsked: () => true, Failure: () => true, Loading: () => true, }), - onclick: () => modalModel.display({ content: (modalModel) => exportRunsModal(runsModel, modalModel), size: 'medium' }), + onclick: () => modalModel.display({ content: (modalModel) => exportModal(model, modalModel, activeColumns), size: 'medium' }), }, 'Export Runs'); diff --git a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js index 758f349e51..b023cbf0a4 100644 --- a/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js +++ b/lib/public/views/Runs/RunPerDataPass/RunsPerDataPassOverviewPage.js @@ -16,7 +16,7 @@ import { table } from '../../../components/common/table/table.js'; import { createRunDetectorsActiveColumns } from '../ActiveColumns/runDetectorsActiveColumns.js'; import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; -import { exportRunsTriggerAndModal } from '../Overview/exportRunsTriggerAndModal.js'; +import { exportTriggerAndModal } from '../Overview/exportTriggerAndModal.js'; import { runsActiveColumns } from '../ActiveColumns/runsActiveColumns.js'; import spinner from '../../../components/common/spinner.js'; import { tooltip } from '../../../components/common/popover/tooltip.js'; @@ -61,7 +61,7 @@ export const RunsPerDataPassOverviewPage = ({ runs: { perDataPassOverviewModel } NotAsked: () => [commonTitle, tooltip(h('.f3', iconWarning()), 'No data was asked for')], }), ), - exportRunsTriggerAndModal(perDataPassOverviewModel, modalModel), + exportTriggerAndModal(perDataPassOverviewModel, modalModel, runsActiveColumns), ]), h('.flex-column.w-100', [ table(runs, activeColumns, null, { profile: 'runsPerDataPass' }), diff --git a/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js b/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js index e3b7c102b5..b223dceefe 100644 --- a/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js +++ b/lib/public/views/Runs/RunPerPeriod/RunsPerLhcPeriodOverviewPage.js @@ -16,7 +16,7 @@ import { table } from '../../../components/common/table/table.js'; import { createRunDetectorsActiveColumns } from '../ActiveColumns/runDetectorsActiveColumns.js'; import { paginationComponent } from '../../../components/Pagination/paginationComponent.js'; import { estimateDisplayableRowsCount } from '../../../utilities/estimateDisplayableRowsCount.js'; -import { exportRunsTriggerAndModal } from '../Overview/exportRunsTriggerAndModal.js'; +import { exportTriggerAndModal } from '../Overview/exportTriggerAndModal.js'; import { runsActiveColumns } from '../ActiveColumns/runsActiveColumns.js'; const TABLEROW_HEIGHT = 59; @@ -48,7 +48,7 @@ export const RunsPerLhcPeriodOverviewPage = ({ runs: { perLhcPeriodOverviewModel return h('', [ h('.flex-row.justify-between.items-center', [ h('h2', `Good, physics runs of ${lhcPeriodName}`), - exportRunsTriggerAndModal(perLhcPeriodOverviewModel, modalModel), + exportTriggerAndModal(perLhcPeriodOverviewModel, modalModel, runsActiveColumns), ]), h('.flex-column.w-100', [ table(detectors.isSuccess() ? runs : detectors, activeColumns, null, { profile: 'runsPerLhcPeriod' }), From 1d71101bfd4f93bbb1b7b8683a06eddab7e54a96 Mon Sep 17 00:00:00 2001 From: xsalonx Date: Tue, 12 Mar 2024 16:46:24 +0100 Subject: [PATCH 06/18] start to work --- lib/public/models/OverviewModel.js | 47 +++++++++++++- .../views/Runs/Overview/RunsOverviewModel.js | 65 ------------------- .../Runs/Overview/exportTriggerAndModal.js | 16 ++--- 3 files changed, 52 insertions(+), 76 deletions(-) diff --git a/lib/public/models/OverviewModel.js b/lib/public/models/OverviewModel.js index 0d67b5f03c..1f7d54c197 100644 --- a/lib/public/models/OverviewModel.js +++ b/lib/public/models/OverviewModel.js @@ -52,8 +52,13 @@ export class OverviewPageModel extends Observable { this._observableItems = ObservableData.builder().initialValue(RemoteData.loading()).build(); this._observableItems.bubbleTo(this); - this._allObservableItems = ObservableData.builder().initialValue(RemoteData.loading()).build(); - this._allObservableItems.bubbleTo(this); + this._exportDataSource = new PaginatedRemoteDataSource(); + this._exportObservableItems = ObservableData.builder() + .initialValue(RemoteData.notAsked()) + .apply((remoteData) => remoteData.apply({ Success: ({ items }) => this.processItems(items) })) + .build(); + this._exportObservableItems.bubbleTo(this); + this._exportDataSource.pipe(this._exportObservableItems); } /** @@ -123,9 +128,23 @@ export class OverviewPageModel extends Observable { */ async load() { const params = await this.getLoadParameters(); + this._exportObservableItems.setCurrent(RemoteData.notAsked()); await this._dataSource.fetch(buildUrl(this.getRootEndpoint(), params)); } + /** + * Fetch all the relevant items from the API + * + * @return {Promise} void + */ + async loadExport() { + return this._exportObservableItems.getCurrent().match({ + Success: () => null, + Loading: () => null, + Other: () => this._exportDataSource.fetch(this.getRootEndpoint()), + }); + } + /** * Return the query params to use to get load the overview data * @@ -139,8 +158,30 @@ export class OverviewPageModel extends Observable { } /** - * Return the current items remote data + * States if the list of NOT paginated runs contains the full list of runs available under the given criteria * + * @return {boolean|null} true if the runs list is not truncated (null if all items are not yet available) + */ + get areExportItemsTruncated() { + return this.exportItems.match({ + Success: (payload) => this.items.match({ + Success: () => payload.length < this._pagination.itemsCount, + Other: () => null, + }), + Other: () => null, + }); + } + + /** + * Return the export items remote data + * @return {RemoteData} the items + */ + get exportItems() { + return this._exportObservableItems.getCurrent(); + } + + /** + * Return the current items remote data * @return {RemoteData} the items */ get items() { diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index e1f4cf6951..a2cf81949a 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -51,9 +51,6 @@ export class RunsOverviewModel extends OverviewPageModel { this._eorReasonsFilterModel.observe(() => this._applyFilters()); this._eorReasonsFilterModel.visualChange$.observe(() => this.notify()); - // Export items - this._allRuns = RemoteData.NotAsked(); - this.reset(false); // eslint-disable-next-line no-return-assign,require-jsdoc const updateDebounceTime = () => this._debouncedLoad = debounce(this.load.bind(this), model.inputDebounceTime); @@ -70,15 +67,6 @@ export class RunsOverviewModel extends OverviewPageModel { return `/api/runs?${paramsString}`; } - // eslint-disable-next-line valid-jsdoc - /** - * @inheritdoc - */ - async load() { - this._allRuns = RemoteData.NotAsked(); - super.load(); - } - /** * Create the export with the variables set in the model, handling errors appropriately * @param {object[]} runs The source content. @@ -799,32 +787,6 @@ export class RunsOverviewModel extends OverviewPageModel { return this._detectorsFilterModel; } - /** - * Return all the runs currently filtered, without paging - * - * @return {RemoteData} the remote data of the runs - */ - get allRuns() { - if (this._allRuns.isNotAsked()) { - this._fetchAllRunsWithoutPagination(); - } - - return this._allRuns; - } - - /** - * States if the list of NOT paginated runs contains the full list of runs available under the given criteria - * - * @return {boolean|null} true if the runs list is not truncated (null if all runs are not yet available) - */ - get isAllRunsTruncated() { - const { allRuns } = this; - if (!allRuns.isSuccess()) { - return null; - } - return allRuns.payload.length < this._pagination.itemsCount; - } - /** * Return the model handling the filtering on run types * @@ -942,33 +904,6 @@ export class RunsOverviewModel extends OverviewPageModel { }; } - /** - * Update the cache containing all the runs without paging - * - * @return {Promise} void - * @private - */ - async _fetchAllRunsWithoutPagination() { - if (this.items.isSuccess() && this.items.payload.length === this._pagination.itemsCount) { - this._allRuns = RemoteData.success([...this.items.payload]); - this.notify(); - return; - } - this._allRuns = RemoteData.loading(); - this.notify(); - - const endpoint = this.getRootEndpoint(); - - try { - const { items } = await getRemoteDataSlice(endpoint); - this._allRuns = RemoteData.success(items); - } catch (errors) { - this._allRuns = RemoteData.failure(errors); - } - - this.notify(); - } - /** * Apply the current filtering and update the remote data list * diff --git a/lib/public/views/Runs/Overview/exportTriggerAndModal.js b/lib/public/views/Runs/Overview/exportTriggerAndModal.js index 519746964f..43c6d9fd28 100644 --- a/lib/public/views/Runs/Overview/exportTriggerAndModal.js +++ b/lib/public/views/Runs/Overview/exportTriggerAndModal.js @@ -27,10 +27,10 @@ const exportForm = (model, items, modalHandler, activeColumns) => { const selectedRunsFields = model.getSelectedRunsFields() || []; const selectedExportType = model.getSelectedExportType() || exportTypes[0]; const runsFields = Object.keys(activeColumns); - const enabled = selectedRunsFields.length > 0; + const enabled = selectedRunsFields.length > 0 && items; return [ - model.isAllRunsTruncated + model.areExportItemsTruncated ? h( '#truncated-export-warning.warning', `The runs export is limited to ${items.length} entries, only the last runs will be exported (sorted by run number)`, @@ -97,11 +97,11 @@ const errorDisplay = () => h('.danger', 'Data fetching failed'); * @return {Component} Return the view of the inputs */ const exportModal = (model, modalHandler, activeColumns) => { - const runsRemoteData = model.allRuns; + model.loadExport(); - return h('div#export-runs-modal', [ - h('h2', 'Export Runs'), - runsRemoteData.match({ + return h('div#export-modal', [ + h('h2', 'Export'), + model.exportItems.match({ NotAsked: () => errorDisplay(), Loading: () => exportForm(model, null, modalHandler, activeColumns), Success: (payload) => exportForm(model, payload, modalHandler, activeColumns), @@ -117,7 +117,7 @@ const exportModal = (model, modalHandler, activeColumns) => { * @param {object} activeColumns active columns * @returns {Component} button */ -export const exportTriggerAndModal = (model, modalModel, activeColumns) => h('button.btn.btn-primary.w-15.h2.mlauto#export-runs-trigger', { +export const exportTriggerAndModal = (model, modalModel, activeColumns) => h('button.btn.btn-primary.w-15.h2.mlauto#export-trigger', { disabled: model.items.match({ Success: (payload) => payload.length === 0, NotAsked: () => true, @@ -125,4 +125,4 @@ export const exportTriggerAndModal = (model, modalModel, activeColumns) => h('bu Loading: () => true, }), onclick: () => modalModel.display({ content: (modalModel) => exportModal(model, modalModel, activeColumns), size: 'medium' }), -}, 'Export Runs'); +}, 'Export'); From 45bce5891b41f435d2df98402f05ff16a3b14f59 Mon Sep 17 00:00:00 2001 From: xsalonx Date: Tue, 12 Mar 2024 16:55:55 +0100 Subject: [PATCH 07/18] rename --- lib/public/models/OverviewModel.js | 39 ++++++++++++++++ .../views/Runs/Overview/RunsOverviewModel.js | 44 +------------------ .../Runs/Overview/exportTriggerAndModal.js | 10 ++--- 3 files changed, 46 insertions(+), 47 deletions(-) diff --git a/lib/public/models/OverviewModel.js b/lib/public/models/OverviewModel.js index 1f7d54c197..e722cbf498 100644 --- a/lib/public/models/OverviewModel.js +++ b/lib/public/models/OverviewModel.js @@ -145,6 +145,45 @@ export class OverviewPageModel extends Observable { }); } + /** + * Get the field values that will be exported + * @return {Array} the field objects of the current export being created + */ + getExportFields() { + return this._exportFields; + } + + /** + * Get the output format of the export + * @return {string} the output format + */ + getExportFormat() { + return this._exportFormat; + } + + /** + * Set the export type parameter of the current export being created + * @param {string} exportFormat Received string from the view + * @return {void} + */ + setExportFormat(exportFormat) { + this._exportFormat = exportFormat; + this.notify(); + } + + /** + * Updates the selected fields ID array according to the HTML attributes of the options + * + * @param {HTMLCollection} exportFields The currently selected fields by the user, + * according to HTML specification + * @return {void} + */ + setExportFields(exportFields) { + this._exportFields = []; + [...exportFields].map((selectedOption) => this._exportFields.push(selectedOption.value)); + this.notify(); + } + /** * Return the query params to use to get load the overview data * diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index a2cf81949a..f89d101f2d 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -76,7 +76,7 @@ export class RunsOverviewModel extends OverviewPageModel { */ async createRunsExport(runs, fileName, exportFormats) { if (runs.length > 0) { - const selectedRunsFields = this.getSelectedRunsFields() || []; + const selectedRunsFields = this.getExportFields() || []; runs = runs.map((selectedRun) => { const entries = Object.entries(pick(selectedRun, selectedRunsFields)); const formattedEntries = entries.map(([key, value]) => { @@ -85,7 +85,7 @@ export class RunsOverviewModel extends OverviewPageModel { }); return Object.fromEntries(formattedEntries); }), - this.getSelectedExportType() === 'CSV' + this.getExportFormat() === 'CSV' ? createCSVExport(runs, `${fileName}.csv`, 'text/csv;charset=utf-8;') : createJSONExport(runs, `${fileName}.json`, 'application/json'); } else { @@ -99,23 +99,6 @@ export class RunsOverviewModel extends OverviewPageModel { } } - /** - * Get the field values that will be exported - * @return {Array} the field objects of the current export being created - */ - getSelectedRunsFields() { - return this.selectedRunsFields; - } - - /** - * Get the output format of the export - * - * @return {string} the output format - */ - getSelectedExportType() { - return this.selectedExportType; - } - /** * Returns all filtering, sorting and pagination settings to their default values * @param {boolean} fetch Whether to refetch all logs after filters have been reset @@ -290,29 +273,6 @@ export class RunsOverviewModel extends OverviewPageModel { return this.activeFilters; } - /** - * Set the export type parameter of the current export being created - * @param {string} selectedExportType Received string from the view - * @return {void} - */ - setSelectedExportType(selectedExportType) { - this.selectedExportType = selectedExportType; - this.notify(); - } - - /** - * Updates the selected fields ID array according to the HTML attributes of the options - * - * @param {HTMLCollection} selectedOptions The currently selected fields by the user, - * according to HTML specification - * @return {undefined} - */ - setSelectedRunsFields(selectedOptions) { - this.selectedRunsFields = []; - [...selectedOptions].map((selectedOption) => this.selectedRunsFields.push(selectedOption.value)); - this.notify(); - } - /** * Returns the current runNumber substring filter * @return {String} The current runNumber substring filter diff --git a/lib/public/views/Runs/Overview/exportTriggerAndModal.js b/lib/public/views/Runs/Overview/exportTriggerAndModal.js index 43c6d9fd28..990ccdc7b5 100644 --- a/lib/public/views/Runs/Overview/exportTriggerAndModal.js +++ b/lib/public/views/Runs/Overview/exportTriggerAndModal.js @@ -24,8 +24,8 @@ import { h } from '/js/src/index.js'; */ const exportForm = (model, items, modalHandler, activeColumns) => { const exportTypes = ['JSON', 'CSV']; - const selectedRunsFields = model.getSelectedRunsFields() || []; - const selectedExportType = model.getSelectedExportType() || exportTypes[0]; + const selectedRunsFields = model.getExportFields() || []; + const selectedExportType = model.getExportFormat() || exportTypes[0]; const runsFields = Object.keys(activeColumns); const enabled = selectedRunsFields.length > 0 && items; @@ -33,7 +33,7 @@ const exportForm = (model, items, modalHandler, activeColumns) => { model.areExportItemsTruncated ? h( '#truncated-export-warning.warning', - `The runs export is limited to ${items.length} entries, only the last runs will be exported (sorted by run number)`, + `The items export is limited to ${items.length} entries, only the last items will be exported`, ) : null, h('label.form-check-label.f4.mt1', { for: 'run-number' }, 'Fields'), @@ -45,7 +45,7 @@ const exportForm = (model, items, modalHandler, activeColumns) => { h('select#fields.form-control', { style: 'min-height: 20rem;', multiple: true, - onchange: ({ target }) => model.setSelectedRunsFields(target.selectedOptions), + onchange: ({ target }) => model.setExportFields(target.selectedOptions), }, [ ...runsFields .filter((name) => !['id', 'actions'].includes(name)) @@ -65,7 +65,7 @@ const exportForm = (model, items, modalHandler, activeColumns) => { value: exportType, checked: selectedExportType.length ? selectedExportType.includes(exportType) : false, name: 'runs-export-type', - onclick: () => model.setSelectedExportType(exportType), + onclick: () => model.setExportFormat(exportType), }), h('label.form-check-label', { for: id, From 3f747e6ca9dc358dc7e2d31f1fd5a6f38a667982 Mon Sep 17 00:00:00 2001 From: xsalonx Date: Wed, 13 Mar 2024 12:43:10 +0100 Subject: [PATCH 08/18] update --- lib/public/models/OverviewModel.js | 34 ++++++++++++++++++ .../views/Runs/Overview/RunsOverviewModel.js | 36 ------------------- .../Runs/Overview/exportTriggerAndModal.js | 19 +++++----- 3 files changed, 43 insertions(+), 46 deletions(-) diff --git a/lib/public/models/OverviewModel.js b/lib/public/models/OverviewModel.js index e722cbf498..0de1123771 100644 --- a/lib/public/models/OverviewModel.js +++ b/lib/public/models/OverviewModel.js @@ -16,6 +16,8 @@ import { ObservableData } from '../utilities/ObservableData.js'; import { PaginatedRemoteDataSource } from '../utilities/fetch/PaginatedRemoteDataSource.js'; import { PaginationModel } from '../components/Pagination/PaginationModel.js'; import { buildUrl } from '../utilities/fetch/buildUrl.js'; +import pick from '../utilities/pick.js'; +import { createCSVExport, createJSONExport } from '../utilities/export.js'; /** * Interface of a model representing an overview page state @@ -145,6 +147,38 @@ export class OverviewPageModel extends Observable { }); } + /** + * 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 particual fields of data units will be formated + * @return {void} + */ + async createRunsExport(runs, fileName, exportFormats) { + if (runs.length > 0) { + const selectedRunsFields = this.getExportFields() || []; + runs = runs.map((selectedRun) => { + const entries = Object.entries(pick(selectedRun, selectedRunsFields)); + const formattedEntries = entries.map(([key, value]) => { + const formatExport = exportFormats[key].exportFormat || ((identity) => identity); + return [key, formatExport(value, selectedRun)]; + }); + return Object.fromEntries(formattedEntries); + }), + this.getExportFormat() === 'CSV' + ? createCSVExport(runs, `${fileName}.csv`, 'text/csv;charset=utf-8;') + : createJSONExport(runs, `${fileName}.json`, 'application/json'); + } else { + this._observableItems.setCurrent(RemoteData.failure([ + { + title: 'No data found', + detail: 'No valid runs were found for provided run number(s)', + }, + ])); + this.notify(); + } + } + /** * Get the field values that will be exported * @return {Array} the field objects of the current export being created diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index f89d101f2d..e18c04bf29 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -11,16 +11,12 @@ * or submit itself to any jurisdiction. */ -import { RemoteData } from '/js/src/index.js'; -import { createCSVExport, createJSONExport } from '../../../utilities/export.js'; import { TagFilterModel } from '../../../components/Filters/common/TagFilterModel.js'; import { debounce } from '../../../utilities/debounce.js'; import { DetectorsFilterModel } from '../../../components/Filters/RunsFilter/DetectorsFilterModel.js'; import { RunTypesSelectionDropdownModel } from '../../../components/runTypes/RunTypesSelectionDropdownModel.js'; import { EorReasonFilterModel } from '../../../components/Filters/RunsFilter/EorReasonsFilterModel.js'; -import pick from '../../../utilities/pick.js'; import { OverviewPageModel } from '../../../models/OverviewModel.js'; -import { getRemoteDataSlice } from '../../../utilities/fetch/getRemoteDataSlice.js'; /** * Model representing handlers for runs page @@ -67,38 +63,6 @@ export class RunsOverviewModel extends OverviewPageModel { return `/api/runs?${paramsString}`; } - /** - * 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 particual fields of data units will be formated - * @return {void} - */ - async createRunsExport(runs, fileName, exportFormats) { - if (runs.length > 0) { - const selectedRunsFields = this.getExportFields() || []; - runs = runs.map((selectedRun) => { - const entries = Object.entries(pick(selectedRun, selectedRunsFields)); - const formattedEntries = entries.map(([key, value]) => { - const formatExport = exportFormats[key].exportFormat || ((identity) => identity); - return [key, formatExport(value, selectedRun)]; - }); - return Object.fromEntries(formattedEntries); - }), - this.getExportFormat() === 'CSV' - ? createCSVExport(runs, `${fileName}.csv`, 'text/csv;charset=utf-8;') - : createJSONExport(runs, `${fileName}.json`, 'application/json'); - } else { - this._observableItems.setCurrent(RemoteData.failure([ - { - title: 'No data found', - detail: 'No valid runs were found for provided run number(s)', - }, - ])); - this.notify(); - } - } - /** * Returns all filtering, sorting and pagination settings to their default values * @param {boolean} fetch Whether to refetch all logs after filters have been reset diff --git a/lib/public/views/Runs/Overview/exportTriggerAndModal.js b/lib/public/views/Runs/Overview/exportTriggerAndModal.js index 990ccdc7b5..07f5fd33a2 100644 --- a/lib/public/views/Runs/Overview/exportTriggerAndModal.js +++ b/lib/public/views/Runs/Overview/exportTriggerAndModal.js @@ -24,10 +24,10 @@ import { h } from '/js/src/index.js'; */ const exportForm = (model, items, modalHandler, activeColumns) => { const exportTypes = ['JSON', 'CSV']; - const selectedRunsFields = model.getExportFields() || []; - const selectedExportType = model.getExportFormat() || exportTypes[0]; - const runsFields = Object.keys(activeColumns); - const enabled = selectedRunsFields.length > 0 && items; + const exportFields = model.getExportFields() || []; + const exportFormat = model.getExportFormat() || exportTypes[0]; + const selectableFields = Object.keys(activeColumns); + const enabled = exportFields.length > 0 && items; return [ model.areExportItemsTruncated @@ -47,11 +47,11 @@ const exportForm = (model, items, modalHandler, activeColumns) => { multiple: true, onchange: ({ target }) => model.setExportFields(target.selectedOptions), }, [ - ...runsFields + ...selectableFields .filter((name) => !['id', 'actions'].includes(name)) .map((name) => h('option', { value: name, - selected: selectedRunsFields.length ? selectedRunsFields.includes(name) : false, + selected: exportFields.length ? exportFields.includes(name) : false, }, name)), ]), h('label.form-check-label.f4.mt1', { for: 'run-number' }, 'Export type'), @@ -63,7 +63,7 @@ const exportForm = (model, items, modalHandler, activeColumns) => { id, type: 'radio', value: exportType, - checked: selectedExportType.length ? selectedExportType.includes(exportType) : false, + checked: exportFormat.length ? exportFormat.includes(exportType) : false, name: 'runs-export-type', onclick: () => model.setExportFormat(exportType), }), @@ -76,13 +76,12 @@ const exportForm = (model, items, modalHandler, activeColumns) => { disabled: !enabled, onclick: async () => { await model.createRunsExport( - items, - 'runs', + 'bkp-export', activeColumns, ); modalHandler.dismiss(); }, - }, items ? 'Export' : 'Loading data'), + }, 'Export'), ]; }; From ce5f604b0a101a0631479b8d0714b7572b746f1c Mon Sep 17 00:00:00 2001 From: xsalonx Date: Wed, 13 Mar 2024 17:12:40 +0100 Subject: [PATCH 09/18] refactor WIP --- lib/public/models/OverviewModel.js | 39 +++++++------------ .../Runs/Overview/exportTriggerAndModal.js | 6 ++- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/lib/public/models/OverviewModel.js b/lib/public/models/OverviewModel.js index 0de1123771..0cff2232f0 100644 --- a/lib/public/models/OverviewModel.js +++ b/lib/public/models/OverviewModel.js @@ -136,7 +136,6 @@ export class OverviewPageModel extends Observable { /** * Fetch all the relevant items from the API - * * @return {Promise} void */ async loadExport() { @@ -149,34 +148,24 @@ export class OverviewPageModel extends Observable { /** * Create the export with the variables set in the model, handling errors appropriately - * @param {object[]} runs The source content. + * @param {object[]} items The source content. * @param {string} fileName The name of the file including the output format. * @param {Object>} exportFormats defines how particual fields of data units will be formated * @return {void} */ - async createRunsExport(runs, fileName, exportFormats) { - if (runs.length > 0) { - const selectedRunsFields = this.getExportFields() || []; - runs = runs.map((selectedRun) => { - const entries = Object.entries(pick(selectedRun, selectedRunsFields)); - const formattedEntries = entries.map(([key, value]) => { - const formatExport = exportFormats[key].exportFormat || ((identity) => identity); - return [key, formatExport(value, selectedRun)]; - }); - return Object.fromEntries(formattedEntries); - }), - this.getExportFormat() === 'CSV' - ? createCSVExport(runs, `${fileName}.csv`, 'text/csv;charset=utf-8;') - : createJSONExport(runs, `${fileName}.json`, 'application/json'); - } else { - this._observableItems.setCurrent(RemoteData.failure([ - { - title: 'No data found', - detail: 'No valid runs were found for provided run number(s)', - }, - ])); - this.notify(); - } + async export(items, fileName, exportFormats) { + const exportFields = this.getExportFields() || []; + const exportData = items.map((item) => { + const entries = Object.entries(pick(item, exportFields)); + const formattedEntries = entries.map(([key, value]) => { + const formatExport = exportFormats[key].exportFormat || ((identity) => identity); + return [key, formatExport(value, item)]; + }); + return Object.fromEntries(formattedEntries); + }); + this.getExportFormat() === 'CSV' + ? createCSVExport(exportData, `${fileName}.csv`, 'text/csv;charset=utf-8;') + : createJSONExport(exportData, `${fileName}.json`, 'application/json'); } /** diff --git a/lib/public/views/Runs/Overview/exportTriggerAndModal.js b/lib/public/views/Runs/Overview/exportTriggerAndModal.js index 07f5fd33a2..c579bbd368 100644 --- a/lib/public/views/Runs/Overview/exportTriggerAndModal.js +++ b/lib/public/views/Runs/Overview/exportTriggerAndModal.js @@ -11,6 +11,7 @@ * or submit itself to any jurisdiction. */ +import spinner from '../../../components/common/spinner.js'; import { h } from '/js/src/index.js'; /** @@ -75,7 +76,8 @@ const exportForm = (model, items, modalHandler, activeColumns) => { h('button.shadow-level1.btn.btn-success.mt2#send', { disabled: !enabled, onclick: async () => { - await model.createRunsExport( + await model.export( + items, 'bkp-export', activeColumns, ); @@ -102,7 +104,7 @@ const exportModal = (model, modalHandler, activeColumns) => { h('h2', 'Export'), model.exportItems.match({ NotAsked: () => errorDisplay(), - Loading: () => exportForm(model, null, modalHandler, activeColumns), + Loading: () => spinner({ size: 3, absolute: false }), Success: (payload) => exportForm(model, payload, modalHandler, activeColumns), Failure: () => errorDisplay(), }), From 291388fac2b42e9bee6ccc739c15790f2e53daa6 Mon Sep 17 00:00:00 2001 From: xsalonx Date: Wed, 13 Mar 2024 17:25:38 +0100 Subject: [PATCH 10/18] export name WIP --- lib/public/models/OverviewModel.js | 6 ++- .../Runs/Overview/exportTriggerAndModal.js | 49 ++++++++++++------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/lib/public/models/OverviewModel.js b/lib/public/models/OverviewModel.js index 0cff2232f0..ef3940d58c 100644 --- a/lib/public/models/OverviewModel.js +++ b/lib/public/models/OverviewModel.js @@ -61,6 +61,8 @@ export class OverviewPageModel extends Observable { .build(); this._exportObservableItems.bubbleTo(this); this._exportDataSource.pipe(this._exportObservableItems); + + this._defaultExportName = 'bokkeeping-export'; } /** @@ -149,12 +151,12 @@ export class OverviewPageModel extends Observable { /** * Create the export with the variables set in the model, handling errors appropriately * @param {object[]} items The source content. - * @param {string} fileName The name of the file including the output format. * @param {Object>} exportFormats defines how particual fields of data units will be formated * @return {void} */ - async export(items, fileName, exportFormats) { + async export(items, exportFormats = {}) { const exportFields = this.getExportFields() || []; + const fileName = this._defaultExportName; const exportData = items.map((item) => { const entries = Object.entries(pick(item, exportFields)); const formattedEntries = entries.map(([key, value]) => { diff --git a/lib/public/views/Runs/Overview/exportTriggerAndModal.js b/lib/public/views/Runs/Overview/exportTriggerAndModal.js index c579bbd368..c5d3be31a9 100644 --- a/lib/public/views/Runs/Overview/exportTriggerAndModal.js +++ b/lib/public/views/Runs/Overview/exportTriggerAndModal.js @@ -55,30 +55,43 @@ const exportForm = (model, items, modalHandler, activeColumns) => { selected: exportFields.length ? exportFields.includes(name) : false, }, name)), ]), - h('label.form-check-label.f4.mt1', { for: 'run-number' }, 'Export type'), - h('label.form-check-label.f6', { for: 'run-number' }, 'Select output format'), - h('.flex-row.g3', exportTypes.map((exportType) => { - const id = `runs-export-type-${exportType}`; - return h('.form-check', [ - h('input.form-check-input', { - id, - type: 'radio', - value: exportType, - checked: exportFormat.length ? exportFormat.includes(exportType) : false, - name: 'runs-export-type', - onclick: () => model.setExportFormat(exportType), + h('.flex-row.justify-between', [ + h('', [ + h('label.form-check-label.f4.mt1', { for: 'run-number' }, 'Export type'), + h('label.form-check-label.f6', { for: 'run-number' }, 'Select output format'), + h('.flex-row.g3', exportTypes.map((exportType) => { + const id = `runs-export-type-${exportType}`; + return h('.form-check', [ + h('input.form-check-input', { + id, + type: 'radio', + value: exportType, + checked: exportFormat.length ? exportFormat.includes(exportType) : false, + name: 'runs-export-type', + onclick: () => model.setExportFormat(exportType), + }), + h('label.form-check-label', { + for: id, + }, exportType), + ]); + })), + ]), + + h('', [ + h('label.form-check-label.f4.mt1', { for: 'export-name' }, 'Export name'), + h('input', { + id: 'export-name', + value: model.exportName, + // eslint-disable-next-line no-return-assign + onchange: (e) => model.exportName = e.target.value, }), - h('label.form-check-label', { - for: id, - }, exportType), - ]); - })), + ]), + ]), h('button.shadow-level1.btn.btn-success.mt2#send', { disabled: !enabled, onclick: async () => { await model.export( items, - 'bkp-export', activeColumns, ); modalHandler.dismiss(); From 766998cbb653143ae81605f4941626130fc8d45e Mon Sep 17 00:00:00 2001 From: xsalonx Date: Thu, 14 Mar 2024 08:54:47 +0100 Subject: [PATCH 11/18] exportName --- lib/public/models/OverviewModel.js | 19 ++++++++++++++++++- .../views/Runs/Overview/RunsOverviewModel.js | 2 ++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/public/models/OverviewModel.js b/lib/public/models/OverviewModel.js index ef3940d58c..0099657261 100644 --- a/lib/public/models/OverviewModel.js +++ b/lib/public/models/OverviewModel.js @@ -62,7 +62,8 @@ export class OverviewPageModel extends Observable { this._exportObservableItems.bubbleTo(this); this._exportDataSource.pipe(this._exportObservableItems); - this._defaultExportName = 'bokkeeping-export'; + this._defaultExportName = 'bkp-export'; + this._exportName = ''; } /** @@ -170,6 +171,22 @@ export class OverviewPageModel extends Observable { : createJSONExport(exportData, `${fileName}.json`, 'application/json'); } + /** + * Get export name + * @return {string} name + */ + get exportName() { + return this._exportName || this._defaultExportName; + } + + /** + * Set export name + * @param {string} exportName name + */ + set exportName(exportName) { + this._exportName = exportName; + } + /** * Get the field values that will be exported * @return {Array} the field objects of the current export being created diff --git a/lib/public/views/Runs/Overview/RunsOverviewModel.js b/lib/public/views/Runs/Overview/RunsOverviewModel.js index e18c04bf29..365b78adbd 100644 --- a/lib/public/views/Runs/Overview/RunsOverviewModel.js +++ b/lib/public/views/Runs/Overview/RunsOverviewModel.js @@ -47,6 +47,8 @@ export class RunsOverviewModel extends OverviewPageModel { this._eorReasonsFilterModel.observe(() => this._applyFilters()); this._eorReasonsFilterModel.visualChange$.observe(() => this.notify()); + this._defaultExportName = 'runs'; + this.reset(false); // eslint-disable-next-line no-return-assign,require-jsdoc const updateDebounceTime = () => this._debouncedLoad = debounce(this.load.bind(this), model.inputDebounceTime); From 2cabce04a0f404da34fe40f17c3e7925b0ce0ffd Mon Sep 17 00:00:00 2001 From: xsalonx Date: Thu, 14 Mar 2024 09:29:44 +0100 Subject: [PATCH 12/18] work --- lib/public/models/OverviewModel.js | 68 +++++++++++-------- .../Runs/Overview/exportTriggerAndModal.js | 27 ++++---- 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/lib/public/models/OverviewModel.js b/lib/public/models/OverviewModel.js index 0099657261..9193b9cb18 100644 --- a/lib/public/models/OverviewModel.js +++ b/lib/public/models/OverviewModel.js @@ -19,6 +19,11 @@ import { buildUrl } from '../utilities/fetch/buildUrl.js'; import pick from '../utilities/pick.js'; import { createCSVExport, createJSONExport } from '../utilities/export.js'; +export const EXPORT_FORMATS = { + JSON: 'JSON', + CSV: 'CSV', +}; + /** * Interface of a model representing an overview page state * @@ -62,8 +67,16 @@ export class OverviewPageModel extends Observable { this._exportObservableItems.bubbleTo(this); this._exportDataSource.pipe(this._exportObservableItems); + /** + * Default name for export data file, can be overriden in inherting classes + */ this._defaultExportName = 'bkp-export'; this._exportName = ''; + + /** + * Default format + */ + this._defaultExportFormat = EXPORT_FORMATS.JSON; } /** @@ -132,9 +145,9 @@ export class OverviewPageModel extends Observable { * @return {Promise} void */ async load() { - const params = await this.getLoadParameters(); + const paginationParams = await this.getLoadParameters(); this._exportObservableItems.setCurrent(RemoteData.notAsked()); - await this._dataSource.fetch(buildUrl(this.getRootEndpoint(), params)); + await this._dataSource.fetch(buildUrl(this.getRootEndpoint(), paginationParams)); } /** @@ -156,8 +169,7 @@ export class OverviewPageModel extends Observable { * @return {void} */ async export(items, exportFormats = {}) { - const exportFields = this.getExportFields() || []; - const fileName = this._defaultExportName; + const { exportFields, exportName } = this; const exportData = items.map((item) => { const entries = Object.entries(pick(item, exportFields)); const formattedEntries = entries.map(([key, value]) => { @@ -166,9 +178,13 @@ export class OverviewPageModel extends Observable { }); return Object.fromEntries(formattedEntries); }); - this.getExportFormat() === 'CSV' - ? createCSVExport(exportData, `${fileName}.csv`, 'text/csv;charset=utf-8;') - : createJSONExport(exportData, `${fileName}.json`, 'application/json'); + if (this.exportFormat === EXPORT_FORMATS.JSON) { + createJSONExport(exportData, `${exportName}.json`, 'application/json'); + } else if (this.exportFormat === EXPORT_FORMATS.CSV) { + createCSVExport(exportData, `${exportName}.csv`, 'text/csv;charset=utf-8;'); + } else { + throw new Error('Incorrect export format'); + } } /** @@ -189,40 +205,36 @@ export class OverviewPageModel extends Observable { /** * Get the field values that will be exported - * @return {Array} the field objects of the current export being created + * @return {string[]} the list of fields of a export items to be included in the export */ - getExportFields() { - return this._exportFields; + get exportFields() { + return this._exportFields || []; } /** - * Get the output format of the export - * @return {string} the output format + * Set the selected fields to be exported + * @param {SelectionOption[]|HTMLCollection} exportFields the list of fields of export items to be included in the export */ - getExportFormat() { - return this._exportFormat; + set exportFields(exportFields) { + this._exportFields = []; + [...exportFields].map((selectedOption) => this._exportFields.push(selectedOption.value)); + this.notify(); } /** - * Set the export type parameter of the current export being created - * @param {string} exportFormat Received string from the view - * @return {void} + * Get the output format of the export + * @return {string} the output format */ - setExportFormat(exportFormat) { - this._exportFormat = exportFormat; - this.notify(); + get exportFormat() { + return this._exportFormat || this._defaultExportFormat; } /** - * Updates the selected fields ID array according to the HTML attributes of the options - * - * @param {HTMLCollection} exportFields The currently selected fields by the user, - * according to HTML specification - * @return {void} + * Set the export type parameter of the export to be created + * @param {string} exportFormat one of acceptable export formats @see EXPORT_FORMATS */ - setExportFields(exportFields) { - this._exportFields = []; - [...exportFields].map((selectedOption) => this._exportFields.push(selectedOption.value)); + set exportFormat(exportFormat) { + this._exportFormat = exportFormat; this.notify(); } diff --git a/lib/public/views/Runs/Overview/exportTriggerAndModal.js b/lib/public/views/Runs/Overview/exportTriggerAndModal.js index c5d3be31a9..ca8f505533 100644 --- a/lib/public/views/Runs/Overview/exportTriggerAndModal.js +++ b/lib/public/views/Runs/Overview/exportTriggerAndModal.js @@ -12,6 +12,7 @@ */ import spinner from '../../../components/common/spinner.js'; +import { EXPORT_FORMATS } from '../../../models/OverviewModel.js'; import { h } from '/js/src/index.js'; /** @@ -24,11 +25,9 @@ import { h } from '/js/src/index.js'; * @return {vnode[]} the form component */ const exportForm = (model, items, modalHandler, activeColumns) => { - const exportTypes = ['JSON', 'CSV']; - const exportFields = model.getExportFields() || []; - const exportFormat = model.getExportFormat() || exportTypes[0]; + const { exportFormat: currentExportFormat, exportFields } = model; const selectableFields = Object.keys(activeColumns); - const enabled = exportFields.length > 0 && items; + const enabled = exportFields.length > 0; return [ model.areExportItemsTruncated @@ -46,7 +45,8 @@ const exportForm = (model, items, modalHandler, activeColumns) => { h('select#fields.form-control', { style: 'min-height: 20rem;', multiple: true, - onchange: ({ target }) => model.setExportFields(target.selectedOptions), + // eslint-disable-next-line no-return-assign + onchange: ({ target: { selectedOptions } }) => model.exportFields = selectedOptions, }, [ ...selectableFields .filter((name) => !['id', 'actions'].includes(name)) @@ -59,20 +59,21 @@ const exportForm = (model, items, modalHandler, activeColumns) => { h('', [ h('label.form-check-label.f4.mt1', { for: 'run-number' }, 'Export type'), h('label.form-check-label.f6', { for: 'run-number' }, 'Select output format'), - h('.flex-row.g3', exportTypes.map((exportType) => { - const id = `runs-export-type-${exportType}`; + h('.flex-row.g3', Object.values(EXPORT_FORMATS).map((exportFormat) => { + const id = `runs-export-type-${exportFormat}`; return h('.form-check', [ h('input.form-check-input', { id, type: 'radio', - value: exportType, - checked: exportFormat.length ? exportFormat.includes(exportType) : false, + value: exportFormat, + checked: exportFormat === currentExportFormat, name: 'runs-export-type', - onclick: () => model.setExportFormat(exportType), + // eslint-disable-next-line no-return-assign + onclick: () => model.exportFormat = exportFormat, }), h('label.form-check-label', { for: id, - }, exportType), + }, exportFormat), ]); })), ]), @@ -83,7 +84,7 @@ const exportForm = (model, items, modalHandler, activeColumns) => { id: 'export-name', value: model.exportName, // eslint-disable-next-line no-return-assign - onchange: (e) => model.exportName = e.target.value, + onchange: ({ target: { value: currentText } }) => model.exportName = currentText, }), ]), ]), @@ -94,7 +95,7 @@ const exportForm = (model, items, modalHandler, activeColumns) => { items, activeColumns, ); - modalHandler.dismiss(); + await modalHandler.dismiss(); }, }, 'Export'), ]; From 67e91ee45d993e729b39c5b97ecb5e8e01ec982c Mon Sep 17 00:00:00 2001 From: xsalonx Date: Thu, 14 Mar 2024 09:35:15 +0100 Subject: [PATCH 13/18] correct ids --- .../Runs/Overview/exportTriggerAndModal.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/public/views/Runs/Overview/exportTriggerAndModal.js b/lib/public/views/Runs/Overview/exportTriggerAndModal.js index ca8f505533..600e341c1a 100644 --- a/lib/public/views/Runs/Overview/exportTriggerAndModal.js +++ b/lib/public/views/Runs/Overview/exportTriggerAndModal.js @@ -25,26 +25,26 @@ import { h } from '/js/src/index.js'; * @return {vnode[]} the form component */ const exportForm = (model, items, modalHandler, activeColumns) => { - const { exportFormat: currentExportFormat, exportFields } = model; + const { exportFormat: currentExportFormat, exportFields, areExportItemsTruncated } = model; const selectableFields = Object.keys(activeColumns); - const enabled = exportFields.length > 0; return [ - model.areExportItemsTruncated + areExportItemsTruncated ? h( '#truncated-export-warning.warning', `The items export is limited to ${items.length} entries, only the last items will be exported`, ) : null, - h('label.form-check-label.f4.mt1', { for: 'run-number' }, 'Fields'), + h('label.form-check-label.f4.mt1', { for: 'export-fields' }, 'Fields'), h( 'label.form-check-label.f6', - { for: 'run-number' }, + { for: 'export-fields' }, 'Select which fields to be exported. (CTRL + click for multiple selection)', ), h('select#fields.form-control', { style: 'min-height: 20rem;', multiple: true, + id: 'export-fields', // eslint-disable-next-line no-return-assign onchange: ({ target: { selectedOptions } }) => model.exportFields = selectedOptions, }, [ @@ -57,8 +57,8 @@ const exportForm = (model, items, modalHandler, activeColumns) => { ]), h('.flex-row.justify-between', [ h('', [ - h('label.form-check-label.f4.mt1', { for: 'run-number' }, 'Export type'), - h('label.form-check-label.f6', { for: 'run-number' }, 'Select output format'), + h('label.form-check-label.f4.mt1', 'Export format'), + h('label.form-check-label.f6', 'Select output format'), h('.flex-row.g3', Object.values(EXPORT_FORMATS).map((exportFormat) => { const id = `runs-export-type-${exportFormat}`; return h('.form-check', [ @@ -88,8 +88,8 @@ const exportForm = (model, items, modalHandler, activeColumns) => { }), ]), ]), - h('button.shadow-level1.btn.btn-success.mt2#send', { - disabled: !enabled, + h('button.shadow-level1.btn.btn-success.mt2#export', { + disabled: exportFields.length === 0, onclick: async () => { await model.export( items, From 7ae0d0d3bfd5ea739d4fc8e5ec531183ec00320f Mon Sep 17 00:00:00 2001 From: xsalonx Date: Thu, 14 Mar 2024 09:54:49 +0100 Subject: [PATCH 14/18] test --- .../Runs/Overview/exportTriggerAndModal.js | 2 +- test/public/runs/overview.test.js | 93 +++++++------------ .../runs/runsPerDataPass.overview.test.js | 20 ++-- .../runs/runsPerPeriod.overview.test.js | 26 +++--- test/utilities/waitForDownload.js | 26 +++--- 5 files changed, 70 insertions(+), 97 deletions(-) diff --git a/lib/public/views/Runs/Overview/exportTriggerAndModal.js b/lib/public/views/Runs/Overview/exportTriggerAndModal.js index 600e341c1a..e6f35b5399 100644 --- a/lib/public/views/Runs/Overview/exportTriggerAndModal.js +++ b/lib/public/views/Runs/Overview/exportTriggerAndModal.js @@ -88,7 +88,7 @@ const exportForm = (model, items, modalHandler, activeColumns) => { }), ]), ]), - h('button.shadow-level1.btn.btn-success.mt2#export', { + h('button.shadow-level1.btn.btn-success.mt2#download-export', { disabled: exportFields.length === 0, onclick: async () => { await model.export( diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 8b43057f76..4ac9f9f48d 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -23,6 +23,7 @@ const { goToPage, checkColumnBalloon, waitForNetworkIdleAndRedraw, + waitForTableDataReload, } = require('../defaults'); const { RunDefinition } = require('../../../lib/server/services/run/getRunDefinition.js'); const { RUN_QUALITIES, RunQualities } = require('../../../lib/domain/enums/RunQualities.js'); @@ -1009,54 +1010,39 @@ module.exports = () => { expect(inputText).to.equal(''); }); - const EXPORT_RUNS_TRIGGER_SELECTOR = '#export-runs-trigger'; + const EXPORT_MODAL_TRIGGER_ID = '#export-trigger'; it('should successfully display runs export button', async () => { await goToPage(page, 'run-overview'); - await page.waitForSelector(EXPORT_RUNS_TRIGGER_SELECTOR); - const runsExportButton = await page.$(EXPORT_RUNS_TRIGGER_SELECTOR); + await page.waitForSelector(EXPORT_MODAL_TRIGGER_ID); + const runsExportButton = await page.$(EXPORT_MODAL_TRIGGER_ID); expect(runsExportButton).to.be.not.null; }); it('should successfully display runs export modal on click on export button', async () => { - let exportModal = await page.$('#export-runs-modal'); - expect(exportModal).to.be.null; - - await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); - await waitForTimeout(100); - exportModal = await page.$('#export-runs-modal'); - - expect(exportModal).to.not.be.null; + await goToPage(page, 'run-overview'); + await page.waitForSelector('#export-modal', { hidden: true, timeout: 250 }); + await pressElement(page, EXPORT_MODAL_TRIGGER_ID); + await page.waitForSelector('#export-modal', { timeout: 250 }); }); it('should successfully display information when export will be truncated', async () => { await goToPage(page, 'run-overview'); - await waitForTimeout(200); - - await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); - await waitForTimeout(100); - - const truncatedExportWarning = await page.$('#export-runs-modal #truncated-export-warning'); - expect(truncatedExportWarning).to.not.be.null; - expect(await truncatedExportWarning.evaluate((warning) => warning.innerText)).to - .equal('The runs export is limited to 100 entries, only the last runs will be exported (sorted by run number)'); + await pressElement(page, EXPORT_MODAL_TRIGGER_ID); + await expectInnerText( + page, + '#export-modal #truncated-export-warning', + 'The runs export is limited to 100 entries, only the last runs will be exported (sorted by run number)', + ); }); it('should successfully display disabled runs export button when there is no runs available', async () => { await goToPage(page, 'run-overview'); - await waitForTimeout(200); - await pressElement(page, '#openFilterToggle'); - await waitForTimeout(200); - // Type a fake run number to have no runs - await page.focus(runNumberInputSelector); - await page.keyboard.type('99999999999'); - await waitForTimeout(300); - + await waitForTableDataReload(page, () => fillInput(page, runNumberInputSelector, '99999999999')); await pressElement(page, '#openFilterToggle'); - - expect(await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.disabled)).to.be.true; + await page.waitForSelector(`${EXPORT_MODAL_TRIGGER_ID}[disabled]`, { timeout: 250 }); }); it('should successfully export filtered runs', async () => { @@ -1073,23 +1059,19 @@ module.exports = () => { }); let downloadFilesNames; - const targetFileName = 'runs.json'; + let targetFileName = 'runs.json'; let runs; - let exportModal; // First export - await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); - await page.waitForSelector('#export-runs-modal'); - await page.waitForSelector('#send:disabled'); - await page.waitForSelector('.form-control'); + await pressElement(page, EXPORT_MODAL_TRIGGER_ID); + await page.waitForSelector('#download-export:disabled', { timeout: 250 }); + await expectInnerText(page, '#download-export', 'Export'); + await page.waitForSelector('.form-control', { timeout: 250 }); await page.select('.form-control', 'runQuality', 'runNumber'); - await page.waitForSelector('#send:enabled'); - const exportButtonText = await page.$eval('#send', (button) => button.innerText); - expect(exportButtonText).to.be.eql('Export'); - - await page.$eval('#send', (button) => button.click()); + await page.waitForSelector('#download-export:enabled'); + await expectInnerText(page, '#download-export', 'Export'); - await waitForDownload(session); + await waitForDownload(session, () => pressElement(page, '#download-export')); // Check download downloadFilesNames = fs.readdirSync(downloadPath); @@ -1104,27 +1086,20 @@ module.exports = () => { // Second export // Apply filtering - const filterInputSelectorPrefix = '#runQualityCheckbox'; - const badFilterSelector = `${filterInputSelectorPrefix}bad`; - - await pressElement(page, '#openFilterToggle'); - await page.waitForSelector(badFilterSelector); - await page.$eval(badFilterSelector, (element) => element.click()); - await page.waitForSelector('.atom-spinner'); - await page.waitForSelector('tbody tr:nth-child(2)'); - await page.waitForSelector(EXPORT_RUNS_TRIGGER_SELECTOR); + await waitForTableDataReload(page, async () => { + await pressElement(page, '#openFilterToggle'); + await pressElement('#runQualityCheckboxbad'); + }); ///// Download - await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); - await page.waitForSelector('#export-runs-modal'); - expect(exportModal).to.not.be.null; - - await page.waitForSelector('.form-control'); + await pressElement(page, EXPORT_MODAL_TRIGGER_ID); + await page.waitForSelector('#export-modal', { timeout: 250 }); + await page.waitForSelector('.form-control', { timeout: 250 }); await page.select('.form-control', 'runQuality', 'runNumber'); - await page.waitForSelector('#send:enabled'); - await page.$eval('#send', (button) => button.click()); + await fillInput(page, '#export-name', 'filtered-runs'); + targetFileName = 'filtered-runs.json'; - await waitForDownload(session); + await waitForDownload(session, () => pressElement(page, '#download-export')); // Check download downloadFilesNames = fs.readdirSync(downloadPath); diff --git a/test/public/runs/runsPerDataPass.overview.test.js b/test/public/runs/runsPerDataPass.overview.test.js index 3aab4ccfe3..7522358f58 100644 --- a/test/public/runs/runsPerDataPass.overview.test.js +++ b/test/public/runs/runsPerDataPass.overview.test.js @@ -209,7 +209,8 @@ module.exports = () => { it('should successfully export runs', async () => { await goToPage(page, 'runs-per-data-pass', { queryParameters: { dataPassId: 3 } }); - const EXPORT_RUNS_TRIGGER_SELECTOR = '#export-runs-trigger'; + + const EXPORT_MODAL_TRIGGER_ID = '#export-trigger'; const downloadPath = path.resolve('./download'); @@ -224,18 +225,15 @@ module.exports = () => { const targetFileName = 'runs.json'; // First export - await page.$eval(EXPORT_RUNS_TRIGGER_SELECTOR, (button) => button.click()); - await page.waitForSelector('#export-runs-modal'); - await page.waitForSelector('#send:disabled'); - await page.waitForSelector('.form-control'); + await pressElement(page, EXPORT_MODAL_TRIGGER_ID); + await page.waitForSelector('#download-export:disabled', { timeout: 250 }); + await expectInnerText(page, '#download-export', 'Export'); + await page.waitForSelector('.form-control', { timeout: 250 }); await page.select('.form-control', 'runQuality', 'runNumber'); - await page.waitForSelector('#send:enabled'); - const exportButtonText = await page.$eval('#send', (button) => button.innerText); - expect(exportButtonText).to.be.eql('Export'); - - await page.$eval('#send', (button) => button.click()); + await page.waitForSelector('#download-export:enabled'); + await expectInnerText(page, '#download-export', 'Export'); - await waitForDownload(session); + await waitForDownload(session, () => pressElement(page, '#download-export')); // Check download const downloadFilesNames = fs.readdirSync(downloadPath); diff --git a/test/public/runs/runsPerPeriod.overview.test.js b/test/public/runs/runsPerPeriod.overview.test.js index 3411e74959..dfe701f5c4 100644 --- a/test/public/runs/runsPerPeriod.overview.test.js +++ b/test/public/runs/runsPerPeriod.overview.test.js @@ -204,17 +204,12 @@ module.exports = () => { expect(urlParameters).to.contain(`runNumber=${expectedRunNumber}`); }); - const EXPORT_RUNS_TRIGGER_SELECTOR = '#export-runs-trigger'; - it('should successfully export all runs per lhc Period', async () => { await goToPage(page, 'runs-per-lhc-period', { queryParameters: { lhcPeriodName: 'LHC22a' } }); - const downloadPath = path.resolve('./download'); + const EXPORT_MODAL_TRIGGER_ID = '#export-trigger'; - await page.evaluate(() => { - // eslint-disable-next-line no-undef - model.runs.perLhcPeriodOverviewModel.pagination.itemsPerPage = 2; - }); + const downloadPath = path.resolve('./download'); // Check accessibility on frontend const session = await page.target().createCDPSession(); @@ -227,14 +222,15 @@ module.exports = () => { const targetFileName = 'runs.json'; // First export - await pressElement(page, EXPORT_RUNS_TRIGGER_SELECTOR); - await page.waitForSelector('select.form-control', { timeout: 200 }); - await page.select('select.form-control', 'runQuality', 'runNumber', 'definition', 'lhcPeriod'); - await expectInnerText(page, '#send:enabled', 'Export'); - await Promise.all([ - waitForDownload(session), - pressElement(page, '#send:enabled'), - ]); + await pressElement(page, EXPORT_MODAL_TRIGGER_ID); + await page.waitForSelector('#download-export:disabled', { timeout: 250 }); + await expectInnerText(page, '#download-export', 'Export'); + await page.waitForSelector('.form-control', { timeout: 250 }); + await page.select('.form-control', 'runQuality', 'runNumber'); + await page.waitForSelector('#download-export:enabled'); + await expectInnerText(page, '#download-export', 'Export'); + + await waitForDownload(session, () => pressElement(page, '#download-export')); // Check download const downloadFilesNames = fs.readdirSync(downloadPath); diff --git a/test/utilities/waitForDownload.js b/test/utilities/waitForDownload.js index c29fdfd269..dc2f0d9c10 100644 --- a/test/utilities/waitForDownload.js +++ b/test/utilities/waitForDownload.js @@ -14,22 +14,26 @@ /** * Create promise which is resolved when last initiated download is completed and rejected when canceled * @param {CDPSession} session puppetear CDP session + * @param {funciton} trigger triggering download * @param {object} options options altering behaviour * @param {number} [options.timeout = 5000] timeout (ms) to reject if not downloaded * @return {Promise} promise * !!! Downloading requires to set 'Browser.setDownloadBehavior' behaviour on the given CDP session */ -async function waitForDownload(session, { timeout = 5000 } = {}) { - return new Promise((resolve, reject) => { - session.on('Browser.downloadProgress', (event) => { - if (event.state === 'completed') { - resolve('download completed'); - } else if (event.state === 'canceled') { - reject('download canceled'); - } - }); - setTimeout(() => reject(`Download timeout after ${timeout} ms`), timeout); - }); +async function waitForDownload(session, trigger, { timeout = 5000 } = {}) { + return Promise.all([ + new Promise((resolve, reject) => { + session.on('Browser.downloadProgress', (event) => { + if (event.state === 'completed') { + resolve('download completed'); + } else if (event.state === 'canceled') { + reject('download canceled'); + } + }); + setTimeout(() => reject(`Download timeout after ${timeout} ms`), timeout); + }), + trigger(), + ]); } exports.waitForDownload = waitForDownload; From bbb2f2905b602b6c16b3dabf8ccc6776e9cfa163 Mon Sep 17 00:00:00 2001 From: xsalonx Date: Thu, 14 Mar 2024 10:38:51 +0100 Subject: [PATCH 15/18] fix test --- lib/public/views/Runs/Overview/exportTriggerAndModal.js | 4 ++-- test/public/runs/overview.test.js | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/public/views/Runs/Overview/exportTriggerAndModal.js b/lib/public/views/Runs/Overview/exportTriggerAndModal.js index e6f35b5399..dbbb53965c 100644 --- a/lib/public/views/Runs/Overview/exportTriggerAndModal.js +++ b/lib/public/views/Runs/Overview/exportTriggerAndModal.js @@ -60,7 +60,7 @@ const exportForm = (model, items, modalHandler, activeColumns) => { h('label.form-check-label.f4.mt1', 'Export format'), h('label.form-check-label.f6', 'Select output format'), h('.flex-row.g3', Object.values(EXPORT_FORMATS).map((exportFormat) => { - const id = `runs-export-type-${exportFormat}`; + const id = `runs-export-format-${exportFormat}`; return h('.form-check', [ h('input.form-check-input', { id, @@ -84,7 +84,7 @@ const exportForm = (model, items, modalHandler, activeColumns) => { id: 'export-name', value: model.exportName, // eslint-disable-next-line no-return-assign - onchange: ({ target: { value: currentText } }) => model.exportName = currentText, + oninput: ({ target: { value: currentText } }) => model.exportName = currentText, }), ]), ]), diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index 4ac9f9f48d..a778ae4483 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -1064,7 +1064,7 @@ module.exports = () => { // First export await pressElement(page, EXPORT_MODAL_TRIGGER_ID); - await page.waitForSelector('#download-export:disabled', { timeout: 250 }); + await page.waitForSelector('#download-export:disabled', { timeout: 500 }); await expectInnerText(page, '#download-export', 'Export'); await page.waitForSelector('.form-control', { timeout: 250 }); await page.select('.form-control', 'runQuality', 'runNumber'); @@ -1088,15 +1088,15 @@ module.exports = () => { // Apply filtering await waitForTableDataReload(page, async () => { await pressElement(page, '#openFilterToggle'); - await pressElement('#runQualityCheckboxbad'); + await pressElement(page, '#runQualityCheckboxbad'); }); ///// Download await pressElement(page, EXPORT_MODAL_TRIGGER_ID); - await page.waitForSelector('#export-modal', { timeout: 250 }); + await page.waitForSelector('#export-modal', { timeout: 500 }); await page.waitForSelector('.form-control', { timeout: 250 }); await page.select('.form-control', 'runQuality', 'runNumber'); - await fillInput(page, '#export-name', 'filtered-runs'); + await fillInput(page, 'input#export-name', 'filtered-runs'); targetFileName = 'filtered-runs.json'; await waitForDownload(session, () => pressElement(page, '#download-export')); @@ -1106,6 +1106,7 @@ module.exports = () => { expect(downloadFilesNames.filter((name) => name == targetFileName)).to.be.lengthOf(1); runs = JSON.parse(fs.readFileSync(path.resolve(downloadPath, targetFileName))); expect(runs).to.have.all.deep.members([{ runNumber: 2, runQuality: 'bad' }, { runNumber: 1, runQuality: 'bad' }]); + fs.unlinkSync(path.resolve(downloadPath, targetFileName)); }); it('should successfully navigate to the LHC fill details page', async () => { From eeadd2557512f7c8cdca2b738b33fd6551645433 Mon Sep 17 00:00:00 2001 From: xsalonx Date: Thu, 14 Mar 2024 10:40:15 +0100 Subject: [PATCH 16/18] test --- test/public/runs/runsPerDataPass.overview.test.js | 2 +- test/public/runs/runsPerPeriod.overview.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/public/runs/runsPerDataPass.overview.test.js b/test/public/runs/runsPerDataPass.overview.test.js index 7522358f58..eb51e85530 100644 --- a/test/public/runs/runsPerDataPass.overview.test.js +++ b/test/public/runs/runsPerDataPass.overview.test.js @@ -226,7 +226,7 @@ module.exports = () => { // First export await pressElement(page, EXPORT_MODAL_TRIGGER_ID); - await page.waitForSelector('#download-export:disabled', { timeout: 250 }); + await page.waitForSelector('#download-export:disabled', { timeout: 500 }); await expectInnerText(page, '#download-export', 'Export'); await page.waitForSelector('.form-control', { timeout: 250 }); await page.select('.form-control', 'runQuality', 'runNumber'); diff --git a/test/public/runs/runsPerPeriod.overview.test.js b/test/public/runs/runsPerPeriod.overview.test.js index dfe701f5c4..3410e1b8e2 100644 --- a/test/public/runs/runsPerPeriod.overview.test.js +++ b/test/public/runs/runsPerPeriod.overview.test.js @@ -223,7 +223,7 @@ module.exports = () => { // First export await pressElement(page, EXPORT_MODAL_TRIGGER_ID); - await page.waitForSelector('#download-export:disabled', { timeout: 250 }); + await page.waitForSelector('#download-export:disabled', { timeout: 500 }); await expectInnerText(page, '#download-export', 'Export'); await page.waitForSelector('.form-control', { timeout: 250 }); await page.select('.form-control', 'runQuality', 'runNumber'); From 26bc21bcd69afc47b1618069511e14cf4168d8b2 Mon Sep 17 00:00:00 2001 From: xsalonx Date: Thu, 14 Mar 2024 10:44:58 +0100 Subject: [PATCH 17/18] test ok --- test/public/runs/runsPerDataPass.overview.test.js | 2 +- test/public/runs/runsPerPeriod.overview.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/public/runs/runsPerDataPass.overview.test.js b/test/public/runs/runsPerDataPass.overview.test.js index eb51e85530..79d6bac990 100644 --- a/test/public/runs/runsPerDataPass.overview.test.js +++ b/test/public/runs/runsPerDataPass.overview.test.js @@ -207,7 +207,7 @@ module.exports = () => { expect(urlParameters).to.contain(`runNumber=${expectedRunNumber}`); }); - it('should successfully export runs', async () => { + it('should successfully export all runs pere data pass', async () => { await goToPage(page, 'runs-per-data-pass', { queryParameters: { dataPassId: 3 } }); const EXPORT_MODAL_TRIGGER_ID = '#export-trigger'; diff --git a/test/public/runs/runsPerPeriod.overview.test.js b/test/public/runs/runsPerPeriod.overview.test.js index 3410e1b8e2..94313b8bce 100644 --- a/test/public/runs/runsPerPeriod.overview.test.js +++ b/test/public/runs/runsPerPeriod.overview.test.js @@ -226,7 +226,7 @@ module.exports = () => { await page.waitForSelector('#download-export:disabled', { timeout: 500 }); await expectInnerText(page, '#download-export', 'Export'); await page.waitForSelector('.form-control', { timeout: 250 }); - await page.select('.form-control', 'runQuality', 'runNumber'); + await page.select('.form-control', 'runQuality', 'runNumber', 'definition', 'lhcPeriod'); await page.waitForSelector('#download-export:enabled'); await expectInnerText(page, '#download-export', 'Export'); From 61e22826ca4b24a47c9517e38d45521e285e55dc Mon Sep 17 00:00:00 2001 From: xsalonx Date: Thu, 14 Mar 2024 11:05:51 +0100 Subject: [PATCH 18/18] a --- test/public/runs/overview.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/public/runs/overview.test.js b/test/public/runs/overview.test.js index a778ae4483..f1ac8044bc 100644 --- a/test/public/runs/overview.test.js +++ b/test/public/runs/overview.test.js @@ -1032,7 +1032,7 @@ module.exports = () => { await expectInnerText( page, '#export-modal #truncated-export-warning', - 'The runs export is limited to 100 entries, only the last runs will be exported (sorted by run number)', + 'The items export is limited to 100 entries, only the last items will be exported', ); });