Skip to content

Commit 52020d9

Browse files
committed
merge main
2 parents e01bd2a + 1ebeee9 commit 52020d9

16 files changed

Lines changed: 526 additions & 373 deletions
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/**
2+
* @license
3+
* Copyright CERN and copyright holders of ALICE O2. This software is
4+
* distributed under the terms of the GNU General Public License v3 (GPL
5+
* Version 3), copied verbatim in the file "COPYING".
6+
*
7+
* See http://alice-o2.web.cern.ch/license for full licensing information.
8+
*
9+
* In applying this license CERN does not waive the privileges and immunities
10+
* granted to it by virtue of its status as an Intergovernmental Organization
11+
* or submit itself to any jurisdiction.
12+
*/
13+
14+
import { DATA_EXPORT_TYPES } from '../../../domain/enums/DataExportTypes.js';
15+
import errorAlert from '../errorAlert.js';
16+
import spinner from '../spinner.js';
17+
import { h } from '/js/src/index.js';
18+
19+
/**
20+
* Export form component, containing the fields to export, the export type and the export button
21+
*
22+
* @param {DataExportModel} exportModel export model
23+
* @param {ModalHandler} modalHandler The modal handler, used to dismiss modal after export
24+
*
25+
* @return {Component} the form component
26+
*/
27+
const exportForm = (exportModel, modalHandler) => {
28+
const { selectedFields, selectedExportType, dataExportConfiguration } = exportModel;
29+
const fields = Object.keys(dataExportConfiguration);
30+
31+
const fieldsSelected = selectedFields.length > 0;
32+
33+
const fieldsSelectionLabels = [
34+
h('label.form-check-label.f4.mt1', { for: 'fields' }, 'Fields'),
35+
h(
36+
'label.form-check-label.f6',
37+
{ for: 'fields' },
38+
'Select which fields to be exported. (CTRL + click for multiple selection)',
39+
),
40+
];
41+
42+
const fieldsSelection = h('select#fields.form-control', {
43+
style: 'min-height: 20rem;',
44+
multiple: true,
45+
onchange: ({ target }) => exportModel.setSelectedFields(target.selectedOptions),
46+
}, [
47+
...fields
48+
.filter((name) => !['id', 'actions'].includes(name))
49+
.map((name) => h('option', {
50+
value: name,
51+
selected: selectedFields.length ? selectedFields.includes(name) : false,
52+
}, name)),
53+
]);
54+
55+
const exportTypeSelectionLabels = [
56+
h('label.form-check-label.f4.mt1', 'Export type'),
57+
h('label.form-check-label.f6', 'Select output format'),
58+
];
59+
60+
const exportTypeSelect = h('.flex-row.g3', DATA_EXPORT_TYPES.map((exportType) => {
61+
const id = `data-export-type-${exportType}`;
62+
return h('.form-check', [
63+
h('input.form-check-input', {
64+
id,
65+
type: 'radio',
66+
value: exportType,
67+
checked: selectedExportType.length ? selectedExportType.includes(exportType) : false,
68+
name: 'export-type',
69+
onclick: () => exportModel.setSelectedExportType(exportType),
70+
}),
71+
h('label.form-check-label', {
72+
for: id,
73+
}, exportType),
74+
]);
75+
}));
76+
77+
const exportBtn = h('button.shadow-level1.btn.btn-success.mt2#send', {
78+
disabled: !fieldsSelected,
79+
onclick: async () => {
80+
await exportModel.createExport();
81+
modalHandler.dismiss();
82+
},
83+
}, 'Export');
84+
85+
const dataLength = exportModel.items.match({ Success: ({ length } = {}) => length, Other: () => null });
86+
const { totalExistingItemsCount } = exportModel;
87+
88+
const truncatedDataInfo = dataLength && dataLength < totalExistingItemsCount
89+
? h(
90+
'#truncated-export-warning.warning',
91+
`The data export is limited to ${dataLength} entries, only the most recent data will be exported`,
92+
)
93+
: null;
94+
95+
return [
96+
truncatedDataInfo,
97+
fieldsSelectionLabels,
98+
fieldsSelection,
99+
exportTypeSelectionLabels,
100+
exportTypeSelect,
101+
exportBtn,
102+
];
103+
};
104+
105+
/**
106+
* A function to construct the exports data screen
107+
*
108+
* @param {DataExportModel} exportModel export model
109+
* @param {ModalHandler} modalHandler The modal handler, used to dismiss modal after export
110+
* @return {Component} Return the view of the inputs
111+
*/
112+
const exportModal = (exportModel, modalHandler) => {
113+
exportModel.callForData();
114+
115+
return h('div#export-data-modal', [
116+
h('h2.w-50', 'Export data'),
117+
exportModel.items.match({
118+
NotAsked: () => errorAlert(),
119+
Failure: (errors) => errorAlert(errors),
120+
Loading: () => spinner({ size: 2, absolute: false }),
121+
Other: () => exportForm(exportModel, modalHandler),
122+
}),
123+
]);
124+
};
125+
126+
/**
127+
* Builds a button which will open popover for data export
128+
*
129+
* @param {DataExportModel} exportModel export model
130+
* @param {ModalModel} modalModel modal model
131+
* @param {object} [displayConfiguration] additional display options
132+
* @param {boolean} [displayConfiguration.autoMarginLeft = true] if true margin left is set to auto, otherwise not
133+
* @returns {Component} button
134+
*/
135+
export const exportTriggerAndModal = (exportModel, modalModel, { autoMarginLeft = true } = {}) =>
136+
h(`button.btn.btn-primary${autoMarginLeft ? '.mlauto' : ''}#export-data-trigger`, {
137+
disabled: exportModel.disabled,
138+
onclick: () => modalModel.display({ content: (modalModel) => exportModal(exportModel, modalModel), size: 'medium' }),
139+
}, 'Export data');
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @license
3+
* Copyright 2019-2020 CERN and copyright holders of ALICE O2.
4+
* See http://alice-o2.web.cern.ch/copyright for details of the copyright holders.
5+
* All rights not expressly granted are reserved.
6+
*
7+
* This software is distributed under the terms of the GNU General Public
8+
* License v3 (GPL Version 3), copied verbatim in the file "COPYING".
9+
*
10+
* In applying this license CERN does not waive the privileges and immunities
11+
* granted to it by virtue of its status as an Intergovernmental Organization
12+
* or submit itself to any jurisdiction.
13+
*/
14+
15+
export const DataExportTypes = Object.freeze({
16+
JSON: 'JSON',
17+
CSV: 'CSV',
18+
});
19+
20+
export const DATA_EXPORT_TYPES = Object.values(DataExportTypes);
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/**
2+
* @license
3+
* Copyright CERN and copyright holders of ALICE O2. This software is
4+
* distributed under the terms of the GNU General Public License v3 (GPL
5+
* Version 3), copied verbatim in the file "COPYING".
6+
*
7+
* See http://alice-o2.web.cern.ch/license for full licensing information.
8+
*
9+
* In applying this license CERN does not waive the privileges and immunities
10+
* granted to it by virtue of its status as an Intergovernmental Organization
11+
* or submit itself to any jurisdiction.
12+
*/
13+
14+
import { Observable } from '/js/src/index.js';
15+
import { createCSVExport, createJSONExport } from '../utilities/export.js';
16+
import pick from '../utilities/pick.js';
17+
import { DataExportTypes } from '../domain/enums/DataExportTypes.js';
18+
19+
/**
20+
* @typedef FieldExportConfiguration
21+
*
22+
* @property {function|null} exportFormat
23+
*/
24+
25+
/**
26+
* @typedef DataExportConfiguration
27+
*
28+
* @type {object<string, FieldExportConfiguration>} mapping: field name -> field export configuration
29+
*/
30+
31+
/**
32+
* Model handling export configuration and creation
33+
*/
34+
export class DataExportModel extends Observable {
35+
/**
36+
* Constructor
37+
*
38+
* @param {ObservableData<RemoteData<T[]>>} items$ observable data used as source for export
39+
* @param {DataExportConfiguration} dataExportConfiguration defines selectable fields and their formatting
40+
* @param {function} onDataNotAskedAction function to be called in case of missing data
41+
*/
42+
constructor(items$, dataExportConfiguration, onDataNotAskedAction) {
43+
super();
44+
45+
this._items$ = items$;
46+
this._items$.bubbleTo(this);
47+
48+
this._selectedFields = [];
49+
50+
this._selectedExportType = DataExportTypes.JSON;
51+
52+
this._exportName = 'data';
53+
54+
this._dataExportConfiguration = dataExportConfiguration;
55+
this._onDataNotAskedAction = onDataNotAskedAction;
56+
57+
this._disabled = null;
58+
59+
this._totalExistingItemsCount = null;
60+
}
61+
62+
/**
63+
* Get data export configuration
64+
*
65+
* @return {DataExportConfiguration} configuration
66+
*/
67+
get dataExportConfiguration() {
68+
return this._dataExportConfiguration;
69+
}
70+
71+
/**
72+
* Get total number of items that model knows they exist
73+
*
74+
* @return {number} totalExistingItemsCount
75+
*/
76+
get totalExistingItemsCount() {
77+
return this._totalExistingItemsCount;
78+
}
79+
80+
/**
81+
* Set total number of items that model should they exist
82+
* @param {number} totalExistingItemsCount count
83+
*/
84+
setTotalExistingItemsCount(totalExistingItemsCount) {
85+
this._totalExistingItemsCount = totalExistingItemsCount;
86+
this.notify();
87+
}
88+
89+
/**
90+
* State whether exporting is enabled
91+
* @return {boolean} if true disabled, otherwise enabled
92+
*/
93+
get disabled() {
94+
return this._disabled;
95+
}
96+
97+
/**
98+
* Set whether exporting is enabled
99+
* @param {boolean} disabled if true disabled, otherwise enabled
100+
*/
101+
setDisabled(disabled) {
102+
this._disabled = disabled;
103+
this.notify();
104+
}
105+
106+
/**
107+
* If the model has no data to be exported, it calls for it via action function provided in constructor
108+
* @return {Promise<void>|null} promise if data were not asked for, null otherwise
109+
*/
110+
callForData() {
111+
if (this._onDataNotAskedAction) {
112+
return this.items.match({
113+
NotAsked: () => this._onDataNotAskedAction(),
114+
Other: () => null,
115+
});
116+
} else {
117+
throw new Error('onDataNotAskedAction was not provided');
118+
}
119+
}
120+
121+
/**
122+
* Return the current items remote data
123+
*
124+
* @return {RemoteData<T[]>} the items
125+
*/
126+
get items() {
127+
return this._items$.getCurrent();
128+
}
129+
130+
/**
131+
* Get export type selected by the user
132+
*
133+
* @return {string} export type
134+
*/
135+
get selectedExportType() {
136+
return this._selectedExportType;
137+
}
138+
139+
/**
140+
* Set export type
141+
*
142+
* @param {string} exportType export type
143+
* @return {void}
144+
*/
145+
setSelectedExportType(exportType) {
146+
this._selectedExportType = exportType;
147+
this.notify();
148+
}
149+
150+
/**
151+
* Get selected fields
152+
*
153+
* @return {string[]} selected fields
154+
*/
155+
get selectedFields() {
156+
return this._selectedFields;
157+
}
158+
159+
/**
160+
* Update selected fields from HTML options list
161+
*
162+
* @param {HTMLCollection|Array} selectedOptions options collection
163+
* @return {void}
164+
*/
165+
setSelectedFields(selectedOptions) {
166+
this._selectedFields = Array.from(selectedOptions).map(({ value }) => value);
167+
this.notify();
168+
}
169+
170+
/**
171+
* Create export using current items observable
172+
*
173+
* @return {Promise<void>} void
174+
*/
175+
async createExport() {
176+
this.items.apply({
177+
Success: (items) => {
178+
const { selectedFields } = this;
179+
180+
const formatted = items.map((item) => {
181+
const selectedEntries = Object.entries(pick(item, selectedFields));
182+
const mappedEntries = selectedEntries.map(([key, value]) => {
183+
const formatter = this.dataExportConfiguration[key]?.exportFormat || ((v) => v);
184+
return [key, formatter(value, item)];
185+
});
186+
187+
return Object.fromEntries(mappedEntries);
188+
});
189+
190+
this.selectedExportType === DataExportTypes.JSON
191+
? createJSONExport(formatted, `${this._exportName}.json`, 'application/json')
192+
: createCSVExport(formatted, `${this._exportName}.csv`, 'text/csv;charset=utf-8;');
193+
},
194+
});
195+
}
196+
}

0 commit comments

Comments
 (0)