@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
596 lines (588 loc) • 72.8 kB
JavaScript
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