Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[O2B-1194] Refactor export functionality #1462

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
143 changes: 140 additions & 3 deletions lib/public/models/OverviewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
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';

export const EXPORT_FORMATS = {

Check warning on line 22 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L22

Added line #L22 was not covered by tests
JSON: 'JSON',
CSV: 'CSV',
};

/**
* Interface of a model representing an overview page state
Expand Down Expand Up @@ -51,6 +58,25 @@

this._observableItems = ObservableData.builder().initialValue(RemoteData.loading()).build();
this._observableItems.bubbleTo(this);

this._exportDataSource = new PaginatedRemoteDataSource();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exports are not paginated

this._exportObservableItems = ObservableData.builder()

Check warning on line 63 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L62-L63

Added lines #L62 - L63 were not covered by tests
.initialValue(RemoteData.notAsked())
.apply((remoteData) => remoteData.apply({ Success: ({ items }) => this.processItems(items) }))

Check warning on line 65 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L65

Added line #L65 was not covered by tests
.build();
this._exportObservableItems.bubbleTo(this);
this._exportDataSource.pipe(this._exportObservableItems);

Check warning on line 68 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L67-L68

Added lines #L67 - L68 were not covered by tests

/**
* Default name for export data file, can be overriden in inherting classes
*/
this._defaultExportName = 'bkp-export';
this._exportName = '';

Check warning on line 74 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L73-L74

Added lines #L73 - L74 were not covered by tests

/**
* Default format
*/
this._defaultExportFormat = EXPORT_FORMATS.JSON;

Check warning on line 79 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L79

Added line #L79 was not covered by tests
}

/**
Expand Down Expand Up @@ -119,8 +145,97 @@
* @return {Promise<void>} void
*/
async load() {
const params = await this.getLoadParameters();
await this._dataSource.fetch(buildUrl(this.getRootEndpoint(), params));
const paginationParams = await this.getLoadParameters();
this._exportObservableItems.setCurrent(RemoteData.notAsked());
await this._dataSource.fetch(buildUrl(this.getRootEndpoint(), paginationParams));

Check warning on line 150 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L148-L150

Added lines #L148 - L150 were not covered by tests
}

/**
* Fetch all the relevant items from the API
* @return {Promise<void>} void
*/
async loadExport() {
return this._exportObservableItems.getCurrent().match({
Success: () => null,
Loading: () => null,
Other: () => this._exportDataSource.fetch(this.getRootEndpoint()),

Check warning on line 161 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L157-L161

Added lines #L157 - L161 were not covered by tests
});
}

/**
* Create the export with the variables set in the model, handling errors appropriately
* @param {object[]} items The source content.
* @param {Object<string, Function<*, string>>} exportFormats defines how particual fields of data units will be formated
* @return {void}
*/
async export(items, exportFormats = {}) {
const { exportFields, exportName } = this;
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)];

Check warning on line 177 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L171-L177

Added lines #L171 - L177 were not covered by tests
});
return Object.fromEntries(formattedEntries);

Check warning on line 179 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L179

Added line #L179 was not covered by tests
});
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;');

Check warning on line 184 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L181-L184

Added lines #L181 - L184 were not covered by tests
} else {
throw new Error('Incorrect export format');

Check warning on line 186 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L186

Added line #L186 was not covered by tests
}
}

/**
* Get export name
* @return {string} name
*/
get exportName() {
return this._exportName || this._defaultExportName;

Check warning on line 195 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L194-L195

Added lines #L194 - L195 were not covered by tests
}

/**
* Set export name
* @param {string} exportName name
*/
set exportName(exportName) {
this._exportName = exportName;

Check warning on line 203 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L202-L203

Added lines #L202 - L203 were not covered by tests
}

/**
* Get the field values that will be exported
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment is misleading, because it seems like you return the value of the fields.
I would rather use Get the list of properties that will be exported for each items

* @return {string[]} the list of fields of a export items to be included in the export
*/
get exportFields() {
return this._exportFields || [];

Check warning on line 211 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L210-L211

Added lines #L210 - L211 were not covered by tests
}

/**
* Set the selected fields to be exported
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set the list of properties that will be exported for each items

* @param {SelectionOption[]|HTMLCollection} exportFields the list of fields of export items to be included in the export
*/
set exportFields(exportFields) {
this._exportFields = [];
[...exportFields].map((selectedOption) => this._exportFields.push(selectedOption.value));
this.notify();

Check warning on line 221 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L218-L221

Added lines #L218 - L221 were not covered by tests
}

/**
* Get the output format of the export
* @return {string} the output format
*/
get exportFormat() {
return this._exportFormat || this._defaultExportFormat;

Check warning on line 229 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L228-L229

Added lines #L228 - L229 were not covered by tests
}

/**
* Set the export type parameter of the export to be created
* @param {string} exportFormat one of acceptable export formats @see EXPORT_FORMATS
*/
set exportFormat(exportFormat) {
this._exportFormat = exportFormat;
this.notify();

Check warning on line 238 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L236-L238

Added lines #L236 - L238 were not covered by tests
}

/**
Expand All @@ -136,8 +251,30 @@
}

/**
* 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runs?

*
* @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({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this second match?

Success: () => payload.length < this._pagination.itemsCount,
Other: () => null,

Check warning on line 262 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L258-L262

Added lines #L258 - L262 were not covered by tests
}),
Other: () => null,

Check warning on line 264 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L264

Added line #L264 was not covered by tests
});
}

/**
* Return the export items remote data
* @return {RemoteData<T[]>} the items
*/
get exportItems() {
return this._exportObservableItems.getCurrent();

Check warning on line 273 in lib/public/models/OverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/models/OverviewModel.js#L272-L273

Added lines #L272 - L273 were not covered by tests
}

/**
* Return the current items remote data
* @return {RemoteData<T[]>} the items
*/
get items() {
Expand Down
141 changes: 1 addition & 140 deletions lib/public/views/Runs/Overview/RunsOverviewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,8 +47,7 @@
this._eorReasonsFilterModel.observe(() => this._applyFilters());
this._eorReasonsFilterModel.visualChange$.observe(() => this.notify());

// Export items
this._allRuns = RemoteData.NotAsked();
this._defaultExportName = 'runs';

Check warning on line 50 in lib/public/views/Runs/Overview/RunsOverviewModel.js

View check run for this annotation

Codecov / codecov/patch

lib/public/views/Runs/Overview/RunsOverviewModel.js#L50

Added line #L50 was not covered by tests

this.reset(false);
// eslint-disable-next-line no-return-assign,require-jsdoc
Expand All @@ -70,64 +65,6 @@
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.
* @param {string} fileName The name of the file including the output format.
* @param {Object<string, Function<*, string>>} 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.getSelectedRunsFields() || [];
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.getSelectedExportType() === '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
*/
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
Expand Down Expand Up @@ -302,29 +239,6 @@
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
Expand Down Expand Up @@ -799,32 +713,6 @@
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
*
Expand Down Expand Up @@ -942,33 +830,6 @@
};
}

/**
* Update the cache containing all the runs without paging
*
* @return {Promise<void>} 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
*
Expand Down
4 changes: 2 additions & 2 deletions lib/public/views/Runs/Overview/RunsOverviewPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
Expand Down
Loading