UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

887 lines (877 loc) 223 kB
import * as i0 from '@angular/core'; import { Injectable, EventEmitter, Component, Input, Output, forwardRef, Directive, ViewChild, ViewChildren, Optional, NgModule } from '@angular/core'; import * as i1 from '@c8y/client'; import { BehaviorSubject, merge, Subject } from 'rxjs'; import { omit, isEmpty, some, isEqual, isNil, get, assign, cloneDeep, unset as unset$1, set as set$1, reject, find, findIndex, pick, has } from 'lodash-es'; import * as i2 from '@angular/common'; import * as i2$1 from '@c8y/ngx-components'; import { gettext, Status, DropAreaComponent, ViewContext, CoreModule, FormsModule, DropAreaModule, DeviceStatusModule, DynamicFormsModule, hookRoute } from '@c8y/ngx-components'; import * as i2$2 from '@angular/router'; import { RouterModule } from '@angular/router'; import * as i4 from '@angular/forms'; import { NG_VALIDATORS, ControlContainer, NgModelGroup, NgForm, ReactiveFormsModule } from '@angular/forms'; import * as i5 from '@angular/cdk/tree'; import { NestedTreeControl, CdkTreeModule } from '@angular/cdk/tree'; import * as i8 from '@c8y/ngx-components/device-protocol-object-mappings'; import { BaseObjectMapping, MeasurementObjectMapping, EventObjectMapping, AlarmObjectMapping, ALARM_SEVERITY, ObjectMappingComponent } from '@c8y/ngx-components/device-protocol-object-mappings'; import * as i4$2 from 'ngx-bootstrap/buttons'; import { ButtonsModule } from 'ngx-bootstrap/buttons'; import * as i5$1 from 'ngx-bootstrap/tooltip'; import { TooltipModule } from 'ngx-bootstrap/tooltip'; import * as i4$1 from 'ngx-bootstrap/collapse'; import { CollapseModule } from 'ngx-bootstrap/collapse'; import * as i7 from 'ngx-bootstrap/dropdown'; import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; import { map, takeUntil } from 'rxjs/operators'; import { clone, toInteger, unset, set } from 'lodash'; import { UpgradeComponent, downgradeComponent } from '@angular/upgrade/static'; import * as i3 from '@ngx-translate/core'; import * as i5$2 from 'ngx-bootstrap/popover'; import { PopoverModule } from 'ngx-bootstrap/popover'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import * as angular from 'angular'; import { registerNgModule } from '@c8y/ng1-modules'; class AddressSpaceService { constructor(fetchClient) { this.client = fetchClient; this.microserviceUrl = '/service/opcua-mgmt-service/address-space'; this.header = { 'Content-Type': 'application/json' }; this.nodeNavigationData$ = new BehaviorSubject({ node: undefined, selectedAncestorIds: [] }); } resetTreeToRootNode() { this.triggerNodeToOpen({ node: undefined, selectedAncestorIds: [] }); } triggerNodeToOpen(nodeNavigationData) { this.nodeNavigationData$.next(nodeNavigationData); } getNodeNavData$() { return this.nodeNavigationData$.asObservable(); } getNode(serverId, nodeId) { if (serverId && serverId.length > 0) { if (nodeId && nodeId.length > 0) { return this.getNodeById(serverId, nodeId); } return this.getRootNode(serverId); } } getRootNode(serverId) { if (serverId && serverId.length > 0) { const options = { method: 'GET', headers: this.header }; return this.client.fetch(`${this.microserviceUrl}/${serverId}`, options); } } getNodeById(serverId, nodeId) { if (serverId && nodeId && serverId.length > 0 && nodeId.length > 0) { const options = { method: 'GET', headers: this.header }; const param = encodeURIComponent(nodeId); return this.client.fetch(`${this.microserviceUrl}/${serverId}?nodeId=${param}`, options); } } getChildrenOf(node, serverId) { if (serverId && node.nodeId && serverId.length > 0 && node.nodeId.length > 0) { const options = { method: 'GET', headers: this.header }; const param = encodeURIComponent(node.nodeId); return this.client.fetch(`${this.microserviceUrl}/${serverId}/children?nodeId=${param}`, options); } } childrenAvailable(nodeReferences) { if (!nodeReferences || nodeReferences.length === 0) { return false; } return nodeReferences.some(ref => !ref.inverse && ref.hierarchical); } async getSearchedNodes(searchKey, serverId) { const url = `service/opcua-mgmt-service/search/${serverId}/`; const options = { headers: this.header, params: { searchString: '*' + searchKey + '*' } }; const res = await this.client.fetch(url, options); return res.json(); } getIcon(nodeClassName) { const iconList = { Object: 'cube', Variable: 'th-list', Method: 'random', View: 'window-maximize', ObjectType: 'c8y-group', VariableType: 'c8y-group', ReferenceType: 'c8y-group', DataType: 'c8y-group' }; return iconList[nodeClassName] || 'circle'; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AddressSpaceService, deps: [{ token: i1.FetchClient }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AddressSpaceService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AddressSpaceService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i1.FetchClient }] }); class OpcuaAddressSpaceDetailComponent { set node(n) { this._node = n; if (n) { this.setNodeData(n); } else { // remove details from current view this.showDetails = false; } } constructor(addressSpaceService) { this.addressSpaceService = addressSpaceService; this.selected = false; this.showDetails = false; this.toggleAttrDetail = new EventEmitter(); } setNodeData(nodeData) { this.showDetails = true; const { attributes, references } = nodeData; this.nodeDataRef = references; const omitList = [ 'attributes', 'references', 'children', 'currentlyLoadingChildren', 'expanded', 'browsePath', 'relativePath', 'parentNode' ]; this.nodeDataAttr = Object.assign({}, attributes, omit(nodeData, omitList)); } toggleDetail(node) { this.showDetails = !this.showDetails; this.toggleAttrDetail.emit(node); } navigateTo(ancestors) { const nodeNavData = { node: this._node, selectedAncestorIds: ancestors }; this.toggleDetail(this._node); this.addressSpaceService.triggerNodeToOpen(nodeNavData); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OpcuaAddressSpaceDetailComponent, deps: [{ token: AddressSpaceService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: OpcuaAddressSpaceDetailComponent, selector: "opcua-address-space-detail", inputs: { node: "node" }, outputs: { toggleAttrDetail: "toggleAttrDetail" }, ngImport: i0, template: "<div\n class=\"card m-b-4 split-row-2 animated fast pointer-all\"\n [ngClass]=\"{ fadeInRightBig: showDetails, fadeOutRightBig: !showDetails }\"\n>\n <div class=\"card-header separator\">\n <h4>{{ 'Attributes' | translate }}</h4>\n <button\n class=\"close m-l-auto visible-sm visible-xs\"\n title=\"{{ 'Close' | translate }}\"\n (click)=\"toggleDetail(nodeDataAttr)\"\n >\n &times;\n </button>\n </div>\n <div\n class=\"card-inner-scroll\"\n tabindex=\"0\"\n >\n <div\n class=\"card-block\"\n tabindex=\"-1\"\n >\n <table class=\"table table-striped table-condensed\">\n <colgroup>\n <col width=\"50%\" />\n <col width=\"50%\" />\n </colgroup>\n <thead>\n <tr>\n <th>{{ 'Attribute' | translate }}</th>\n <th>{{ 'Value' | translate }}</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let item of nodeDataAttr | keyvalue\">\n <td>{{ item.key }}</td>\n <td\n class=\"text-break-word\"\n *ngIf=\"item.key === 'absolutePaths'\"\n >\n {{ item.value | json }}\n </td>\n <td\n class=\"text-break-word\"\n *ngIf=\"item.key === 'ancestorNodeIds'\"\n >\n <a\n *ngFor=\"let value of item.value\"\n (click)=\"navigateTo(value)\"\n >\n {{ value | json }}\n </a>\n </td>\n <td *ngIf=\"item.key !== 'absolutePaths' && item.key !== 'ancestorNodeIds'\">\n {{ item.value }}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n</div>\n<div\n class=\"card split-row-2 animated fast pointer-all\"\n style=\"height: calc(50% - 4px)\"\n [ngClass]=\"{ fadeInRightBig: showDetails, fadeOutRightBig: !showDetails }\"\n>\n <div class=\"card-header separator\">\n <h4>{{ 'References' | translate }}</h4>\n </div>\n <div\n class=\"card-inner-scroll\"\n tabindex=\"0\"\n >\n <div\n class=\"card-block\"\n tabindex=\"-1\"\n >\n <table class=\"table table-striped table-condensed\">\n <colgroup>\n <col width=\"50%\" />\n <col width=\"50%\" />\n </colgroup>\n <thead>\n <tr>\n <th>{{ 'Attribute' | translate }}</th>\n <th>{{ 'Value' | translate }}</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let item of nodeDataRef\">\n <td>{{ item.referenceLabel }}</td>\n <td class=\"text-break-word\">{{ item.targetLabel }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i2$1.C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: i2.JsonPipe, name: "json" }, { kind: "pipe", type: i2.KeyValuePipe, name: "keyvalue" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OpcuaAddressSpaceDetailComponent, decorators: [{ type: Component, args: [{ selector: 'opcua-address-space-detail', template: "<div\n class=\"card m-b-4 split-row-2 animated fast pointer-all\"\n [ngClass]=\"{ fadeInRightBig: showDetails, fadeOutRightBig: !showDetails }\"\n>\n <div class=\"card-header separator\">\n <h4>{{ 'Attributes' | translate }}</h4>\n <button\n class=\"close m-l-auto visible-sm visible-xs\"\n title=\"{{ 'Close' | translate }}\"\n (click)=\"toggleDetail(nodeDataAttr)\"\n >\n &times;\n </button>\n </div>\n <div\n class=\"card-inner-scroll\"\n tabindex=\"0\"\n >\n <div\n class=\"card-block\"\n tabindex=\"-1\"\n >\n <table class=\"table table-striped table-condensed\">\n <colgroup>\n <col width=\"50%\" />\n <col width=\"50%\" />\n </colgroup>\n <thead>\n <tr>\n <th>{{ 'Attribute' | translate }}</th>\n <th>{{ 'Value' | translate }}</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let item of nodeDataAttr | keyvalue\">\n <td>{{ item.key }}</td>\n <td\n class=\"text-break-word\"\n *ngIf=\"item.key === 'absolutePaths'\"\n >\n {{ item.value | json }}\n </td>\n <td\n class=\"text-break-word\"\n *ngIf=\"item.key === 'ancestorNodeIds'\"\n >\n <a\n *ngFor=\"let value of item.value\"\n (click)=\"navigateTo(value)\"\n >\n {{ value | json }}\n </a>\n </td>\n <td *ngIf=\"item.key !== 'absolutePaths' && item.key !== 'ancestorNodeIds'\">\n {{ item.value }}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n</div>\n<div\n class=\"card split-row-2 animated fast pointer-all\"\n style=\"height: calc(50% - 4px)\"\n [ngClass]=\"{ fadeInRightBig: showDetails, fadeOutRightBig: !showDetails }\"\n>\n <div class=\"card-header separator\">\n <h4>{{ 'References' | translate }}</h4>\n </div>\n <div\n class=\"card-inner-scroll\"\n tabindex=\"0\"\n >\n <div\n class=\"card-block\"\n tabindex=\"-1\"\n >\n <table class=\"table table-striped table-condensed\">\n <colgroup>\n <col width=\"50%\" />\n <col width=\"50%\" />\n </colgroup>\n <thead>\n <tr>\n <th>{{ 'Attribute' | translate }}</th>\n <th>{{ 'Value' | translate }}</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let item of nodeDataRef\">\n <td>{{ item.referenceLabel }}</td>\n <td class=\"text-break-word\">{{ item.targetLabel }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n</div>\n" }] }], ctorParameters: () => [{ type: AddressSpaceService }], propDecorators: { node: [{ type: Input }], toggleAttrDetail: [{ type: Output }] } }); class OpcuaService { constructor(client, inventoryService, router, alertService) { this.client = client; this.inventoryService = inventoryService; this.router = router; this.alertService = alertService; this.microserviceUrl = '/service/opcua-mgmt-service/server'; this.deviceTypeProtocolUrl = '/service/opcua-mgmt-service/deviceTypes'; this.header = { 'Content-Type': 'application/json' }; this.binaryService = inventoryService.binary; } getServers(id) { if (id && id.length > 0) { const options = { method: 'GET', headers: this.header }; return this.client.fetch(`${this.microserviceUrl}/${id}`, options); } } createServer(data) { if (this.doesGatewayIdExist(data)) { this.cleanUpPayload(data); const options = { method: 'POST', headers: this.header, body: JSON.stringify(data) }; return this.client.fetch(`${this.microserviceUrl}`, options); } } async updateServer(server) { if (this.doesGatewayIdExist(server) && this.doesIdExist(server)) { this.cleanUpPayload(server); const options = { method: 'POST', headers: this.header, body: JSON.stringify(server) }; const res = await this.client.fetch(`${this.microserviceUrl}`, options); let data; try { data = await res.json(); } catch (e) { // nothing } if (res.status !== 200) { this.alertService.addServerFailure({ data, res }); } else { return data; } } } removeServer(data) { if (this.doesGatewayIdExist(data) && this.doesIdExist(data)) { const options = { method: 'DELETE' }; return this.client.fetch(`${this.microserviceUrl}/${data.gatewayId}/${data.id}`, options); } } getKeystore(binaryId) { if (binaryId && binaryId.length > 0) { return this.inventoryService.detail(binaryId); } return null; } uploadKeystore(file) { if (file && file.size > 0) { return this.binaryService.create(file); } return Promise.reject('Invalid file'); } async updateKeystore(id, file) { if (id && id.length > 0 && file && file.size > 0) { const { res } = await this.removeKeystore(id); if (res && res.status === 204) { return this.uploadKeystore(file); } } return Promise.reject('Invalid file'); } removeKeystore(id) { if (id && id.length > 0) { return this.binaryService.delete(id); } } getMoId() { const currentUrl = this.router.routerState.snapshot.url; const isDevice = new RegExp(/device\/\d+/).test(currentUrl); if (isDevice) { return currentUrl.match(/\d+/)[0]; } return ''; } getId() { const currentUrl = this.router.routerState.snapshot.url; const isDeviceprotocol = new RegExp(/deviceprotocols/).test(currentUrl); if (isDeviceprotocol && RegExp(/\d+$/).test(currentUrl)) { return currentUrl.match(/\d+$/)[0]; } } async getDeviceProtocol(id) { const options = { method: 'GET', headers: this.header }; return this.client.fetch(`${this.deviceTypeProtocolUrl}/${id}`, options); } async updateDeviceProtocol(data) { const options = { method: 'PUT', headers: this.header, body: JSON.stringify(data) }; return this.client.fetch(`${this.deviceTypeProtocolUrl}/${data.id}`, options); } async createDeviceProtocol(data) { const options = { method: 'POST', headers: this.header, body: JSON.stringify(data) }; return this.client.fetch(`${this.deviceTypeProtocolUrl}`, options); } doesGatewayIdExist(data) { return data && data.gatewayId && data.gatewayId.length > 0; } doesIdExist(data) { return data && data.id && data.id.length > 0 && data.id !== 'new'; } cleanUpPayload(data) { if (data) { if (data.id && data.id === 'new') { delete data.id; } if (data.quickInfo) { delete data.quickInfo; } } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OpcuaService, deps: [{ token: i1.FetchClient }, { token: i1.InventoryService }, { token: i2$2.Router }, { token: i2$1.AlertService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OpcuaService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OpcuaService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i1.FetchClient }, { type: i1.InventoryService }, { type: i2$2.Router }, { type: i2$1.AlertService }] }); class OpcuaAgentGuard { constructor() { this.type = 'c8y_OPCUA_Device_Agent'; } canActivate({ data }) { const { contextData } = data; return contextData && contextData.type === this.type; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OpcuaAgentGuard, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OpcuaAgentGuard }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OpcuaAgentGuard, decorators: [{ type: Injectable }] }); class OpcuaDeviceProtocolBrowsePathValidation { constructor(el) { this.el = el; } validate(control) { if (control.value) { if (!this.isValidJson(control.value)) { return { invalidBrowsePathNotation: true }; } else { if (this.isBrowsePathUnique(control.value)) { return { browsePathNotUnique: true }; } } } return null; } isValidJson(value) { try { const browsePath = JSON.parse(value); return !isEmpty(browsePath); } catch (error) { return false; } } toArray(str) { return JSON.parse(str); } isBrowsePathUnique(value) { const mappings = this.getMappings(); const found = some(mappings, item => { if (isEqual(item.browsePath, this.toArray(value)) && item.id !== this.model.id) { return item; } }); return found ? true : false; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OpcuaDeviceProtocolBrowsePathValidation, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.13", type: OpcuaDeviceProtocolBrowsePathValidation, selector: "[c8yBrowsePathValidator][ngModel]", inputs: { getMappings: "getMappings", model: "model" }, providers: [ { provide: NG_VALIDATORS, useExisting: forwardRef(() => OpcuaDeviceProtocolBrowsePathValidation), multi: true } ], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OpcuaDeviceProtocolBrowsePathValidation, decorators: [{ type: Directive, args: [{ selector: '[c8yBrowsePathValidator][ngModel]', providers: [ { provide: NG_VALIDATORS, useExisting: forwardRef(() => OpcuaDeviceProtocolBrowsePathValidation), multi: true } ] }] }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { getMappings: [{ type: Input }], model: [{ type: Input }] } }); class OpcuaCustomActionObjectMapping extends BaseObjectMapping { constructor() { super({ icon: 'bell', label: gettext('Custom action'), formlyFieldConfig: { key: 'customAction', fieldGroup: [ { type: 'array', key: 'headers', props: { label: gettext('headers'), addText: gettext('Add Header'), required: true }, className: 'formly-group-array-cols d-block min-height-fit', fieldArray: { fieldGroup: [ { key: 'key', type: 'string', focus: true, props: { placeholder: 'Authorization', label: gettext('Key'), required: true, smallFormGroup: true }, wrappers: ['c8y-form-field'] }, { key: 'value', type: 'string', props: { placeholder: 'Basic <credentials>', label: gettext('Value'), required: true, smallFormGroup: true } } ] } }, { className: 'row', wrappers: ['c8y-legend-wrapper'], props: { label: gettext('Action') }, fieldGroup: [ { type: 'string', key: 'endpoint', props: { label: gettext('Endpoint'), smallFormGroup: true, required: true }, className: 'col-md-6 p-0' }, { type: 'textarea', key: 'bodyTemplate', props: { label: gettext('Body template'), smallFormGroup: true, required: true, description: gettext('The following placeholders are available: ${value}`KEEP_ORIGINAL`, ${serverId}`KEEP_ORIGINAL`, ${nodeId}`KEEP_ORIGINAL`, ${deviceId}`KEEP_ORIGINAL`.') }, className: 'col-md-6' } ] } ] } }); } } class OpcuaMeasurementObjectMapping extends MeasurementObjectMapping { constructor(smallFormGroup = true) { super(null, 'measurementCreation', smallFormGroup); this.smallFormGroup = smallFormGroup; } } class OpcuaEventObjectMapping extends EventObjectMapping { constructor(smallFormGroup = true) { super(null, 'eventCreation', smallFormGroup, undefined, [ { key: 'type', type: 'string', props: { label: gettext('Type'), required: true, smallFormGroup }, className: 'col-md-3 col-sm-6' }, { key: 'text', type: 'string', props: { label: gettext('Text'), required: true, smallFormGroup }, className: 'col-md-3 col-sm-6' } ]); this.smallFormGroup = smallFormGroup; } } class OpcuaAlarmObjectMapping extends AlarmObjectMapping { constructor(smallFormGroup = true) { super(null, 'alarmCreation', smallFormGroup, undefined, [ { key: 'severity', type: 'select', props: { label: gettext('Severity'), options: [...Object.values(ALARM_SEVERITY).map(value => ({ label: value, value }))], required: true, smallFormGroup }, className: 'col-md-3 col-sm-6' }, { key: 'type', type: 'string', props: { label: gettext('Type'), smallFormGroup, required: true }, className: 'col-md-3 col-sm-6' }, { key: 'text', type: 'string', props: { label: gettext('Text'), smallFormGroup, required: true }, className: 'col-md-3 col-sm-6' } ]); this.smallFormGroup = smallFormGroup; } } class DynamicDataSource { get data() { return this.dataChange.value; } set data(value) { this.treeControl.dataNodes = value; this.dataChange.next(value); } constructor(treeControl, addressSpaceService, serverId) { this.treeControl = treeControl; this.addressSpaceService = addressSpaceService; this.serverId = serverId; this.dataChange = new BehaviorSubject([]); this.treeControl.isExpanded = (node) => node.expanded; } connect(collectionViewer) { this.treeControl.expansionModel.changed.subscribe((change) => { if (change.added || change.removed) { this.handleTreeControl(change); } }); return merge(collectionViewer.viewChange, this.dataChange).pipe(map(() => this.data)); } /** Handle expand/collapse behaviors */ handleTreeControl(change) { if (change.added) { change.added.forEach(node => this.toggleNode(node, true)); } if (change.removed) { change.removed .slice() .reverse() .forEach(node => this.toggleNode(node, false)); } } /** * Toggle the node, remove from display list */ async toggleNode(addressSpaceNode, expand) { if (!addressSpaceNode.children || addressSpaceNode.children.length === 0) { addressSpaceNode.currentlyLoadingChildren = true; const res = await this.addressSpaceService.getChildrenOf(addressSpaceNode, this.serverId); const children = (await res.json()); addressSpaceNode.children = children || []; addressSpaceNode.children = addressSpaceNode.children.map((node) => { node.parentNode = addressSpaceNode; return node; }); addressSpaceNode.currentlyLoadingChildren = false; this.treeControl.expand(addressSpaceNode); } addressSpaceNode.expanded = expand && addressSpaceNode.children.length > 0; this.refreshNestedTree(this.data); return Promise.resolve(addressSpaceNode); } catch() { // do nothing } refreshNestedTree(treeData) { // necessary to rerender tree, otherwise new nodes will not // appear, but they are added to the list. this.data = []; this.dataChange.next(treeData); this.triggerResize(); // to resize the modal window when creating a new device protocol } triggerResize() { setTimeout(() => { try { window.dispatchEvent(new Event('resize')); } catch (error) { // do nothing } }, 200); } } class OpcuaAddressSpaceTreeComponent { set moId(id) { this._moId = id || undefined; } constructor(addressSpaceService, opcuaService, alertService) { this.addressSpaceService = addressSpaceService; this.opcuaService = opcuaService; this.alertService = alertService; this.focusEmitter = new EventEmitter(); this.selectedNode = new EventEmitter(); this.dataSource = null; this.loading = false; this.destroy$ = new Subject(); this.getChildren = (node) => (node.expanded ? node.children : []); this.hasChild = (_, _nodeData) => this.addressSpaceService.childrenAvailable(_nodeData.references); } ngOnInit() { this.initializeDataSet(); } ngOnChanges(changes) { if (changes.moId && changes.moId.previousValue && changes.moId.currentValue !== changes.moId.previousValue) { this.initializeDataSet(); } } initializeDataSet() { this.nodeNavDataSubscription = this.addressSpaceService .getNodeNavData$() .pipe(takeUntil(this.destroy$)) .subscribe(nodeNavData => this.openNode(nodeNavData)); this.subscriptionRef = this.focusEmitter.subscribe(node => { this.focused = this.isFocusedNode(node) ? undefined : node; }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); // clean up the address-space-tree this.addressSpaceService.resetTreeToRootNode(); if (this.nodeNavDataSubscription && !this.nodeNavDataSubscription.closed) { this.nodeNavDataSubscription.unsubscribe(); } if (this.subscriptionRef && !this.subscriptionRef.closed) { this.subscriptionRef.unsubscribe(); } } async openNode(nodeNavData) { const { node, selectedAncestorIds } = nodeNavData; let nodeId; // We just set the nodeId when the selectedAncestorIds variable an empty array. // If selectedAncestorIds contain any id we assume that the tree should be travsersed beginning // from the root node. if (node && node.nodeId && selectedAncestorIds && selectedAncestorIds.length === 0) { nodeId = node.nodeId; } // Always recreate the tree when routing to a specific nested node, // because previous modifications to the tree-structure could cause errors // while traversing with 'old' tree-data // ----------------- // setupTree is able to handle nodeId = undefined await this.setupTree(nodeId); if (!selectedAncestorIds || selectedAncestorIds.length === 0) { return; } if (nodeNavData && this.dataSource) { const clonedAncestors = clone(selectedAncestorIds); clonedAncestors.shift(); const n = await this.dataSource.toggleNode(this.dataSource.data[0], true); this.setChildNodes(n.children, clonedAncestors); this.toggleFocusedNode(node); } } setChildNodes(nodes, ids) { if (nodes) { ids.forEach(async (id) => { const match = nodes.find(n => n.nodeId === id); if (match && ids.length > 0) { const idx = ids.findIndex(value => value === id); if (idx >= 0) { ids.splice(idx, 1); } const toggledNode = await this.dataSource.toggleNode(match, true); this.setChildNodes(toggledNode.children, ids); } }); } } async setupTree(nodeId) { this.loading = true; if (!this._moId || this._moId.length === 0) { this._moId = this.opcuaService.getMoId(); } // addressSpaceService.getNode returns either the root node of the server (moId) // or if nodeId !== undefined the node with given nodeId const res = await this.addressSpaceService.getNode(this._moId, nodeId); if (res) { if (res.status !== 200) { const data = res.json ? await res.json() : undefined; this.alertService.addServerFailure({ data, res }); this.dataSource = undefined; } else { const rootNode = (await res.json()); this.nestedTreeControl = new NestedTreeControl(this.getChildren); this.dataSource = new DynamicDataSource(this.nestedTreeControl, this.addressSpaceService, this._moId); this.dataSource.data = [rootNode]; } this.loading = false; } else { this.loading = false; } } getMoId() { if (!this._moId || this._moId.length === 0) { return this.opcuaService.getMoId(); } return this._moId; } getIcon(nodeClassName) { return this.addressSpaceService.getIcon(nodeClassName); } toggleFocusedNode(node) { const relativePath = []; this.getRelativePath(node, relativePath); node.relativePath = relativePath; this.selectedNode.emit(node); this.focused = this.isFocusedNode(node) ? undefined : node; } isFocusedNode(node) { if (this.focused) { return node.nodeId === this.focused.nodeId; } return false; } getRelativePath(node, relativePath) { if (node.parentNode) { relativePath.unshift(node.browseName); this.getRelativePath(node.parentNode, relativePath); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OpcuaAddressSpaceTreeComponent, deps: [{ token: AddressSpaceService }, { token: OpcuaService }, { token: i2$1.AlertService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: OpcuaAddressSpaceTreeComponent, selector: "opcua-address-space-tree", inputs: { moId: "moId", node: "node", focusEmitter: "focusEmitter" }, outputs: { selectedNode: "selectedNode" }, usesOnChanges: true, ngImport: i0, template: "<div\n class=\"card-block\"\n *ngIf=\"dataSource && !loading\"\n>\n <cdk-tree\n [dataSource]=\"dataSource\"\n [treeControl]=\"nestedTreeControl\"\n >\n <!-- This is the tree node template for leaf nodes -->\n <cdk-nested-tree-node\n class=\"interact\"\n *cdkTreeNodeDef=\"let node\"\n (click)=\"toggleFocusedNode(node)\"\n [ngClass]=\"{ strong: isFocusedNode(node) }\"\n >\n <span>\n <i\n class=\"m-r-4 interact\"\n [c8yIcon]=\"getIcon(node.nodeClassName)\"\n [ngClass]=\"{ strong: isFocusedNode(node) }\"\n ></i>\n {{ node.displayName }}\n </span>\n </cdk-nested-tree-node>\n <!-- This is the tree node template for expandable nodes -->\n <cdk-nested-tree-node *cdkTreeNodeDef=\"let node; when: hasChild\">\n <div role=\"group\">\n <div class=\"d-flex a-i-center\">\n <button\n class=\"btn-clean text-primary m-r-4\"\n title=\"{{ 'Expand node' | translate }}\"\n cdkTreeNodeToggle\n [disabled]=\"node.currentlyLoadingChildren\"\n >\n <i\n [ngClass]=\"{\n 'dlt-c8y-icon-plus-square': !node.expanded,\n 'dlt-c8y-icon-minus-square': node.expanded\n }\"\n ></i>\n </button>\n <i\n class=\"m-r-4 interact\"\n [c8yIcon]=\"getIcon(node.nodeClassName)\"\n ></i>\n <span\n class=\"interact\"\n (click)=\"toggleFocusedNode(node)\"\n [ngClass]=\"{ strong: isFocusedNode(node) }\"\n >\n {{ node.displayName }}\n </span>\n <span\n class=\"m-l-4\"\n [style.visibility]=\"node.currentlyLoadingChildren ? 'visible' : 'hidden'\"\n >\n <i class=\"dlt-c8y-icon-circle-o-notch icon-spin\"></i>\n </span>\n </div>\n <ng-container cdkTreeNodeOutlet></ng-container>\n </div>\n </cdk-nested-tree-node>\n </cdk-tree>\n</div>\n<div\n class=\"p-t-8\"\n *ngIf=\"loading\"\n>\n <c8y-loading></c8y-loading>\n</div>\n<div\n class=\"alert alert-info m-t-16\"\n *ngIf=\"!dataSource && !loading\"\n translate\n>\n No source data available to fetch address space.\n</div>\n", dependencies: [{ kind: "directive", type: i2$1.IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: i2$1.C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2$1.LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "directive", type: i5.CdkNestedTreeNode, selector: "cdk-nested-tree-node", exportAs: ["cdkNestedTreeNode"] }, { kind: "directive", type: i5.CdkTreeNodeDef, selector: "[cdkTreeNodeDef]", inputs: ["cdkTreeNodeDefWhen"] }, { kind: "directive", type: i5.CdkTreeNodeToggle, selector: "[cdkTreeNodeToggle]", inputs: ["cdkTreeNodeToggleRecursive"] }, { kind: "component", type: i5.CdkTree, selector: "cdk-tree", inputs: ["dataSource", "treeControl", "levelAccessor", "childrenAccessor", "trackBy", "expansionKey"], exportAs: ["cdkTree"] }, { kind: "directive", type: i5.CdkTreeNodeOutlet, selector: "[cdkTreeNodeOutlet]" }, { kind: "pipe", type: i2$1.C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OpcuaAddressSpaceTreeComponent, decorators: [{ type: Component, args: [{ selector: 'opcua-address-space-tree', template: "<div\n class=\"card-block\"\n *ngIf=\"dataSource && !loading\"\n>\n <cdk-tree\n [dataSource]=\"dataSource\"\n [treeControl]=\"nestedTreeControl\"\n >\n <!-- This is the tree node template for leaf nodes -->\n <cdk-nested-tree-node\n class=\"interact\"\n *cdkTreeNodeDef=\"let node\"\n (click)=\"toggleFocusedNode(node)\"\n [ngClass]=\"{ strong: isFocusedNode(node) }\"\n >\n <span>\n <i\n class=\"m-r-4 interact\"\n [c8yIcon]=\"getIcon(node.nodeClassName)\"\n [ngClass]=\"{ strong: isFocusedNode(node) }\"\n ></i>\n {{ node.displayName }}\n </span>\n </cdk-nested-tree-node>\n <!-- This is the tree node template for expandable nodes -->\n <cdk-nested-tree-node *cdkTreeNodeDef=\"let node; when: hasChild\">\n <div role=\"group\">\n <div class=\"d-flex a-i-center\">\n <button\n class=\"btn-clean text-primary m-r-4\"\n title=\"{{ 'Expand node' | translate }}\"\n cdkTreeNodeToggle\n [disabled]=\"node.currentlyLoadingChildren\"\n >\n <i\n [ngClass]=\"{\n 'dlt-c8y-icon-plus-square': !node.expanded,\n 'dlt-c8y-icon-minus-square': node.expanded\n }\"\n ></i>\n </button>\n <i\n class=\"m-r-4 interact\"\n [c8yIcon]=\"getIcon(node.nodeClassName)\"\n ></i>\n <span\n class=\"interact\"\n (click)=\"toggleFocusedNode(node)\"\n [ngClass]=\"{ strong: isFocusedNode(node) }\"\n >\n {{ node.displayName }}\n </span>\n <span\n class=\"m-l-4\"\n [style.visibility]=\"node.currentlyLoadingChildren ? 'visible' : 'hidden'\"\n >\n <i class=\"dlt-c8y-icon-circle-o-notch icon-spin\"></i>\n </span>\n </div>\n <ng-container cdkTreeNodeOutlet></ng-container>\n </div>\n </cdk-nested-tree-node>\n </cdk-tree>\n</div>\n<div\n class=\"p-t-8\"\n *ngIf=\"loading\"\n>\n <c8y-loading></c8y-loading>\n</div>\n<div\n class=\"alert alert-info m-t-16\"\n *ngIf=\"!dataSource && !loading\"\n translate\n>\n No source data available to fetch address space.\n</div>\n" }] }], ctorParameters: () => [{ type: AddressSpaceService }, { type: OpcuaService }, { type: i2$1.AlertService }], propDecorators: { moId: [{ type: Input }], node: [{ type: Input }], focusEmitter: [{ type: Input }], selectedNode: [{ type: Output }] } }); class OpcuaDeviceProtocolDataReportingComponent { constructor() { this.onSubscriptionChange = new EventEmitter(); this.subscription = { type: 'None' }; this.subscriptionParameters = { samplingRate: undefined, deadbandType: 'None', deadbandValue: undefined, ranges: '', queueSize: undefined, dataChangeTrigger: 'Status', discardOldest: true }; this.cyclicReadParameters = { rate: undefined }; this.types = [ { value: 'None', label: gettext('None') }, { value: 'CyclicRead', label: gettext('Cyclic read') }, { value: 'Subscription', label: gettext('Subscription') } ]; this.filters = [ { value: 'None', label: gettext('None') }, { value: 'Absolute', label: gettext('Absolute') }, { value: 'Percent', label: gettext('Percent') } ]; this.triggers = [ { value: 'Status', label: gettext('Status') }, { value: 'StatusValue', label: gettext('Status/Value') }, { value: 'StatusValueTimestamp', label: gettext('Status/Value/Timestamp') } ]; this.discard = [ { value: true, label: gettext('oldest`data`') }, { value: false, label: gettext('newest`data`') } ]; this.requireCyclic = false; this.requireSubscription = false; this.subscriptionTypeName = 'subscriptionType'; this.parseReadingInterval = $event => toInteger($event.target.value); } set model(_model) { if (_model.subscriptionType) { this.subscription = _model.subscriptionType; if (this.subscription.type === 'CyclicRead') { this.cyclicReadParameters = _model.subscriptionType.cyclicReadParameters; this.requireCyclic = true; this.requireSubscription = false; } if (this.subscription.type === 'Subscription') { this.subscriptionParameters = _model.subscriptionType.subscriptionParameters; this.requireCyclic = false; this.requireSubscription = true; } if (this.subscription.type === 'None') { this.requireCyclic = false; this.requireSubscription = false; } } this._model = _model; this.subscriptionTypeName = 'subscriptionType' + _model.id; } updateModel() { setTimeout(() => { unset(this.subscription, 'subscriptionParameters'); unset(this.subscription, 'cyclicReadParameters'); if (this.subscription.type === 'CyclicRead') { this.requireCyclic = true; this.requireSubscription = false; set(this.subscription, 'cyclicReadParameters', this.cyclicReadParameters); } else if (this.subscription.type === 'Subscription') { this.requireCyclic = false; this.requireSubscription = true; set(this.subscription, 'subscriptionParameters', this.subscriptionParameters); } else if (this.subscription.type === 'None') { this.requireCyclic = false; this.requireSubscription = false; } this.onSubscriptionChange.emit(this.subscription); }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: OpcuaDeviceProtocolDataReportingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: OpcuaDeviceProtocolDataReportingComponent, selector: "opcua-device-protocol-data-reporting", inputs: { model: "model", groupName: "groupName" }, outputs: { onSubscriptionChange: "onSubscriptionChange" }, ngImport: i0, template: "<c8y-form-group>\n <label translate>Mechanism</label>\n <label\n title=\"{{ mechanism.label | translate }}\"\n class=\"c8y-radio radio-inline\"\n *ngFor=\"let mechanism of types\"\n >\n <input\n type=\"radio\"\n name=\"{{subscriptionTypeName}}\"\n [value]=\"mechanism.value\"\n [(ngModel)]=\"subscription.type\"\n (change)=\"updateModel()\"\n required\n [attr.data-cy]=\"mechanism.value\"\n />\n <span></span>\n <span>{{ mechanism.label | translate }}</span>\n </label>\n</c8y-form-group>\n\n<div class=\"row collapse\" [collapse]=\"subscription.type != 'CyclicRead'\" [isAnimated]=\"true\">\n <div class=\"col-sm-6 col-md-4 col-lg-3\">\n <c8y-form-group>\n <label translate>Reading interval</label>\n <div class=\"input-group\">\n <input\n class=\"form-control\"\n type=\"number\"\n name=\"rate\"\n [(ngModel)]=\"cyclicReadParameters.rate\"\n placeholder=\"{{ 'e.g.' | translate }} 50\"\n [required]=\"requireCyclic\"\n min=\"50\"\n [attr.data-cy]=\"'cyclicReadRate'\"\n />\n <span class=\"input-group-addon units\" title=\"{{ 'Milliseconds' | translate }}\">\n {{ 'msec' | translate }}\n </span>\n </div>\n </c8y-form-group>\n </div>\n</div>\n\n<div class=\"row collapse\" [collapse]=\"subscription.type != 'Subscription'\" [isAnimated]=\"true\">\n <div class=\"col-sm-6 col-lg-4\">\n <c8y-form-group>\n <label translate>Sampling interval</label>\n <div class=\"input-group\">\n <input\n class=\"form-control\"\n type=\"number\"\n name=\"samplingRate\"\n [(ngModel)]=\"subscriptionPa