@c8y/ngx-components 1021.11.1 → 1021.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/alarms/alarms-date-filter.component.d.ts +34 -0
- package/alarms/alarms-date-filter.component.d.ts.map +1 -0
- package/alarms/alarms-filter.component.d.ts +2 -1
- package/alarms/alarms-filter.component.d.ts.map +1 -1
- package/alarms/alarms-list.component.d.ts +2 -17
- package/alarms/alarms-list.component.d.ts.map +1 -1
- package/alarms/alarms-view.service.d.ts +5 -2
- package/alarms/alarms-view.service.d.ts.map +1 -1
- package/alarms/alarms.component.d.ts +3 -1
- package/alarms/alarms.component.d.ts.map +1 -1
- package/alarms/alarms.model.d.ts +47 -1
- package/alarms/alarms.model.d.ts.map +1 -1
- package/alarms/alarms.module.d.ts +15 -13
- package/alarms/alarms.module.d.ts.map +1 -1
- package/branding/shared/data/store-branding.service.d.ts +5 -0
- package/branding/shared/data/store-branding.service.d.ts.map +1 -1
- package/branding/shared/lazy/branding/branding.component.d.ts.map +1 -1
- package/context-dashboard/context-dashboard.model.d.ts +1 -0
- package/context-dashboard/context-dashboard.model.d.ts.map +1 -1
- package/context-dashboard/context-dashboard.service.d.ts +7 -5
- package/context-dashboard/context-dashboard.service.d.ts.map +1 -1
- package/context-dashboard/dashboard-detail.component.d.ts +4 -7
- package/context-dashboard/dashboard-detail.component.d.ts.map +1 -1
- package/context-dashboard/dashboard-detail.service.d.ts +4 -4
- package/context-dashboard/dashboard-detail.service.d.ts.map +1 -1
- package/context-dashboard/dashboard-settings/dashboard-general-settings.component.d.ts +2 -2
- package/context-dashboard/dashboard-settings/dashboard-general-settings.component.d.ts.map +1 -1
- package/context-dashboard/dashboard-settings/typed-dashboard-settings.component.d.ts +19 -6
- package/context-dashboard/dashboard-settings/typed-dashboard-settings.component.d.ts.map +1 -1
- package/core/action-bar/action-bar-item.component.d.ts +1 -1
- package/core/action-bar/action-bar-item.component.d.ts.map +1 -1
- package/core/action-bar/action-bar.module.d.ts +6 -6
- package/core/action-bar/action-bar.module.d.ts.map +1 -1
- package/core/bootstrap/bootstrap.component.d.ts +3 -1
- package/core/bootstrap/bootstrap.component.d.ts.map +1 -1
- package/core/common/aggregation/aggregation.model.d.ts +39 -0
- package/core/common/aggregation/aggregation.model.d.ts.map +1 -0
- package/core/common/aggregation/aggregation.service.d.ts +17 -0
- package/core/common/aggregation/aggregation.service.d.ts.map +1 -0
- package/core/common/index.d.ts +4 -0
- package/core/common/index.d.ts.map +1 -1
- package/core/common/inter-app.service.d.ts +48 -0
- package/core/common/inter-app.service.d.ts.map +1 -0
- package/core/common/interval-based-reload.abstract.d.ts +120 -0
- package/core/common/interval-based-reload.abstract.d.ts.map +1 -0
- package/core/dashboard/dashboard.module.d.ts +26 -20
- package/core/dashboard/dashboard.module.d.ts.map +1 -1
- package/core/dashboard/index.d.ts +3 -0
- package/core/dashboard/index.d.ts.map +1 -1
- package/core/dashboard/widget-auto-refresh-context/auto-refresh-control.component.d.ts +31 -0
- package/core/dashboard/widget-auto-refresh-context/auto-refresh-control.component.d.ts.map +1 -0
- package/core/dashboard/widget-auto-refresh-context/auto-refresh-select-control.component.d.ts +17 -0
- package/core/dashboard/widget-auto-refresh-context/auto-refresh-select-control.component.d.ts.map +1 -0
- package/core/dashboard/widget-auto-refresh-context/global-refresh-loading.operator.d.ts +4 -0
- package/core/dashboard/widget-auto-refresh-context/global-refresh-loading.operator.d.ts.map +1 -0
- package/core/dashboard/widget-auto-refresh-context/index.d.ts +8 -0
- package/core/dashboard/widget-auto-refresh-context/index.d.ts.map +1 -0
- package/core/dashboard/widget-auto-refresh-context/widget-auto-refresh-context-icon-bar.component.d.ts +7 -0
- package/core/dashboard/widget-auto-refresh-context/widget-auto-refresh-context-icon-bar.component.d.ts.map +1 -0
- package/core/dashboard/widget-auto-refresh-context/widget-auto-refresh-context.component.d.ts +29 -0
- package/core/dashboard/widget-auto-refresh-context/widget-auto-refresh-context.component.d.ts.map +1 -0
- package/core/dashboard/widget-auto-refresh-context/widget-auto-refresh-context.model.d.ts +15 -0
- package/core/dashboard/widget-auto-refresh-context/widget-auto-refresh-context.model.d.ts.map +1 -0
- package/core/dashboard/widget-auto-refresh-context/widget-global-auto-refresh.service.d.ts +169 -0
- package/core/dashboard/widget-auto-refresh-context/widget-global-auto-refresh.service.d.ts.map +1 -0
- package/core/dashboard/widget-change-event.model.d.ts +1 -1
- package/core/dashboard/widget-change-event.model.d.ts.map +1 -1
- package/core/dashboard/widgets-dashboard.component.d.ts +6 -3
- package/core/dashboard/widgets-dashboard.component.d.ts.map +1 -1
- package/core/dashboard/wiget-time-context/aggregation-picker/aggregation-picker.component.d.ts +2 -2
- package/core/dashboard/wiget-time-context/aggregation-picker/aggregation-picker.component.d.ts.map +1 -1
- package/core/dashboard/wiget-time-context/widget-time-context-helper.service.d.ts +2 -1
- package/core/dashboard/wiget-time-context/widget-time-context-helper.service.d.ts.map +1 -1
- package/core/dashboard/wiget-time-context/widget-time-context-icon-bar/widget-time-context-icon-bar.component.d.ts +1 -1
- package/core/dashboard/wiget-time-context/widget-time-context-icon-bar/widget-time-context-icon-bar.component.d.ts.map +1 -1
- package/core/dashboard/wiget-time-context/widget-time-context-query.service.d.ts.map +1 -1
- package/core/dashboard/wiget-time-context/widget-time-context.component.d.ts +12 -8
- package/core/dashboard/wiget-time-context/widget-time-context.component.d.ts.map +1 -1
- package/core/dashboard/wiget-time-context/widget-time-context.model.d.ts +7 -26
- package/core/dashboard/wiget-time-context/widget-time-context.model.d.ts.map +1 -1
- package/core/date-time-picker/date-time-picker.component.d.ts.map +1 -1
- package/core/header/header.module.d.ts +18 -18
- package/core/header/header.module.d.ts.map +1 -1
- package/core/header/title/title.component.d.ts +1 -1
- package/core/header/title/title.component.d.ts.map +1 -1
- package/core/i18n/cached-locale-dictionary.service.d.ts +5 -1
- package/core/i18n/cached-locale-dictionary.service.d.ts.map +1 -1
- package/core/i18n/i18n.module.d.ts +0 -4
- package/core/i18n/i18n.module.d.ts.map +1 -1
- package/core/i18n/index.d.ts +2 -1
- package/core/i18n/index.d.ts.map +1 -1
- package/core/i18n/translation-loader.service.d.ts +50 -0
- package/core/i18n/translation-loader.service.d.ts.map +1 -0
- package/core/i18n/translation-utils.d.ts +14 -0
- package/core/i18n/translation-utils.d.ts.map +1 -0
- package/core/plugins/plugins-resolve.service.d.ts +7 -10
- package/core/plugins/plugins-resolve.service.d.ts.map +1 -1
- package/datapoint-selector/datapoint-attributes-form/datapoint-attributes-form-validation.service.d.ts +2 -1
- package/datapoint-selector/datapoint-attributes-form/datapoint-attributes-form-validation.service.d.ts.map +1 -1
- package/datapoint-selector/datapoint-attributes-form/datapoint-attributes-form.component.d.ts +11 -2
- package/datapoint-selector/datapoint-attributes-form/datapoint-attributes-form.component.d.ts.map +1 -1
- package/datapoint-selector/datapoint-selection.model.d.ts +6 -0
- package/datapoint-selector/datapoint-selection.model.d.ts.map +1 -1
- package/datapoints-export-selector/c8y-ngx-components-datapoints-export-selector.d.ts.map +1 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/data-fetching.service.d.ts +117 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/data-fetching.service.d.ts.map +1 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/data-processing.service.d.ts +90 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/data-processing.service.d.ts.map +1 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-export-selector-file-exporter.component.d.ts +160 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-export-selector-file-exporter.component.d.ts.map +1 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-export-selector-file-exporter.service.d.ts +89 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-export-selector-file-exporter.service.d.ts.map +1 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-export-selector-preview/datapoints-export-selector-preview.component.d.ts +11 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-export-selector-preview/datapoints-export-selector-preview.component.d.ts.map +1 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-exports-selector-data-scope/datapoints-exports-selector-data-scope.component.d.ts +27 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-exports-selector-data-scope/datapoints-exports-selector-data-scope.component.d.ts.map +1 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-exports-selector-file-types/datapoints-exports-selector-file-types.component.d.ts +12 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-exports-selector-file-types/datapoints-exports-selector-file-types.component.d.ts.map +1 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-exports-selector-time-range/datapoints-exports-selector-time-range.component.d.ts +22 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-exports-selector-time-range/datapoints-exports-selector-time-range.component.d.ts.map +1 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/generators/csv-generator.d.ts +3 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/generators/csv-generator.d.ts.map +1 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/generators/excel-generator.d.ts +9 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/generators/excel-generator.d.ts.map +1 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/utils.service.d.ts +56 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/utils.service.d.ts.map +1 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-modal.component.d.ts +21 -0
- package/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-modal.component.d.ts.map +1 -0
- package/datapoints-export-selector/datapoints-export-selector.component.d.ts +14 -0
- package/datapoints-export-selector/datapoints-export-selector.component.d.ts.map +1 -0
- package/datapoints-export-selector/datapoints-export-selector.model.d.ts +232 -0
- package/datapoints-export-selector/datapoints-export-selector.model.d.ts.map +1 -0
- package/datapoints-export-selector/index.d.ts +15 -0
- package/datapoints-export-selector/index.d.ts.map +1 -0
- package/device-list/device-list.module.d.ts +4 -2
- package/device-list/device-list.module.d.ts.map +1 -1
- package/esm2022/alarm-event-selector/alarm-event-selection-list/alarm-event-selection-list.component.mjs +4 -4
- package/esm2022/alarm-event-selector/alarm-event-selector-list-item/alarm-event-selector-list-item.component.mjs +3 -3
- package/esm2022/alarms/alarms-date-filter.component.mjs +139 -0
- package/esm2022/alarms/alarms-filter.component.mjs +11 -5
- package/esm2022/alarms/alarms-list.component.mjs +5 -16
- package/esm2022/alarms/alarms-type-filter.component.mjs +3 -3
- package/esm2022/alarms/alarms-view.service.mjs +16 -2
- package/esm2022/alarms/alarms.component.mjs +12 -6
- package/esm2022/alarms/alarms.model.mjs +17 -1
- package/esm2022/alarms/alarms.module.mjs +11 -4
- package/esm2022/branding/shared/data/store-branding.service.mjs +24 -1
- package/esm2022/branding/shared/lazy/branding/branding.component.mjs +3 -1
- package/esm2022/context-dashboard/context-dashboard.component.mjs +4 -4
- package/esm2022/context-dashboard/context-dashboard.model.mjs +1 -1
- package/esm2022/context-dashboard/context-dashboard.service.mjs +47 -21
- package/esm2022/context-dashboard/dashboard-detail.component.mjs +8 -16
- package/esm2022/context-dashboard/dashboard-detail.service.mjs +13 -6
- package/esm2022/context-dashboard/dashboard-settings/dashboard-general-settings.component.mjs +3 -3
- package/esm2022/context-dashboard/dashboard-settings/typed-dashboard-settings.component.mjs +32 -11
- package/esm2022/core/action-bar/action-bar-item.component.mjs +3 -3
- package/esm2022/core/action-bar/action-bar.component.mjs +3 -3
- package/esm2022/core/action-bar/action-bar.module.mjs +16 -5
- package/esm2022/core/bootstrap/bootstrap.component.mjs +21 -16
- package/esm2022/core/common/aggregation/aggregation.model.mjs +46 -0
- package/esm2022/core/common/aggregation/aggregation.service.mjs +34 -0
- package/esm2022/core/common/humanize-app-name.model.mjs +2 -2
- package/esm2022/core/common/humanize-app-name.pipe.mjs +2 -2
- package/esm2022/core/common/index.mjs +5 -1
- package/esm2022/core/common/inter-app.service.mjs +76 -0
- package/esm2022/core/common/interval-based-reload.abstract.mjs +110 -0
- package/esm2022/core/countdown-interval/countdown-interval.component.mjs +3 -3
- package/esm2022/core/dashboard/dashboard-child.component.mjs +6 -5
- package/esm2022/core/dashboard/dashboard.module.mjs +22 -4
- package/esm2022/core/dashboard/index.mjs +4 -1
- package/esm2022/core/dashboard/widget-auto-refresh-context/auto-refresh-control.component.mjs +101 -0
- package/esm2022/core/dashboard/widget-auto-refresh-context/auto-refresh-select-control.component.mjs +50 -0
- package/esm2022/core/dashboard/widget-auto-refresh-context/global-refresh-loading.operator.mjs +7 -0
- package/esm2022/core/dashboard/widget-auto-refresh-context/index.mjs +8 -0
- package/esm2022/core/dashboard/widget-auto-refresh-context/widget-auto-refresh-context-icon-bar.component.mjs +18 -0
- package/esm2022/core/dashboard/widget-auto-refresh-context/widget-auto-refresh-context.component.mjs +127 -0
- package/esm2022/core/dashboard/widget-auto-refresh-context/widget-auto-refresh-context.model.mjs +5 -0
- package/esm2022/core/dashboard/widget-auto-refresh-context/widget-global-auto-refresh.service.mjs +233 -0
- package/esm2022/core/dashboard/widget-change-event.model.mjs +1 -1
- package/esm2022/core/dashboard/widgets-dashboard.component.mjs +35 -30
- package/esm2022/core/dashboard/wiget-time-context/aggregation-picker/aggregation-picker.component.mjs +4 -4
- package/esm2022/core/dashboard/wiget-time-context/realtime-control/realtime-control.component.mjs +2 -2
- package/esm2022/core/dashboard/wiget-time-context/widget-time-context-helper.service.mjs +3 -2
- package/esm2022/core/dashboard/wiget-time-context/widget-time-context-icon-bar/widget-time-context-icon-bar.component.mjs +5 -5
- package/esm2022/core/dashboard/wiget-time-context/widget-time-context-query.service.mjs +7 -4
- package/esm2022/core/dashboard/wiget-time-context/widget-time-context.component.mjs +37 -36
- package/esm2022/core/dashboard/wiget-time-context/widget-time-context.model.mjs +1 -72
- package/esm2022/core/date-time-picker/date-time-picker.component.mjs +9 -3
- package/esm2022/core/header/header.module.mjs +7 -5
- package/esm2022/core/header/title/title.component.mjs +3 -3
- package/esm2022/core/i18n/cached-locale-dictionary.service.mjs +1 -1
- package/esm2022/core/i18n/i18n.module.mjs +7 -14
- package/esm2022/core/i18n/index.mjs +3 -2
- package/esm2022/core/i18n/translate.parser.mjs +2 -2
- package/esm2022/core/i18n/translation-loader.service.mjs +132 -0
- package/esm2022/core/i18n/translation-utils.mjs +24 -0
- package/esm2022/core/list-display-switch/list-display-switch.component.mjs +3 -3
- package/esm2022/core/plugins/plugins-resolve.service.mjs +12 -37
- package/esm2022/datapoint-selector/datapoint-attributes-form/datapoint-attributes-form-validation.service.mjs +13 -6
- package/esm2022/datapoint-selector/datapoint-attributes-form/datapoint-attributes-form.component.mjs +22 -5
- package/esm2022/datapoint-selector/datapoint-selection-list/datapoint-selection-list.component.mjs +3 -3
- package/esm2022/datapoint-selector/datapoint-selection.model.mjs +1 -1
- package/esm2022/datapoint-selector/datapoint-selector-list-item/datapoint-selector-list-item.component.mjs +3 -3
- package/esm2022/datapoints-export-selector/c8y-ngx-components-datapoints-export-selector.mjs +5 -0
- package/esm2022/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/data-fetching.service.mjs +407 -0
- package/esm2022/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/data-processing.service.mjs +207 -0
- package/esm2022/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-export-selector-file-exporter.component.mjs +467 -0
- package/esm2022/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-export-selector-file-exporter.service.mjs +187 -0
- package/esm2022/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-export-selector-preview/datapoints-export-selector-preview.component.mjs +27 -0
- package/esm2022/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-exports-selector-data-scope/datapoints-exports-selector-data-scope.component.mjs +41 -0
- package/esm2022/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-exports-selector-file-types/datapoints-exports-selector-file-types.component.mjs +23 -0
- package/esm2022/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/datapoints-exports-selector-time-range/datapoints-exports-selector-time-range.component.mjs +42 -0
- package/esm2022/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/generators/csv-generator.mjs +120 -0
- package/esm2022/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/generators/excel-generator.mjs +282 -0
- package/esm2022/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-file-exporter/utils.service.mjs +76 -0
- package/esm2022/datapoints-export-selector/datapoints-export-selector-modal/datapoints-export-selector-modal.component.mjs +46 -0
- package/esm2022/datapoints-export-selector/datapoints-export-selector.component.mjs +42 -0
- package/esm2022/datapoints-export-selector/datapoints-export-selector.model.mjs +43 -0
- package/esm2022/datapoints-export-selector/index.mjs +15 -0
- package/esm2022/device-list/add-smart-group.component.mjs +4 -3
- package/esm2022/device-list/device-list.module.mjs +22 -4
- package/esm2022/interval-picker/c8y-ngx-components-interval-picker.mjs +5 -0
- package/esm2022/interval-picker/index.mjs +3 -0
- package/esm2022/interval-picker/interval-picker.component.mjs +68 -0
- package/esm2022/interval-picker/interval-picker.model.mjs +47 -0
- package/esm2022/map/cluster-map.component.mjs +41 -13
- package/esm2022/map/map-status.component.mjs +6 -5
- package/esm2022/map/map.component.mjs +9 -5
- package/esm2022/map/map.model.mjs +1 -1
- package/esm2022/translation-editor/c8y-ngx-components-translation-editor.mjs +5 -0
- package/esm2022/translation-editor/data/c8y-ngx-components-translation-editor-data.mjs +5 -0
- package/esm2022/translation-editor/data/index.mjs +2 -0
- package/esm2022/translation-editor/data/translation-store.service.mjs +175 -0
- package/esm2022/translation-editor/index.mjs +10 -0
- package/esm2022/translation-editor/lazy/add-translation-modal/add-translation-modal.component.mjs +70 -0
- package/esm2022/translation-editor/lazy/c8y-ngx-components-translation-editor-lazy.mjs +5 -0
- package/esm2022/translation-editor/lazy/index.mjs +2 -0
- package/esm2022/translation-editor/lazy/manage-translation-cell-renderer/manage-translation-cell-renderer.component.mjs +53 -0
- package/esm2022/translation-editor/lazy/translation-editor/translation-editor.component.mjs +124 -0
- package/esm2022/translation-editor/translation-editor-naviagtor-factory.service.mjs +31 -0
- package/esm2022/widgets/cockpit/index.mjs +4 -2
- package/esm2022/widgets/cockpit-exports/index.mjs +8 -1
- package/esm2022/widgets/definitions/alarms/alarm-list/index.mjs +6 -2
- package/esm2022/widgets/definitions/alarms/all-critical-alarms/index.mjs +6 -2
- package/esm2022/widgets/definitions/alarms/recent-alarms/index.mjs +6 -2
- package/esm2022/widgets/definitions/datapoints-table/c8y-ngx-components-widgets-definitions-datapoints-table.mjs +5 -0
- package/esm2022/widgets/definitions/datapoints-table/index.mjs +34 -0
- package/esm2022/widgets/definitions/index.mjs +2 -1
- package/esm2022/widgets/definitions/map/index.mjs +4 -2
- package/esm2022/widgets/implementations/alarms/alarm-list-widget-config/alarm-list-widget-config.component.mjs +29 -15
- package/esm2022/widgets/implementations/alarms/alarm-list-widget-view/alarm-list-widget.component.mjs +57 -25
- package/esm2022/widgets/implementations/alarms/alarm-list-widget.model.mjs +2 -1
- package/esm2022/widgets/implementations/alarms/alarm-widget-alarms-reload.component.mjs +19 -21
- package/esm2022/widgets/implementations/datapoints-table/c8y-ngx-components-widgets-implementations-datapoints-table.mjs +5 -0
- package/esm2022/widgets/implementations/datapoints-table/datapoints-table-config/datapoints-table-config.component.mjs +383 -0
- package/esm2022/widgets/implementations/datapoints-table/datapoints-table-config/datapoints-table-config.service.mjs +124 -0
- package/esm2022/widgets/implementations/datapoints-table/datapoints-table-view/adjust-aggregated-time-range.pipe.mjs +191 -0
- package/esm2022/widgets/implementations/datapoints-table/datapoints-table-view/apply-range-class.pipe.mjs +36 -0
- package/esm2022/widgets/implementations/datapoints-table/datapoints-table-view/column-title.pipe.mjs +45 -0
- package/esm2022/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-reload/datapoints-reload.component.mjs +116 -0
- package/esm2022/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-table/datapoints-table.component.mjs +116 -0
- package/esm2022/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-table/dynamic-column.directive.mjs +43 -0
- package/esm2022/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-table-view.component.mjs +282 -0
- package/esm2022/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-table-view.service.mjs +430 -0
- package/esm2022/widgets/implementations/datapoints-table/datapoints-table-view/virtual-scroll-listener.directive.mjs +75 -0
- package/esm2022/widgets/implementations/datapoints-table/datapoints-table-widget.model.mjs +76 -0
- package/esm2022/widgets/implementations/datapoints-table/datapoints-table.service.mjs +52 -0
- package/esm2022/widgets/implementations/datapoints-table/date-range-picker.component.mjs +68 -0
- package/esm2022/widgets/implementations/datapoints-table/index.mjs +10 -0
- package/esm2022/widgets/implementations/map/map-widget-config.component.mjs +19 -11
- package/esm2022/widgets/implementations/map/map-widget.component.mjs +20 -2
- package/esm2022/widgets/implementations/map/map-widget.model.mjs +1 -1
- package/fesm2022/c8y-ngx-components-alarm-event-selector.mjs +5 -5
- package/fesm2022/c8y-ngx-components-alarm-event-selector.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-alarms.mjs +189 -31
- package/fesm2022/c8y-ngx-components-alarms.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-branding-shared-data.mjs +23 -0
- package/fesm2022/c8y-ngx-components-branding-shared-data.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-branding-shared-lazy.mjs +2 -0
- package/fesm2022/c8y-ngx-components-branding-shared-lazy.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-context-dashboard.mjs +135 -91
- package/fesm2022/c8y-ngx-components-context-dashboard.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-datapoint-selector.mjs +37 -13
- package/fesm2022/c8y-ngx-components-datapoint-selector.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-datapoints-export-selector.mjs +1928 -0
- package/fesm2022/c8y-ngx-components-datapoints-export-selector.mjs.map +1 -0
- package/fesm2022/c8y-ngx-components-device-list.mjs +24 -5
- package/fesm2022/c8y-ngx-components-device-list.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-interval-picker.mjs +120 -0
- package/fesm2022/c8y-ngx-components-interval-picker.mjs.map +1 -0
- package/fesm2022/c8y-ngx-components-map.mjs +52 -19
- package/fesm2022/c8y-ngx-components-map.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-translation-editor-data.mjs +181 -0
- package/fesm2022/c8y-ngx-components-translation-editor-data.mjs.map +1 -0
- package/fesm2022/c8y-ngx-components-translation-editor-lazy.mjs +236 -0
- package/fesm2022/c8y-ngx-components-translation-editor-lazy.mjs.map +1 -0
- package/fesm2022/c8y-ngx-components-translation-editor.mjs +46 -0
- package/fesm2022/c8y-ngx-components-translation-editor.mjs.map +1 -0
- package/fesm2022/c8y-ngx-components-widgets-cockpit-exports.mjs +7 -0
- package/fesm2022/c8y-ngx-components-widgets-cockpit-exports.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-cockpit.mjs +3 -1
- package/fesm2022/c8y-ngx-components-widgets-cockpit.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-alarms-alarm-list.mjs +5 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-alarms-alarm-list.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-alarms-all-critical-alarms.mjs +5 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-alarms-all-critical-alarms.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-alarms-recent-alarms.mjs +5 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-alarms-recent-alarms.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-datapoints-table.mjs +41 -0
- package/fesm2022/c8y-ngx-components-widgets-definitions-datapoints-table.mjs.map +1 -0
- package/fesm2022/c8y-ngx-components-widgets-definitions-map.mjs +3 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions-map.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-definitions.mjs +1 -0
- package/fesm2022/c8y-ngx-components-widgets-definitions.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-alarms.mjs +101 -58
- package/fesm2022/c8y-ngx-components-widgets-implementations-alarms.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-table.mjs +1967 -0
- package/fesm2022/c8y-ngx-components-widgets-implementations-datapoints-table.mjs.map +1 -0
- package/fesm2022/c8y-ngx-components-widgets-implementations-map.mjs +37 -11
- package/fesm2022/c8y-ngx-components-widgets-implementations-map.mjs.map +1 -1
- package/fesm2022/c8y-ngx-components.mjs +5093 -4369
- package/fesm2022/c8y-ngx-components.mjs.map +1 -1
- package/interval-picker/c8y-ngx-components-interval-picker.d.ts.map +1 -0
- package/interval-picker/index.d.ts +3 -0
- package/interval-picker/index.d.ts.map +1 -0
- package/{core/dashboard/wiget-time-context/interval-picker → interval-picker}/interval-picker.component.d.ts +10 -2
- package/interval-picker/interval-picker.component.d.ts.map +1 -0
- package/interval-picker/interval-picker.model.d.ts +15 -0
- package/interval-picker/interval-picker.model.d.ts.map +1 -0
- package/locales/de.po +289 -18
- package/locales/en.po +12 -0
- package/locales/en_US.po +3 -0
- package/locales/es.po +253 -18
- package/locales/fr.po +253 -18
- package/locales/ja_JP.po +282 -18
- package/locales/locales.pot +266 -21
- package/locales/nl.po +253 -18
- package/locales/pl.po +253 -18
- package/locales/pt_BR.po +253 -18
- package/map/cluster-map.component.d.ts +8 -3
- package/map/cluster-map.component.d.ts.map +1 -1
- package/map/map-status.component.d.ts +0 -3
- package/map/map-status.component.d.ts.map +1 -1
- package/map/map.component.d.ts +4 -2
- package/map/map.component.d.ts.map +1 -1
- package/map/map.model.d.ts +2 -1
- package/map/map.model.d.ts.map +1 -1
- package/package.json +1 -1
- package/translation-editor/c8y-ngx-components-translation-editor.d.ts.map +1 -0
- package/translation-editor/data/c8y-ngx-components-translation-editor-data.d.ts.map +1 -0
- package/translation-editor/data/index.d.ts +2 -0
- package/translation-editor/data/index.d.ts.map +1 -0
- package/translation-editor/data/translation-store.service.d.ts +61 -0
- package/translation-editor/data/translation-store.service.d.ts.map +1 -0
- package/translation-editor/index.d.ts +2 -0
- package/translation-editor/index.d.ts.map +1 -0
- package/translation-editor/lazy/add-translation-modal/add-translation-modal.component.d.ts +30 -0
- package/translation-editor/lazy/add-translation-modal/add-translation-modal.component.d.ts.map +1 -0
- package/translation-editor/lazy/c8y-ngx-components-translation-editor-lazy.d.ts.map +1 -0
- package/translation-editor/lazy/index.d.ts +2 -0
- package/translation-editor/lazy/index.d.ts.map +1 -0
- package/translation-editor/lazy/manage-translation-cell-renderer/manage-translation-cell-renderer.component.d.ts +22 -0
- package/translation-editor/lazy/manage-translation-cell-renderer/manage-translation-cell-renderer.component.d.ts.map +1 -0
- package/translation-editor/lazy/translation-editor/translation-editor.component.d.ts +34 -0
- package/translation-editor/lazy/translation-editor/translation-editor.component.d.ts.map +1 -0
- package/translation-editor/translation-editor-naviagtor-factory.service.d.ts +11 -0
- package/translation-editor/translation-editor-naviagtor-factory.service.d.ts.map +1 -0
- package/widgets/cockpit/index.d.ts +13 -0
- package/widgets/cockpit/index.d.ts.map +1 -1
- package/widgets/cockpit-exports/index.d.ts +6 -0
- package/widgets/cockpit-exports/index.d.ts.map +1 -1
- package/widgets/definitions/alarms/alarm-list/index.d.ts +2 -0
- package/widgets/definitions/alarms/alarm-list/index.d.ts.map +1 -1
- package/widgets/definitions/alarms/all-critical-alarms/index.d.ts +2 -0
- package/widgets/definitions/alarms/all-critical-alarms/index.d.ts.map +1 -1
- package/widgets/definitions/alarms/recent-alarms/index.d.ts +2 -0
- package/widgets/definitions/alarms/recent-alarms/index.d.ts.map +1 -1
- package/widgets/definitions/datapoints-table/c8y-ngx-components-widgets-definitions-datapoints-table.d.ts.map +1 -0
- package/widgets/definitions/datapoints-table/index.d.ts +14 -0
- package/widgets/definitions/datapoints-table/index.d.ts.map +1 -0
- package/widgets/definitions/index.d.ts +1 -0
- package/widgets/definitions/index.d.ts.map +1 -1
- package/widgets/definitions/map/index.d.ts +2 -1
- package/widgets/definitions/map/index.d.ts.map +1 -1
- package/widgets/implementations/alarms/alarm-list-widget-config/alarm-list-widget-config.component.d.ts +5 -9
- package/widgets/implementations/alarms/alarm-list-widget-config/alarm-list-widget-config.component.d.ts.map +1 -1
- package/widgets/implementations/alarms/alarm-list-widget-view/alarm-list-widget.component.d.ts +17 -10
- package/widgets/implementations/alarms/alarm-list-widget-view/alarm-list-widget.component.d.ts.map +1 -1
- package/widgets/implementations/alarms/alarm-list-widget.model.d.ts +5 -1
- package/widgets/implementations/alarms/alarm-list-widget.model.d.ts.map +1 -1
- package/widgets/implementations/alarms/alarm-widget-alarms-reload.component.d.ts +5 -11
- package/widgets/implementations/alarms/alarm-widget-alarms-reload.component.d.ts.map +1 -1
- package/widgets/implementations/datapoints-table/c8y-ngx-components-widgets-implementations-datapoints-table.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/datapoints-table-config/datapoints-table-config.component.d.ts +129 -0
- package/widgets/implementations/datapoints-table/datapoints-table-config/datapoints-table-config.component.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/datapoints-table-config/datapoints-table-config.service.d.ts +56 -0
- package/widgets/implementations/datapoints-table/datapoints-table-config/datapoints-table-config.service.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/adjust-aggregated-time-range.pipe.d.ts +88 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/adjust-aggregated-time-range.pipe.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/apply-range-class.pipe.d.ts +19 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/apply-range-class.pipe.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/column-title.pipe.d.ts +26 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/column-title.pipe.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-reload/datapoints-reload.component.d.ts +63 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-reload/datapoints-reload.component.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-table/datapoints-table.component.d.ts +38 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-table/datapoints-table.component.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-table/dynamic-column.directive.d.ts +13 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-table/dynamic-column.directive.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-table-view.component.d.ts +123 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-table-view.component.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-table-view.service.d.ts +196 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/datapoints-table-view.service.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/virtual-scroll-listener.directive.d.ts +41 -0
- package/widgets/implementations/datapoints-table/datapoints-table-view/virtual-scroll-listener.directive.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/datapoints-table-widget.model.d.ts +175 -0
- package/widgets/implementations/datapoints-table/datapoints-table-widget.model.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/datapoints-table.service.d.ts +12 -0
- package/widgets/implementations/datapoints-table/datapoints-table.service.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/date-range-picker.component.d.ts +34 -0
- package/widgets/implementations/datapoints-table/date-range-picker.component.d.ts.map +1 -0
- package/widgets/implementations/datapoints-table/index.d.ts +10 -0
- package/widgets/implementations/datapoints-table/index.d.ts.map +1 -0
- package/widgets/implementations/map/map-widget-config.component.d.ts +4 -4
- package/widgets/implementations/map/map-widget-config.component.d.ts.map +1 -1
- package/widgets/implementations/map/map-widget.component.d.ts +8 -3
- package/widgets/implementations/map/map-widget.component.d.ts.map +1 -1
- package/widgets/implementations/map/map-widget.model.d.ts +4 -3
- package/widgets/implementations/map/map-widget.model.d.ts.map +1 -1
- package/core/dashboard/wiget-time-context/interval-picker/interval-picker.component.d.ts.map +0 -1
- package/core/i18n/translate.loader.d.ts +0 -31
- package/core/i18n/translate.loader.d.ts.map +0 -1
- package/esm2022/core/dashboard/wiget-time-context/interval-picker/interval-picker.component.mjs +0 -55
- package/esm2022/core/i18n/translate.loader.mjs +0 -71
|
@@ -0,0 +1,1928 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Injectable, Component, Input, EventEmitter, Output, InjectionToken, Inject, ViewChild, HostListener } from '@angular/core';
|
|
3
|
+
import * as i3 from '@angular/platform-browser';
|
|
4
|
+
import * as i2 from '@c8y/client';
|
|
5
|
+
import * as i1 from '@c8y/ngx-components';
|
|
6
|
+
import { gettext, AGGREGATION_VALUES, CoreModule, AGGREGATION_LABELS, AGGREGATION_VALUES_ARR, CommonModule, ModalModule } from '@c8y/ngx-components';
|
|
7
|
+
import * as i4 from '@ngx-translate/core';
|
|
8
|
+
import JSZip from 'jszip';
|
|
9
|
+
import * as i2$1 from '@angular/common';
|
|
10
|
+
import { KeyValuePipe } from '@angular/common';
|
|
11
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
12
|
+
import * as i3$2 from '@angular/forms';
|
|
13
|
+
import { FormsModule, FormControl, ReactiveFormsModule } from '@angular/forms';
|
|
14
|
+
import saveAs from 'file-saver';
|
|
15
|
+
import { Subject, debounceTime, merge, takeUntil } from 'rxjs';
|
|
16
|
+
import { TimeSpanInMs } from '@c8y/ngx-components/interval-picker';
|
|
17
|
+
import { A11yModule } from '@angular/cdk/a11y';
|
|
18
|
+
import * as i3$1 from 'ngx-bootstrap/popover';
|
|
19
|
+
import { PopoverModule } from 'ngx-bootstrap/popover';
|
|
20
|
+
import { isNil, isEmpty, isNumber } from 'lodash-es';
|
|
21
|
+
import * as i1$1 from 'ngx-bootstrap/modal';
|
|
22
|
+
import * as i3$3 from 'ngx-bootstrap/tooltip';
|
|
23
|
+
import { TooltipModule } from 'ngx-bootstrap/tooltip';
|
|
24
|
+
|
|
25
|
+
const HAS_ERROR = 'has-error';
|
|
26
|
+
const MEASUREMENTS_PREVIEW_ITEMS_LIMIT = 5;
|
|
27
|
+
const SERIES_DATA_MERGED_FILE_NAME = 'seriesDataMergedFileName';
|
|
28
|
+
const EXPORT_MODE_LABELS = {
|
|
29
|
+
FULL: gettext('Full`export type`'),
|
|
30
|
+
COMPACT: gettext('Compact`export type`')
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Each export type is based on a different API:
|
|
34
|
+
* - COMPACT - series
|
|
35
|
+
* - FULL - measurements
|
|
36
|
+
* All differences between export modes:
|
|
37
|
+
* Compact:
|
|
38
|
+
* Processes up to 5,000 records per data point, or up to the data retention limit (API limit)
|
|
39
|
+
* Creates a single merged file containing all the data
|
|
40
|
+
* Provides minimum and maximum values (API feature)
|
|
41
|
+
* Preview is not available
|
|
42
|
+
* Supports optional data aggregation (API feature)
|
|
43
|
+
* Full:
|
|
44
|
+
* Processes up to 1,000,000 records per data point, or up to the data retention limit (API limit)
|
|
45
|
+
* For exports exceeding 50,000 records, data will be sent via email
|
|
46
|
+
* Creates a compressed ZIP file containing separate data files for each selected data point
|
|
47
|
+
* Preview is available
|
|
48
|
+
* Does not support data aggregation
|
|
49
|
+
*/
|
|
50
|
+
const EXPORT_MODE_VALUES = {
|
|
51
|
+
full: 'FULL',
|
|
52
|
+
compact: 'COMPACT'
|
|
53
|
+
};
|
|
54
|
+
const FILE_COMPRESSION_TYPES_VALUES = {
|
|
55
|
+
store: 'STORE',
|
|
56
|
+
deflate: 'DEFLATE'
|
|
57
|
+
};
|
|
58
|
+
const TIME_RANGE_INTERVAL_UNITS_VALUES = {
|
|
59
|
+
minutes: 'minutes',
|
|
60
|
+
hours: 'hours',
|
|
61
|
+
days: 'days',
|
|
62
|
+
weeks: 'weeks',
|
|
63
|
+
months: 'months',
|
|
64
|
+
custom: 'custom'
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
class UtilsService {
|
|
68
|
+
transformDataStructure(data) {
|
|
69
|
+
const result = {};
|
|
70
|
+
data.forEach(sourceItem => {
|
|
71
|
+
const { source: sourceId, data: sourceData } = sourceItem;
|
|
72
|
+
result[sourceId] = {};
|
|
73
|
+
sourceData.series.forEach((seriesInfo, index) => {
|
|
74
|
+
const { type, name, unit } = seriesInfo;
|
|
75
|
+
/**
|
|
76
|
+
* Unique key to distinguish between different series from same source,
|
|
77
|
+
* e.g.: c8y_Acceleration.accelerationX, c8y_Acceleration.accelerationY, c8y_Acceleration.accelerationZ
|
|
78
|
+
*/
|
|
79
|
+
const seriesKey = `${type}.${name}`;
|
|
80
|
+
result[sourceId][seriesKey] = {
|
|
81
|
+
values: {},
|
|
82
|
+
seriesDetails: { name, unit, type }
|
|
83
|
+
};
|
|
84
|
+
let hasValues = false;
|
|
85
|
+
Object.entries(sourceData.values).forEach(([timestamp, measurements]) => {
|
|
86
|
+
if (!measurements) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const measurement = measurements[index];
|
|
90
|
+
if (measurement !== null && measurement !== undefined) {
|
|
91
|
+
result[sourceId][seriesKey].values[timestamp] = measurement;
|
|
92
|
+
hasValues = true;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
if (!hasValues) {
|
|
96
|
+
result[sourceId][seriesKey].values = null;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Formats a date range into a specific string format, handling UTC dates correctly.
|
|
104
|
+
*
|
|
105
|
+
* @param fromDate - The start date in ISO format (e.g., "2023-12-04T12:40:00.000Z")
|
|
106
|
+
* @param toDate - The end date in ISO format (e.g., "2023-12-06T23:50:00.000Z")
|
|
107
|
+
* @returns A string representing the date range in the format "ddmmmyyhhmm-ddmmmyyhhmm"
|
|
108
|
+
* where d=day, m=month, y=year, h=hour, m=minute
|
|
109
|
+
*
|
|
110
|
+
* Example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* const fromDate = "2023-12-04T12:40:00.000Z";
|
|
113
|
+
* const toDate = "2023-12-06T23:50:00.000Z";
|
|
114
|
+
* const formattedRange = getFormattedDateRange(fromDate, toDate);
|
|
115
|
+
* console.log(formattedRange); // Output: "04dec231240-06dec232350"
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
getFormattedDateRange(fromDate, toDate) {
|
|
119
|
+
const formatDate = (dateString) => {
|
|
120
|
+
const date = new Date(dateString);
|
|
121
|
+
if (isNaN(date.getTime())) {
|
|
122
|
+
throw new Error(`Invalid date string: ${dateString}`);
|
|
123
|
+
}
|
|
124
|
+
const day = date.getUTCDate().toString().padStart(2, '0');
|
|
125
|
+
const month = date.toLocaleString('en', { month: 'short', timeZone: 'UTC' }).toLowerCase();
|
|
126
|
+
const hours = date.getUTCHours().toString().padStart(2, '0');
|
|
127
|
+
const minutes = date.getUTCMinutes().toString().padStart(2, '0');
|
|
128
|
+
const seconds = date.getUTCSeconds().toString().padStart(2, '0');
|
|
129
|
+
return `${day}${month}${hours}${minutes}${seconds}`;
|
|
130
|
+
};
|
|
131
|
+
return `${formatDate(fromDate)}-${formatDate(toDate)}`;
|
|
132
|
+
}
|
|
133
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: UtilsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
134
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: UtilsService, providedIn: 'root' }); }
|
|
135
|
+
}
|
|
136
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: UtilsService, decorators: [{
|
|
137
|
+
type: Injectable,
|
|
138
|
+
args: [{ providedIn: 'root' }]
|
|
139
|
+
}] });
|
|
140
|
+
|
|
141
|
+
class DataFetchingService {
|
|
142
|
+
constructor(alertService, measurementService, sanitizer, translateService, utilsService) {
|
|
143
|
+
this.alertService = alertService;
|
|
144
|
+
this.measurementService = measurementService;
|
|
145
|
+
this.sanitizer = sanitizer;
|
|
146
|
+
this.translateService = translateService;
|
|
147
|
+
this.utilsService = utilsService;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Checks if any of measurements requests may exceeded the limit,
|
|
151
|
+
* after which the export data is processed by the backend and the generated CSV/Excel file is sent by email.
|
|
152
|
+
*
|
|
153
|
+
* The threshold is set to 50_000 records in application's core properties (export.data.synchronous.limit).
|
|
154
|
+
*
|
|
155
|
+
* @param exportConfig - The export configuration.
|
|
156
|
+
* @returns A promise that returns an array of objects representing datapoints files that will be sent by email.
|
|
157
|
+
*/
|
|
158
|
+
async getDatapointsExceedingLimit(exportConfig) {
|
|
159
|
+
const backendProcessingLimit = 50_000;
|
|
160
|
+
const promises = exportConfig.datapointDetails.map(async (details) => {
|
|
161
|
+
try {
|
|
162
|
+
const filter = {
|
|
163
|
+
dateFrom: exportConfig.dateFrom,
|
|
164
|
+
dateTo: exportConfig.dateTo,
|
|
165
|
+
pageSize: 1,
|
|
166
|
+
source: details.source,
|
|
167
|
+
valueFragmentSeries: details.valueFragmentSeries,
|
|
168
|
+
valueFragmentType: details.valueFragmentType,
|
|
169
|
+
withTotalElements: true
|
|
170
|
+
};
|
|
171
|
+
const response = await this.measurementService.list(filter);
|
|
172
|
+
if (response?.paging?.totalElements > backendProcessingLimit) {
|
|
173
|
+
return {
|
|
174
|
+
datapointDetail: {
|
|
175
|
+
source: details.source,
|
|
176
|
+
valueFragmentSeries: details.valueFragmentSeries,
|
|
177
|
+
valueFragmentType: details.valueFragmentType
|
|
178
|
+
},
|
|
179
|
+
totalElements: response.paging.totalElements
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
this.alertService.addServerFailure(error);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
const results = await Promise.all(promises);
|
|
191
|
+
const datapointsExceedingLimit = results.filter(result => result !== null);
|
|
192
|
+
return datapointsExceedingLimit;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Retrieves the message to be displayed when the limit of datapoints is exceeded during file export.
|
|
196
|
+
*
|
|
197
|
+
* @param hasNoExportableData - Indicates if there is no exportable data because the date range of all selected datapoints exceeds 1,000,000 records.
|
|
198
|
+
* @param emailDeliverableCount - The number of datapoint exports that exceed the limit, will be proceeded by the backend and sent by email.
|
|
199
|
+
* @param browserDownloadableCount - The number of datapoint exports that can be downloaded directly.
|
|
200
|
+
* @param skippedDatapointCount - The number of datapoint exports skipped due to exceeding the measurements API limit of 1,000,000 records.
|
|
201
|
+
* @param totalDatapointsSelectedForExportCount - Total number of datapoint selected for exports.
|
|
202
|
+
* @returns The message that can be injected.
|
|
203
|
+
*/
|
|
204
|
+
getLimitExceededMessage(hasNoExportableData, emailDeliverableCount, browserDownloadableCount, nonRetrievableCount, totalDatapointsSelectedForExportCount) {
|
|
205
|
+
if (hasNoExportableData) {
|
|
206
|
+
const message = this.translateService.instant(gettext(`The data for selected datapoint(s) exceed 1,000,000 records, which is the limit for backend processing. To export this data, please reduce the date range.`));
|
|
207
|
+
return message;
|
|
208
|
+
}
|
|
209
|
+
const message = this.translateService.instant(gettext(`After clicking "Download":<br>
|
|
210
|
+
<ul>
|
|
211
|
+
<li><strong>{{$browserDownloadableCount}}</strong> datapoint(s) exports will be downloaded directly within one file: <em>exported_[csv/excel].zip</em></li>
|
|
212
|
+
<li><strong>{{$emailDeliverableCount}}</strong> datapoint(s) exports require further processing. The files will be sent to you via separate emails once completed, which may take some time.</li>
|
|
213
|
+
<li><strong>{{$nonRetrievableCount}}</strong> datapoint(s) exports exceeded 1,000,000 records, which is the limit for backend processing. To export these data, please reduce the date range. Otherwise, the data will neither be downloaded nor sent via email.</li>
|
|
214
|
+
</ul>
|
|
215
|
+
<p>The total number of data points that can be exported is: <strong>{{$totalExportableDatapointsCount}} out of {{$totalDatapointsSelectedForExportCount}}</strong>.
|
|
216
|
+
<p><strong>Note:</strong> The file name convention of files within zip file is: <code>[source]_[fragment_series].[csv/xls]</code></p>`), {
|
|
217
|
+
$browserDownloadableCount: browserDownloadableCount,
|
|
218
|
+
$emailDeliverableCount: emailDeliverableCount,
|
|
219
|
+
$nonRetrievableCount: nonRetrievableCount,
|
|
220
|
+
$totalDatapointsSelectedForExportCount: totalDatapointsSelectedForExportCount,
|
|
221
|
+
$totalExportableDatapointsCount: browserDownloadableCount + emailDeliverableCount
|
|
222
|
+
});
|
|
223
|
+
const trimmedMessage = this.removeZeroCountListItems(message, [
|
|
224
|
+
browserDownloadableCount,
|
|
225
|
+
emailDeliverableCount,
|
|
226
|
+
nonRetrievableCount
|
|
227
|
+
]);
|
|
228
|
+
return trimmedMessage;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Displays an information alert about sending data via email.
|
|
232
|
+
*
|
|
233
|
+
* Only measurements API can send files via email.
|
|
234
|
+
*
|
|
235
|
+
* @param fileType - The type of file to be sent.
|
|
236
|
+
* @param datapointsExceedingLimit - The array of datapoints exceeding the limit.
|
|
237
|
+
*/
|
|
238
|
+
showSendViaEmailInfoAlert(fileType, datapointsExceedingLimit) {
|
|
239
|
+
const messageTemplate = this.translateService.instant(gettext(`You will get {{$fileTypeText}} file(s) for {{$count}} series via email.`), { $count: datapointsExceedingLimit.length, $fileTypeText: fileType });
|
|
240
|
+
const formattedList = datapointsExceedingLimit
|
|
241
|
+
.map(datapoint => `• ${datapoint.datapointDetail.source}_${datapoint.datapointDetail.valueFragmentType}_${datapoint.datapointDetail.valueFragmentSeries}`)
|
|
242
|
+
.join('\n');
|
|
243
|
+
this.alertService.info(messageTemplate, formattedList);
|
|
244
|
+
}
|
|
245
|
+
async fetchMeasurementDataFilesAndPairWithSourceDetails(acceptFileType, exportConfig) {
|
|
246
|
+
const measurementFileConfig = {
|
|
247
|
+
exportConfig,
|
|
248
|
+
acceptFileType
|
|
249
|
+
};
|
|
250
|
+
const filePromises = exportConfig.datapointDetails.map(details => this.fetchAndProcessMeasurementFile(details, measurementFileConfig));
|
|
251
|
+
return Promise.all(filePromises);
|
|
252
|
+
}
|
|
253
|
+
async fetchAndProcessMeasurementFile(details, measurementFileConfig) {
|
|
254
|
+
const filter = this.prepareMeasurementsFilter(details, measurementFileConfig.exportConfig);
|
|
255
|
+
const header = { accept: measurementFileConfig.acceptFileType };
|
|
256
|
+
try {
|
|
257
|
+
const measurementFileResponse = await this.measurementService.getMeasurementsFile(filter, header);
|
|
258
|
+
// If the status is 200, then the file is in the response.
|
|
259
|
+
// If the status is 202, then the file is being processed by the backend and will be sent by email.
|
|
260
|
+
if (measurementFileResponse.status === 200) {
|
|
261
|
+
return this.mergeMeasurementsWithItsSourceDetails(details, measurementFileResponse);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
this.alertService.addServerFailure(error);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async mergeMeasurementsWithItsSourceDetails(details, measurementFile) {
|
|
269
|
+
return {
|
|
270
|
+
source: details.source,
|
|
271
|
+
valueFragmentSeries: details.valueFragmentSeries,
|
|
272
|
+
valueFragmentType: details.valueFragmentType,
|
|
273
|
+
fetchedMeasurementsBlobFile: await measurementFile.blob()
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
async fetchAndPrepareDataToExport(exportConfig, isMeasurement) {
|
|
277
|
+
return isMeasurement
|
|
278
|
+
? await this.fetchAndPrepareMeasurementDataToExportForPreview(exportConfig)
|
|
279
|
+
: await this.fetchAndPrepareSeriesDataToExport(exportConfig);
|
|
280
|
+
}
|
|
281
|
+
async getSourcesWithPermissionsToRead(datapointDetails) {
|
|
282
|
+
const dateFrom = new Date();
|
|
283
|
+
const dateTo = new Date(dateFrom);
|
|
284
|
+
const fetchedDataPromises = datapointDetails.map(async (datapoint) => {
|
|
285
|
+
const rawFilter = {
|
|
286
|
+
dateFrom,
|
|
287
|
+
dateTo,
|
|
288
|
+
source: datapoint.source
|
|
289
|
+
};
|
|
290
|
+
try {
|
|
291
|
+
const { res } = await this.fetchSeriesData(rawFilter);
|
|
292
|
+
if (res?.status === 200) {
|
|
293
|
+
return datapoint.source;
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
// If an error occurs during fetch, return null to prevent promise rejection
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
const fetchedSources = await Promise.all(fetchedDataPromises);
|
|
303
|
+
return fetchedSources.filter((source) => source !== null);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Fetches and prepares measurement data for preview.
|
|
307
|
+
*
|
|
308
|
+
* Empty DataToExport object can be returned, because unlike series data,
|
|
309
|
+
* measurement data is not further processed besides showing only in the preview.
|
|
310
|
+
* CSV/Excel files are generated by the backend for measurements.
|
|
311
|
+
*
|
|
312
|
+
* @param exportConfig - The export configuration.
|
|
313
|
+
* @returns A promise that resolves to an array of DataToExport objects or null when no data is fetched.
|
|
314
|
+
*/
|
|
315
|
+
async fetchAndPrepareMeasurementDataToExportForPreview(exportConfig) {
|
|
316
|
+
const dataToExportPromises = exportConfig.datapointDetails.map(async (details) => {
|
|
317
|
+
const fetchedMeasurementData = await this.fetchMeasurementDataForPreview(details, exportConfig);
|
|
318
|
+
if (fetchedMeasurementData?.data?.length) {
|
|
319
|
+
return this.processMeasurementDataForPreview(details, fetchedMeasurementData.data);
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
});
|
|
323
|
+
return await Promise.all(dataToExportPromises);
|
|
324
|
+
}
|
|
325
|
+
async fetchMeasurementDataForPreview(details, exportConfig) {
|
|
326
|
+
let measurements;
|
|
327
|
+
const filter = this.prepareMeasurementsFilter(details, exportConfig, MEASUREMENTS_PREVIEW_ITEMS_LIMIT);
|
|
328
|
+
try {
|
|
329
|
+
// TODO: consider adding a cache mechanism -> MTM-60289
|
|
330
|
+
// When user switches between report types, and rest of the settings are the same, then the data should not be fetched again?
|
|
331
|
+
measurements = await this.measurementService.list(filter);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
this.alertService.addServerFailure(error);
|
|
335
|
+
}
|
|
336
|
+
return measurements;
|
|
337
|
+
}
|
|
338
|
+
prepareMeasurementsFilter(details, exportConfig, pageSize) {
|
|
339
|
+
const filter = {
|
|
340
|
+
// Round the 'from' time to the previous full minute.
|
|
341
|
+
dateFrom: this.adjustDate(exportConfig.dateFrom, -1),
|
|
342
|
+
// Round the 'to' time to the nearest full minute.
|
|
343
|
+
dateTo: this.adjustDate(exportConfig.dateTo, 1),
|
|
344
|
+
source: details.source,
|
|
345
|
+
valueFragmentSeries: details.valueFragmentSeries,
|
|
346
|
+
valueFragmentType: details.valueFragmentType
|
|
347
|
+
};
|
|
348
|
+
if (pageSize) {
|
|
349
|
+
filter.pageSize = pageSize;
|
|
350
|
+
}
|
|
351
|
+
return filter;
|
|
352
|
+
}
|
|
353
|
+
processMeasurementDataForPreview(details, data) {
|
|
354
|
+
const unit = data[0][details.valueFragmentType][details.valueFragmentSeries]?.unit || '';
|
|
355
|
+
const values = {};
|
|
356
|
+
data.forEach(measurement => {
|
|
357
|
+
values[measurement.time] =
|
|
358
|
+
measurement[details.valueFragmentType][details.valueFragmentSeries].value;
|
|
359
|
+
});
|
|
360
|
+
return { ...details, unit, timeValueMap: values };
|
|
361
|
+
}
|
|
362
|
+
async fetchAndPrepareSeriesDataToExport(exportConfig) {
|
|
363
|
+
const datapointsValuesDataMap = this.groupSeriesByDeviceId(exportConfig.datapointDetails);
|
|
364
|
+
const fetchedDataPromises = Array.from(datapointsValuesDataMap).map(async ([source, series]) => {
|
|
365
|
+
const { dateFrom, dateTo, aggregation } = exportConfig;
|
|
366
|
+
const rawFilter = {
|
|
367
|
+
dateFrom,
|
|
368
|
+
dateTo,
|
|
369
|
+
source,
|
|
370
|
+
series,
|
|
371
|
+
aggregationType: aggregation
|
|
372
|
+
};
|
|
373
|
+
const { data } = await this.fetchSeriesData(rawFilter);
|
|
374
|
+
return { source, data };
|
|
375
|
+
});
|
|
376
|
+
const fetchedDataGroupedBySource = await Promise.all(fetchedDataPromises);
|
|
377
|
+
return this.processSeriesData(exportConfig.datapointDetails, fetchedDataGroupedBySource);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Returns a map of active data points device IDs with their corresponding series.
|
|
381
|
+
*
|
|
382
|
+
* Example output:
|
|
383
|
+
* ```typescript
|
|
384
|
+
* new Map([
|
|
385
|
+
* ['844657202', ['c8y_Temperature.T']],
|
|
386
|
+
* ['32666427', ['c8y_Battery.Battery']]
|
|
387
|
+
* ]);
|
|
388
|
+
* ```
|
|
389
|
+
* @param datapointDetails - An array of data points details.
|
|
390
|
+
* @returns A map where the key is the data point ID and the value is an array of data point series.
|
|
391
|
+
*/
|
|
392
|
+
groupSeriesByDeviceId(datapointDetails) {
|
|
393
|
+
return datapointDetails.reduce((map, { valueFragmentType, valueFragmentSeries, source }) => {
|
|
394
|
+
const value = `${valueFragmentType}.${valueFragmentSeries}`;
|
|
395
|
+
const existingValue = map.get(source) ?? [];
|
|
396
|
+
map.set(source, [...existingValue, value]);
|
|
397
|
+
return map;
|
|
398
|
+
}, new Map());
|
|
399
|
+
}
|
|
400
|
+
async fetchSeriesData(rawFilter) {
|
|
401
|
+
let seriesData;
|
|
402
|
+
const filter = this.prepareSeriesFilter({
|
|
403
|
+
dateFrom: rawFilter.dateFrom,
|
|
404
|
+
dateTo: rawFilter.dateTo,
|
|
405
|
+
source: rawFilter.source,
|
|
406
|
+
series: rawFilter.series,
|
|
407
|
+
aggregationType: rawFilter.aggregationType
|
|
408
|
+
});
|
|
409
|
+
try {
|
|
410
|
+
// TODO: consider adding a cache mechanism -> MTM-60289
|
|
411
|
+
// When user switches between report types, and rest of the settings are the same, then the data should not be fetched again?
|
|
412
|
+
seriesData = await this.measurementService.listSeries(filter);
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
this.alertService.addServerFailure(error);
|
|
416
|
+
}
|
|
417
|
+
return seriesData;
|
|
418
|
+
}
|
|
419
|
+
prepareSeriesFilter(filters) {
|
|
420
|
+
const { dateFrom, dateTo, source, series, aggregationType } = filters;
|
|
421
|
+
// Round the 'from' time to the previous full minute.
|
|
422
|
+
const from = this.adjustDate(dateFrom, -1);
|
|
423
|
+
// Round the 'to' time to the nearest full minute.
|
|
424
|
+
const to = this.adjustDate(dateTo, 1);
|
|
425
|
+
const filter = {
|
|
426
|
+
dateFrom: from,
|
|
427
|
+
dateTo: to,
|
|
428
|
+
source: source,
|
|
429
|
+
revert: true
|
|
430
|
+
};
|
|
431
|
+
// The 'NONE' aggregation type is not handled by a backend, so even when is selected, it is not passed as a filter parameter.
|
|
432
|
+
if (aggregationType && aggregationType !== AGGREGATION_VALUES.none) {
|
|
433
|
+
filter.aggregationType = aggregationType;
|
|
434
|
+
}
|
|
435
|
+
// TODO: it is a temporal workaround for a bug in the backend,
|
|
436
|
+
// where the series with more than one dot are impossible to retrieve:
|
|
437
|
+
// https://cumulocity.atlassian.net/browse/MTM-59277
|
|
438
|
+
if (series) {
|
|
439
|
+
const hasMoreThanOneDot = series.some(s => s.split('.').length > 2);
|
|
440
|
+
if (!hasMoreThanOneDot) {
|
|
441
|
+
filter.series = series;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return filter;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Processes the fetched series data and prepares it for export.
|
|
448
|
+
*
|
|
449
|
+
* @param datapointDetails - An array of data point details.
|
|
450
|
+
* @param fetchedDataMap - A map of fetched series data grouped by source.
|
|
451
|
+
* @returns An array of DataToExport objects.
|
|
452
|
+
*/
|
|
453
|
+
processSeriesData(datapointDetails, fetchedDataMap) {
|
|
454
|
+
const valuesGroupedBySource = this.utilsService.transformDataStructure(fetchedDataMap);
|
|
455
|
+
return datapointDetails.map((details) => {
|
|
456
|
+
let unit;
|
|
457
|
+
let data;
|
|
458
|
+
/**
|
|
459
|
+
* Unique key to distinguish between different series from same source,
|
|
460
|
+
* e.g.: c8y_Acceleration.accelerationX, c8y_Acceleration.accelerationY, c8y_Acceleration.accelerationZ
|
|
461
|
+
*/
|
|
462
|
+
const seriesKey = `${details.valueFragmentType}.${details.valueFragmentSeries}`;
|
|
463
|
+
if (valuesGroupedBySource[details.source][seriesKey]) {
|
|
464
|
+
unit = valuesGroupedBySource[details.source][seriesKey].seriesDetails.unit;
|
|
465
|
+
data = valuesGroupedBySource[details.source][seriesKey].values;
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
...details,
|
|
469
|
+
unit,
|
|
470
|
+
timeValueMap: data
|
|
471
|
+
};
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Adjusts the given date by adding the specified number of minutes and setting seconds to 0.
|
|
476
|
+
*
|
|
477
|
+
* @param date - The date to be adjusted in string format.
|
|
478
|
+
* @param minutes - The number of minutes to add to the date.
|
|
479
|
+
* @returns The adjusted date in the format 'YYYY-MM-DDTHH:mm:ss+HH:MM'.
|
|
480
|
+
*/
|
|
481
|
+
adjustDate(date, minutes) {
|
|
482
|
+
const dateTime = new Date(date);
|
|
483
|
+
dateTime.setMinutes(dateTime.getMinutes() + minutes);
|
|
484
|
+
dateTime.setSeconds(0);
|
|
485
|
+
const offset = -dateTime.getTimezoneOffset();
|
|
486
|
+
const offsetSign = offset >= 0 ? '+' : '-';
|
|
487
|
+
const pad = (num) => (num < 10 ? '0' + num : num.toString());
|
|
488
|
+
const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
|
|
489
|
+
const offsetMinutes = pad(Math.abs(offset) % 60);
|
|
490
|
+
const year = dateTime.getFullYear();
|
|
491
|
+
const month = pad(dateTime.getMonth() + 1);
|
|
492
|
+
const day = pad(dateTime.getDate());
|
|
493
|
+
const hours = pad(dateTime.getHours());
|
|
494
|
+
const minutesString = pad(dateTime.getMinutes());
|
|
495
|
+
const seconds = '00';
|
|
496
|
+
return `${year}-${month}-${day}T${hours}:${minutesString}:${seconds}${offsetSign}${offsetHours}:${offsetMinutes}`;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Trims the given HTML message by removing list items that correspond to zero counts.
|
|
500
|
+
*
|
|
501
|
+
* @param message - The HTML string containing the message with list items.
|
|
502
|
+
* @param counts - An array of number values corresponding to each list item.
|
|
503
|
+
* @param countToTrim - A count that will be trimmed with corresponding list item.
|
|
504
|
+
* @returns A trimmed HTML string with list items removed where the corresponding count is zero.
|
|
505
|
+
*
|
|
506
|
+
* Example:
|
|
507
|
+
* ```typescript
|
|
508
|
+
* const message = '<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>';
|
|
509
|
+
* const counts = [1, 0, 2];
|
|
510
|
+
* const trimmedMessage = this.removeZeroCountListItems(message, counts);
|
|
511
|
+
* // Result: '<ul><li>Item 1</li><li>Item 3</li></ul>'
|
|
512
|
+
* ```
|
|
513
|
+
*/
|
|
514
|
+
removeZeroCountListItems(message, counts, countToTrim = 0) {
|
|
515
|
+
const parser = new DOMParser();
|
|
516
|
+
const doc = parser.parseFromString(message, 'text/html');
|
|
517
|
+
const listItems = doc.querySelectorAll('ul li');
|
|
518
|
+
listItems.forEach((item, index) => {
|
|
519
|
+
if (counts[index] === countToTrim) {
|
|
520
|
+
item.remove();
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
return doc.body.innerHTML;
|
|
524
|
+
}
|
|
525
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DataFetchingService, deps: [{ token: i1.AlertService }, { token: i2.MeasurementService }, { token: i3.DomSanitizer }, { token: i4.TranslateService }, { token: UtilsService }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
526
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DataFetchingService, providedIn: 'root' }); }
|
|
527
|
+
}
|
|
528
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DataFetchingService, decorators: [{
|
|
529
|
+
type: Injectable,
|
|
530
|
+
args: [{
|
|
531
|
+
providedIn: 'root'
|
|
532
|
+
}]
|
|
533
|
+
}], ctorParameters: () => [{ type: i1.AlertService }, { type: i2.MeasurementService }, { type: i3.DomSanitizer }, { type: i4.TranslateService }, { type: UtilsService }] });
|
|
534
|
+
|
|
535
|
+
class DatapointsExportSelectorFileExporterService {
|
|
536
|
+
constructor(aggregationService, alertService, datapointsExportSelectorDataProcessingService, translateService) {
|
|
537
|
+
this.aggregationService = aggregationService;
|
|
538
|
+
this.alertService = alertService;
|
|
539
|
+
this.datapointsExportSelectorDataProcessingService = datapointsExportSelectorDataProcessingService;
|
|
540
|
+
this.translateService = translateService;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Determines the interval between two dates.
|
|
544
|
+
*
|
|
545
|
+
* @param dateFrom - The starting date in ISO 8601 string format.
|
|
546
|
+
* @param dateTo - The ending date in ISO 8601 string format.
|
|
547
|
+
* @returns The time range interval unit.
|
|
548
|
+
*/
|
|
549
|
+
determineInterval(dateFrom, dateTo) {
|
|
550
|
+
const from = new Date(dateFrom);
|
|
551
|
+
const to = new Date(dateTo);
|
|
552
|
+
const differenceInMilliseconds = to.getTime() - from.getTime();
|
|
553
|
+
const intervals = [
|
|
554
|
+
{ value: TimeSpanInMs.MINUTE, label: TIME_RANGE_INTERVAL_UNITS_VALUES.minutes }, // milliseconds in one minute
|
|
555
|
+
{ value: TimeSpanInMs.HOUR, label: TIME_RANGE_INTERVAL_UNITS_VALUES.hours }, // milliseconds in one hour
|
|
556
|
+
{ value: TimeSpanInMs.DAY, label: TIME_RANGE_INTERVAL_UNITS_VALUES.days }, // milliseconds in one day
|
|
557
|
+
{ value: TimeSpanInMs.WEEK, label: TIME_RANGE_INTERVAL_UNITS_VALUES.weeks }, // milliseconds in one week
|
|
558
|
+
{ value: TimeSpanInMs.MONTH, label: TIME_RANGE_INTERVAL_UNITS_VALUES.months } // approximation for milliseconds in one month
|
|
559
|
+
];
|
|
560
|
+
for (let i = 0; i < intervals.length; i++) {
|
|
561
|
+
if (differenceInMilliseconds <= intervals[i].value) {
|
|
562
|
+
return intervals[i].label;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return TIME_RANGE_INTERVAL_UNITS_VALUES.custom;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Updates the disabled state of aggregation options based on the current value of the time interval form control.
|
|
569
|
+
*
|
|
570
|
+
* This method:
|
|
571
|
+
* - Retrieves the current date range from the form controls.
|
|
572
|
+
* - Determines the current time interval based on the date range.
|
|
573
|
+
* - Sets the disabled state for each aggregation option based on predefined conditions.
|
|
574
|
+
*
|
|
575
|
+
* The disabled state is stored in the `disabledAggregationOptions` object,
|
|
576
|
+
* where the key is the aggregation value and the value is a boolean indicating whether the option should be disabled.
|
|
577
|
+
*
|
|
578
|
+
* The `disabledConditions` object defines the conditions under which each aggregation option should be disabled.
|
|
579
|
+
*
|
|
580
|
+
* @param dateFrom - The value of the "date from" input.
|
|
581
|
+
* @param dateTo - The value of the "date to" input.
|
|
582
|
+
* @param disabledAggregationOptions - The object containing the disabled state of aggregation options.
|
|
583
|
+
* @returns The updated object with the disabled state of aggregation options.
|
|
584
|
+
*/
|
|
585
|
+
updateDisabledStateOfAggregationOptionEntries(dateFrom, dateTo, disabledAggregationOptions) {
|
|
586
|
+
disabledAggregationOptions = this.aggregationService.getDisabledAggregationOptions(dateFrom, dateTo);
|
|
587
|
+
return disabledAggregationOptions;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Sets the aggregation control to the first available (non-disabled) option if the current option is disabled.
|
|
591
|
+
*
|
|
592
|
+
* This method:
|
|
593
|
+
* - Retrieves the current value of the aggregation control.
|
|
594
|
+
* - Checks if the current aggregation option is disabled.
|
|
595
|
+
* - If the current option is disabled, sets the control to the first available (non-disabled) option based on the following order:
|
|
596
|
+
* - If the current value is `DAILY`, it switches to `HOURLY` if it's not disabled, otherwise to `MINUTELY` if `HOURLY` is also disabled.
|
|
597
|
+
* - If the current value is `HOURLY`, it switches to `MINUTELY` if it's not disabled.
|
|
598
|
+
* - If all options are disabled, it sets the value to `NONE`.
|
|
599
|
+
*
|
|
600
|
+
* The disabled state is stored in the `disabledAggregationOptions` object,
|
|
601
|
+
* where the key is the aggregation value and the value is a boolean indicating whether the option is disabled.
|
|
602
|
+
*
|
|
603
|
+
* The `AGGREGATION_VALUES` object defines the possible aggregation values.
|
|
604
|
+
*
|
|
605
|
+
* @param aggregationValue - The value of the current aggregation option.
|
|
606
|
+
* @param disabledAggregationOptions - An object containing disabled aggregation options.
|
|
607
|
+
* @returns The new aggregation option to be set.
|
|
608
|
+
*/
|
|
609
|
+
setToFirstAvailableAggregationOptionIfCurrentIsDisabled(aggregationValue, disabledAggregationOptions) {
|
|
610
|
+
const currentValue = aggregationValue;
|
|
611
|
+
const disabledOptions = disabledAggregationOptions;
|
|
612
|
+
if (disabledOptions[currentValue]) {
|
|
613
|
+
const { daily, hourly, minutely, none } = AGGREGATION_VALUES;
|
|
614
|
+
let newAggregationValue = none;
|
|
615
|
+
if (currentValue === daily) {
|
|
616
|
+
if (!disabledOptions[hourly]) {
|
|
617
|
+
newAggregationValue = hourly;
|
|
618
|
+
}
|
|
619
|
+
else if (!disabledOptions[minutely]) {
|
|
620
|
+
newAggregationValue = minutely;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
else if (currentValue === hourly && !disabledOptions[minutely]) {
|
|
624
|
+
newAggregationValue = minutely;
|
|
625
|
+
}
|
|
626
|
+
return newAggregationValue;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
async getMeasurementExportedDataBlob(extension, dataToExport) {
|
|
630
|
+
try {
|
|
631
|
+
return await this.getMeasurementDataZipBlob(extension, dataToExport);
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
this.showZipCreationErrorAlert();
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async getSeriesExportedDataBlob(fileType, dataToExport, mergedExportDetails) {
|
|
639
|
+
try {
|
|
640
|
+
return await this.getSeriesDataBlob(fileType, dataToExport, mergedExportDetails);
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
this.showZipCreationErrorAlert();
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
cleanupCachedData() {
|
|
648
|
+
this.cachedRawExportSeriesData = null;
|
|
649
|
+
this.cachedFlatteredAndSortedSeriesExportData = null;
|
|
650
|
+
}
|
|
651
|
+
showZipCreationErrorAlert() {
|
|
652
|
+
const alertMessage = this.translateService.instant(gettext('Could not create zip file.'));
|
|
653
|
+
this.alertService.danger(alertMessage);
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
getMeasurementDataZipBlob(extension, dataToExportWithBackendCreatedFile) {
|
|
657
|
+
const files = [];
|
|
658
|
+
this.createRawMeasurementExportedFiles(dataToExportWithBackendCreatedFile, extension, files);
|
|
659
|
+
return this.datapointsExportSelectorDataProcessingService.zipFiles(files);
|
|
660
|
+
}
|
|
661
|
+
createRawMeasurementExportedFiles(dataToExportWithBackendCreatedFile, fileExtension, files) {
|
|
662
|
+
dataToExportWithBackendCreatedFile.forEach(data => {
|
|
663
|
+
if (data) {
|
|
664
|
+
const fragmentSeries = `${data.valueFragmentType}_${data.valueFragmentSeries}`;
|
|
665
|
+
const fileName = this.datapointsExportSelectorDataProcessingService.createFileName(data.source, fragmentSeries, fileExtension);
|
|
666
|
+
const fileData = data.fetchedMeasurementsBlobFile;
|
|
667
|
+
const exportedFile = { fileName, fileData };
|
|
668
|
+
files.push(exportedFile);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Converts data to a specified file type and returns the generated blob.
|
|
674
|
+
*
|
|
675
|
+
* Unlike measurements, data must be transformed to an exportable file structure before exporting.
|
|
676
|
+
*
|
|
677
|
+
* @param fileType - The type of file to which the data points should be exported. This can be 'csv' or 'excel'.
|
|
678
|
+
* @param dataToExport - An array of processed measurement data combined with the respective properties of the datapoint.
|
|
679
|
+
* @param mergedExportDetails - The details for the merged export, contains date range and aggregation.
|
|
680
|
+
* @returns A promise that resolves to the generated ZIP blob or null if an error occurs.
|
|
681
|
+
*/
|
|
682
|
+
async getSeriesDataBlob(fileType, dataToExport, mergedExportDetails) {
|
|
683
|
+
this.transformSeriesDataToExportableFileStructure(dataToExport);
|
|
684
|
+
const exportParams = {
|
|
685
|
+
flattenedAndSortedExportData: this.cachedFlatteredAndSortedSeriesExportData,
|
|
686
|
+
fileType,
|
|
687
|
+
mergedExportDetails: mergedExportDetails
|
|
688
|
+
};
|
|
689
|
+
return await this.datapointsExportSelectorDataProcessingService.exportSeriesData(exportParams);
|
|
690
|
+
}
|
|
691
|
+
transformSeriesDataToExportableFileStructure(dataToExport) {
|
|
692
|
+
if (!this.cachedRawExportSeriesData) {
|
|
693
|
+
this.cachedRawExportSeriesData =
|
|
694
|
+
this.datapointsExportSelectorDataProcessingService.transformToExportFileStructure(dataToExport);
|
|
695
|
+
this.cachedFlatteredAndSortedSeriesExportData = this.cachedRawExportSeriesData
|
|
696
|
+
.flat()
|
|
697
|
+
.sort((a, b) => {
|
|
698
|
+
return new Date(a.time).getTime() - new Date(b.time).getTime();
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DatapointsExportSelectorFileExporterService, deps: [{ token: i1.AggregationService }, { token: i1.AlertService }, { token: DataProcessingService }, { token: i4.TranslateService }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
703
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DatapointsExportSelectorFileExporterService, providedIn: 'root' }); }
|
|
704
|
+
}
|
|
705
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DatapointsExportSelectorFileExporterService, decorators: [{
|
|
706
|
+
type: Injectable,
|
|
707
|
+
args: [{
|
|
708
|
+
providedIn: 'root'
|
|
709
|
+
}]
|
|
710
|
+
}], ctorParameters: () => [{ type: i1.AggregationService }, { type: i1.AlertService }, { type: DataProcessingService }, { type: i4.TranslateService }] });
|
|
711
|
+
|
|
712
|
+
class DataPointsExportSelectorPreviewComponent {
|
|
713
|
+
constructor() {
|
|
714
|
+
this.MEASUREMENTS_PREVIEW_ITEMS_LIMIT = MEASUREMENTS_PREVIEW_ITEMS_LIMIT;
|
|
715
|
+
}
|
|
716
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DataPointsExportSelectorPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
717
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.9", type: DataPointsExportSelectorPreviewComponent, isStandalone: true, selector: "c8y-datapoints-export-selector-preview", inputs: { hasFetchedDataAnyValuesToExport: "hasFetchedDataAnyValuesToExport", isPreviewLoading: "isPreviewLoading", previewTableData: "previewTableData" }, ngImport: i0, template: "<div class=\"p-t-16 p-l-16 p-r-16 m-b-0\">\n <div class=\"d-flex a-i-center\">\n <label\n class=\"m-b-0 d-flex a-i-center gap-4\"\n [title]=\"'Preview`of exported file`' | translate\"\n >\n {{ 'Preview`of exported file`' | translate }}\n <button\n class=\"btn-help\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"popoverPreviewTemplate\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n [adaptivePosition]=\"true\"\n ></button>\n <ng-template #popoverPreviewTemplate>\n <span translate>\n <p>The preview shows the structure of the raw file from a single source.</p>\n <p>If no data is available, only the column headers are visible.</p>\n <p>\n The preview is limited to\n <b>{{ MEASUREMENTS_PREVIEW_ITEMS_LIMIT }}</b>\n records.\n </p>\n </span>\n </ng-template>\n </label>\n </div>\n <div\n class=\"table-responsive\"\n style=\"min-height: 275px\"\n >\n <table class=\"table\">\n <thead>\n <tr>\n <th>{{ 'Time' | translate }}</th>\n <th>{{ 'Source' | translate }}</th>\n <th>{{ 'Device name' | translate }}</th>\n <th>\n {{ 'Fragment and series' | translate }}\n </th>\n <th>{{ 'Value' | translate }}</th>\n <th>{{ 'Unit' | translate }}</th>\n </tr>\n </thead>\n <ng-container *ngIf=\"hasFetchedDataAnyValuesToExport || isPreviewLoading; else emptyState\">\n <ng-container *ngIf=\"!isPreviewLoading; else loading\">\n <tbody>\n <tr *ngFor=\"let row of previewTableData\">\n <td>{{ row.time }}</td>\n <td>{{ row.source }}</td>\n <td>{{ row.device_name }}</td>\n <td>\n {{ row.fragment_series }}\n </td>\n <td>{{ row.value }}</td>\n <td>{{ row.unit }}</td>\n </tr>\n </tbody>\n </ng-container>\n </ng-container>\n <ng-template #emptyState>\n <tbody>\n <tr>\n <td colspan=\"8\">\n <div class=\"d-col a-i-center\">\n <c8y-ui-empty-state\n [icon]=\"'search'\"\n [title]=\"'No data available.' | translate\"\n [horizontal]=\"true\"\n data-cy=\"datapoints-table-list--empty-state\"\n ></c8y-ui-empty-state>\n </div>\n </td>\n </tr>\n </tbody>\n </ng-template>\n <ng-template #loading>\n <tbody>\n <tr>\n <td colspan=\"8\">\n <c8y-loading></c8y-loading>\n </td>\n </tr>\n </tbody>\n </ng-template>\n </table>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: A11yModule }, { kind: "ngmodule", type: CoreModule }, { kind: "component", type: i1.EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }, { kind: "directive", type: i1.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: i2$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i1.LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "directive", type: i3$1.PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }] }); }
|
|
718
|
+
}
|
|
719
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DataPointsExportSelectorPreviewComponent, decorators: [{
|
|
720
|
+
type: Component,
|
|
721
|
+
args: [{ selector: 'c8y-datapoints-export-selector-preview', standalone: true, imports: [A11yModule, CoreModule, PopoverModule], template: "<div class=\"p-t-16 p-l-16 p-r-16 m-b-0\">\n <div class=\"d-flex a-i-center\">\n <label\n class=\"m-b-0 d-flex a-i-center gap-4\"\n [title]=\"'Preview`of exported file`' | translate\"\n >\n {{ 'Preview`of exported file`' | translate }}\n <button\n class=\"btn-help\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"popoverPreviewTemplate\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n [adaptivePosition]=\"true\"\n ></button>\n <ng-template #popoverPreviewTemplate>\n <span translate>\n <p>The preview shows the structure of the raw file from a single source.</p>\n <p>If no data is available, only the column headers are visible.</p>\n <p>\n The preview is limited to\n <b>{{ MEASUREMENTS_PREVIEW_ITEMS_LIMIT }}</b>\n records.\n </p>\n </span>\n </ng-template>\n </label>\n </div>\n <div\n class=\"table-responsive\"\n style=\"min-height: 275px\"\n >\n <table class=\"table\">\n <thead>\n <tr>\n <th>{{ 'Time' | translate }}</th>\n <th>{{ 'Source' | translate }}</th>\n <th>{{ 'Device name' | translate }}</th>\n <th>\n {{ 'Fragment and series' | translate }}\n </th>\n <th>{{ 'Value' | translate }}</th>\n <th>{{ 'Unit' | translate }}</th>\n </tr>\n </thead>\n <ng-container *ngIf=\"hasFetchedDataAnyValuesToExport || isPreviewLoading; else emptyState\">\n <ng-container *ngIf=\"!isPreviewLoading; else loading\">\n <tbody>\n <tr *ngFor=\"let row of previewTableData\">\n <td>{{ row.time }}</td>\n <td>{{ row.source }}</td>\n <td>{{ row.device_name }}</td>\n <td>\n {{ row.fragment_series }}\n </td>\n <td>{{ row.value }}</td>\n <td>{{ row.unit }}</td>\n </tr>\n </tbody>\n </ng-container>\n </ng-container>\n <ng-template #emptyState>\n <tbody>\n <tr>\n <td colspan=\"8\">\n <div class=\"d-col a-i-center\">\n <c8y-ui-empty-state\n [icon]=\"'search'\"\n [title]=\"'No data available.' | translate\"\n [horizontal]=\"true\"\n data-cy=\"datapoints-table-list--empty-state\"\n ></c8y-ui-empty-state>\n </div>\n </td>\n </tr>\n </tbody>\n </ng-template>\n <ng-template #loading>\n <tbody>\n <tr>\n <td colspan=\"8\">\n <c8y-loading></c8y-loading>\n </td>\n </tr>\n </tbody>\n </ng-template>\n </table>\n </div>\n</div>\n" }]
|
|
722
|
+
}], propDecorators: { hasFetchedDataAnyValuesToExport: [{
|
|
723
|
+
type: Input
|
|
724
|
+
}], isPreviewLoading: [{
|
|
725
|
+
type: Input
|
|
726
|
+
}], previewTableData: [{
|
|
727
|
+
type: Input
|
|
728
|
+
}] } });
|
|
729
|
+
|
|
730
|
+
class DataPointsExportSelectorDataScopeComponent {
|
|
731
|
+
constructor() {
|
|
732
|
+
this.onAggregationChange = new EventEmitter();
|
|
733
|
+
this.onExportTypeChange = new EventEmitter();
|
|
734
|
+
this.AGGREGATION_LABELS = AGGREGATION_LABELS;
|
|
735
|
+
this.AGGREGATION_VALUES_ARR = AGGREGATION_VALUES_ARR;
|
|
736
|
+
this.EXPORT_MODE_LABELS = EXPORT_MODE_LABELS;
|
|
737
|
+
this.EXPORT_MODE_VALUES_ARR = [EXPORT_MODE_VALUES.full, EXPORT_MODE_VALUES.compact];
|
|
738
|
+
}
|
|
739
|
+
emitAggregationChange(aggregation) {
|
|
740
|
+
this.onAggregationChange.emit(aggregation);
|
|
741
|
+
}
|
|
742
|
+
emitExportTypeChange(exportType) {
|
|
743
|
+
this.onExportTypeChange.emit(exportType);
|
|
744
|
+
}
|
|
745
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DataPointsExportSelectorDataScopeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
746
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.9", type: DataPointsExportSelectorDataScopeComponent, isStandalone: true, selector: "c8y-datapoints-export-selector-data-scope", inputs: { disabledAggregationOptions: "disabledAggregationOptions", formGroup: "formGroup" }, outputs: { onAggregationChange: "onAggregationChange", onExportTypeChange: "onExportTypeChange" }, ngImport: i0, template: "<fieldset class=\"c8y-fieldset\">\n <legend class=\"d-flex a-i-center\">\n {{ 'Data scope' | translate }}\n <button\n class=\"btn-help\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"popoverDataScopeTemplate\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n [adaptivePosition]=\"true\"\n ></button>\n <ng-template #popoverDataScopeTemplate>\n <p\n class=\"m-b-8\"\n translate\n >\n Choose type of an export to generate, the available options are:\n </p>\n <p><strong translate>Compact</strong></p>\n <ul class=\"p-l-16\">\n <li translate>\n Up to 5,000 records per data point, or up to the data retention limit\n </li>\n <li translate>Creates a single merged file containing all the data</li>\n <li translate>Preview is not available</li>\n <li translate>Supports data aggregation (optional)</li>\n </ul>\n <p><strong translate>Full</strong></p>\n <ul class=\"p-l-16\">\n <li translate>\n Up to 1,000,000 records per data point, or up to the data retention limit\n </li>\n <li translate>\n If the export exceeds 50,000 records, the data will be sent via email\n </li>\n <li translate>\n Creates a compressed ZIP file that contains separate data files for each of your\n selected points\n </li>\n <li translate>Preview is available</li>\n <li translate>No data aggregation</li>\n </ul>\n </ng-template>\n </legend>\n <c8y-form-group class=\"m-b-8\">\n <label>\n {{ 'Export mode' | translate }}\n </label>\n <div\n class=\"c8y-select-wrapper\"\n [formGroup]=\"formGroup\"\n >\n <select\n class=\"form-control text-12\"\n [title]=\"'Export mode' | translate\"\n id=\"exportMode\"\n formControlName=\"exportMode\"\n (ngModelChange)=\"emitExportTypeChange($event)\"\n >\n <option\n *ngFor=\"let exportModeValue of EXPORT_MODE_VALUES_ARR\"\n [ngValue]=\"exportModeValue\"\n >\n {{ EXPORT_MODE_LABELS[exportModeValue] | translate }}\n </option>\n </select>\n </div>\n </c8y-form-group>\n <c8y-form-group class=\"m-b-8\">\n <label>\n {{ 'Aggregation' | translate }}\n </label>\n <div\n class=\"c8y-select-wrapper\"\n [formGroup]=\"formGroup\"\n >\n <select\n class=\"form-control text-12\"\n [title]=\"'Aggregation' | translate\"\n id=\"aggregation\"\n formControlName=\"aggregation\"\n (ngModelChange)=\"emitAggregationChange($event)\"\n >\n <option\n *ngFor=\"let aggregationValue of AGGREGATION_VALUES_ARR\"\n [ngValue]=\"aggregationValue\"\n [disabled]=\"disabledAggregationOptions[aggregationValue]\"\n >\n {{ AGGREGATION_LABELS[aggregationValue] | translate }}\n </option>\n </select>\n </div>\n </c8y-form-group>\n </fieldset>", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }, { kind: "directive", type: i1.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: i2$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i3$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "component", type: i1.FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "directive", type: i3$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i3$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: PopoverModule }, { kind: "directive", type: i3$1.PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }] }); }
|
|
747
|
+
}
|
|
748
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DataPointsExportSelectorDataScopeComponent, decorators: [{
|
|
749
|
+
type: Component,
|
|
750
|
+
args: [{ selector: 'c8y-datapoints-export-selector-data-scope', standalone: true, imports: [CoreModule, FormsModule, PopoverModule], template: "<fieldset class=\"c8y-fieldset\">\n <legend class=\"d-flex a-i-center\">\n {{ 'Data scope' | translate }}\n <button\n class=\"btn-help\"\n [attr.aria-label]=\"'Help' | translate\"\n [popover]=\"popoverDataScopeTemplate\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n type=\"button\"\n [adaptivePosition]=\"true\"\n ></button>\n <ng-template #popoverDataScopeTemplate>\n <p\n class=\"m-b-8\"\n translate\n >\n Choose type of an export to generate, the available options are:\n </p>\n <p><strong translate>Compact</strong></p>\n <ul class=\"p-l-16\">\n <li translate>\n Up to 5,000 records per data point, or up to the data retention limit\n </li>\n <li translate>Creates a single merged file containing all the data</li>\n <li translate>Preview is not available</li>\n <li translate>Supports data aggregation (optional)</li>\n </ul>\n <p><strong translate>Full</strong></p>\n <ul class=\"p-l-16\">\n <li translate>\n Up to 1,000,000 records per data point, or up to the data retention limit\n </li>\n <li translate>\n If the export exceeds 50,000 records, the data will be sent via email\n </li>\n <li translate>\n Creates a compressed ZIP file that contains separate data files for each of your\n selected points\n </li>\n <li translate>Preview is available</li>\n <li translate>No data aggregation</li>\n </ul>\n </ng-template>\n </legend>\n <c8y-form-group class=\"m-b-8\">\n <label>\n {{ 'Export mode' | translate }}\n </label>\n <div\n class=\"c8y-select-wrapper\"\n [formGroup]=\"formGroup\"\n >\n <select\n class=\"form-control text-12\"\n [title]=\"'Export mode' | translate\"\n id=\"exportMode\"\n formControlName=\"exportMode\"\n (ngModelChange)=\"emitExportTypeChange($event)\"\n >\n <option\n *ngFor=\"let exportModeValue of EXPORT_MODE_VALUES_ARR\"\n [ngValue]=\"exportModeValue\"\n >\n {{ EXPORT_MODE_LABELS[exportModeValue] | translate }}\n </option>\n </select>\n </div>\n </c8y-form-group>\n <c8y-form-group class=\"m-b-8\">\n <label>\n {{ 'Aggregation' | translate }}\n </label>\n <div\n class=\"c8y-select-wrapper\"\n [formGroup]=\"formGroup\"\n >\n <select\n class=\"form-control text-12\"\n [title]=\"'Aggregation' | translate\"\n id=\"aggregation\"\n formControlName=\"aggregation\"\n (ngModelChange)=\"emitAggregationChange($event)\"\n >\n <option\n *ngFor=\"let aggregationValue of AGGREGATION_VALUES_ARR\"\n [ngValue]=\"aggregationValue\"\n [disabled]=\"disabledAggregationOptions[aggregationValue]\"\n >\n {{ AGGREGATION_LABELS[aggregationValue] | translate }}\n </option>\n </select>\n </div>\n </c8y-form-group>\n </fieldset>" }]
|
|
751
|
+
}], propDecorators: { disabledAggregationOptions: [{
|
|
752
|
+
type: Input
|
|
753
|
+
}], formGroup: [{
|
|
754
|
+
type: Input
|
|
755
|
+
}], onAggregationChange: [{
|
|
756
|
+
type: Output
|
|
757
|
+
}], onExportTypeChange: [{
|
|
758
|
+
type: Output
|
|
759
|
+
}] } });
|
|
760
|
+
|
|
761
|
+
class DataPointsExportSelectorFileTypesComponent {
|
|
762
|
+
constructor() {
|
|
763
|
+
this.dynamicFilesTypeMetadata = {};
|
|
764
|
+
}
|
|
765
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DataPointsExportSelectorFileTypesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
766
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.9", type: DataPointsExportSelectorFileTypesComponent, isStandalone: true, selector: "c8y-datapoints-export-selector-file-types", inputs: { dynamicFilesTypeMetadata: "dynamicFilesTypeMetadata", formGroup: "formGroup" }, ngImport: i0, template: "<fieldset class=\"c8y-fieldset\">\n <legend class=\"d-flex a-i-center\">{{ 'File types' | translate }}</legend>\n <div [formGroup]=\"formGroup\">\n <div formGroupName=\"fileTypes\">\n <c8y-form-group\n class=\"m-b-8\"\n *ngFor=\"let item of dynamicFilesTypeMetadata | keyvalue\"\n >\n <label>{{ item.value.label | translate }}</label>\n <label\n class=\"c8y-checkbox m-t-0\"\n title=\"{{ item.value.title }}\"\n >\n <input\n type=\"checkbox\"\n formControlName=\"{{ item.key }}\"\n />\n <span></span>\n <i\n class=\"m-l-8 m-r-4\"\n c8yIcon=\"{{ item.value.icon }}\"\n ></i>\n </label>\n </c8y-form-group>\n </div>\n </div>\n</fieldset>\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i1.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }, { kind: "directive", type: i2$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "pipe", type: i2$1.KeyValuePipe, name: "keyvalue" }, { kind: "directive", type: i3$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i3$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "component", type: i1.FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "directive", type: i1.RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "directive", type: i3$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i3$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i3$2.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "ngmodule", type: FormsModule }] }); }
|
|
767
|
+
}
|
|
768
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DataPointsExportSelectorFileTypesComponent, decorators: [{
|
|
769
|
+
type: Component,
|
|
770
|
+
args: [{ selector: 'c8y-datapoints-export-selector-file-types', standalone: true, imports: [CoreModule, FormsModule], template: "<fieldset class=\"c8y-fieldset\">\n <legend class=\"d-flex a-i-center\">{{ 'File types' | translate }}</legend>\n <div [formGroup]=\"formGroup\">\n <div formGroupName=\"fileTypes\">\n <c8y-form-group\n class=\"m-b-8\"\n *ngFor=\"let item of dynamicFilesTypeMetadata | keyvalue\"\n >\n <label>{{ item.value.label | translate }}</label>\n <label\n class=\"c8y-checkbox m-t-0\"\n title=\"{{ item.value.title }}\"\n >\n <input\n type=\"checkbox\"\n formControlName=\"{{ item.key }}\"\n />\n <span></span>\n <i\n class=\"m-l-8 m-r-4\"\n c8yIcon=\"{{ item.value.icon }}\"\n ></i>\n </label>\n </c8y-form-group>\n </div>\n </div>\n</fieldset>\n" }]
|
|
771
|
+
}], propDecorators: { dynamicFilesTypeMetadata: [{
|
|
772
|
+
type: Input
|
|
773
|
+
}], formGroup: [{
|
|
774
|
+
type: Input
|
|
775
|
+
}] } });
|
|
776
|
+
|
|
777
|
+
class DataPointsExportSelectorTimeRangeComponent {
|
|
778
|
+
constructor() {
|
|
779
|
+
this.onDateFromChange = new EventEmitter();
|
|
780
|
+
this.onDateToChange = new EventEmitter();
|
|
781
|
+
this.DATE_FROM = 'dateFrom';
|
|
782
|
+
this.DATE_TO = 'dateTo';
|
|
783
|
+
this.FROM_DATE = gettext('From`date`');
|
|
784
|
+
this.HAS_ERROR = HAS_ERROR;
|
|
785
|
+
this.INVALID_DATE_TIME = 'invalidDateTime';
|
|
786
|
+
this.THIS_DATE_IS_INVALID = gettext('This date is invalid.');
|
|
787
|
+
this.THIS_DATE_IS_AFTER_THE_LAST_ALLOWED_DATE = gettext('This date is after the latest allowed date.');
|
|
788
|
+
this.THIS_DATE_IS_BEFORE_THE_EARLIEST_ALLOWED_DATE = gettext('This date is before the earliest allowed date.');
|
|
789
|
+
this.TO_DATE = gettext('To`date`');
|
|
790
|
+
}
|
|
791
|
+
emitDateFromChange(updatedDate) {
|
|
792
|
+
this.onDateFromChange.emit(updatedDate);
|
|
793
|
+
}
|
|
794
|
+
emitDateToChange(updatedDate) {
|
|
795
|
+
this.onDateToChange.emit(updatedDate);
|
|
796
|
+
}
|
|
797
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DataPointsExportSelectorTimeRangeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
798
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.9", type: DataPointsExportSelectorTimeRangeComponent, isStandalone: true, selector: "c8y-datapoints-export-selector-time-range", inputs: { formGroup: "formGroup" }, outputs: { onDateFromChange: "onDateFromChange", onDateToChange: "onDateToChange" }, ngImport: i0, template: "<fieldset class=\"c8y-fieldset\">\n <legend>{{ 'Time range' | translate }}</legend>\n <c8y-form-group\n class=\"m-b-8\"\n [ngClass]=\"formGroup.controls.dateFrom.errors ? HAS_ERROR : ''\"\n >\n <label\n [title]=\"FROM_DATE | translate\"\n [for]=\"DATE_FROM\"\n >\n {{ FROM_DATE | translate }}\n </label>\n <div>\n <c8y-date-time-picker\n id=\"DATE_FROM\"\n [maxDate]=\"formGroup.value.dateTo\"\n [placeholder]=\"FROM_DATE | translate\"\n [formControl]=\"formGroup.controls.dateFrom\"\n [ngClass]=\"formGroup.controls.dateFrom.errors ? HAS_ERROR : ''\"\n (ngModelChange)=\"emitDateFromChange($event)\"\n ></c8y-date-time-picker>\n <c8y-messages [show]=\"formGroup.controls.dateFrom.errors\">\n <c8y-message\n name=\"dateAfterRangeMax\"\n [text]=\"THIS_DATE_IS_AFTER_THE_LAST_ALLOWED_DATE | translate\"\n ></c8y-message>\n <c8y-message\n name=\"INVALID_DATE_TIME\"\n [text]=\"THIS_DATE_IS_INVALID | translate\"\n ></c8y-message>\n </c8y-messages>\n </div>\n </c8y-form-group>\n <c8y-form-group\n class=\"m-b-8\"\n [ngClass]=\"formGroup.controls.dateTo.errors ? HAS_ERROR : ''\"\n >\n <label\n [title]=\"TO_DATE | translate\"\n [for]=\"DATE_TO\"\n >\n {{ TO_DATE | translate }}\n </label>\n <div>\n <c8y-date-time-picker\n id=\"DATE_TO\"\n [minDate]=\"formGroup.value.dateFrom\"\n [placeholder]=\"TO_DATE | translate\"\n [formControl]=\"formGroup.controls.dateTo\"\n [ngClass]=\"formGroup.controls.dateTo.errors ? HAS_ERROR : ''\"\n (ngModelChange)=\"emitDateToChange($event)\"\n ></c8y-date-time-picker>\n <c8y-messages [show]=\"formGroup.controls.dateTo.errors\">\n <c8y-message\n name=\"dateBeforeRangeMin\"\n [text]=\"THIS_DATE_IS_BEFORE_THE_EARLIEST_ALLOWED_DATE | translate\"\n ></c8y-message>\n <c8y-message\n name=\"INVALID_DATE_TIME\"\n [text]=\"THIS_DATE_IS_INVALID | translate\"\n ></c8y-message>\n </c8y-messages>\n </div>\n </c8y-form-group>\n</fieldset>\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }, { kind: "directive", type: i2$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "component", type: i1.FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "directive", type: i1.MessageDirective, selector: "c8y-message", inputs: ["name", "text"] }, { kind: "component", type: i1.MessagesComponent, selector: "c8y-messages", inputs: ["show", "defaults", "helpMessage"] }, { kind: "directive", type: i3$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i1.DateTimePickerComponent, selector: "c8y-date-time-picker", inputs: ["minDate", "maxDate", "placeholder", "dateInputFormat", "adaptivePosition", "size", "dateType", "config"], outputs: ["onDateSelected"] }, { kind: "ngmodule", type: FormsModule }] }); }
|
|
799
|
+
}
|
|
800
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DataPointsExportSelectorTimeRangeComponent, decorators: [{
|
|
801
|
+
type: Component,
|
|
802
|
+
args: [{ selector: 'c8y-datapoints-export-selector-time-range', standalone: true, imports: [CoreModule, FormsModule], template: "<fieldset class=\"c8y-fieldset\">\n <legend>{{ 'Time range' | translate }}</legend>\n <c8y-form-group\n class=\"m-b-8\"\n [ngClass]=\"formGroup.controls.dateFrom.errors ? HAS_ERROR : ''\"\n >\n <label\n [title]=\"FROM_DATE | translate\"\n [for]=\"DATE_FROM\"\n >\n {{ FROM_DATE | translate }}\n </label>\n <div>\n <c8y-date-time-picker\n id=\"DATE_FROM\"\n [maxDate]=\"formGroup.value.dateTo\"\n [placeholder]=\"FROM_DATE | translate\"\n [formControl]=\"formGroup.controls.dateFrom\"\n [ngClass]=\"formGroup.controls.dateFrom.errors ? HAS_ERROR : ''\"\n (ngModelChange)=\"emitDateFromChange($event)\"\n ></c8y-date-time-picker>\n <c8y-messages [show]=\"formGroup.controls.dateFrom.errors\">\n <c8y-message\n name=\"dateAfterRangeMax\"\n [text]=\"THIS_DATE_IS_AFTER_THE_LAST_ALLOWED_DATE | translate\"\n ></c8y-message>\n <c8y-message\n name=\"INVALID_DATE_TIME\"\n [text]=\"THIS_DATE_IS_INVALID | translate\"\n ></c8y-message>\n </c8y-messages>\n </div>\n </c8y-form-group>\n <c8y-form-group\n class=\"m-b-8\"\n [ngClass]=\"formGroup.controls.dateTo.errors ? HAS_ERROR : ''\"\n >\n <label\n [title]=\"TO_DATE | translate\"\n [for]=\"DATE_TO\"\n >\n {{ TO_DATE | translate }}\n </label>\n <div>\n <c8y-date-time-picker\n id=\"DATE_TO\"\n [minDate]=\"formGroup.value.dateFrom\"\n [placeholder]=\"TO_DATE | translate\"\n [formControl]=\"formGroup.controls.dateTo\"\n [ngClass]=\"formGroup.controls.dateTo.errors ? HAS_ERROR : ''\"\n (ngModelChange)=\"emitDateToChange($event)\"\n ></c8y-date-time-picker>\n <c8y-messages [show]=\"formGroup.controls.dateTo.errors\">\n <c8y-message\n name=\"dateBeforeRangeMin\"\n [text]=\"THIS_DATE_IS_BEFORE_THE_EARLIEST_ALLOWED_DATE | translate\"\n ></c8y-message>\n <c8y-message\n name=\"INVALID_DATE_TIME\"\n [text]=\"THIS_DATE_IS_INVALID | translate\"\n ></c8y-message>\n </c8y-messages>\n </div>\n </c8y-form-group>\n</fieldset>\n" }]
|
|
803
|
+
}], propDecorators: { formGroup: [{
|
|
804
|
+
type: Input
|
|
805
|
+
}], onDateFromChange: [{
|
|
806
|
+
type: Output
|
|
807
|
+
}], onDateToChange: [{
|
|
808
|
+
type: Output
|
|
809
|
+
}] } });
|
|
810
|
+
|
|
811
|
+
class CSVDataTransformer {
|
|
812
|
+
transformToMergedFormat(exportData) {
|
|
813
|
+
const timeSeries = new Map();
|
|
814
|
+
const uniqueColumnIdentifiers = new Set();
|
|
815
|
+
exportData.forEach(({ time, device_name, fragment_series, unit, value_min, value_max, source }) => {
|
|
816
|
+
const measurementIdentifier = this.createMeasurementIdentifier(device_name, fragment_series, unit, source);
|
|
817
|
+
if (!timeSeries.has(time)) {
|
|
818
|
+
timeSeries.set(time, {});
|
|
819
|
+
}
|
|
820
|
+
const timeEntry = timeSeries.get(time);
|
|
821
|
+
if (timeEntry) {
|
|
822
|
+
const minKey = this.formatMinKey(measurementIdentifier);
|
|
823
|
+
const maxKey = this.formatMaxKey(measurementIdentifier);
|
|
824
|
+
timeEntry[minKey] = value_min;
|
|
825
|
+
timeEntry[maxKey] = value_max;
|
|
826
|
+
uniqueColumnIdentifiers.add(minKey);
|
|
827
|
+
uniqueColumnIdentifiers.add(maxKey);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
return {
|
|
831
|
+
timeSeries,
|
|
832
|
+
uniqueColumnIdentifiers: Array.from(uniqueColumnIdentifiers)
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
createMeasurementIdentifier(deviceName, fragmentSeries, unit, source) {
|
|
836
|
+
return unit
|
|
837
|
+
? `(${source}) ${deviceName} -> ${fragmentSeries} [${unit}]`
|
|
838
|
+
: `(${source}) ${deviceName} -> ${fragmentSeries}`;
|
|
839
|
+
}
|
|
840
|
+
formatMinKey(baseKey) {
|
|
841
|
+
return `${baseKey} (min)`;
|
|
842
|
+
}
|
|
843
|
+
formatMaxKey(baseKey) {
|
|
844
|
+
return `${baseKey} (max)`;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
class CSVFileCreator {
|
|
848
|
+
async createFile(content, mimeType) {
|
|
849
|
+
return new Blob([content], { type: mimeType });
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
class CSVGenerator {
|
|
853
|
+
static getLabel() {
|
|
854
|
+
return gettext('Comma-separated value');
|
|
855
|
+
}
|
|
856
|
+
static getTitle() {
|
|
857
|
+
return 'CSV';
|
|
858
|
+
}
|
|
859
|
+
static getType() {
|
|
860
|
+
return 'csv';
|
|
861
|
+
}
|
|
862
|
+
static getIcon() {
|
|
863
|
+
return 'table';
|
|
864
|
+
}
|
|
865
|
+
static getFileExtension() {
|
|
866
|
+
return 'csv';
|
|
867
|
+
}
|
|
868
|
+
static getAcceptType() {
|
|
869
|
+
return 'text/csv';
|
|
870
|
+
}
|
|
871
|
+
static getMimeType() {
|
|
872
|
+
return 'text/csv';
|
|
873
|
+
}
|
|
874
|
+
static getZipName() {
|
|
875
|
+
return 'export_csv.zip';
|
|
876
|
+
}
|
|
877
|
+
static async convertToMergedCSV(exportData, mergedExportDetails) {
|
|
878
|
+
const instance = CSVGenerator.getInstance();
|
|
879
|
+
const { timeSeries, uniqueColumnIdentifiers } = instance.dataTransformer.transformToMergedFormat(exportData);
|
|
880
|
+
const rows = [
|
|
881
|
+
instance.createMergedFileHeaderRow(uniqueColumnIdentifiers, mergedExportDetails),
|
|
882
|
+
...instance.createMergedFileDataRows(timeSeries, uniqueColumnIdentifiers)
|
|
883
|
+
];
|
|
884
|
+
const csvContent = rows.map(row => row.join(',')).join('\n');
|
|
885
|
+
return instance.fileCreator.createFile(csvContent, CSVGenerator.getMimeType());
|
|
886
|
+
}
|
|
887
|
+
static getInstance() {
|
|
888
|
+
if (!CSVGenerator.instance) {
|
|
889
|
+
CSVGenerator.instance = new CSVGenerator();
|
|
890
|
+
}
|
|
891
|
+
return CSVGenerator.instance;
|
|
892
|
+
}
|
|
893
|
+
constructor() {
|
|
894
|
+
this.dataTransformer = new CSVDataTransformer();
|
|
895
|
+
this.fileCreator = new CSVFileCreator();
|
|
896
|
+
}
|
|
897
|
+
createMergedFileHeaderRow(uniqueColumnIdentifiers, details) {
|
|
898
|
+
const time = details ? `time => from ${details.dateFrom} to ${details.dateTo}` : 'time';
|
|
899
|
+
return [time, ...uniqueColumnIdentifiers];
|
|
900
|
+
}
|
|
901
|
+
createMergedFileDataRows(timeSeries, uniqueColumnIdentifiers) {
|
|
902
|
+
return Array.from(timeSeries.entries())
|
|
903
|
+
.filter(([time, values]) => this.isValidEntry(time, values))
|
|
904
|
+
.map(([time, values]) => this.createDataRow(time, values, uniqueColumnIdentifiers));
|
|
905
|
+
}
|
|
906
|
+
isValidEntry(time, values) {
|
|
907
|
+
return time !== null && Object.values(values).some(value => value != null);
|
|
908
|
+
}
|
|
909
|
+
createDataRow(time, values, uniqueColumnIdentifiers) {
|
|
910
|
+
const row = [time];
|
|
911
|
+
uniqueColumnIdentifiers.forEach(key => {
|
|
912
|
+
const value = values[key];
|
|
913
|
+
row.push(value?.toString() || '');
|
|
914
|
+
});
|
|
915
|
+
return row;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const CSVGeneratorAdapter = {
|
|
919
|
+
getLabel: CSVGenerator.getLabel.bind(CSVGenerator),
|
|
920
|
+
getIcon: CSVGenerator.getIcon.bind(CSVGenerator),
|
|
921
|
+
getFileExtension: CSVGenerator.getFileExtension.bind(CSVGenerator),
|
|
922
|
+
getMimeType: CSVGenerator.getMimeType.bind(CSVGenerator),
|
|
923
|
+
generateMerged: CSVGenerator.convertToMergedCSV.bind(CSVGenerator),
|
|
924
|
+
getType: CSVGenerator.getType.bind(CSVGenerator),
|
|
925
|
+
getTitle: CSVGenerator.getTitle.bind(CSVGenerator),
|
|
926
|
+
getZipName: CSVGenerator.getZipName.bind(CSVGenerator),
|
|
927
|
+
getAcceptType: CSVGenerator.getAcceptType.bind(CSVGenerator)
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
// Utility functions
|
|
931
|
+
class XmlUtils {
|
|
932
|
+
static escapeXml(unsafe) {
|
|
933
|
+
return unsafe.replace(/[<>&'"]/g, c => {
|
|
934
|
+
switch (c) {
|
|
935
|
+
case '<':
|
|
936
|
+
return '<';
|
|
937
|
+
case '>':
|
|
938
|
+
return '>';
|
|
939
|
+
case '&':
|
|
940
|
+
return '&';
|
|
941
|
+
case "'":
|
|
942
|
+
return ''';
|
|
943
|
+
case '"':
|
|
944
|
+
return '"';
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
class ExcelDataTransformer {
|
|
950
|
+
transformToMergedFormat(exportData) {
|
|
951
|
+
const timeSeries = new Map();
|
|
952
|
+
const uniqueColumnIdentifiers = new Set();
|
|
953
|
+
exportData.forEach(({ time, device_name, fragment_series, unit, value_min, value_max, source }) => {
|
|
954
|
+
const measurementIdentifier = this.createMeasurementIdentifier(device_name, fragment_series, unit, source);
|
|
955
|
+
if (!timeSeries.has(time)) {
|
|
956
|
+
timeSeries.set(time, {});
|
|
957
|
+
}
|
|
958
|
+
const timeEntry = timeSeries.get(time);
|
|
959
|
+
if (timeEntry) {
|
|
960
|
+
const minKey = this.formatMinKey(measurementIdentifier);
|
|
961
|
+
const maxKey = this.formatMaxKey(measurementIdentifier);
|
|
962
|
+
timeEntry[minKey] = value_min;
|
|
963
|
+
timeEntry[maxKey] = value_max;
|
|
964
|
+
uniqueColumnIdentifiers.add(minKey);
|
|
965
|
+
uniqueColumnIdentifiers.add(maxKey);
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
return {
|
|
969
|
+
timeSeries,
|
|
970
|
+
uniqueColumnIdentifiers: Array.from(uniqueColumnIdentifiers)
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
createMeasurementIdentifier(deviceName, fragmentSeries, unit, source) {
|
|
974
|
+
return unit
|
|
975
|
+
? `(${source}) ${deviceName} -> ${fragmentSeries} [${unit}]`
|
|
976
|
+
: `(${source}) ${deviceName} -> ${fragmentSeries}`;
|
|
977
|
+
}
|
|
978
|
+
formatMinKey(baseKey) {
|
|
979
|
+
return `${baseKey} (min)`;
|
|
980
|
+
}
|
|
981
|
+
formatMaxKey(baseKey) {
|
|
982
|
+
return `${baseKey} (max)`;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
class ExcelXmlGenerator {
|
|
986
|
+
constructor() {
|
|
987
|
+
this.columnWidths = {
|
|
988
|
+
time: 25,
|
|
989
|
+
source: 10,
|
|
990
|
+
device_name: 20,
|
|
991
|
+
'fragment.series': 20,
|
|
992
|
+
value_min: 15,
|
|
993
|
+
value_max: 15,
|
|
994
|
+
unit: 5
|
|
995
|
+
};
|
|
996
|
+
this.defaultWidth = 30;
|
|
997
|
+
this.RELS_XML = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
998
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">\n' +
|
|
999
|
+
' <Relationship Id="rId1" Target="xl/workbook.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"/>\n' +
|
|
1000
|
+
' <Relationship Id="rId2" Target="docProps/app.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"/>\n' +
|
|
1001
|
+
' <Relationship Id="rId3" Target="docProps/core.xml" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"/>\n' +
|
|
1002
|
+
'</Relationships>';
|
|
1003
|
+
this.CONTENT_TYPES_XML = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' +
|
|
1004
|
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">\n' +
|
|
1005
|
+
' <Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/>\n' +
|
|
1006
|
+
' <Default ContentType="application/xml" Extension="xml"/>\n' +
|
|
1007
|
+
' <Override ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml" PartName="/docProps/app.xml"/>\n' +
|
|
1008
|
+
' <Override ContentType="application/vnd.openxmlformats-package.core-properties+xml" PartName="/docProps/core.xml"/>\n' +
|
|
1009
|
+
' <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" PartName="/xl/sharedStrings.xml"/>\n' +
|
|
1010
|
+
' <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" PartName="/xl/workbook.xml"/>\n' +
|
|
1011
|
+
' <Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" PartName="/xl/worksheets/sheet1.xml"/>\n' +
|
|
1012
|
+
'</Types>';
|
|
1013
|
+
this.WORKBOOK_RELS_XML = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
1014
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">\n' +
|
|
1015
|
+
' <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>\n' +
|
|
1016
|
+
' <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>\n' +
|
|
1017
|
+
'</Relationships>';
|
|
1018
|
+
this.SHARED_STRINGS_XML = '<?xml version="1.0" encoding="UTF-8"?>\n' +
|
|
1019
|
+
'<sst count="0" uniqueCount="0" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"/>';
|
|
1020
|
+
}
|
|
1021
|
+
generateWorkbookXml(sheetName = 'Measurements') {
|
|
1022
|
+
return ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
1023
|
+
`<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name='${sheetName}' r:id="rId3" sheetId="1"/></sheets></workbook>`);
|
|
1024
|
+
}
|
|
1025
|
+
generateSheetXml(data) {
|
|
1026
|
+
const headers = data[0];
|
|
1027
|
+
const colsXml = this.generateColsXml(headers);
|
|
1028
|
+
const rowsXml = this.generateRowsXml(data);
|
|
1029
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
1030
|
+
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
|
1031
|
+
<cols>${colsXml}</cols>
|
|
1032
|
+
<sheetData>${rowsXml}</sheetData>
|
|
1033
|
+
</worksheet>`;
|
|
1034
|
+
}
|
|
1035
|
+
generateAppXml(name) {
|
|
1036
|
+
return ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
1037
|
+
'<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">\n' +
|
|
1038
|
+
` <Application>${name}</Application>\n` +
|
|
1039
|
+
' <DocSecurity>0</DocSecurity>\n' +
|
|
1040
|
+
' <ScaleCrop>false</ScaleCrop>\n' +
|
|
1041
|
+
' <HeadingPairs>\n' +
|
|
1042
|
+
' <vt:vector size="2" baseType="variant">\n' +
|
|
1043
|
+
' <vt:variant>\n' +
|
|
1044
|
+
' <vt:lpstr>Worksheets</vt:lpstr>\n' +
|
|
1045
|
+
' </vt:variant>\n' +
|
|
1046
|
+
' <vt:variant>\n' +
|
|
1047
|
+
' <vt:i4>1</vt:i4>\n' +
|
|
1048
|
+
' </vt:variant>\n' +
|
|
1049
|
+
' </vt:vector>\n' +
|
|
1050
|
+
' </HeadingPairs>\n' +
|
|
1051
|
+
' <TitlesOfParts>\n' +
|
|
1052
|
+
' <vt:vector size="1" baseType="lpstr">\n' +
|
|
1053
|
+
' <vt:lpstr>Sheet1</vt:lpstr>\n' +
|
|
1054
|
+
' </vt:vector>\n' +
|
|
1055
|
+
' </TitlesOfParts>\n' +
|
|
1056
|
+
' <Company></Company>\n' +
|
|
1057
|
+
' <LinksUpToDate>false</LinksUpToDate>\n' +
|
|
1058
|
+
' <SharedDoc>false</SharedDoc>\n' +
|
|
1059
|
+
' <HyperlinksChanged>false</HyperlinksChanged>\n' +
|
|
1060
|
+
' <AppVersion>16.0300</AppVersion>\n' +
|
|
1061
|
+
'</Properties>');
|
|
1062
|
+
}
|
|
1063
|
+
generateCoreXml(name) {
|
|
1064
|
+
const now = new Date().toISOString();
|
|
1065
|
+
return ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
1066
|
+
'<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\n' +
|
|
1067
|
+
` <dc:creator>${name}</dc:creator>\n` +
|
|
1068
|
+
` <cp:lastModifiedBy>${name}</cp:lastModifiedBy>\n` +
|
|
1069
|
+
` <dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created>\n` +
|
|
1070
|
+
` <dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified>\n` +
|
|
1071
|
+
'</cp:coreProperties>');
|
|
1072
|
+
}
|
|
1073
|
+
generateColsXml(headers) {
|
|
1074
|
+
return headers
|
|
1075
|
+
.map((header, index) => {
|
|
1076
|
+
const width = this.columnWidths[header] || this.defaultWidth;
|
|
1077
|
+
return `<col min="${index + 1}" max="${index + 1}" width="${width}" customWidth="1"/>`;
|
|
1078
|
+
})
|
|
1079
|
+
.join('');
|
|
1080
|
+
}
|
|
1081
|
+
generateRowsXml(data) {
|
|
1082
|
+
return data
|
|
1083
|
+
.map(row => `<row>${row
|
|
1084
|
+
.map(cell => {
|
|
1085
|
+
if (isNil(cell) || isEmpty(cell)) {
|
|
1086
|
+
return `<c t="inlineStr"><is><t></t></is></c>`; // String cell
|
|
1087
|
+
}
|
|
1088
|
+
else if (isNumber(Number(cell)) && !isNaN(Number(cell))) {
|
|
1089
|
+
return `<c t="n"><v>${Number(cell)}</v></c>`; // Number cell
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
return `<c t="inlineStr"><is><t>${XmlUtils.escapeXml(cell)}</t></is></c>`; // String cell
|
|
1093
|
+
}
|
|
1094
|
+
})
|
|
1095
|
+
.join('')}</row>`)
|
|
1096
|
+
.join('');
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
class ZipFileCreator {
|
|
1100
|
+
async createZipFile(contents, compressionType, compressionLevel) {
|
|
1101
|
+
const zip = new JSZip();
|
|
1102
|
+
const blob = 'blob';
|
|
1103
|
+
contents.forEach((content, filename) => {
|
|
1104
|
+
zip.file(filename, content);
|
|
1105
|
+
});
|
|
1106
|
+
const options = { type: blob };
|
|
1107
|
+
if (compressionType !== FILE_COMPRESSION_TYPES_VALUES.store) {
|
|
1108
|
+
options.compression = compressionType;
|
|
1109
|
+
options.compressionOptions = { level: compressionLevel };
|
|
1110
|
+
}
|
|
1111
|
+
return (await zip.generateAsync(options));
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
class ExcelGenerator {
|
|
1115
|
+
static getLabel() {
|
|
1116
|
+
return gettext('Microsoft Excel');
|
|
1117
|
+
}
|
|
1118
|
+
static getTitle() {
|
|
1119
|
+
return 'Excel';
|
|
1120
|
+
}
|
|
1121
|
+
static getType() {
|
|
1122
|
+
return 'excel';
|
|
1123
|
+
}
|
|
1124
|
+
static getIcon() {
|
|
1125
|
+
return 'excel';
|
|
1126
|
+
}
|
|
1127
|
+
static getFileExtension() {
|
|
1128
|
+
return 'xlsx';
|
|
1129
|
+
}
|
|
1130
|
+
static getAcceptType() {
|
|
1131
|
+
return 'application/vnd.ms-excel';
|
|
1132
|
+
}
|
|
1133
|
+
static getMimeType() {
|
|
1134
|
+
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
1135
|
+
}
|
|
1136
|
+
static getZipName() {
|
|
1137
|
+
return 'export_excel.zip';
|
|
1138
|
+
}
|
|
1139
|
+
static async convertToMergedExcel(exportData, mergedExportDetails) {
|
|
1140
|
+
const instance = ExcelGenerator.getInstance();
|
|
1141
|
+
const { timeSeries, uniqueColumnIdentifiers } = instance.dataTransformer.transformToMergedFormat(exportData);
|
|
1142
|
+
const rows = [
|
|
1143
|
+
instance.createMergedFileHeaderRow(uniqueColumnIdentifiers, mergedExportDetails),
|
|
1144
|
+
...instance.createMergedFileDataRows(timeSeries, uniqueColumnIdentifiers)
|
|
1145
|
+
];
|
|
1146
|
+
return instance.createExcelFile(rows, mergedExportDetails);
|
|
1147
|
+
}
|
|
1148
|
+
static getInstance() {
|
|
1149
|
+
if (!ExcelGenerator.instance) {
|
|
1150
|
+
ExcelGenerator.instance = new ExcelGenerator();
|
|
1151
|
+
}
|
|
1152
|
+
return ExcelGenerator.instance;
|
|
1153
|
+
}
|
|
1154
|
+
constructor() {
|
|
1155
|
+
this.dataTransformer = new ExcelDataTransformer();
|
|
1156
|
+
this.xmlGenerator = new ExcelXmlGenerator();
|
|
1157
|
+
this.fileCreator = new ZipFileCreator();
|
|
1158
|
+
}
|
|
1159
|
+
async createExcelFile(data, details) {
|
|
1160
|
+
const contents = new Map([
|
|
1161
|
+
['[Content_Types].xml', this.xmlGenerator.CONTENT_TYPES_XML],
|
|
1162
|
+
['_rels/.rels', this.xmlGenerator.RELS_XML],
|
|
1163
|
+
[
|
|
1164
|
+
'xl/workbook.xml',
|
|
1165
|
+
this.xmlGenerator.generateWorkbookXml(details?.aggregation ? details?.aggregation : 'NO AGGREGATION')
|
|
1166
|
+
],
|
|
1167
|
+
['xl/worksheets/sheet1.xml', this.xmlGenerator.generateSheetXml(data)],
|
|
1168
|
+
['xl/sharedStrings.xml', this.xmlGenerator.SHARED_STRINGS_XML],
|
|
1169
|
+
['docProps/app.xml', this.xmlGenerator.generateAppXml('Custom Script')],
|
|
1170
|
+
['docProps/core.xml', this.xmlGenerator.generateCoreXml('Custom Script')],
|
|
1171
|
+
['xl/_rels/workbook.xml.rels', this.xmlGenerator.WORKBOOK_RELS_XML]
|
|
1172
|
+
]);
|
|
1173
|
+
return this.fileCreator.createZipFile(contents, FILE_COMPRESSION_TYPES_VALUES.deflate, 9);
|
|
1174
|
+
}
|
|
1175
|
+
createMergedFileHeaderRow(uniqueColumnIdentifiers, details) {
|
|
1176
|
+
const time = details ? `time => from ${details.dateFrom} to ${details.dateTo}` : 'time';
|
|
1177
|
+
return [time, ...uniqueColumnIdentifiers];
|
|
1178
|
+
}
|
|
1179
|
+
createMergedFileDataRows(timeSeries, uniqueColumnIdentifiers) {
|
|
1180
|
+
return Array.from(timeSeries.entries())
|
|
1181
|
+
.filter(([time, values]) => this.isValidEntry(time, values))
|
|
1182
|
+
.map(([time, values]) => this.createDataRow(time, values, uniqueColumnIdentifiers));
|
|
1183
|
+
}
|
|
1184
|
+
isValidEntry(time, values) {
|
|
1185
|
+
return (time !== null && Object.values(values).some(value => value !== null && value !== undefined));
|
|
1186
|
+
}
|
|
1187
|
+
createDataRow(time, values, uniqueColumnIdentifiers) {
|
|
1188
|
+
const row = [time];
|
|
1189
|
+
uniqueColumnIdentifiers.forEach(key => {
|
|
1190
|
+
const value = values[key];
|
|
1191
|
+
row.push(value !== undefined && value !== null ? value.toString() : '');
|
|
1192
|
+
});
|
|
1193
|
+
return row;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const ExcelGeneratorAdapter = {
|
|
1197
|
+
getLabel: ExcelGenerator.getLabel.bind(ExcelGenerator),
|
|
1198
|
+
getIcon: ExcelGenerator.getIcon.bind(ExcelGenerator),
|
|
1199
|
+
getFileExtension: ExcelGenerator.getFileExtension.bind(ExcelGenerator),
|
|
1200
|
+
getMimeType: ExcelGenerator.getMimeType.bind(ExcelGenerator),
|
|
1201
|
+
generateMerged: ExcelGenerator.convertToMergedExcel.bind(ExcelGenerator),
|
|
1202
|
+
getType: ExcelGenerator.getType.bind(ExcelGenerator),
|
|
1203
|
+
getTitle: ExcelGenerator.getTitle.bind(ExcelGenerator),
|
|
1204
|
+
getZipName: ExcelGenerator.getZipName.bind(ExcelGenerator),
|
|
1205
|
+
getAcceptType: ExcelGenerator.getAcceptType.bind(ExcelGenerator)
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
const FILE_GENERATORS = new InjectionToken('FILE_GENERATORS', {
|
|
1209
|
+
providedIn: 'root',
|
|
1210
|
+
factory: () => [ExcelGeneratorAdapter, CSVGeneratorAdapter]
|
|
1211
|
+
});
|
|
1212
|
+
const dateRangeValidator = (control) => {
|
|
1213
|
+
const dateFrom = control.get('dateFrom');
|
|
1214
|
+
const dateTo = control.get('dateTo');
|
|
1215
|
+
if (!dateFrom || !dateTo) {
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
const dateFromValue = dateFrom.value;
|
|
1219
|
+
const dateToValue = dateTo.value;
|
|
1220
|
+
if (dateFromValue && dateToValue) {
|
|
1221
|
+
if (new Date(dateFromValue) > new Date(dateToValue)) {
|
|
1222
|
+
dateFrom.setErrors({ dateAfterRangeMax: true });
|
|
1223
|
+
dateTo.setErrors({ dateBeforeRangeMin: true });
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
dateFrom.setErrors(null);
|
|
1227
|
+
dateTo.setErrors(null);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
return null;
|
|
1231
|
+
};
|
|
1232
|
+
class DatapointsExportSelectorFileExporterComponent {
|
|
1233
|
+
constructor(datapointsExportModalService, datapointsExportSelectorService, datapointsExportSelectorFileExporterService, formBuilder, generators) {
|
|
1234
|
+
this.datapointsExportModalService = datapointsExportModalService;
|
|
1235
|
+
this.datapointsExportSelectorService = datapointsExportSelectorService;
|
|
1236
|
+
this.datapointsExportSelectorFileExporterService = datapointsExportSelectorFileExporterService;
|
|
1237
|
+
this.formBuilder = formBuilder;
|
|
1238
|
+
this.generators = generators;
|
|
1239
|
+
this.onDownloadButtonStateChange = new EventEmitter();
|
|
1240
|
+
/**
|
|
1241
|
+
* Represents aggregation selector options that are disabled.
|
|
1242
|
+
* This state is determined based on the current date range.
|
|
1243
|
+
*/
|
|
1244
|
+
this.disabledAggregationOptions = {};
|
|
1245
|
+
/**
|
|
1246
|
+
* Contains all datapoints that number of records exceeds a threshold, where data for these datapoints will be processed by the backend.
|
|
1247
|
+
* Applies only to measurement API which can return processed CSV, Excel or XML data.
|
|
1248
|
+
*/
|
|
1249
|
+
this.datapointsExceedingBrowserDownloadLimit = [];
|
|
1250
|
+
/**
|
|
1251
|
+
* Indicates whether any of DataToExport objects timeValueMap property is not undefined (series data)
|
|
1252
|
+
* or dataPointToExport is not undefined at all (measurement data).
|
|
1253
|
+
*/
|
|
1254
|
+
this.hasFetchedDataAnyValuesToExport = false;
|
|
1255
|
+
this.hasPermissionToReadAnyMeasurements = false;
|
|
1256
|
+
this.isPreviewLoading = true;
|
|
1257
|
+
/**
|
|
1258
|
+
* Indicates whether the full or compact type of export is selected.
|
|
1259
|
+
* Full type of export is based on measurement API data.
|
|
1260
|
+
* Compact type of export is based on series API data.
|
|
1261
|
+
*/
|
|
1262
|
+
this.isFullTypeOfExport = false;
|
|
1263
|
+
/**
|
|
1264
|
+
* Indicates whether the aggregation value was changed programmatically.
|
|
1265
|
+
* It prevents a case where fetching may be triggered twice when export type is changed.
|
|
1266
|
+
* Both options alone can trigger fetching data.
|
|
1267
|
+
*/
|
|
1268
|
+
this.isAggregationChangedProgrammatically = false;
|
|
1269
|
+
/**
|
|
1270
|
+
* Indicates whether any of the export file types (CSV, Excel) are checked.
|
|
1271
|
+
*/
|
|
1272
|
+
this.isAnyOfExportFileTypeChecked = false;
|
|
1273
|
+
this.dateFromSubject = new Subject();
|
|
1274
|
+
this.dateToSubject = new Subject();
|
|
1275
|
+
this.destroy$ = new Subject();
|
|
1276
|
+
this.dynamicFilesTypeMetadata = {};
|
|
1277
|
+
this.setFilesType(this.generators);
|
|
1278
|
+
this.dateFromSubject
|
|
1279
|
+
.pipe(debounceTime(300), takeUntilDestroyed())
|
|
1280
|
+
.subscribe(async (updatedDate) => {
|
|
1281
|
+
await this.updateDateAndFetchData('dateFrom', updatedDate);
|
|
1282
|
+
});
|
|
1283
|
+
this.dateToSubject
|
|
1284
|
+
.pipe(debounceTime(300), takeUntilDestroyed())
|
|
1285
|
+
.subscribe(async (updatedDate) => {
|
|
1286
|
+
await this.updateDateAndFetchData('dateTo', updatedDate);
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
setFilesType(generators) {
|
|
1290
|
+
generators.forEach(g => {
|
|
1291
|
+
this.dynamicFilesTypeMetadata[g.getType()] = {
|
|
1292
|
+
label: g.getLabel(),
|
|
1293
|
+
icon: g.getIcon(),
|
|
1294
|
+
title: g.getTitle(),
|
|
1295
|
+
extension: g.getFileExtension(),
|
|
1296
|
+
zipName: g.getZipName(),
|
|
1297
|
+
type: g.getType()
|
|
1298
|
+
};
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
async ngOnInit() {
|
|
1302
|
+
const sourcesPermittedToRead = await this.datapointsExportSelectorService.getSourcesWithPermissionsToRead(this.exportConfig.datapointDetails);
|
|
1303
|
+
if (sourcesPermittedToRead.length === 0) {
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
if (sourcesPermittedToRead.length !== this.exportConfig.datapointDetails.length) {
|
|
1307
|
+
this.exportConfig.datapointDetails = this.exportConfig.datapointDetails.filter(dataPoint => sourcesPermittedToRead.includes(String(dataPoint.source)));
|
|
1308
|
+
}
|
|
1309
|
+
this.hasPermissionToReadAnyMeasurements = true;
|
|
1310
|
+
this.storeInitialChangeableConfig();
|
|
1311
|
+
this.fileTypeSelectionState = this.getInitialSelection(this.dynamicFilesTypeMetadata, [0]);
|
|
1312
|
+
this.formGroup = this.createForm();
|
|
1313
|
+
this.dataToExport = await this.datapointsExportSelectorService.fetchAndPrepareDataToExport(this.exportConfig, this.isFullTypeOfExport);
|
|
1314
|
+
this.handleDateSelectorChanges();
|
|
1315
|
+
this.determineShowingPreviewOrEmptyState();
|
|
1316
|
+
this.handleExportModeChanges();
|
|
1317
|
+
this.handleFileTypeSelectionChanges();
|
|
1318
|
+
this.handleFormValidationChanges();
|
|
1319
|
+
this.updateFileTypeControl();
|
|
1320
|
+
this.updateDisabledAggregationOptionsState();
|
|
1321
|
+
}
|
|
1322
|
+
ngOnDestroy() {
|
|
1323
|
+
this.destroy$.next();
|
|
1324
|
+
this.destroy$.complete();
|
|
1325
|
+
this.loadInitialChangeableConfig();
|
|
1326
|
+
}
|
|
1327
|
+
async exportAndDownload() {
|
|
1328
|
+
const fileExports = Object.entries(this.dynamicFilesTypeMetadata).map(([_, value]) => ({
|
|
1329
|
+
fileType: value.type,
|
|
1330
|
+
zipName: value.zipName
|
|
1331
|
+
}));
|
|
1332
|
+
const selectedFileExports = this.getOnlySelectedFileExports(fileExports);
|
|
1333
|
+
await Promise.all(selectedFileExports.map(async ({ fileType, zipName }) => {
|
|
1334
|
+
const blob = await this.exportFile(fileType);
|
|
1335
|
+
await this.downloadFile(blob, fileType, zipName);
|
|
1336
|
+
}));
|
|
1337
|
+
this.datapointsExportSelectorFileExporterService.cleanupCachedData();
|
|
1338
|
+
}
|
|
1339
|
+
getOnlySelectedFileExports(fileExports) {
|
|
1340
|
+
return fileExports.filter(f => this.fileTypeSelectionState[f.fileType] === true);
|
|
1341
|
+
}
|
|
1342
|
+
async exportFile(fileType) {
|
|
1343
|
+
return this.isFullTypeOfExport
|
|
1344
|
+
? await this.exportMeasurementFile(fileType)
|
|
1345
|
+
: await this.exportSeriesFile(fileType);
|
|
1346
|
+
}
|
|
1347
|
+
async downloadFile(blob, fileType, measurementsZipFileName) {
|
|
1348
|
+
this.datapointsExceedingBrowserDownloadLimit =
|
|
1349
|
+
this.datapointsExceedingBrowserDownloadLimit.filter(datapoint => !this.dataPointsExceedingOneMillionLimit.includes(datapoint));
|
|
1350
|
+
const shouldShowAlert = this.datapointsExceedingBrowserDownloadLimit.length > 0;
|
|
1351
|
+
// Handles a case where all files are sent by email.
|
|
1352
|
+
if (!blob && shouldShowAlert) {
|
|
1353
|
+
this.datapointsExportSelectorService.showSendViaEmailInfoAlert(fileType, this.datapointsExceedingBrowserDownloadLimit);
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (blob) {
|
|
1357
|
+
// Handles the case where some of the files are sent by email.
|
|
1358
|
+
if (shouldShowAlert) {
|
|
1359
|
+
this.datapointsExportSelectorService.showSendViaEmailInfoAlert(fileType, this.datapointsExceedingBrowserDownloadLimit);
|
|
1360
|
+
}
|
|
1361
|
+
this.isFullTypeOfExport
|
|
1362
|
+
? saveAs(blob, measurementsZipFileName)
|
|
1363
|
+
: saveAs(blob, blob[SERIES_DATA_MERGED_FILE_NAME]);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Exports a measurement file of the specified type.
|
|
1368
|
+
*
|
|
1369
|
+
* Measurements API does provide a way to fetch a preprocessed CSV/Excel file.
|
|
1370
|
+
*
|
|
1371
|
+
* At this point a backed file needs to be fetched.
|
|
1372
|
+
* Measurement data used for a preview contains just 5 first records
|
|
1373
|
+
* and it's not fetched with CSV/Excel headers.
|
|
1374
|
+
*
|
|
1375
|
+
* @param fileType - The type of file to export.
|
|
1376
|
+
* @returns A Promise that resolves to a Blob representing the exported file or null when no data to export or it's sent via email.
|
|
1377
|
+
*/
|
|
1378
|
+
async exportMeasurementFile(fileType) {
|
|
1379
|
+
const { acceptType, extension } = this.datapointsExportModalService.fileTypesConfigs[fileType];
|
|
1380
|
+
const updatedExportConfig = this.excludeDatapointsThatExceedsApiLimit(this.exportConfig);
|
|
1381
|
+
const dataToExport = await this.datapointsExportSelectorService.fetchMeasurementDataFilesAndPairWithSourceDetails(acceptType, updatedExportConfig);
|
|
1382
|
+
const isAllUndefined = dataToExport.every(data => data === undefined);
|
|
1383
|
+
if (isAllUndefined) {
|
|
1384
|
+
return null;
|
|
1385
|
+
}
|
|
1386
|
+
return this.datapointsExportSelectorFileExporterService.getMeasurementExportedDataBlob(extension, dataToExport);
|
|
1387
|
+
}
|
|
1388
|
+
excludeDatapointsThatExceedsApiLimit(exportConfig) {
|
|
1389
|
+
const skippedDatapointsSource = this.dataPointsExceedingOneMillionLimit.map(datapoint => datapoint.datapointDetail.source.toString());
|
|
1390
|
+
const filteredDatapoints = exportConfig.datapointDetails.filter(datapoint => !skippedDatapointsSource.includes(String(datapoint.source)));
|
|
1391
|
+
exportConfig.datapointDetails = filteredDatapoints;
|
|
1392
|
+
return exportConfig;
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Exports the series data to a file of the specified type.
|
|
1396
|
+
*
|
|
1397
|
+
* Series API does not provide a way to fetch a preprocessed CSV/Excel file.
|
|
1398
|
+
*
|
|
1399
|
+
* Series data used for exporting a file is already fetched for a preview
|
|
1400
|
+
* and can be reused for exporting.
|
|
1401
|
+
*
|
|
1402
|
+
* @param fileType - The type of file to export (e.g., CSV, Excel).
|
|
1403
|
+
* @returns A Promise that resolves to a Blob representing the exported file.
|
|
1404
|
+
*/
|
|
1405
|
+
async exportSeriesFile(fileType) {
|
|
1406
|
+
const mergedExportDetails = {
|
|
1407
|
+
aggregation: this.exportConfig.aggregation,
|
|
1408
|
+
dateFrom: this.exportConfig.dateFrom,
|
|
1409
|
+
dateTo: this.exportConfig.dateTo
|
|
1410
|
+
};
|
|
1411
|
+
return this.datapointsExportSelectorFileExporterService.getSeriesExportedDataBlob(fileType, this.dataToExport, mergedExportDetails);
|
|
1412
|
+
}
|
|
1413
|
+
async onAggregationChange(aggregation) {
|
|
1414
|
+
this.exportConfig.aggregation = aggregation;
|
|
1415
|
+
if (!this.isAggregationChangedProgrammatically) {
|
|
1416
|
+
await this.updateExportConfigAndFetchData();
|
|
1417
|
+
this.updateDownloadButtonState();
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
onDateFromChange(updatedDate) {
|
|
1421
|
+
this.dateFromSubject.next(updatedDate);
|
|
1422
|
+
}
|
|
1423
|
+
onDateToChange(updatedDate) {
|
|
1424
|
+
this.dateToSubject.next(updatedDate);
|
|
1425
|
+
}
|
|
1426
|
+
async updateDateAndFetchData(dateType, updatedDate) {
|
|
1427
|
+
this.exportConfig[dateType] = updatedDate;
|
|
1428
|
+
await this.updateExportConfigAndFetchData();
|
|
1429
|
+
if (this.isFullTypeOfExport) {
|
|
1430
|
+
await this.determineIfMeasurementResponseWillBeProcessedByBackend();
|
|
1431
|
+
}
|
|
1432
|
+
else {
|
|
1433
|
+
this.updateDownloadButtonState();
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
async onExportTypeChange(exportType) {
|
|
1437
|
+
if (exportType === EXPORT_MODE_VALUES.full) {
|
|
1438
|
+
this.isAggregationChangedProgrammatically = true;
|
|
1439
|
+
this.formGroup.controls.aggregation.setValue(AGGREGATION_VALUES.none);
|
|
1440
|
+
this.isAggregationChangedProgrammatically = false;
|
|
1441
|
+
this.isFullTypeOfExport = true;
|
|
1442
|
+
await this.updateExportConfigAndFetchData();
|
|
1443
|
+
await this.determineIfMeasurementResponseWillBeProcessedByBackend();
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
this.resetFullExportRelatedProperties();
|
|
1447
|
+
this.isFullTypeOfExport = false;
|
|
1448
|
+
await this.updateExportConfigAndFetchData();
|
|
1449
|
+
this.updateDisabledAggregationOptionsState();
|
|
1450
|
+
this.updateDownloadButtonState();
|
|
1451
|
+
}
|
|
1452
|
+
updateDisabledAggregationOptionsState() {
|
|
1453
|
+
this.disabledAggregationOptions =
|
|
1454
|
+
this.datapointsExportSelectorFileExporterService.updateDisabledStateOfAggregationOptionEntries(this.formGroup.controls.dateFrom.value, this.formGroup.controls.dateTo.value, this.disabledAggregationOptions);
|
|
1455
|
+
}
|
|
1456
|
+
createForm() {
|
|
1457
|
+
return this.formBuilder.group({
|
|
1458
|
+
dateFrom: new FormControl(this.exportConfig.dateFrom),
|
|
1459
|
+
dateTo: new FormControl(this.exportConfig.dateTo),
|
|
1460
|
+
aggregation: new FormControl(this.exportConfig.aggregation ? this.exportConfig.aggregation : AGGREGATION_VALUES.none),
|
|
1461
|
+
exportMode: new FormControl(EXPORT_MODE_VALUES.compact),
|
|
1462
|
+
fileTypes: this.formBuilder.group(this.generators.reduce((a, c) => {
|
|
1463
|
+
a[c.getType()] = false;
|
|
1464
|
+
return a;
|
|
1465
|
+
}, {}))
|
|
1466
|
+
}, {
|
|
1467
|
+
validators: [dateRangeValidator]
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
preparePreview() {
|
|
1471
|
+
this.isPreviewLoading = true;
|
|
1472
|
+
this.previewTableData =
|
|
1473
|
+
this.datapointsExportModalService.transformToExportFileStructureForPreview(this.dataToExport);
|
|
1474
|
+
this.isPreviewLoading = false;
|
|
1475
|
+
}
|
|
1476
|
+
handleDateSelectorChanges() {
|
|
1477
|
+
const aggregationControl = this.formGroup.controls.aggregation;
|
|
1478
|
+
const dateFromSelector = this.formGroup.controls.dateFrom;
|
|
1479
|
+
const dateToSelector = this.formGroup.controls.dateTo;
|
|
1480
|
+
const updateAggregationOptions = () => {
|
|
1481
|
+
if (!this.isFullTypeOfExport) {
|
|
1482
|
+
this.updateDisabledAggregationOptionsState();
|
|
1483
|
+
const firstAvailableAggregationValue = this.datapointsExportSelectorFileExporterService.setToFirstAvailableAggregationOptionIfCurrentIsDisabled(aggregationControl.value, this.disabledAggregationOptions);
|
|
1484
|
+
if (firstAvailableAggregationValue) {
|
|
1485
|
+
aggregationControl.setValue(firstAvailableAggregationValue);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
merge(dateFromSelector.valueChanges, dateToSelector.valueChanges)
|
|
1490
|
+
.pipe(takeUntil(this.destroy$))
|
|
1491
|
+
.subscribe(updateAggregationOptions);
|
|
1492
|
+
}
|
|
1493
|
+
handleExportModeChanges() {
|
|
1494
|
+
const aggregationControl = this.formGroup.controls.aggregation;
|
|
1495
|
+
this.formGroup.controls.exportMode.valueChanges
|
|
1496
|
+
.pipe(takeUntil(this.destroy$))
|
|
1497
|
+
.subscribe(exportModeValue => {
|
|
1498
|
+
if (exportModeValue.toString() === EXPORT_MODE_VALUES.full) {
|
|
1499
|
+
aggregationControl.disable();
|
|
1500
|
+
}
|
|
1501
|
+
else {
|
|
1502
|
+
aggregationControl.enable();
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
handleFileTypeSelectionChanges() {
|
|
1507
|
+
this.formGroup.controls.fileTypes.valueChanges
|
|
1508
|
+
.pipe(takeUntil(this.destroy$))
|
|
1509
|
+
.subscribe(fileTypesSelectionState => {
|
|
1510
|
+
this.updateFileTypeSelectionState(fileTypesSelectionState);
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
updateFileTypeSelectionState(checkboxesState) {
|
|
1514
|
+
this.isAnyOfExportFileTypeChecked = Object.values(checkboxesState).some(value => value === true);
|
|
1515
|
+
this.fileTypeSelectionState = checkboxesState;
|
|
1516
|
+
this.updateDownloadButtonState();
|
|
1517
|
+
}
|
|
1518
|
+
handleFormValidationChanges() {
|
|
1519
|
+
this.formGroup.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
|
1520
|
+
this.updateDownloadButtonState();
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
updateFileTypeControl() {
|
|
1524
|
+
const fileTypesControl = this.formGroup.controls.fileTypes;
|
|
1525
|
+
if (fileTypesControl) {
|
|
1526
|
+
const currentValue = fileTypesControl.value || {};
|
|
1527
|
+
const updatedValue = { ...currentValue };
|
|
1528
|
+
Object.entries(this.fileTypeSelectionState).forEach(([fileType, isSelected]) => {
|
|
1529
|
+
if (fileType in currentValue) {
|
|
1530
|
+
updatedValue[fileType] = isSelected;
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1533
|
+
fileTypesControl.patchValue(updatedValue);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
determineShowingPreviewOrEmptyState() {
|
|
1537
|
+
const hasDataToShow = this.dataToExport.some(dataToExportItem => this.isFullTypeOfExport
|
|
1538
|
+
? !!dataToExportItem
|
|
1539
|
+
: dataToExportItem.timeValueMap && Object.keys(dataToExportItem.timeValueMap).length > 0);
|
|
1540
|
+
this.hasFetchedDataAnyValuesToExport = hasDataToShow;
|
|
1541
|
+
if (hasDataToShow) {
|
|
1542
|
+
this.preparePreview();
|
|
1543
|
+
}
|
|
1544
|
+
else {
|
|
1545
|
+
this.isPreviewLoading = false;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
async determineIfMeasurementResponseWillBeProcessedByBackend() {
|
|
1549
|
+
this.isPreviewLoading = true;
|
|
1550
|
+
this.datapointsExceedingBrowserDownloadLimit =
|
|
1551
|
+
await this.datapointsExportSelectorService.getDatapointsExceedingLimit(this.exportConfig);
|
|
1552
|
+
const { browserDownloadableCount, emailDeliverableCount, nonRetrievableCount } = this.calculateDatapointCounts();
|
|
1553
|
+
this.hasNoExportableData =
|
|
1554
|
+
browserDownloadableCount === 0 && emailDeliverableCount === 0 && nonRetrievableCount > 0;
|
|
1555
|
+
if (this.datapointsExceedingBrowserDownloadLimit.length > 0 && !this.hasNoExportableData) {
|
|
1556
|
+
this.limitExceededMessage = this.datapointsExportSelectorService.getLimitExceededMessage(this.hasNoExportableData, emailDeliverableCount, browserDownloadableCount, nonRetrievableCount, this.exportConfig.datapointDetails.length);
|
|
1557
|
+
}
|
|
1558
|
+
if (this.datapointsExceedingBrowserDownloadLimit.length > 0 && this.hasNoExportableData) {
|
|
1559
|
+
this.limitExceededMessage = this.datapointsExportSelectorService.getLimitExceededMessage(this.hasNoExportableData);
|
|
1560
|
+
}
|
|
1561
|
+
this.updateDownloadButtonState();
|
|
1562
|
+
this.isPreviewLoading = false;
|
|
1563
|
+
}
|
|
1564
|
+
calculateDatapointCounts() {
|
|
1565
|
+
const RECORD_API_LIMIT = 1_000_000;
|
|
1566
|
+
this.dataPointsExceedingOneMillionLimit = this.datapointsExceedingBrowserDownloadLimit.filter(datapointsExceedingBrowserDownloadLimit => datapointsExceedingBrowserDownloadLimit.totalElements > RECORD_API_LIMIT);
|
|
1567
|
+
const nonRetrievableCount = this.dataPointsExceedingOneMillionLimit.length;
|
|
1568
|
+
const emailDeliverableCount = this.datapointsExceedingBrowserDownloadLimit.length - nonRetrievableCount;
|
|
1569
|
+
const browserDownloadableCount = this.exportConfig.datapointDetails.length - emailDeliverableCount - nonRetrievableCount;
|
|
1570
|
+
return {
|
|
1571
|
+
browserDownloadableCount,
|
|
1572
|
+
emailDeliverableCount,
|
|
1573
|
+
nonRetrievableCount
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
storeInitialChangeableConfig() {
|
|
1577
|
+
this.initialDateFrom = this.exportConfig.dateFrom;
|
|
1578
|
+
this.initialDateTo = this.exportConfig.dateTo;
|
|
1579
|
+
this.initialDatapointDetails = this.exportConfig.datapointDetails;
|
|
1580
|
+
if (!this.isFullTypeOfExport) {
|
|
1581
|
+
this.initialAggregation = this.exportConfig.aggregation;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
loadInitialChangeableConfig() {
|
|
1585
|
+
this.exportConfig.dateFrom = this.initialDateFrom;
|
|
1586
|
+
this.exportConfig.dateTo = this.initialDateTo;
|
|
1587
|
+
this.exportConfig.datapointDetails = this.initialDatapointDetails;
|
|
1588
|
+
if (!this.isFullTypeOfExport) {
|
|
1589
|
+
this.exportConfig.aggregation = this.initialAggregation;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
async updateExportConfigAndFetchData() {
|
|
1593
|
+
if (this.formGroup.invalid) {
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
this.isPreviewLoading = true;
|
|
1597
|
+
this.onDownloadButtonStateChange.emit(false);
|
|
1598
|
+
this.dataToExport = await this.datapointsExportSelectorService.fetchAndPrepareDataToExport(this.exportConfig, this.isFullTypeOfExport);
|
|
1599
|
+
this.determineShowingPreviewOrEmptyState();
|
|
1600
|
+
}
|
|
1601
|
+
updateDownloadButtonState() {
|
|
1602
|
+
const canEnableDownloadButton = this.hasFetchedDataAnyValuesToExport &&
|
|
1603
|
+
!this.formGroup.invalid &&
|
|
1604
|
+
this.isAnyOfExportFileTypeChecked &&
|
|
1605
|
+
!this.hasNoExportableData;
|
|
1606
|
+
this.onDownloadButtonStateChange.emit(canEnableDownloadButton);
|
|
1607
|
+
}
|
|
1608
|
+
resetFullExportRelatedProperties() {
|
|
1609
|
+
this.hasNoExportableData = false;
|
|
1610
|
+
this.datapointsExceedingBrowserDownloadLimit = [];
|
|
1611
|
+
}
|
|
1612
|
+
getInitialSelection(fileType, selectByDefault) {
|
|
1613
|
+
const keys = Object.keys(fileType);
|
|
1614
|
+
if (keys.length === 0) {
|
|
1615
|
+
return {};
|
|
1616
|
+
}
|
|
1617
|
+
const initialSelection = keys.reduce((acc, key) => {
|
|
1618
|
+
acc[key] = false;
|
|
1619
|
+
return acc;
|
|
1620
|
+
}, {});
|
|
1621
|
+
selectByDefault.forEach(index => {
|
|
1622
|
+
if (index >= 0 && index < keys.length) {
|
|
1623
|
+
initialSelection[keys[index]] = true;
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
return initialSelection;
|
|
1627
|
+
}
|
|
1628
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DatapointsExportSelectorFileExporterComponent, deps: [{ token: DataProcessingService }, { token: DataFetchingService }, { token: DatapointsExportSelectorFileExporterService }, { token: i3$2.FormBuilder }, { token: FILE_GENERATORS }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1629
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.9", type: DatapointsExportSelectorFileExporterComponent, isStandalone: true, selector: "c8y-datapoints-export-selector-file-exporter", inputs: { exportConfig: "exportConfig" }, outputs: { onDownloadButtonStateChange: "onDownloadButtonStateChange" }, ngImport: i0, template: "<div class=\"p-b-16\">\n <ng-container *ngIf=\"hasPermissionToReadAnyMeasurements; else hasNoRoleToReadAnyMeasurements\">\n <div class=\"p-16 text-center separator-bottom sticky-top bg-component\">\n <p class=\"text-medium text-16\">\n {{ 'Configure export' | translate }}\n </p>\n </div>\n <div\n class=\"p-t-24 p-r-16 p-l-16 gap-8 p-b-16 flex-wrap\"\n [ngClass]=\"{ 'separator-bottom': isFullTypeOfExport }\"\n >\n <div class=\"row\">\n <div class=\"col-md-4\">\n <c8y-datapoints-export-selector-time-range\n *ngIf=\"this.hasPermissionToReadAnyMeasurements\"\n [formGroup]=\"formGroup\"\n (onDateFromChange)=\"onDateFromChange($event)\"\n (onDateToChange)=\"onDateToChange($event)\"\n ></c8y-datapoints-export-selector-time-range>\n </div>\n <div class=\"col-md-4\">\n <c8y-datapoints-export-selector-data-scope\n *ngIf=\"this.hasPermissionToReadAnyMeasurements\"\n [disabledAggregationOptions]=\"disabledAggregationOptions\"\n [formGroup]=\"formGroup\"\n (onAggregationChange)=\"onAggregationChange($event)\"\n (onExportTypeChange)=\"onExportTypeChange($event)\"\n ></c8y-datapoints-export-selector-data-scope>\n </div>\n <div class=\"col-md-4\">\n <c8y-datapoints-export-selector-file-types\n *ngIf=\"this.hasPermissionToReadAnyMeasurements\"\n [dynamicFilesTypeMetadata]=\"dynamicFilesTypeMetadata\"\n [formGroup]=\"formGroup\"\n ></c8y-datapoints-export-selector-file-types>\n </div>\n </div>\n </div>\n <ng-container\n *ngIf=\"!isFullTypeOfExport && !hasFetchedDataAnyValuesToExport && !isPreviewLoading\"\n >\n <div class=\"p-t-24 p-r-16 p-l-16 p-b-16 separator-bottom d-flex\">\n <div\n class=\"alert alert-warning center-block\"\n role=\"alert\"\n data-cy=\"file-exporter--no-data-available\"\n translate\n >\n No data available.\n </div>\n </div>\n </ng-container>\n <ng-container *ngIf=\"datapointsExceedingBrowserDownloadLimit.length > 0\">\n <div class=\"p-t-24 p-r-16 p-l-16 p-b-16 separator-bottom d-flex\">\n <div\n [class]=\"\n hasNoExportableData\n ? 'alert alert-warning center-block'\n : 'alert alert-info center-block'\n \"\n role=\"alert\"\n [innerHTML]=\"limitExceededMessage\"\n ></div>\n </div>\n </ng-container>\n <ng-container *ngIf=\"isFullTypeOfExport\">\n <c8y-datapoints-export-selector-preview\n *ngIf=\"this.hasPermissionToReadAnyMeasurements\"\n [hasFetchedDataAnyValuesToExport]=\"hasFetchedDataAnyValuesToExport\"\n [isPreviewLoading]=\"isPreviewLoading\"\n [previewTableData]=\"previewTableData\"\n ></c8y-datapoints-export-selector-preview>\n </ng-container>\n </ng-container>\n <ng-template #hasNoRoleToReadAnyMeasurements>\n <div class=\"p-t-24 p-r-16 p-l-16 p-b-16 d-flex\">\n <div\n class=\"alert alert-info center-block\"\n role=\"alert\"\n translate\n >\n <p>To export data, you must meet at least one of these criteria:</p>\n <ul>\n <li>\n Have\n <b>READ permission for \"Measurements\" permission type</b>\n (either as a global role or for the specific source)\n </li>\n <li>\n Be the\n <b>owner of the source</b>\n you want to export data from\n </li>\n </ul>\n <p>Don't meet these requirements? Contact your system administrator for assistance.</p>\n </div>\n </div>\n </ng-template>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CoreModule }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }, { kind: "directive", type: i1.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: i2$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: DataPointsExportSelectorDataScopeComponent, selector: "c8y-datapoints-export-selector-data-scope", inputs: ["disabledAggregationOptions", "formGroup"], outputs: ["onAggregationChange", "onExportTypeChange"] }, { kind: "component", type: DataPointsExportSelectorFileTypesComponent, selector: "c8y-datapoints-export-selector-file-types", inputs: ["dynamicFilesTypeMetadata", "formGroup"] }, { kind: "component", type: DataPointsExportSelectorPreviewComponent, selector: "c8y-datapoints-export-selector-preview", inputs: ["hasFetchedDataAnyValuesToExport", "isPreviewLoading", "previewTableData"] }, { kind: "component", type: DataPointsExportSelectorTimeRangeComponent, selector: "c8y-datapoints-export-selector-time-range", inputs: ["formGroup"], outputs: ["onDateFromChange", "onDateToChange"] }, { kind: "ngmodule", type: ReactiveFormsModule }] }); }
|
|
1630
|
+
}
|
|
1631
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DatapointsExportSelectorFileExporterComponent, decorators: [{
|
|
1632
|
+
type: Component,
|
|
1633
|
+
args: [{ selector: 'c8y-datapoints-export-selector-file-exporter', standalone: true, imports: [
|
|
1634
|
+
CoreModule,
|
|
1635
|
+
DataPointsExportSelectorDataScopeComponent,
|
|
1636
|
+
DataPointsExportSelectorFileTypesComponent,
|
|
1637
|
+
DataPointsExportSelectorPreviewComponent,
|
|
1638
|
+
DataPointsExportSelectorTimeRangeComponent,
|
|
1639
|
+
KeyValuePipe,
|
|
1640
|
+
ReactiveFormsModule
|
|
1641
|
+
], template: "<div class=\"p-b-16\">\n <ng-container *ngIf=\"hasPermissionToReadAnyMeasurements; else hasNoRoleToReadAnyMeasurements\">\n <div class=\"p-16 text-center separator-bottom sticky-top bg-component\">\n <p class=\"text-medium text-16\">\n {{ 'Configure export' | translate }}\n </p>\n </div>\n <div\n class=\"p-t-24 p-r-16 p-l-16 gap-8 p-b-16 flex-wrap\"\n [ngClass]=\"{ 'separator-bottom': isFullTypeOfExport }\"\n >\n <div class=\"row\">\n <div class=\"col-md-4\">\n <c8y-datapoints-export-selector-time-range\n *ngIf=\"this.hasPermissionToReadAnyMeasurements\"\n [formGroup]=\"formGroup\"\n (onDateFromChange)=\"onDateFromChange($event)\"\n (onDateToChange)=\"onDateToChange($event)\"\n ></c8y-datapoints-export-selector-time-range>\n </div>\n <div class=\"col-md-4\">\n <c8y-datapoints-export-selector-data-scope\n *ngIf=\"this.hasPermissionToReadAnyMeasurements\"\n [disabledAggregationOptions]=\"disabledAggregationOptions\"\n [formGroup]=\"formGroup\"\n (onAggregationChange)=\"onAggregationChange($event)\"\n (onExportTypeChange)=\"onExportTypeChange($event)\"\n ></c8y-datapoints-export-selector-data-scope>\n </div>\n <div class=\"col-md-4\">\n <c8y-datapoints-export-selector-file-types\n *ngIf=\"this.hasPermissionToReadAnyMeasurements\"\n [dynamicFilesTypeMetadata]=\"dynamicFilesTypeMetadata\"\n [formGroup]=\"formGroup\"\n ></c8y-datapoints-export-selector-file-types>\n </div>\n </div>\n </div>\n <ng-container\n *ngIf=\"!isFullTypeOfExport && !hasFetchedDataAnyValuesToExport && !isPreviewLoading\"\n >\n <div class=\"p-t-24 p-r-16 p-l-16 p-b-16 separator-bottom d-flex\">\n <div\n class=\"alert alert-warning center-block\"\n role=\"alert\"\n data-cy=\"file-exporter--no-data-available\"\n translate\n >\n No data available.\n </div>\n </div>\n </ng-container>\n <ng-container *ngIf=\"datapointsExceedingBrowserDownloadLimit.length > 0\">\n <div class=\"p-t-24 p-r-16 p-l-16 p-b-16 separator-bottom d-flex\">\n <div\n [class]=\"\n hasNoExportableData\n ? 'alert alert-warning center-block'\n : 'alert alert-info center-block'\n \"\n role=\"alert\"\n [innerHTML]=\"limitExceededMessage\"\n ></div>\n </div>\n </ng-container>\n <ng-container *ngIf=\"isFullTypeOfExport\">\n <c8y-datapoints-export-selector-preview\n *ngIf=\"this.hasPermissionToReadAnyMeasurements\"\n [hasFetchedDataAnyValuesToExport]=\"hasFetchedDataAnyValuesToExport\"\n [isPreviewLoading]=\"isPreviewLoading\"\n [previewTableData]=\"previewTableData\"\n ></c8y-datapoints-export-selector-preview>\n </ng-container>\n </ng-container>\n <ng-template #hasNoRoleToReadAnyMeasurements>\n <div class=\"p-t-24 p-r-16 p-l-16 p-b-16 d-flex\">\n <div\n class=\"alert alert-info center-block\"\n role=\"alert\"\n translate\n >\n <p>To export data, you must meet at least one of these criteria:</p>\n <ul>\n <li>\n Have\n <b>READ permission for \"Measurements\" permission type</b>\n (either as a global role or for the specific source)\n </li>\n <li>\n Be the\n <b>owner of the source</b>\n you want to export data from\n </li>\n </ul>\n <p>Don't meet these requirements? Contact your system administrator for assistance.</p>\n </div>\n </div>\n </ng-template>\n</div>\n" }]
|
|
1642
|
+
}], ctorParameters: () => [{ type: DataProcessingService }, { type: DataFetchingService }, { type: DatapointsExportSelectorFileExporterService }, { type: i3$2.FormBuilder }, { type: undefined, decorators: [{
|
|
1643
|
+
type: Inject,
|
|
1644
|
+
args: [FILE_GENERATORS]
|
|
1645
|
+
}] }], propDecorators: { exportConfig: [{
|
|
1646
|
+
type: Input
|
|
1647
|
+
}], onDownloadButtonStateChange: [{
|
|
1648
|
+
type: Output
|
|
1649
|
+
}] } });
|
|
1650
|
+
|
|
1651
|
+
class DataProcessingService {
|
|
1652
|
+
constructor(fileGenerators, utils) {
|
|
1653
|
+
this.fileGenerators = fileGenerators;
|
|
1654
|
+
this.utils = utils;
|
|
1655
|
+
this.mergeMapping = {};
|
|
1656
|
+
this.setGenerators(this.fileGenerators);
|
|
1657
|
+
this.fileTypesConfigs = this.fileGenerators.reduce((acc, g) => {
|
|
1658
|
+
acc[g.getType()] = {
|
|
1659
|
+
extension: g.getFileExtension(),
|
|
1660
|
+
mimeType: g.getMimeType(),
|
|
1661
|
+
acceptType: g.getAcceptType()
|
|
1662
|
+
};
|
|
1663
|
+
return acc;
|
|
1664
|
+
}, {});
|
|
1665
|
+
}
|
|
1666
|
+
setGenerators(fileGenerators) {
|
|
1667
|
+
fileGenerators.forEach(g => {
|
|
1668
|
+
this.mergeMapping[g.getType()] = g.generateMerged;
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Transforms the data into a structured format for an export.
|
|
1673
|
+
*
|
|
1674
|
+
* @param dataToExport - An array of processed measurement data combined with the respective properties of the datapoint.
|
|
1675
|
+
* @returns Provides a two-dimensional array of ExportData,
|
|
1676
|
+
* where each inner array contains ExportData entries representing the transformed measurements and metadata of each datapoint.
|
|
1677
|
+
*/
|
|
1678
|
+
transformToExportFileStructure(dataToExport) {
|
|
1679
|
+
const exportData = [];
|
|
1680
|
+
for (const data of dataToExport) {
|
|
1681
|
+
const rowsData = this.processDataToExport(data);
|
|
1682
|
+
exportData.push(rowsData);
|
|
1683
|
+
}
|
|
1684
|
+
return exportData;
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Transforms the input datapoints with values into a structured format for a preview.
|
|
1688
|
+
*
|
|
1689
|
+
* @param dataToExport - An array of processed measurement data combined with the respective properties of the datapoint.
|
|
1690
|
+
* @returns Provides an array of up to 5 ExportData elements (no more is needed for a preview) or an empty array,
|
|
1691
|
+
*/
|
|
1692
|
+
transformToExportFileStructureForPreview(dataToExport) {
|
|
1693
|
+
const data = dataToExport.find(data => {
|
|
1694
|
+
return data !== null && data.timeValueMap && data.unit;
|
|
1695
|
+
});
|
|
1696
|
+
if (!data) {
|
|
1697
|
+
return [];
|
|
1698
|
+
}
|
|
1699
|
+
const exportData = this.processDataToExport(data);
|
|
1700
|
+
return exportData.slice(0, MEASUREMENTS_PREVIEW_ITEMS_LIMIT);
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Processes a single dataToExport and transforms it into an array of ExportData.
|
|
1704
|
+
*
|
|
1705
|
+
* Used further for creating series data based export and also for measurements and series data based preview.
|
|
1706
|
+
* Series data provides min and max values for each timestamp.
|
|
1707
|
+
*
|
|
1708
|
+
* @param dataToExport - A processed measurement or series data combined with the respective properties of the datapoint to be precessed
|
|
1709
|
+
* @returns An array of ExportData representing the processed datapoint,
|
|
1710
|
+
* that can be used as a row data in the exported files or for preview.
|
|
1711
|
+
*/
|
|
1712
|
+
processDataToExport(dataToExport) {
|
|
1713
|
+
const rowsData = [];
|
|
1714
|
+
// When measurements or series data is not available, add an empty header row with only device_name, source and fragment_series.
|
|
1715
|
+
// This data is needed later to be used to create the file name and merged file header string.
|
|
1716
|
+
const responseDoesNotContainData = !dataToExport.timeValueMap?.size && !dataToExport.unit;
|
|
1717
|
+
if (responseDoesNotContainData) {
|
|
1718
|
+
rowsData.push({
|
|
1719
|
+
time: null,
|
|
1720
|
+
source: dataToExport.source,
|
|
1721
|
+
device_name: dataToExport.deviceName,
|
|
1722
|
+
fragment_series: `${dataToExport.valueFragmentType}.${dataToExport.valueFragmentSeries}`,
|
|
1723
|
+
value: null,
|
|
1724
|
+
value_min: null,
|
|
1725
|
+
value_max: null,
|
|
1726
|
+
unit: null
|
|
1727
|
+
});
|
|
1728
|
+
return rowsData;
|
|
1729
|
+
}
|
|
1730
|
+
for (const [time, value] of Object.entries(dataToExport.timeValueMap)) {
|
|
1731
|
+
// Measurements data contains only one value as a number, series data contains object with min and max values.
|
|
1732
|
+
const isMeasurementData = typeof value === 'number';
|
|
1733
|
+
if (isMeasurementData) {
|
|
1734
|
+
rowsData.push(this.createExportData(dataToExport, time, isMeasurementData, value));
|
|
1735
|
+
}
|
|
1736
|
+
else {
|
|
1737
|
+
rowsData.push(this.createExportData(dataToExport, time, isMeasurementData, value.min, value.max));
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
return rowsData;
|
|
1741
|
+
}
|
|
1742
|
+
createExportData(dataToExport, time, isMeasurement, value, valueMax) {
|
|
1743
|
+
const commonExportData = {
|
|
1744
|
+
time: new Date(time).toISOString(),
|
|
1745
|
+
source: dataToExport.source,
|
|
1746
|
+
device_name: dataToExport.deviceName,
|
|
1747
|
+
fragment_series: `${dataToExport.valueFragmentType}.${dataToExport.valueFragmentSeries}`,
|
|
1748
|
+
unit: dataToExport.unit
|
|
1749
|
+
};
|
|
1750
|
+
return isMeasurement
|
|
1751
|
+
? { ...commonExportData, value }
|
|
1752
|
+
: { ...commonExportData, value_min: value, value_max: valueMax };
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Exports the given data to merged CSV or Excel files.
|
|
1756
|
+
*
|
|
1757
|
+
* @param exportData - An array containing ExportData objects that will be used as parts of the merged file.
|
|
1758
|
+
* @param fileType - Indicates the type of the file to be exported.
|
|
1759
|
+
* @param mergedExportDetails - An object containing the date range from which the export was created and aggregation type.
|
|
1760
|
+
* @returns An objects containing file name and its respective content as Blob.
|
|
1761
|
+
*
|
|
1762
|
+
* Example of exported merged file structure:
|
|
1763
|
+
* date from date to
|
|
1764
|
+
* 2024-04-15T12:14:00.000Z 2024-07-16T14:14:28+02:00
|
|
1765
|
+
* time G6Fit -> c8y_Acceleration.accelerationX [G] G6Fit -> c8y_Acceleration.accelerationY [G]
|
|
1766
|
+
* 2024-05-13T13:45:10.500+02:00 0.0109026359624273 0.789461628278069
|
|
1767
|
+
*
|
|
1768
|
+
* Example of exported file name format:
|
|
1769
|
+
* 04dec231240-06dec232350.xlsx
|
|
1770
|
+
*/
|
|
1771
|
+
async exportSeriesDataToMergedFiles(exportData, fileType, mergedExportDetails) {
|
|
1772
|
+
const fileTypeConfig = this.fileTypesConfigs[fileType];
|
|
1773
|
+
mergedExportDetails.dateFrom = new Date(mergedExportDetails.dateFrom).toISOString();
|
|
1774
|
+
mergedExportDetails.dateTo = new Date(mergedExportDetails.dateTo).toISOString();
|
|
1775
|
+
const mergedFileContent = this.mergeMapping[fileType](exportData, mergedExportDetails);
|
|
1776
|
+
const mergedFileNameDateRange = this.utils.getFormattedDateRange(mergedExportDetails.dateFrom, mergedExportDetails.dateTo);
|
|
1777
|
+
return {
|
|
1778
|
+
fileName: `${mergedFileNameDateRange}.${fileTypeConfig.extension}`,
|
|
1779
|
+
fileData: new Blob([await mergedFileContent], { type: fileTypeConfig.mimeType })
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Zips all created files.
|
|
1784
|
+
*
|
|
1785
|
+
* @param files - An array of objects containing file names and their respective content as Blobs.
|
|
1786
|
+
* @returns A Promise that resolves to a Blob representing the generated zip file.
|
|
1787
|
+
*/
|
|
1788
|
+
async zipFiles(files) {
|
|
1789
|
+
const zip = new JSZip();
|
|
1790
|
+
files.forEach(file => {
|
|
1791
|
+
zip.file(file.fileName, file.fileData);
|
|
1792
|
+
});
|
|
1793
|
+
return await this.generateZipBlob(zip, FILE_COMPRESSION_TYPES_VALUES.deflate, 9);
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Exports the given data to CSV or Excel files.
|
|
1797
|
+
*
|
|
1798
|
+
* @param params - An object containing all the necessary parameters for the export and zip process.
|
|
1799
|
+
* @returns A Promise that resolves to a Blob representing the exported file.
|
|
1800
|
+
*/
|
|
1801
|
+
async exportSeriesData(params) {
|
|
1802
|
+
const { flattenedAndSortedExportData, fileType, mergedExportDetails } = params;
|
|
1803
|
+
const exportedMergedFile = await this.exportSeriesDataToMergedFiles(flattenedAndSortedExportData, fileType, mergedExportDetails);
|
|
1804
|
+
const blob = new Blob([exportedMergedFile.fileData], { type: fileType });
|
|
1805
|
+
Object.defineProperty(blob, SERIES_DATA_MERGED_FILE_NAME, {
|
|
1806
|
+
value: exportedMergedFile.fileName,
|
|
1807
|
+
writable: false,
|
|
1808
|
+
enumerable: false,
|
|
1809
|
+
configurable: true
|
|
1810
|
+
});
|
|
1811
|
+
return blob;
|
|
1812
|
+
}
|
|
1813
|
+
createFileName(source, fragmentSeries, fileExtension) {
|
|
1814
|
+
return `${source}_${fragmentSeries}.${fileExtension}`;
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Generates a zip blob using the provided zip object.
|
|
1818
|
+
*
|
|
1819
|
+
* @param zip - The zip object used for generating the blob.
|
|
1820
|
+
* @param compressionType - The compression type for the zip file. 'STORE' is no compression, 'DEFLATE' is compression.
|
|
1821
|
+
* @param compressionLevel - The compression level for the zip file.
|
|
1822
|
+
* Level 1 is quickest, 9 is best compressed.
|
|
1823
|
+
* @returns A Promise that resolves to a Blob containing the generated zip file.
|
|
1824
|
+
*/
|
|
1825
|
+
async generateZipBlob(zip, compressionType = FILE_COMPRESSION_TYPES_VALUES.store, compressionLevel = 1) {
|
|
1826
|
+
const blob = 'blob';
|
|
1827
|
+
if (compressionType === FILE_COMPRESSION_TYPES_VALUES.store) {
|
|
1828
|
+
return await zip.generateAsync({ type: blob });
|
|
1829
|
+
}
|
|
1830
|
+
return await zip.generateAsync({
|
|
1831
|
+
type: blob,
|
|
1832
|
+
compression: compressionType,
|
|
1833
|
+
compressionOptions: {
|
|
1834
|
+
level: compressionLevel
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DataProcessingService, deps: [{ token: FILE_GENERATORS }, { token: UtilsService }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1839
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DataProcessingService, providedIn: 'root' }); }
|
|
1840
|
+
}
|
|
1841
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DataProcessingService, decorators: [{
|
|
1842
|
+
type: Injectable,
|
|
1843
|
+
args: [{
|
|
1844
|
+
providedIn: 'root'
|
|
1845
|
+
}]
|
|
1846
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
1847
|
+
type: Inject,
|
|
1848
|
+
args: [FILE_GENERATORS]
|
|
1849
|
+
}] }, { type: UtilsService }] });
|
|
1850
|
+
|
|
1851
|
+
class DatapointsExportSelectorModalComponent {
|
|
1852
|
+
constructor(bsModalRef) {
|
|
1853
|
+
this.bsModalRef = bsModalRef;
|
|
1854
|
+
this.isDownloadEnabled = false;
|
|
1855
|
+
this.labels = { ok: gettext('Download'), cancel: gettext('Cancel') };
|
|
1856
|
+
this.result = new Promise(resolve => {
|
|
1857
|
+
this._close = resolve;
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
handleKeyboardEvent(event) {
|
|
1861
|
+
if (event.key === 'Escape') {
|
|
1862
|
+
this.dismiss();
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
dismiss() {
|
|
1866
|
+
this._close(false);
|
|
1867
|
+
this.bsModalRef.hide();
|
|
1868
|
+
}
|
|
1869
|
+
async exportAndDownload() {
|
|
1870
|
+
await this.datapointsExportSelectorFileExporterComponent.exportAndDownload();
|
|
1871
|
+
this.dismiss();
|
|
1872
|
+
}
|
|
1873
|
+
changeDownloadButtonState(isEnabled) {
|
|
1874
|
+
this.isDownloadEnabled = isEnabled;
|
|
1875
|
+
}
|
|
1876
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DatapointsExportSelectorModalComponent, deps: [{ token: i1$1.BsModalRef }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1877
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.9", type: DatapointsExportSelectorModalComponent, isStandalone: true, selector: "c8y-datapoints-export-selector-modal", host: { listeners: { "document:keydown": "handleKeyboardEvent($event)" } }, viewQueries: [{ propertyName: "datapointsExportSelectorFileExporterComponent", first: true, predicate: DatapointsExportSelectorFileExporterComponent, descendants: true }], ngImport: i0, template: "<c8y-modal\n [title]=\"'Generate export' | translate\"\n [labels]=\"labels\"\n [disabled]=\"!isDownloadEnabled\"\n [headerClasses]=\"'dialog-header'\"\n (onDismiss)=\"dismiss()\"\n (onClose)=\"exportAndDownload()\"\n>\n <ng-container c8y-modal-title>\n <span [c8yIcon]=\"'data-export'\"></span>\n </ng-container>\n <c8y-datapoints-export-selector-file-exporter\n [exportConfig]=\"exportConfig\"\n (onDownloadButtonStateChange)=\"changeDownloadButtonState($event)\"\n ></c8y-datapoints-export-selector-file-exporter>\n</c8y-modal>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }, { kind: "component", type: DatapointsExportSelectorFileExporterComponent, selector: "c8y-datapoints-export-selector-file-exporter", inputs: ["exportConfig"], outputs: ["onDownloadButtonStateChange"] }, { kind: "ngmodule", type: ModalModule }, { kind: "component", type: i1.ModalComponent, selector: "c8y-modal", inputs: ["disabled", "close", "dismiss", "title", "body", "customFooter", "headerClasses", "labels"], outputs: ["onDismiss", "onClose"] }] }); }
|
|
1878
|
+
}
|
|
1879
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DatapointsExportSelectorModalComponent, decorators: [{
|
|
1880
|
+
type: Component,
|
|
1881
|
+
args: [{ selector: 'c8y-datapoints-export-selector-modal', standalone: true, imports: [CommonModule, DatapointsExportSelectorFileExporterComponent, ModalModule], template: "<c8y-modal\n [title]=\"'Generate export' | translate\"\n [labels]=\"labels\"\n [disabled]=\"!isDownloadEnabled\"\n [headerClasses]=\"'dialog-header'\"\n (onDismiss)=\"dismiss()\"\n (onClose)=\"exportAndDownload()\"\n>\n <ng-container c8y-modal-title>\n <span [c8yIcon]=\"'data-export'\"></span>\n </ng-container>\n <c8y-datapoints-export-selector-file-exporter\n [exportConfig]=\"exportConfig\"\n (onDownloadButtonStateChange)=\"changeDownloadButtonState($event)\"\n ></c8y-datapoints-export-selector-file-exporter>\n</c8y-modal>\n" }]
|
|
1882
|
+
}], ctorParameters: () => [{ type: i1$1.BsModalRef }], propDecorators: { datapointsExportSelectorFileExporterComponent: [{
|
|
1883
|
+
type: ViewChild,
|
|
1884
|
+
args: [DatapointsExportSelectorFileExporterComponent, { static: false }]
|
|
1885
|
+
}], handleKeyboardEvent: [{
|
|
1886
|
+
type: HostListener,
|
|
1887
|
+
args: ['document:keydown', ['$event']]
|
|
1888
|
+
}] } });
|
|
1889
|
+
|
|
1890
|
+
class DatapointsExportSelectorComponent {
|
|
1891
|
+
constructor(bsModalService) {
|
|
1892
|
+
this.bsModalService = bsModalService;
|
|
1893
|
+
this.isOpen = new EventEmitter();
|
|
1894
|
+
}
|
|
1895
|
+
async openExportModal() {
|
|
1896
|
+
const exportConfig = this.exportConfig;
|
|
1897
|
+
const initialState = {
|
|
1898
|
+
exportConfig
|
|
1899
|
+
};
|
|
1900
|
+
this.isOpen.emit(true);
|
|
1901
|
+
const modalRef = this.bsModalService.show(DatapointsExportSelectorModalComponent, {
|
|
1902
|
+
class: 'modal-lg',
|
|
1903
|
+
ariaDescribedby: 'modal-body',
|
|
1904
|
+
ariaLabelledBy: 'modal-title',
|
|
1905
|
+
initialState,
|
|
1906
|
+
ignoreBackdropClick: true
|
|
1907
|
+
}).content;
|
|
1908
|
+
// Result will be 'false' on modal close or dismiss
|
|
1909
|
+
this.isOpen.emit(await modalRef.result);
|
|
1910
|
+
}
|
|
1911
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DatapointsExportSelectorComponent, deps: [{ token: i1$1.BsModalService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1912
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.9", type: DatapointsExportSelectorComponent, isStandalone: true, selector: "c8y-datapoints-export-selector", inputs: { exportConfig: "exportConfig" }, outputs: { isOpen: "isOpen" }, ngImport: i0, template: "<div class=\"input-group p-t-4 p-b-4 max-width-fit m-l-auto\">\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Generate export' | translate\"\n tooltip=\"{{ 'Generate export' | translate }}\"\n container=\"body\"\n type=\"button\"\n (click)=\"openExportModal()\"\n [adaptivePosition]=\"false\"\n [disabled]=\"!exportConfig\"\n [delay]=\"500\"\n >\n <i\n class=\"icon-14\"\n c8yIcon=\"data-export\"\n ></i>\n </button>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i3$3.TooltipDirective, selector: "[tooltip], [tooltipHtml]", inputs: ["adaptivePosition", "tooltip", "placement", "triggers", "container", "containerClass", "boundariesElement", "isOpen", "isDisabled", "delay", "tooltipHtml", "tooltipPlacement", "tooltipIsOpen", "tooltipEnable", "tooltipAppendToBody", "tooltipAnimation", "tooltipClass", "tooltipContext", "tooltipPopupDelay", "tooltipFadeDuration", "tooltipTrigger"], outputs: ["tooltipChange", "onShown", "onHidden", "tooltipStateChanged"], exportAs: ["bs-tooltip"] }] }); }
|
|
1913
|
+
}
|
|
1914
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.9", ngImport: i0, type: DatapointsExportSelectorComponent, decorators: [{
|
|
1915
|
+
type: Component,
|
|
1916
|
+
args: [{ selector: 'c8y-datapoints-export-selector', standalone: true, imports: [CommonModule, TooltipModule], template: "<div class=\"input-group p-t-4 p-b-4 max-width-fit m-l-auto\">\n <div class=\"input-group-btn\">\n <button\n class=\"btn btn-default\"\n [attr.aria-label]=\"'Generate export' | translate\"\n tooltip=\"{{ 'Generate export' | translate }}\"\n container=\"body\"\n type=\"button\"\n (click)=\"openExportModal()\"\n [adaptivePosition]=\"false\"\n [disabled]=\"!exportConfig\"\n [delay]=\"500\"\n >\n <i\n class=\"icon-14\"\n c8yIcon=\"data-export\"\n ></i>\n </button>\n </div>\n</div>\n" }]
|
|
1917
|
+
}], ctorParameters: () => [{ type: i1$1.BsModalService }], propDecorators: { exportConfig: [{
|
|
1918
|
+
type: Input
|
|
1919
|
+
}], isOpen: [{
|
|
1920
|
+
type: Output
|
|
1921
|
+
}] } });
|
|
1922
|
+
|
|
1923
|
+
/**
|
|
1924
|
+
* Generated bundle index. Do not edit.
|
|
1925
|
+
*/
|
|
1926
|
+
|
|
1927
|
+
export { CSVGeneratorAdapter, DataFetchingService, DataPointsExportSelectorDataScopeComponent, DataPointsExportSelectorFileTypesComponent, DataPointsExportSelectorPreviewComponent, DataPointsExportSelectorTimeRangeComponent, DataProcessingService, DatapointsExportSelectorComponent, DatapointsExportSelectorFileExporterComponent, DatapointsExportSelectorFileExporterService, DatapointsExportSelectorModalComponent, EXPORT_MODE_LABELS, EXPORT_MODE_VALUES, ExcelDataTransformer, ExcelGeneratorAdapter, FILE_COMPRESSION_TYPES_VALUES, FILE_GENERATORS, HAS_ERROR, MEASUREMENTS_PREVIEW_ITEMS_LIMIT, SERIES_DATA_MERGED_FILE_NAME, TIME_RANGE_INTERVAL_UNITS_VALUES, UtilsService, dateRangeValidator };
|
|
1928
|
+
//# sourceMappingURL=c8y-ngx-components-datapoints-export-selector.mjs.map
|