UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

596 lines (588 loc) 72.8 kB
import * as i0 from '@angular/core'; import { inject, Component, Injectable, signal, effect, EventEmitter, Input, Output, DestroyRef, ViewChild } from '@angular/core'; import * as i1 from '@c8y/ngx-components'; import { gettext, CoreModule, ContextRouteService, ClipboardService, ViewContext, AlertService, Permissions, CommonModule, FormsModule as FormsModule$1 } from '@c8y/ngx-components'; import * as i5$1 from '@c8y/ngx-components/alarm-event-selector'; import { AlarmEventSelectorModule } from '@c8y/ngx-components/alarm-event-selector'; import { TimeContextComponent } from '@c8y/ngx-components/time-context'; import * as i4$1 from '@c8y/ngx-components/datapoint-selector'; import { DatapointSelectorModule } from '@c8y/ngx-components/datapoint-selector'; import { ChartEventsService, ChartAlarmsService, ChartsComponent } from '@c8y/ngx-components/echart'; import * as i3 from '@angular/forms'; import { FormControl, FormGroup, ReactiveFormsModule, FormBuilder, FormsModule } from '@angular/forms'; import * as i4 from 'ngx-bootstrap/dropdown'; import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; import * as i6 from 'ngx-bootstrap/tooltip'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; import { PopoverModule } from 'ngx-bootstrap/popover'; import { BsModalService } from 'ngx-bootstrap/modal'; import * as i5 from '@angular/cdk/a11y'; import { A11yModule } from '@angular/cdk/a11y'; import { merge, of } from 'rxjs'; import { map } from 'rxjs/operators'; import { ReportDashboardService, ReportDashboardModule } from '@c8y/ngx-components/report-dashboard'; import * as i3$1 from '@angular/common'; import * as i1$1 from '@c8y/ngx-components/context-dashboard'; import { ContextDashboardService } from '@c8y/ngx-components/context-dashboard'; import { DatapointsExportSelectorComponent } from '@c8y/ngx-components/datapoints-export-selector'; import { InventoryService } from '@c8y/client'; import { ActivatedRoute, Router } from '@angular/router'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; class SendAsWidgetToReportModal { constructor() { this.reports = []; this.result = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); this.labels = { cancel: gettext('Cancel'), ok: gettext('Send') }; this.reportDashboardService = inject(ReportDashboardService); } async ngOnInit() { this.reports = (await this.reportDashboardService.listReports({ pageSize: 2000 })).data; if (this.reports.length > 0) { this.form = this.initForm(); } this.numberOfSelectedReports$ = merge(this.form.valueChanges, of(this.form.value)).pipe(map(value => Object.values(value).filter(Boolean).length)); } save() { this._resolve(this.form.value); } cancel() { this._reject(); } initForm() { const controls = this.reports.reduce((acc, report) => ({ ...acc, [report.id]: new FormControl(false) }), {}); return new FormGroup(controls); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SendAsWidgetToReportModal, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: SendAsWidgetToReportModal, isStandalone: true, selector: "c8y-apply-branding-to-app-modal", ngImport: i0, template: "<c8y-modal\n [title]=\"'Send as widget to reports' | translate\"\n [disabled]=\"!form || form.invalid\"\n [headerClasses]=\"'dialog-header'\"\n (onDismiss)=\"cancel()\"\n (onClose)=\"save()\"\n [labels]=\"labels\"\n>\n <ng-container c8y-modal-title>\n <span c8yIcon=\"c8y-reports\"></span>\n </ng-container>\n\n <c8y-list-group\n class=\"m-b-0 no-border-last\"\n *ngIf=\"form\"\n [formGroup]=\"form\"\n >\n <c8y-li>\n <p\n class=\"text-center text-medium\"\n *ngIf=\"numberOfSelectedReports$ | async as numberOfReports; else noSelectedReports\"\n translate\n [translateParams]=\"{ numberOfReports: numberOfReports }\"\n ngNonBindable\n >\n {{ numberOfReports }} reports selected for widget\n </p>\n <ng-template #noSelectedReports>\n <p\n class=\"text-center text-medium\"\n translate\n >\n Select one or more reports to send the Data points graph widget with the current configuration\n </p>\n </ng-template>\n </c8y-li>\n <c8y-li *ngFor=\"let report of reports\">\n <c8y-li-checkbox\n [attr.data-cy]=\"'branding-apply-branding-to-app-checkbox-' + report.id\"\n [formControlName]=\"report.id\"\n ></c8y-li-checkbox>\n <c8y-li-icon class=\"p-l-0\">\n <i c8yIcon=\"{{ report.icon }}\"></i>\n </c8y-li-icon>\n <div class=\"text-truncate\" title=\"{{report.name}}\">{{ report.name }}</div>\n </c8y-li>\n </c8y-list-group>\n\n <c8y-ui-empty-state\n [icon]=\"'c8y-reports'\"\n [title]=\"'There are no reports defined.' | translate\"\n [subtitle]=\"'Add a report first.' | translate\"\n *ngIf=\"reports.length === 0\"\n ></c8y-ui-empty-state>\n</c8y-modal>\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: CoreModule }, { kind: "component", type: i1.EmptyStateComponent, selector: "c8y-ui-empty-state", inputs: ["icon", "title", "subtitle", "horizontal"] }, { kind: "directive", type: i1.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }, { kind: "directive", type: i1.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: i3$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i3$1.AsyncPipe, name: "async" }, { kind: "component", type: i1.ModalComponent, selector: "c8y-modal", inputs: ["disabled", "close", "dismiss", "title", "body", "customFooter", "headerClasses", "labels"], outputs: ["onDismiss", "onClose"] }, { kind: "component", type: i1.ListGroupComponent, selector: "c8y-list-group" }, { kind: "component", type: i1.ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: i1.ListItemIconComponent, selector: "c8y-list-item-icon, c8y-li-icon", inputs: ["icon", "status"] }, { kind: "component", type: i1.ListItemCheckboxComponent, selector: "c8y-list-item-checkbox, c8y-li-checkbox", inputs: ["selected", "indeterminate", "disabled", "displayAsSwitch"], outputs: ["onSelect"] }, { kind: "ngmodule", type: ReportDashboardModule }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: SendAsWidgetToReportModal, decorators: [{ type: Component, args: [{ selector: 'c8y-apply-branding-to-app-modal', standalone: true, imports: [ReactiveFormsModule, CoreModule, ReportDashboardModule], template: "<c8y-modal\n [title]=\"'Send as widget to reports' | translate\"\n [disabled]=\"!form || form.invalid\"\n [headerClasses]=\"'dialog-header'\"\n (onDismiss)=\"cancel()\"\n (onClose)=\"save()\"\n [labels]=\"labels\"\n>\n <ng-container c8y-modal-title>\n <span c8yIcon=\"c8y-reports\"></span>\n </ng-container>\n\n <c8y-list-group\n class=\"m-b-0 no-border-last\"\n *ngIf=\"form\"\n [formGroup]=\"form\"\n >\n <c8y-li>\n <p\n class=\"text-center text-medium\"\n *ngIf=\"numberOfSelectedReports$ | async as numberOfReports; else noSelectedReports\"\n translate\n [translateParams]=\"{ numberOfReports: numberOfReports }\"\n ngNonBindable\n >\n {{ numberOfReports }} reports selected for widget\n </p>\n <ng-template #noSelectedReports>\n <p\n class=\"text-center text-medium\"\n translate\n >\n Select one or more reports to send the Data points graph widget with the current configuration\n </p>\n </ng-template>\n </c8y-li>\n <c8y-li *ngFor=\"let report of reports\">\n <c8y-li-checkbox\n [attr.data-cy]=\"'branding-apply-branding-to-app-checkbox-' + report.id\"\n [formControlName]=\"report.id\"\n ></c8y-li-checkbox>\n <c8y-li-icon class=\"p-l-0\">\n <i c8yIcon=\"{{ report.icon }}\"></i>\n </c8y-li-icon>\n <div class=\"text-truncate\" title=\"{{report.name}}\">{{ report.name }}</div>\n </c8y-li>\n </c8y-list-group>\n\n <c8y-ui-empty-state\n [icon]=\"'c8y-reports'\"\n [title]=\"'There are no reports defined.' | translate\"\n [subtitle]=\"'Add a report first.' | translate\"\n *ngIf=\"reports.length === 0\"\n ></c8y-ui-empty-state>\n</c8y-modal>\n" }] }] }); class CreateNewReportModalComponent { constructor(contextDashboardService) { this.contextDashboardService = contextDashboardService; this.reportName = ''; this.labels = { cancel: gettext('Cancel'), ok: gettext('Send') }; this.result = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); this.styling = { themeClass: 'dashboard-theme-light', headerClass: 'panel-title-regular' }; this.DEFAULT_DASHBOARD_ICON = 'th'; this.DEFAULT_DASHBOARD_PRIORITY = 5000; this.DEFAULT_DASHBOARD_MARGIN = 12; } async save() { const dashboard = { name: this.reportName, icon: this.DEFAULT_DASHBOARD_ICON, c8y_IsNavigatorNode: null, priority: this.DEFAULT_DASHBOARD_PRIORITY, description: '', widgetMargin: this.DEFAULT_DASHBOARD_MARGIN, classes: { [this.styling.headerClass]: true }, widgetClasses: { [this.styling.headerClass]: true } }; try { const { name, icon, c8y_IsNavigatorNode, priority, description } = dashboard; const report = (await this.contextDashboardService.createReport({ name, icon, c8y_IsNavigatorNode, priority, description })).data; await this.contextDashboardService.create(dashboard, undefined, `${this.contextDashboardService.REPORT_PARTIAL_NAME}${report.id}`); this._resolve(report); } catch (ex) { this._reject(ex); } } cancel() { this._reject(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CreateNewReportModalComponent, deps: [{ token: i1$1.ContextDashboardService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: CreateNewReportModalComponent, isStandalone: true, selector: "c8y-create-new-report-modal", ngImport: i0, template: "<c8y-modal\n [title]=\"'Create new report with widget' | translate\"\n [headerClasses]=\"'dialog-header'\"\n [disabled]=\"reportName === ''\"\n (onDismiss)=\"cancel()\"\n (onClose)=\"save()\"\n [labels]=\"labels\"\n>\n <ng-container c8y-modal-title>\n <span c8yIcon=\"c8y-reports\"></span>\n </ng-container>\n\n <p class=\"text-center bg-component text-balance sticky-top p-l-24 p-r-24 p-t-8 p-b-8 separator-bottom\">\n {{' Create a new report with the Data points graph widget using the current configuration.' | translate}}\n </p>\n <div class=\"p-24 p-t-8\">\n <c8y-form-group>\n <label\n for=\"reportName\"\n translate\n >\n Report name\n </label>\n <input\n class=\"form-control\"\n id=\"reportName\"\n placeholder=\"{{ 'e.g. My data point Report' }}\"\n name=\"name\"\n type=\"text\"\n autocomplete=\"off\"\n required\n [(ngModel)]=\"reportName\"\n />\n <c8y-messages></c8y-messages>\n </c8y-form-group>\n </div>\n</c8y-modal>\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "ngmodule", type: CoreModule }, { kind: "directive", type: i1.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "pipe", type: i1.C8yTranslatePipe, name: "translate" }, { kind: "directive", type: i1.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "component", type: i1.ModalComponent, selector: "c8y-modal", inputs: ["disabled", "close", "dismiss", "title", "body", "customFooter", "headerClasses", "labels"], outputs: ["onDismiss", "onClose"] }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i1.FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "component", type: i1.MessagesComponent, selector: "c8y-messages", inputs: ["show", "defaults", "helpMessage"] }, { kind: "directive", type: i1.RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "ngmodule", type: ReportDashboardModule }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CreateNewReportModalComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-create-new-report-modal', standalone: true, imports: [ReactiveFormsModule, CoreModule, ReportDashboardModule], template: "<c8y-modal\n [title]=\"'Create new report with widget' | translate\"\n [headerClasses]=\"'dialog-header'\"\n [disabled]=\"reportName === ''\"\n (onDismiss)=\"cancel()\"\n (onClose)=\"save()\"\n [labels]=\"labels\"\n>\n <ng-container c8y-modal-title>\n <span c8yIcon=\"c8y-reports\"></span>\n </ng-container>\n\n <p class=\"text-center bg-component text-balance sticky-top p-l-24 p-r-24 p-t-8 p-b-8 separator-bottom\">\n {{' Create a new report with the Data points graph widget using the current configuration.' | translate}}\n </p>\n <div class=\"p-24 p-t-8\">\n <c8y-form-group>\n <label\n for=\"reportName\"\n translate\n >\n Report name\n </label>\n <input\n class=\"form-control\"\n id=\"reportName\"\n placeholder=\"{{ 'e.g. My data point Report' }}\"\n name=\"name\"\n type=\"text\"\n autocomplete=\"off\"\n required\n [(ngModel)]=\"reportName\"\n />\n <c8y-messages></c8y-messages>\n </c8y-form-group>\n </div>\n</c8y-modal>\n" }] }], ctorParameters: () => [{ type: i1$1.ContextDashboardService }] }); class DataExplorerService { constructor() { this.inventory = inject(InventoryService); } async fetchReportDashboard(reportId) { return (await this.inventory.list({ pageSize: 1, query: `has('c8y_Dashboard!name!report_${reportId}')` })).data[0]; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DataExplorerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DataExplorerService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DataExplorerService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class WorkspaceConfigurationService { constructor() { this.LOCAL_STORAGE_KEY = 'c8y-configs'; this.LOCAL_STORAGE_DEFAULT_ID_KEY = 'c8y-default-config-id'; this.contextIdSignal = signal(null); effect(() => { const signalValue = this.contextIdSignal(); if (signalValue !== null) { this.LOCAL_STORAGE_KEY = `c8y-configs-${signalValue}`; this.LOCAL_STORAGE_DEFAULT_ID_KEY = `c8y-default-config-id-${signalValue}`; } if (signalValue == null) { this.LOCAL_STORAGE_KEY = 'c8y-configs'; this.LOCAL_STORAGE_DEFAULT_ID_KEY = 'c8y-default-config-id'; } }); } getConfigurations() { const configurations = localStorage.getItem(this.LOCAL_STORAGE_KEY); return configurations ? JSON.parse(configurations) : []; } getDefaultConfigurationId() { return localStorage.getItem(this.LOCAL_STORAGE_DEFAULT_ID_KEY); } saveConfigurations(configurations, id) { localStorage.setItem(this.LOCAL_STORAGE_KEY, JSON.stringify(configurations)); localStorage.setItem(this.LOCAL_STORAGE_DEFAULT_ID_KEY, id); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: WorkspaceConfigurationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: WorkspaceConfigurationService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: WorkspaceConfigurationService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); const adjectives = [ 'caffeinated', 'sleepy', 'hungry', 'puzzled', 'overexcited', 'daydreaming', 'chocolate-loving', 'coffee-powered', 'cookie-craving', 'disco-ready', 'weekend-mode', 'pizza-powered', 'nap-seeking', 'wifi-hunting', 'battery-hungry', 'donut-powered', 'tea-sipping', 'keyboard-loving', 'screen-staring', 'mouse-chasing', 'code-dreaming', 'pixel-perfect', 'bug-finding', 'zoom-tired', 'meeting-dodging', 'deadline-racing', 'coffee-seeking', 'sandwich-craving', 'debug-ready', 'rest-needing' ]; const nouns = [ 'sensor', 'robot', 'thermostat', 'gateway', 'dashboard', 'widget', 'gadget', 'button', 'antenna', 'beacon', 'adapter', 'gizmo', 'hub', 'switch', 'chip', 'controller', 'display', 'terminal', 'processor', 'transmitter', 'receiver', 'pod', 'device', 'module', 'relay', 'node', 'bridge', 'screen', 'router', 'box' ]; class NameGeneratorService { generateName() { const getRandomElement = (arr) => arr[Math.floor(Math.random() * arr.length)]; const randomAdjective = getRandomElement(adjectives); const randomNoun = getRandomElement(nouns); return `${randomAdjective}_${randomNoun}`; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NameGeneratorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NameGeneratorService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NameGeneratorService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class WorkspaceConfigComponent { constructor() { this.onConfigurationChange = new EventEmitter(); this.configurations = []; this.activeConfigTooltip = gettext('Active configuration cannot be removed'); this.removeConfigTooltip = gettext('Remove configuration'); this.formBuilder = inject(FormBuilder); this.workspaceConfigurationService = inject(WorkspaceConfigurationService); this.contextRouteService = inject(ContextRouteService); this.clipboardService = inject(ClipboardService); this.activatedRoute = inject(ActivatedRoute); this.router = inject(Router); this.nameGeneratorService = inject(NameGeneratorService); } ngOnInit() { this.initializeContextSourceId(); this.initializeConfigurations(); this.initWorkspaceForm(); } ngOnChanges(changes) { if (changes.updatedConfig && !changes.updatedConfig.firstChange) { this.updateConfigurations(); } } addConfig(duplicatedConfig) { const name = this.nameGeneratorService.generateName(); const workspace = { id: new Date().toISOString(), label: name, config: { datapoints: [], alarmsEventsConfigs: [] } }; if (duplicatedConfig) { workspace.config = duplicatedConfig; } this.configurations = [workspace, ...this.configurations]; this.initWorkspaceForm(); this.changeConfiguration(true, workspace); } changeConfiguration(selected, configuration) { if (!selected) { return; } this.currentConfiguration = configuration; localStorage.setItem(this.workspaceConfigurationService.LOCAL_STORAGE_DEFAULT_ID_KEY, this.currentConfiguration.id); this.onConfigurationChange.emit(configuration.config); } updateConfigurationLabel(configuration) { this.configurations = this.configurations.map(c => c.id === configuration.id ? configuration : c); this.workspaceConfigurationService.saveConfigurations(this.configurations, this.currentConfiguration?.id || ''); this.currentConfiguration = configuration; this.onConfigurationChange.emit(configuration.config); } deleteConfiguration(configuration) { this.configurations = this.configurations.filter(c => c.id !== configuration.id); this.initWorkspaceForm(); this.workspaceConfigurationService.saveConfigurations(this.configurations, this.currentConfiguration?.id || ''); } clearAll() { this.configurations = [this.currentConfiguration]; this.initWorkspaceForm(); this.workspaceConfigurationService.saveConfigurations(this.configurations, this.currentConfiguration?.id || ''); } async shareConfig(configuration) { await this.clipboardService.writeText(JSON.stringify(configuration.config)); } addConfigFromUrl(queryParams) { this.addConfig(); const config = JSON.parse(queryParams.config); this.onConfigurationChange.emit(config); } updateConfigurations() { this.currentConfiguration.config = this.updatedConfig; this.configurations = this.configurations.map(currentConfig => currentConfig.id === this.currentConfiguration.id ? this.currentConfiguration : currentConfig); const queryParams = { configId: this.currentConfiguration.id, label: this.currentConfiguration.label, config: JSON.stringify(this.updatedConfig) }; this.workspaceConfigurationService.saveConfigurations(this.configurations, this.currentConfiguration.id); const control = this.configurationsFormGroup.controls['configurations']; const index = this.configurations.findIndex(c => c.id === this.currentConfiguration.id); if (index !== -1) { control.at(index).patchValue({ label: this.currentConfiguration.label, config: this.currentConfiguration.config, id: this.currentConfiguration.id }); } this.router.navigate([], { queryParams }); } initializeConfigurations() { const configurations = this.workspaceConfigurationService.getConfigurations(); const defaultId = this.workspaceConfigurationService.getDefaultConfigurationId(); const queryParams = this.router.parseUrl(this.router.url).queryParams; if (configurations.length) { this.configurations = configurations; this.currentConfiguration = this.configurations.find(c => c.id === defaultId) || this.configurations[0]; this.onConfigurationChange.emit(this.currentConfiguration.config); if (queryParams?.configId && !this.configurations.find(c => c.id === queryParams.configId)) { this.addConfigFromUrl(queryParams); } else if (queryParams?.configId && this.configurations.find(c => c.id === queryParams.configId) && this.currentConfiguration.id !== queryParams.configId) { this.currentConfiguration = this.configurations.find(c => c.id === queryParams.configId); this.changeConfiguration(true, this.currentConfiguration); } } if (!this.currentConfiguration) { if (Object.keys(queryParams).length === 0) { this.addConfig(); return; } this.addConfigFromUrl(queryParams); } } initWorkspaceForm() { this.configurationsFormGroup = this.formBuilder.group({ configurations: this.formBuilder.array([]) }); this.patchForm(); } patchForm() { const control = this.configurationsFormGroup.controls['configurations']; this.configurations.forEach(workspace => { control.push(this.patchValues(workspace)); }); } patchValues(workspace) { return this.formBuilder.group({ label: [workspace.label], config: [workspace.config], id: [workspace.id] }); } initializeContextSourceId() { const routeContext = this.contextRouteService.getContextData(this.activatedRoute); if (!routeContext) { return; } const { context, contextData } = routeContext; if ([ViewContext.Device, ViewContext.Group].includes(context)) { this.workspaceConfigurationService.contextIdSignal.set(contextData?.id); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: WorkspaceConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: WorkspaceConfigComponent, isStandalone: true, selector: "c8y-workspace-config", inputs: { updatedConfig: "updatedConfig" }, outputs: { onConfigurationChange: "onConfigurationChange" }, usesOnChanges: true, ngImport: i0, template: "<div\n class=\"dropdown\"\n #actionbar_dropdown=\"bs-dropdown\"\n [cdkTrapFocus]=\"actionbar_dropdown.isOpen\"\n dropdown\n [insideClick]=\"true\"\n>\n\n <button\n class=\"dropdown-toggle form-control l-h-tight d-flex a-i-center\"\n attr.aria-label=\"{{ currentConfiguration.label }}\"\n tooltip=\"{{ 'Selected configuration' | translate }}\"\n placement=\"top\"\n container=\"body\"\n data-cy=\"current-configuration-dropdown-button\"\n [adaptivePosition]=\"false\"\n [delay]=\"500\"\n dropdownToggle\n >\n <i\n class=\"m-r-4\"\n c8yIcon=\"list\"\n ></i>\n <div class=\"d-col text-left fit-w\">\n <span class=\"text-12\">\n {{ 'Configuration' | translate }}\n </span>\n <span class=\"text-10 text-muted text-truncate\">\n {{ currentConfiguration.label }}\n </span>\n </div>\n <span class=\"caret m-r-16 m-l-4\"></span>\n </button>\n <div\n class=\"dropdown-menu dropdown-menu-wide dropdown-menu-action-bar\"\n *dropdownMenu\n >\n <div class=\"sticky-top separator-bottom p-t-8 p-b-8 p-l-16 p-r-16\">\n <p>\n <strong>{{ 'Data explorer configurations' | translate }}</strong>\n </p>\n <p>\n <small>{{ 'Easily switch and manage configurations.' | translate }}</small>\n </p>\n </div>\n <c8y-list-group class=\"no-border-last\">\n <form [formGroup]=\"configurationsFormGroup\">\n <div formArrayName=\"configurations\">\n <c8y-li\n class=\"p-0\"\n *ngFor=\"\n let configuration of configurationsFormGroup.controls.configurations['controls'];\n let i = index\n \"\n [dense]=\"true\"\n >\n <c8y-li-radio\n [selected]=\"configuration.value.id === currentConfiguration.id\"\n (onSelect)=\"changeConfiguration($event, configuration.value)\"\n ></c8y-li-radio>\n <div class=\"d-flex a-i-center gap-8\">\n <div\n class=\"flex-grow min-width-0\"\n [formGroupName]=\"i\"\n >\n <label\n class=\"editable\"\n [ngClass]=\"{\n updated:\n configuration.controls.label.touched && configuration.controls.label.dirty\n }\"\n >\n <input\n class=\"form-control\"\n [style.width.ch]=\"configuration.value.label || 25\"\n [attr.aria-label]=\"'Configuration label' | translate\"\n placeholder=\"{{ 'Configuration 1' | translate }}\"\n type=\"text\"\n autocomplete=\"off\"\n required\n formControlName=\"label\"\n (blur)=\"updateConfigurationLabel(configuration.value)\"\n />\n </label>\n </div>\n <div class=\"flex-nogrow d-flex gap-8\">\n <button\n class=\"btn-dot btn\"\n [attr.aria-label]=\"'Duplicate configuration' | translate\"\n tooltip=\"{{ 'Duplicate configuration' | translate }}\"\n placement=\"left\"\n (click)=\"addConfig(configuration.value.config)\"\n [delay]=\"500\"\n >\n <i c8yIcon=\"copy\"></i>\n </button>\n\n <button\n class=\"btn-dot btn btn-dot--danger\"\n [attr.aria-label]=\"'Remove configurations' | translate\"\n tooltip=\"{{\n (configuration.value.id === currentConfiguration.id\n ? activeConfigTooltip\n : removeConfigTooltip\n ) | translate\n }}\"\n placement=\"left\"\n [delay]=\"500\"\n (click)=\"$event.stopPropagation(); deleteConfiguration(configuration.value)\"\n [disabled]=\"configuration.value.id === currentConfiguration.id\"\n >\n <i c8yIcon=\"minus-circle\"></i>\n </button>\n </div>\n </div>\n </c8y-li>\n </div>\n </form>\n </c8y-list-group>\n <div class=\"sticky-bottom separator-top\">\n <div class=\"d-flex p-l-16 p-r-16 p-t-8 p-b-8\">\n <button\n class=\"btn btn-danger btn-sm flex-grow\"\n (click)=\"clearAll()\"\n [disabled]=\"configurations.length < 2\"\n >\n <i [c8yIcon]=\"'delete'\"></i>\n {{ 'Delete all configurations' | translate }}\n </button>\n <button\n class=\"btn btn-default btn-sm flex-grow\"\n type=\"button\"\n (click)=\"addConfig()\"\n >\n <i [c8yIcon]=\"'add-circle-outline'\"></i>\n {{ 'Add configuration' | translate }}\n </button>\n </div>\n </div>\n </div>\n</div>\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: i3$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "component", type: i1.ListGroupComponent, selector: "c8y-list-group" }, { kind: "component", type: i1.ListItemComponent, selector: "c8y-list-item, c8y-li", inputs: ["active", "highlighted", "emptyActions", "dense", "collapsed", "selectable"], outputs: ["collapsedChange"] }, { kind: "component", type: i1.ListItemRadioComponent, selector: "c8y-list-item-radio, c8y-li-radio", inputs: ["selected", "name", "disabled", "value"], outputs: ["onSelect"] }, { kind: "directive", type: i3.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i3.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i3.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "directive", type: i3.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { kind: "ngmodule", type: BsDropdownModule }, { kind: "directive", type: i4.BsDropdownMenuDirective, selector: "[bsDropdownMenu],[dropdownMenu]", exportAs: ["bs-dropdown-menu"] }, { kind: "directive", type: i4.BsDropdownToggleDirective, selector: "[bsDropdownToggle],[dropdownToggle]", exportAs: ["bs-dropdown-toggle"] }, { kind: "directive", type: i4.BsDropdownDirective, selector: "[bsDropdown], [dropdown]", inputs: ["placement", "triggers", "container", "dropup", "autoClose", "isAnimated", "insideClick", "isDisabled", "isOpen"], outputs: ["isOpenChange", "onShown", "onHidden"], exportAs: ["bs-dropdown"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i5.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i6.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"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: WorkspaceConfigComponent, decorators: [{ type: Component, args: [{ selector: 'c8y-workspace-config', standalone: true, imports: [CoreModule, BsDropdownModule, FormsModule, A11yModule, TooltipModule], template: "<div\n class=\"dropdown\"\n #actionbar_dropdown=\"bs-dropdown\"\n [cdkTrapFocus]=\"actionbar_dropdown.isOpen\"\n dropdown\n [insideClick]=\"true\"\n>\n\n <button\n class=\"dropdown-toggle form-control l-h-tight d-flex a-i-center\"\n attr.aria-label=\"{{ currentConfiguration.label }}\"\n tooltip=\"{{ 'Selected configuration' | translate }}\"\n placement=\"top\"\n container=\"body\"\n data-cy=\"current-configuration-dropdown-button\"\n [adaptivePosition]=\"false\"\n [delay]=\"500\"\n dropdownToggle\n >\n <i\n class=\"m-r-4\"\n c8yIcon=\"list\"\n ></i>\n <div class=\"d-col text-left fit-w\">\n <span class=\"text-12\">\n {{ 'Configuration' | translate }}\n </span>\n <span class=\"text-10 text-muted text-truncate\">\n {{ currentConfiguration.label }}\n </span>\n </div>\n <span class=\"caret m-r-16 m-l-4\"></span>\n </button>\n <div\n class=\"dropdown-menu dropdown-menu-wide dropdown-menu-action-bar\"\n *dropdownMenu\n >\n <div class=\"sticky-top separator-bottom p-t-8 p-b-8 p-l-16 p-r-16\">\n <p>\n <strong>{{ 'Data explorer configurations' | translate }}</strong>\n </p>\n <p>\n <small>{{ 'Easily switch and manage configurations.' | translate }}</small>\n </p>\n </div>\n <c8y-list-group class=\"no-border-last\">\n <form [formGroup]=\"configurationsFormGroup\">\n <div formArrayName=\"configurations\">\n <c8y-li\n class=\"p-0\"\n *ngFor=\"\n let configuration of configurationsFormGroup.controls.configurations['controls'];\n let i = index\n \"\n [dense]=\"true\"\n >\n <c8y-li-radio\n [selected]=\"configuration.value.id === currentConfiguration.id\"\n (onSelect)=\"changeConfiguration($event, configuration.value)\"\n ></c8y-li-radio>\n <div class=\"d-flex a-i-center gap-8\">\n <div\n class=\"flex-grow min-width-0\"\n [formGroupName]=\"i\"\n >\n <label\n class=\"editable\"\n [ngClass]=\"{\n updated:\n configuration.controls.label.touched && configuration.controls.label.dirty\n }\"\n >\n <input\n class=\"form-control\"\n [style.width.ch]=\"configuration.value.label || 25\"\n [attr.aria-label]=\"'Configuration label' | translate\"\n placeholder=\"{{ 'Configuration 1' | translate }}\"\n type=\"text\"\n autocomplete=\"off\"\n required\n formControlName=\"label\"\n (blur)=\"updateConfigurationLabel(configuration.value)\"\n />\n </label>\n </div>\n <div class=\"flex-nogrow d-flex gap-8\">\n <button\n class=\"btn-dot btn\"\n [attr.aria-label]=\"'Duplicate configuration' | translate\"\n tooltip=\"{{ 'Duplicate configuration' | translate }}\"\n placement=\"left\"\n (click)=\"addConfig(configuration.value.config)\"\n [delay]=\"500\"\n >\n <i c8yIcon=\"copy\"></i>\n </button>\n\n <button\n class=\"btn-dot btn btn-dot--danger\"\n [attr.aria-label]=\"'Remove configurations' | translate\"\n tooltip=\"{{\n (configuration.value.id === currentConfiguration.id\n ? activeConfigTooltip\n : removeConfigTooltip\n ) | translate\n }}\"\n placement=\"left\"\n [delay]=\"500\"\n (click)=\"$event.stopPropagation(); deleteConfiguration(configuration.value)\"\n [disabled]=\"configuration.value.id === currentConfiguration.id\"\n >\n <i c8yIcon=\"minus-circle\"></i>\n </button>\n </div>\n </div>\n </c8y-li>\n </div>\n </form>\n </c8y-list-group>\n <div class=\"sticky-bottom separator-top\">\n <div class=\"d-flex p-l-16 p-r-16 p-t-8 p-b-8\">\n <button\n class=\"btn btn-danger btn-sm flex-grow\"\n (click)=\"clearAll()\"\n [disabled]=\"configurations.length < 2\"\n >\n <i [c8yIcon]=\"'delete'\"></i>\n {{ 'Delete all configurations' | translate }}\n </button>\n <button\n class=\"btn btn-default btn-sm flex-grow\"\n type=\"button\"\n (click)=\"addConfig()\"\n >\n <i [c8yIcon]=\"'add-circle-outline'\"></i>\n {{ 'Add configuration' | translate }}\n </button>\n </div>\n </div>\n </div>\n</div>\n" }] }], propDecorators: { updatedConfig: [{ type: Input }], onConfigurationChange: [{ type: Output }] } }); class DatapointExplorerComponent { #destroyRef; constructor() { this.config = { datapoints: [], alarmsEventsConfigs: [] }; this.alarms = []; this.events = []; this.datapointsOutOfSync = new Map(); this.hasAtLeastOneDatapointActive = true; this.hasAtLeastOneAlarmActive = true; this.isMarkedAreaEnabled = false; this.alarmsOrEventsHaveNoMatchingDps = false; this.canAddReport = false; this.datapointSelectDefaultFormOptions = { showRange: true, showChart: true }; this.#destroyRef = inject(DestroyRef); this.dataExplorerService = inject(DataExplorerService); this.formBuilder = inject(FormBuilder); this.alertService = inject(AlertService); this.bsModalService = inject(BsModalService); this.permissions = inject(Permissions); this.contextDashboardService = inject(ContextDashboardService); this.activatedRoute = inject(ActivatedRoute); this.contextRouteService = inject(ContextRouteService); this.formGroup = this.initForm(); } ngOnInit() { this.contextAsset = this.initializeContextSourceId(); this.checkForMatchingDatapoints(); this.canAddReport = this.permissions.hasAnyRole([ Permissions.ROLE_INVENTORY_CREATE, Permissions.ROLE_INVENTORY_ADMIN, Permissions.ROLE_MANAGED_OBJECT_ADMIN, Permissions.ROLE_MANAGED_OBJECT_CREATE ]); this.formGroup.valueChanges .pipe(takeUntilDestroyed(this.#destroyRef)) .subscribe(configChange => { const { alarms, events, ...configValues } = configChange; this.config = { ...configValues, alarmsEventsConfigs: [...(alarms || []), ...(events || [])] }; this.updateExportConfig(); this.checkForMatchingDatapoints(); }); } onTimeContextChange(timeProps) { const realtime = this.formGroup.controls.realtime.value; if (timeProps.realtime !== realtime) { this.formGroup.patchValue({ realtime: timeProps.realtime }); } if (timeProps.realtime) { if (timeProps.currentDateContextInterval !== this.formGroup.value.interval) { this.formGroup.patchValue({ interval: timeProps.currentDateContextInterval }); } return; } this.formGroup.patchValue({ dateFrom: timeProps.currentDateContextFromDate, dateTo: timeProps.currentDateContextToDate, interval: timeProps.currentDateContextInterval, aggregation: timeProps.aggregation || null }); } onConfigurationChange(config) { if (config.dateFrom && config.dateTo) { this.timeProps = { dateFrom: new Date(config?.dateFrom), dateTo: new Date(config?.dateTo), interval: config?.interval, realtime: config?.realtime, aggregation: config?.realtime ? null : config?.aggregation }; } this.alarms = config.alarmsEventsConfigs.filter(ae => ae.timelineType === 'ALARM'); this.events = config.alarmsEventsConfigs.filter(ae => ae.timelineType === 'EVENT'); this.formGroup.patchValue({ alarms: this.alarms, events: this.events, ...config }); } onSliderZoom(timeProps) { this.formGroup.patchValue(timeProps); this.timeProps = { ...timeProps, realtime: false }; } updateTimeRangeOnRealtime(timeRange) { this.formGroup.patchValue(timeRange, { emitEvent: false }); } createNewReportWithWidget() { const modal = this.bsModalService.show(CreateNewReportModalComponent, { ignoreBackdropClick: true, keyboard: false, class: 'modal-sm' }); const content = modal.content; content.result .then(async (report) => { const reportDashboard = await this.dataExplorerService.fetchReportDashboard(report.id); const widget = this.createWidgetConfig(); reportDashboard.c8y_Dashboard.children = { [widget.id]: widget }; await this.contextDashboardService.update(reportDashboard); this.alertService.success(gettext('Report and widget created.')); }) .catch(e => { if (e) { this.alertService.danger(gettext('Failed to create report and widget.')); this.alertService.addServerFailure(e); } // else: modal was closed }); } sendAsWidgetToReport() { const modal = this.bsModalService.show(SendAsWidgetToReportModal, { ignoreBackdropClick: true, keyboard: false, class: 'modal-sm' }); const content = modal.content; content.result .then(async (reports) => { const selectedReports = Object.entries(reports).filter(([, value]) => value); const reportPromises = selectedReports.map(async ([reportId]) => { const reportDashboard = await this.dataExplorerService.fetchReportDashboard(reportId); const widget = this.createWidgetConfig(); const children = reportDashboard.c8y_Dashboard.children || {}; reportDashboard.c8y_Dashboard.children = { ...children, [widget.id]: widget }; await this.contextDashboardService.update(reportDashboard); }); await Promise.all(reportPromises); this.alertService.success(gettext('Widget created.')); }) .catch(e => { if (e) { this.alertService.danger(gettext('Failed to create widget.')); this.alertService.addServerFailure(e); } // else: modal was closed }); } updateAlarmsAndEvents(alarmsEventsConfigs) { this.alarms = alarmsEventsConfigs.filter(this.isAlarm); this.events = alarmsEventsConfigs.filter(this.isEvent); this.hasAtLeastOneAlarmActive = this.hasActiveAlarms(this.alarms); } handleDatapointOutOfSync(dpOutOfSync) { const key = (dp) => dp.__target?.id + dp.fragment + dp.series; const dpMatch = this.config.datapoints?.find(dp => key(dp) === key(dpOutOfSync)); if (!dpMatch) { return; } this.datapointsOutOfSync.set(dpMatch, true); } isAlarm(item) { return item.timelineType === 'ALARM'; } isEvent(item) { return item.timelineType === 'EVENT'; } hasActiveAlarms(alarms) { return alarms.length > 0 && alarms.some(alarm => alarm.__active); } checkForMatchingDatapoints() { const allMatch = this.config?.alarmsEventsConfigs?.every(ae => this.formGroup.value.datapoints?.some(dp => dp.__target?.id === ae.__target?.id)); queueMicrotask(() => { this.alarmsOrEventsHaveNoMatchingDps = !allMatch; }); } updateExportConfig() { const datapointDetails = this.config.datapoints .filter(({ __active }) => __active) .map(({ __target, fragment, series }) => ({ deviceName: __target.name, source: __target.id, valueFragmentSeries: series, valueFragmentType: fragme