UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

684 lines (677 loc) 133 kB
import * as i0 from '@angular/core'; import { Injectable, Input, Component, EventEmitter, Output, InjectionToken, Inject, HostListener, ViewChild } from '@angular/core'; import * as i2 from '@c8y/client'; import * as i1 from '@c8y/ngx-components'; import { gettext, AGGREGATION_VALUES, CoreModule, AGGREGATION_LABELS, AGGREGATION_VALUES_ARR, CommonModule, ModalModule } from '@c8y/ngx-components'; import * as i3 from '@ngx-translate/core'; import JSZip from 'jszip'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import * as i3$2 from '@angular/forms'; import { FormsModule, FormControl, ReactiveFormsModule } from '@angular/forms'; import saveAs from 'file-saver'; import { Subject, debounceTime, merge, takeUntil } from 'rxjs'; import { A11yModule } from '@angular/cdk/a11y'; import * as i3$1 from 'ngx-bootstrap/popover'; import { PopoverModule } from 'ngx-bootstrap/popover'; import * as i2$1 from '@angular/common'; import { isNil, isEmpty, isNumber } from 'lodash-es'; import * as i1$1 from 'ngx-bootstrap/modal'; import * as i4 from 'ngx-bootstrap/tooltip'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; const HAS_ERROR = 'has-error'; const MEASUREMENTS_PREVIEW_ITEMS_LIMIT = 5; const SERIES_DATA_MERGED_FILE_NAME = 'seriesDataMergedFileName'; const EXPORT_MODE_LABELS = { FULL: gettext('Full`export type`'), COMPACT: gettext('Compact`export type`') }; /** * Each export type is based on a different API: * - COMPACT - series * - FULL - measurements * All differences between export modes: * Compact: * Processes up to 5,000 records per data point, or up to the data retention limit (API limit) * Creates a single merged file containing all the data * Provides minimum and maximum values (API feature) * Preview is not available * Supports optional data aggregation (API feature) * Full: * Processes up to 1,000,000 records per data point, or up to the data retention limit (API limit) * For exports exceeding 50,000 records, data will be sent via email * Creates a compressed ZIP file containing separate data files for each selected data point * Preview is available * Does not support data aggregation */ const EXPORT_MODE_VALUES = { full: 'FULL', compact: 'COMPACT' }; const FILE_COMPRESSION_TYPES_VALUES = { store: 'STORE', deflate: 'DEFLATE' }; const PRODUCT_EXPERIENCE_DATAPOINTS_EXPORT_SELECTOR = { EVENTS: { EXPORT_SELECTOR: 'exportSelector' }, COMPONENTS: { DATAPOINTS_EXPORT_SELECTOR: 'datapoints-export-selector', DATAPOINTS_EXPORT_SELECTOR_FILE_EXPORTER: 'datapoints-export-selector-file-exporter' }, ACTIONS: { OPEN_MODAL: 'openModal', DOWNLOAD_STARTED: 'downloadStarted' }, EXPORT_CONFIG: { FULL_EXPORT_TYPE: 'fullExportType', COMPACT_EXPORT_TYPE: 'compactExportType' } }; class UtilsService { transformDataStructure(data) { const result = {}; data.forEach(sourceItem => { const { source: sourceId, data: sourceData } = sourceItem; result[sourceId] = {}; sourceData.series.forEach((seriesInfo, index) => { const { type, name, unit } = seriesInfo; /** * Unique key to distinguish between different series from same source, * e.g.: c8y_Acceleration.accelerationX, c8y_Acceleration.accelerationY, c8y_Acceleration.accelerationZ */ const seriesKey = `${type}.${name}`; result[sourceId][seriesKey] = { values: {}, seriesDetails: { name, unit, type } }; let hasValues = false; Object.entries(sourceData.values).forEach(([timestamp, measurements]) => { if (!measurements) { return; } const measurement = measurements[index]; if (measurement !== null && measurement !== undefined) { result[sourceId][seriesKey].values[timestamp] = measurement; hasValues = true; } }); if (!hasValues) { result[sourceId][seriesKey].values = null; } }); }); return result; } /** * Formats a date range into a specific string format, handling UTC dates correctly. * * @param fromDate - The start date in ISO format (e.g., "2023-12-04T12:40:00.000Z") * @param toDate - The end date in ISO format (e.g., "2023-12-06T23:50:00.000Z") * @returns A string representing the date range in the format "ddmmmyyhhmm-ddmmmyyhhmm" * where d=day, m=month, y=year, h=hour, m=minute * * Example * ```typescript * const fromDate = "2023-12-04T12:40:00.000Z"; * const toDate = "2023-12-06T23:50:00.000Z"; * const formattedRange = getFormattedDateRange(fromDate, toDate); * console.log(formattedRange); // Output: "04dec231240-06dec232350" * ``` */ getFormattedDateRange(fromDate, toDate) { const formatDate = (dateString) => { const date = new Date(dateString); if (isNaN(date.getTime())) { throw new Error(`Invalid date string: ${dateString}`); } const day = date.getUTCDate().toString().padStart(2, '0'); const month = date.toLocaleString('en', { month: 'short', timeZone: 'UTC' }).toLowerCase(); const hours = date.getUTCHours().toString().padStart(2, '0'); const minutes = date.getUTCMinutes().toString().padStart(2, '0'); const seconds = date.getUTCSeconds().toString().padStart(2, '0'); return `${day}${month}${hours}${minutes}${seconds}`; }; return `${formatDate(fromDate)}-${formatDate(toDate)}`; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UtilsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UtilsService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: UtilsService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class DataFetchingService { constructor(alertService, measurementService, translateService, utilsService) { this.alertService = alertService; this.measurementService = measurementService; this.translateService = translateService; this.utilsService = utilsService; } /** * Checks if any of measurements requests may exceeded the limit, * after which the export data is processed by the backend and the generated CSV/Excel file is sent by email. * * The threshold is set to 50_000 records in application's core properties (export.data.synchronous.limit). * * @param exportConfig - The export configuration. * @returns A promise that returns an array of objects representing datapoints files that will be sent by email. */ async getDatapointsExceedingLimit(exportConfig) { const backendProcessingLimit = 50_000; const promises = exportConfig.datapointDetails.map(async (details) => { try { const filter = { dateFrom: exportConfig.dateFrom, dateTo: exportConfig.dateTo, pageSize: 1, source: details.source, valueFragmentSeries: details.valueFragmentSeries, valueFragmentType: details.valueFragmentType, withTotalElements: true }; const response = await this.measurementService.list(filter); if (response?.paging?.totalElements > backendProcessingLimit) { return { datapointDetail: { source: details.source, valueFragmentSeries: details.valueFragmentSeries, valueFragmentType: details.valueFragmentType }, totalElements: response.paging.totalElements }; } else { return null; } } catch (error) { this.alertService.addServerFailure(error); return null; } }); const results = await Promise.all(promises); const datapointsExceedingLimit = results.filter(result => result !== null); return datapointsExceedingLimit; } /** * Retrieves the message to be displayed when the limit of datapoints is exceeded during file export. * * @param hasNoExportableData - Indicates if there is no exportable data because the date range of all selected datapoints exceeds 1,000,000 records. * @param emailDeliverableCount - The number of datapoint exports that exceed the limit, will be proceeded by the backend and sent by email. * @param browserDownloadableCount - The number of datapoint exports that can be downloaded directly. * @param skippedDatapointCount - The number of datapoint exports skipped due to exceeding the measurements API limit of 1,000,000 records. * @param totalDatapointsSelectedForExportCount - Total number of datapoint selected for exports. * @returns The message that can be injected. */ getLimitExceededMessage(hasNoExportableData, emailDeliverableCount, browserDownloadableCount, nonRetrievableCount, totalDatapointsSelectedForExportCount) { if (hasNoExportableData) { 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.`)); return message; } const message = this.translateService.instant(gettext(`After clicking "Download":<br> <ul> <li><strong>{{$browserDownloadableCount}}</strong> datapoint(s) exports will be downloaded directly within one file: <em>exported_[csv/excel].zip</em></li> <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> <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> </ul> <p>The total number of data points that can be exported is: <strong>{{$totalExportableDatapointsCount}} out of {{$totalDatapointsSelectedForExportCount}}</strong>.</p> <p><strong>Note:</strong> The file name convention of files within zip file is: <code>[source]_[fragment_series].[csv/xls]</code></p>`), { $browserDownloadableCount: browserDownloadableCount, $emailDeliverableCount: emailDeliverableCount, $nonRetrievableCount: nonRetrievableCount, $totalDatapointsSelectedForExportCount: totalDatapointsSelectedForExportCount, $totalExportableDatapointsCount: browserDownloadableCount + emailDeliverableCount }); const trimmedMessage = this.removeZeroCountListItems(message, [ browserDownloadableCount, emailDeliverableCount, nonRetrievableCount ]); return trimmedMessage; } /** * Trims the given HTML message by removing list items that correspond to zero counts. * * @param message - The HTML string containing the message with list items. * @param counts - An array of number values corresponding to each list item. * @param countToTrim - A count that will be trimmed with corresponding list item. * @returns A trimmed HTML string with list items removed where the corresponding count is zero. * * Example: * ```typescript * const message = '<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>'; * const counts = [1, 0, 2]; * const trimmedMessage = this.removeZeroCountListItems(message, counts); * // Result: '<ul><li>Item 1</li><li>Item 3</li></ul>' * ``` */ removeZeroCountListItems(message, counts, countToTrim = 0) { const parser = new DOMParser(); const doc = parser.parseFromString(message, 'text/html'); const listItems = doc.querySelectorAll('ul li'); listItems.forEach((item, index) => { if (counts[index] === countToTrim) { item.remove(); } }); return doc.body.innerHTML; } /** * Displays an information alert about sending data via email. * * Only measurements API can send files via email. * * @param fileType - The type of file to be sent. * @param datapointsExceedingLimit - The array of datapoints exceeding the limit. */ showSendViaEmailInfoAlert(fileType, datapointsExceedingLimit) { const messageTemplate = this.translateService.instant(gettext(`You will get {{$fileTypeText}} file(s) for {{$count}} series via email.`), { $count: datapointsExceedingLimit.length, $fileTypeText: fileType }); const formattedList = datapointsExceedingLimit .map(datapoint => `• ${datapoint.datapointDetail.source}_${datapoint.datapointDetail.valueFragmentType}_${datapoint.datapointDetail.valueFragmentSeries}`) .join('\n'); this.alertService.info(messageTemplate, formattedList); } async fetchMeasurementDataFilesAndPairWithSourceDetails(acceptFileType, exportConfig) { const measurementFileConfig = { exportConfig, acceptFileType }; const filePromises = exportConfig.datapointDetails.map(details => this.fetchAndProcessMeasurementFile(details, measurementFileConfig)); return Promise.all(filePromises); } async fetchAndProcessMeasurementFile(details, measurementFileConfig, roundSeconds = false) { const filter = this.prepareMeasurementsFilter(details, measurementFileConfig.exportConfig, roundSeconds); const header = { accept: measurementFileConfig.acceptFileType }; try { const measurementFileResponse = await this.measurementService.getMeasurementsFile(filter, header); // If the status is 200, then the file is in the response. // If the status is 202, then the file is being processed by the backend and will be sent by email. if (measurementFileResponse.status === 200) { return this.mergeMeasurementsWithItsSourceDetails(details, measurementFileResponse); } } catch (error) { this.alertService.addServerFailure(error); } } async mergeMeasurementsWithItsSourceDetails(details, measurementFile) { return { source: details.source, valueFragmentSeries: details.valueFragmentSeries, valueFragmentType: details.valueFragmentType, fetchedMeasurementsBlobFile: await measurementFile.blob() }; } async fetchAndPrepareDataToExport(exportConfig, isMeasurement) { return isMeasurement ? await this.fetchAndPrepareMeasurementDataToExportForPreview(exportConfig) : await this.fetchAndPrepareSeriesDataToExport(exportConfig); } prepareMeasurementsFilter(details, exportConfig, roundSeconds, pageSize) { const filter = { dateFrom: this.adjustDate(exportConfig.dateFrom, 0, roundSeconds), dateTo: this.adjustDate(exportConfig.dateTo, 0, roundSeconds), source: details.source, valueFragmentSeries: details.valueFragmentSeries, valueFragmentType: details.valueFragmentType }; if (pageSize) { filter.pageSize = pageSize; } return filter; } processMeasurementDataForPreview(details, data) { const unit = data[0][details.valueFragmentType][details.valueFragmentSeries]?.unit || ''; const values = {}; data.forEach(measurement => { values[measurement.time] = measurement[details.valueFragmentType][details.valueFragmentSeries].value; }); return { ...details, unit, timeValueMap: values }; } /** * Returns a map of active data points device IDs with their corresponding series. * * Example output: * ```typescript * new Map([ * ['844657202', ['c8y_Temperature.T']], * ['32666427', ['c8y_Battery.Battery']] * ]); * ``` * @param datapointDetails - An array of data points details. * @returns A map where the key is the data point ID and the value is an array of data point series. */ groupSeriesByDeviceId(datapointDetails) { return datapointDetails.reduce((map, { valueFragmentType, valueFragmentSeries, source }) => { const value = `${valueFragmentType}.${valueFragmentSeries}`; const existingValue = map.get(source) ?? []; map.set(source, [...existingValue, value]); return map; }, new Map()); } /** * Processes the fetched series data and prepares it for export. * * @param datapointDetails - An array of data point details. * @param fetchedDataMap - A map of fetched series data grouped by source. * @returns An array of DataToExport objects. */ processSeriesData(datapointDetails, fetchedDataMap) { const valuesGroupedBySource = this.utilsService.transformDataStructure(fetchedDataMap); return datapointDetails.map((details) => { let unit; let data; /** * Unique key to distinguish between different series from same source, * e.g.: c8y_Acceleration.accelerationX, c8y_Acceleration.accelerationY, c8y_Acceleration.accelerationZ */ const seriesKey = `${details.valueFragmentType}.${details.valueFragmentSeries}`; if (valuesGroupedBySource[details.source][seriesKey]) { unit = valuesGroupedBySource[details.source][seriesKey].seriesDetails.unit; data = valuesGroupedBySource[details.source][seriesKey].values; } return { ...details, unit, timeValueMap: data }; }); } async getSourcesWithPermissionsToRead(datapointDetails) { const dateFrom = new Date(); const dateTo = new Date(dateFrom); const fetchedDataPromises = datapointDetails.map(async (datapoint) => { const rawFilter = { dateFrom, dateTo, source: datapoint.source }; try { const { res } = await this.fetchSeriesData(rawFilter); if (res?.status === 200) { return datapoint.source; } return null; } catch (error) { // If an error occurs during fetch, return null to prevent promise rejection return null; } }); const fetchedSources = await Promise.all(fetchedDataPromises); return fetchedSources.filter((source) => source !== null); } /** * Adjusts the given date by adding the specified number of minutes and setting seconds to 0. * * @param date - The date to be adjusted in string format. * @param minutes - The number of minutes to add to the date. * @param roundSeconds - Whether to round the seconds or not. * If true, the seconds will be rounded to 0. * If false, the seconds will be displayed as they are. * @returns The adjusted date in ISO string format. */ adjustDate(date, minutes, roundSeconds = true) { const dateTime = new Date(date); if (isNaN(dateTime.getTime())) { throw new Error('Invalid date'); } dateTime.setUTCMinutes(dateTime.getUTCMinutes() + minutes); if (roundSeconds) { dateTime.setUTCSeconds(0, 0); } return dateTime.toISOString(); } /** * Asynchronously loads series data based on the provided parameters. * * This method constructs a filter for retrieving series data within a specified date range, * from a specific source, and optionally applying an aggregation type. * * @param rawFilter - The parameters for loading series data, including date range, source, series names, and aggregation type. * @param roundSeconds - Indicates whether to round the seconds in the date range to the nearest whole number. * @returns A promise that resolves to series data wrapped in result object. */ async fetchSeriesData(rawFilter, roundSeconds = false) { let seriesData; const filter = this.prepareSeriesFilter({ dateFrom: rawFilter.dateFrom, dateTo: rawFilter.dateTo, source: rawFilter.source, series: rawFilter.series, aggregationType: rawFilter.aggregationType }, roundSeconds); try { seriesData = await this.measurementService.listSeries(filter); } catch (error) { this.alertService.addServerFailure(error); } return seriesData; } /** * Fetches and prepares measurement data for preview. * * Empty DataToExport object can be returned, because unlike series data, * measurement data is not further processed besides showing only in the preview. * CSV/Excel files are generated by the backend for measurements. * * @param exportConfig - The export configuration. * @param roundSeconds - Indicates whether to round the seconds in the date range to the nearest whole number. * @returns A promise that resolves to an array of DataToExport objects or null when no data is fetched. */ async fetchAndPrepareMeasurementDataToExportForPreview(exportConfig, roundSeconds = false) { const dataToExportPromises = exportConfig.datapointDetails.map(async (details) => { const fetchedMeasurementData = await this.fetchMeasurementDataForPreview(details, exportConfig, roundSeconds); if (fetchedMeasurementData?.data?.length) { return this.processMeasurementDataForPreview(details, fetchedMeasurementData.data); } return null; }); return await Promise.all(dataToExportPromises); } async fetchMeasurementDataForPreview(details, exportConfig, roundSeconds) { let measurements; const filter = this.prepareMeasurementsFilter(details, exportConfig, roundSeconds, MEASUREMENTS_PREVIEW_ITEMS_LIMIT); try { measurements = await this.measurementService.list(filter); } catch (error) { this.alertService.addServerFailure(error); } return measurements; } async fetchAndPrepareSeriesDataToExport(exportConfig) { const datapointsValuesDataMap = this.groupSeriesByDeviceId(exportConfig.datapointDetails); const fetchedDataPromises = Array.from(datapointsValuesDataMap).map(async ([source, series]) => { const { dateFrom, dateTo, aggregation } = exportConfig; const rawFilter = { dateFrom, dateTo, source, series, aggregationType: aggregation }; const { data } = await this.fetchSeriesData(rawFilter); return { source, data }; }); const fetchedDataGroupedBySource = await Promise.all(fetchedDataPromises); return this.processSeriesData(exportConfig.datapointDetails, fetchedDataGroupedBySource); } prepareSeriesFilter(filters, roundSeconds) { const { dateFrom, dateTo, source, series, aggregationType } = filters; const from = this.adjustDate(dateFrom, 0, roundSeconds); const to = this.adjustDate(dateTo, 0, roundSeconds); const filter = { dateFrom: from, dateTo: to, source: source, revert: true }; // The 'NONE' aggregation type is not handled by a backend, so even when is selected, it is not passed as a filter parameter. if (aggregationType && aggregationType !== AGGREGATION_VALUES.none) { filter.aggregationType = aggregationType; } // TODO: it is a temporal workaround for a bug in the backend, // where the series with more than one dot are impossible to retrieve: // https://cumulocity.atlassian.net/browse/MTM-59277 if (series) { const hasMoreThanOneDot = series.some(s => s.split('.').length > 2); if (!hasMoreThanOneDot) { filter.series = series; } } return filter; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DataFetchingService, deps: [{ token: i1.AlertService }, { token: i2.MeasurementService }, { token: i3.TranslateService }, { token: UtilsService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DataFetchingService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DataFetchingService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.AlertService }, { type: i2.MeasurementService }, { type: i3.TranslateService }, { type: UtilsService }] }); class DatapointsExportSelectorFileExporterService { constructor(alertService, datapointsExportSelectorDataProcessingService, translateService) { this.alertService = alertService; this.datapointsExportSelectorDataProcessingService = datapointsExportSelectorDataProcessingService; this.translateService = translateService; } async getMeasurementExportedDataBlob(extension, dataToExport) { try { return await this.getMeasurementDataZipBlob(extension, dataToExport); } catch (error) { this.showZipCreationErrorAlert(); return null; } } async getSeriesExportedDataBlob(fileType, dataToExport, mergedExportDetails) { try { return await this.getSeriesDataBlob(fileType, dataToExport, mergedExportDetails); } catch (error) { this.showZipCreationErrorAlert(); return null; } } cleanupCachedData() { this.cachedRawExportSeriesData = null; this.cachedFlatteredAndSortedSeriesExportData = null; } showZipCreationErrorAlert() { const alertMessage = this.translateService.instant(gettext('Could not create zip file.')); this.alertService.danger(alertMessage); return null; } getMeasurementDataZipBlob(extension, dataToExportWithBackendCreatedFile) { const files = []; this.createRawMeasurementExportedFiles(dataToExportWithBackendCreatedFile, extension, files); return this.datapointsExportSelectorDataProcessingService.zipFiles(files); } createRawMeasurementExportedFiles(dataToExportWithBackendCreatedFile, fileExtension, files) { dataToExportWithBackendCreatedFile.forEach(data => { if (data) { const fragmentSeries = `${data.valueFragmentType}_${data.valueFragmentSeries}`; const fileName = this.datapointsExportSelectorDataProcessingService.createFileName(data.source, fragmentSeries, fileExtension); const fileData = data.fetchedMeasurementsBlobFile; const exportedFile = { fileName, fileData }; files.push(exportedFile); } }); } /** * Converts data to a specified file type and returns the generated blob. * * Unlike measurements, data must be transformed to an exportable file structure before exporting. * * @param fileType - The type of file to which the data points should be exported. This can be 'csv' or 'excel'. * @param dataToExport - An array of processed measurement data combined with the respective properties of the datapoint. * @param mergedExportDetails - The details for the merged export, contains date range and aggregation. * @returns A promise that resolves to the generated ZIP blob or null if an error occurs. */ async getSeriesDataBlob(fileType, dataToExport, mergedExportDetails) { this.transformSeriesDataToExportableFileStructure(dataToExport); const exportParams = { flattenedAndSortedExportData: this.cachedFlatteredAndSortedSeriesExportData, fileType, mergedExportDetails: mergedExportDetails }; return await this.datapointsExportSelectorDataProcessingService.exportSeriesData(exportParams); } transformSeriesDataToExportableFileStructure(dataToExport) { if (!this.cachedRawExportSeriesData) { this.cachedRawExportSeriesData = this.datapointsExportSelectorDataProcessingService.transformToExportFileStructure(dataToExport); this.cachedFlatteredAndSortedSeriesExportData = this.cachedRawExportSeriesData .flat() .sort((a, b) => { return new Date(a.time).getTime() - new Date(b.time).getTime(); }); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DatapointsExportSelectorFileExporterService, deps: [{ token: i1.AlertService }, { token: DataProcessingService }, { token: i3.TranslateService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DatapointsExportSelectorFileExporterService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DatapointsExportSelectorFileExporterService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i1.AlertService }, { type: DataProcessingService }, { type: i3.TranslateService }] }); class DataPointsExportSelectorPreviewComponent { constructor() { this.MEASUREMENTS_PREVIEW_ITEMS_LIMIT = MEASUREMENTS_PREVIEW_ITEMS_LIMIT; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DataPointsExportSelectorPreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", 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 data-cy=\"preview--help\"\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"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DataPointsExportSelectorPreviewComponent, decorators: [{ type: Component, 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 data-cy=\"preview--help\"\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" }] }], propDecorators: { hasFetchedDataAnyValuesToExport: [{ type: Input }], isPreviewLoading: [{ type: Input }], previewTableData: [{ type: Input }] } }); class DataPointsExportSelectorDataScopeComponent { constructor() { this.onAggregationChange = new EventEmitter(); this.onExportTypeChange = new EventEmitter(); this.AGGREGATION_LABELS = AGGREGATION_LABELS; this.AGGREGATION_VALUES_ARR = AGGREGATION_VALUES_ARR; this.EXPORT_MODE_LABELS = EXPORT_MODE_LABELS; this.EXPORT_MODE_VALUES_ARR = [EXPORT_MODE_VALUES.full, EXPORT_MODE_VALUES.compact]; } emitAggregationChange(aggregation) { this.onAggregationChange.emit(aggregation); } emitExportTypeChange(exportType) { this.onExportTypeChange.emit(exportType); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DataPointsExportSelectorDataScopeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", 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 data-cy=\"data-scope--help\"\n [adaptivePosition]=\"true\"\n ></button>\n <ng-template #popoverDataScopeTemplate>\n <p\n class=\"m-b-8\"\n translate\n >\n Choose export type from available options:\n </p>\n <p><strong translate>Compact</strong></p>\n <ul class=\"p-l-16\">\n <li translate>Up to 5,000 records per data point or data retention limit</li>\n <li translate>Single merged file for all data</li>\n <li translate>No preview available</li>\n <li translate>Optional data aggregation supported</li>\n </ul>\n <p><strong translate>Full</strong></p>\n <ul class=\"p-l-16\">\n <li translate>Up to 1,000,000 records per data point or data retention limit</li>\n <li translate>Email delivery if exceeds 50,000 records</li>\n <li translate>Separate files for each data point in ZIP format</li>\n <li translate>Preview 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 data-cy=\"data-scope--export-selector\"\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 data-cy=\"data-scope--aggregation-selector\"\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>\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.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"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DataPointsExportSelectorDataScopeComponent, decorators: [{ type: Component, 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 data-cy=\"data-scope--help\"\n [adaptivePosition]=\"true\"\n ></button>\n <ng-template #popoverDataScopeTemplate>\n <p\n class=\"m-b-8\"\n translate\n >\n Choose export type from available options:\n </p>\n <p><strong translate>Compact</strong></p>\n <ul class=\"p-l-16\">\n <li translate>Up to 5,000 records per data point or data retention limit</li>\n <li translate>Single merged file for all data</li>\n <li translate>No preview available</li>\n <li translate>Optional data aggregation supported</li>\n </ul>\n <p><strong translate>Full</strong></p>\n <ul class=\"p-l-16\">\n <li translate>Up to 1,000,000 records per data point or data retention limit</li>\n <li translate>Email delivery if exceeds 50,000 records</li>\n <li translate>Separate files for each data point in ZIP format</li>\n <li translate>Preview 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 data-cy=\"data-scope--export-selector\"\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 data-cy=\"data-scope--aggregation-selector\"\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>\n" }] }], propDecorators: { disabledAggregationOptions: [{ type: Input }], formGroup: [{ type: Input }], onAggregationChange: [{ type: Output }], onExportTypeChange: [{ type: Output }] } }); class DataPointsExportSelectorFileTypesComponent { constructor() { this.dynamicFilesTypeMetadata = {}; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: DataPointsExportSelectorFileTypesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", 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 [for