UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

851 lines (843 loc) 245 kB
import * as i0 from '@angular/core'; import { Injectable, EventEmitter, Output, Input, Component, forwardRef, Directive, signal, inject, DestroyRef, ViewChild, ViewChildren, Optional, NgModule } from '@angular/core'; import * as i1 from '@c8y/client'; import { OperationStatus } from '@c8y/client'; import { BehaviorSubject, merge, Subject, debounceTime } from 'rxjs'; import { omit, isEmpty, some, isEqual, reject, isNil, get, assign, cloneDeep, unset as unset$1, set as set$1, find, findIndex, pick, has } from 'lodash-es'; import { NgClass, NgFor, NgIf, JsonPipe, KeyValuePipe } from '@angular/common'; import * as i2$1 from '@c8y/ngx-components'; import { C8yTranslatePipe, IconDirective, LoadingComponent, C8yTranslateDirective, FormGroupComponent, SelectLegacyComponent, ListItemComponent, FilterInputComponent, ListItemBodyComponent, ListItemCheckboxComponent, InputGroupListContainerDirective, InputGroupListComponent, RequiredInputPlaceholderDirective, MinValidationDirective, DatePipe, MessagesComponent, MessageDirective, TitleComponent, BreadcrumbComponent, BreadcrumbItemComponent, EmptyStateComponent, Status, DeviceStatusComponent, DefaultValidationDirective, DropAreaComponent, ViewContext, CoreModule, FormsModule as FormsModule$1, DropAreaModule, DeviceStatusModule, DynamicFormsModule, hookRoute } from '@c8y/ngx-components'; import * as i2 from '@angular/router'; import { RouterModule } from '@angular/router'; import * as i2$2 from '@angular/forms'; import { NG_VALIDATORS, FormsModule, NgModelGroup, ControlContainer, NgForm, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { NestedTreeControl, CdkTree, CdkTreeNodeDef, CdkNestedTreeNode, CdkTreeNodeToggle, CdkTreeNodeOutlet, CdkTreeModule } from '@angular/cdk/tree'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { gettext } from '@c8y/ngx-components/gettext'; import { BaseObjectMapping, MeasurementObjectMapping, EventObjectMapping, AlarmObjectMapping, ALARM_SEVERITY, ObjectMappingComponent } from '@c8y/ngx-components/device-protocol-object-mappings'; import { OperationDetailsComponent, OperationDetailsModule } from '@c8y/ngx-components/operations/operation-details'; import { ButtonCheckboxDirective, ButtonsModule } from 'ngx-bootstrap/buttons'; import * as i4$1 from 'ngx-bootstrap/collapse'; import { CollapseDirective, CollapseModule } from 'ngx-bootstrap/collapse'; import * as i5 from 'ngx-bootstrap/dropdown'; import { BsDropdownDirective, BsDropdownToggleDirective, BsDropdownMenuDirective, BsDropdownModule } from 'ngx-bootstrap/dropdown'; import * as i3$1 from 'ngx-bootstrap/popover'; import { PopoverDirective, PopoverModule } from 'ngx-bootstrap/popover'; import * as i2$3 from 'ngx-bootstrap/tooltip'; import { TooltipDirective, TooltipModule } from 'ngx-bootstrap/tooltip'; import { clone, toInteger, unset, set, cloneDeep as cloneDeep$1 } from 'lodash'; import { map, takeUntil, filter } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { UpgradeComponent, downgradeComponent } from '@angular/upgrade/static'; import * as i4 from '@ngx-formly/core'; import { FormlyModule } from '@ngx-formly/core'; import * as i3 from '@ngx-translate/core'; 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: "20.3.19", ngImport: i0, type: AddressSpaceService, deps: [{ token: i1.FetchClient }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: AddressSpaceService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", 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: "20.3.19", ngImport: i0, type: OpcuaAddressSpaceDetailComponent, deps: [{ token: AddressSpaceService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: OpcuaAddressSpaceDetailComponent, isStandalone: true, 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: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }, { kind: "pipe", type: JsonPipe, name: "json" }, { kind: "pipe", type: KeyValuePipe, name: "keyvalue" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OpcuaAddressSpaceDetailComponent, decorators: [{ type: Component, args: [{ selector: 'opcua-address-space-detail', imports: [NgClass, NgFor, NgIf, C8yTranslatePipe, JsonPipe, KeyValuePipe], 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'; this.microserviceUrlDepr = '/service/opcua-mgmt-service/server'; this.deviceTypeProtocolUrl = '/service/opcua-mgmt-service/deviceTypes'; this.header = { 'Content-Type': 'application/json' }; this.binaryService = inventoryService.binary; } 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.microserviceUrlDepr}`, 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.microserviceUrlDepr}`, 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.microserviceUrlDepr}/${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); } getServers(id) { if (id && id.length > 0) { const options = { method: 'GET', headers: this.header }; return this.client.fetch(`${this.microserviceUrlDepr}/${id}`, options); } } getServer(id) { if (id && id.length > 0) { const options = { method: 'GET', headers: this.header }; return this.client .fetch(`${this.microserviceUrl}/servers/${id}`, options) .then(this.handleErrorStatusCodes); } } /** * Checks the response for errors and throws exceptions, otherwise returns the response as is. * * @param response The response from server. * * @returns If no errors are detected, it returns the same response. * * @throws If an error is detected, it throws `{ res, data }`, where `data` contains error details from server. */ async handleErrorStatusCodes(response) { if (response.status >= 400) { let data = null; try { data = await response.json(); } catch (ex) { try { data = await response.text(); } catch (ex) { // do nothing } } throw { res: response, data }; } return response; } 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: "20.3.19", ngImport: i0, type: OpcuaService, deps: [{ token: i1.FetchClient }, { token: i1.InventoryService }, { token: i2.Router }, { token: i2$1.AlertService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OpcuaService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OpcuaService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i1.FetchClient }, { type: i1.InventoryService }, { type: i2.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: "20.3.19", ngImport: i0, type: OpcuaAgentGuard, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OpcuaAgentGuard }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", 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: "20.3.19", ngImport: i0, type: OpcuaDeviceProtocolBrowsePathValidation, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.19", type: OpcuaDeviceProtocolBrowsePathValidation, isStandalone: true, 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: "20.3.19", 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 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: "20.3.19", 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: "20.3.19", type: OpcuaAddressSpaceTreeComponent, isStandalone: true, 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: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CdkTree, selector: "cdk-tree", inputs: ["dataSource", "treeControl", "levelAccessor", "childrenAccessor", "trackBy", "expansionKey"], exportAs: ["cdkTree"] }, { kind: "directive", type: CdkTreeNodeDef, selector: "[cdkTreeNodeDef]", inputs: ["cdkTreeNodeDefWhen"] }, { kind: "directive", type: CdkNestedTreeNode, selector: "cdk-nested-tree-node", exportAs: ["cdkNestedTreeNode"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: IconDirective, selector: "[c8yIcon]", inputs: ["c8yIcon"] }, { kind: "directive", type: CdkTreeNodeToggle, selector: "[cdkTreeNodeToggle]", inputs: ["cdkTreeNodeToggleRecursive"] }, { kind: "directive", type: CdkTreeNodeOutlet, selector: "[cdkTreeNodeOutlet]" }, { kind: "component", type: LoadingComponent, selector: "c8y-loading", inputs: ["layout", "progress", "message"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OpcuaAddressSpaceTreeComponent, decorators: [{ type: Component, args: [{ selector: 'opcua-address-space-tree', imports: [ NgIf, CdkTree, CdkTreeNodeDef, CdkNestedTreeNode, NgClass, IconDirective, CdkTreeNodeToggle, CdkTreeNodeOutlet, LoadingComponent, C8yTranslateDirective, C8yTranslatePipe ], 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 OpcuaAddressSpaceComponent { constructor(addressSpaceService, opcuaService, operationService, operationRealtimeService, alert, modalService) { this.addressSpaceService = addressSpaceService; this.opcuaService = opcuaService; this.operationService = operationService; this.operationRealtimeService = operationRealtimeService; this.alert = alert; this.modalService = modalService; this.selectednode = false; this.loading = false; this.searchInProgress = false; this.isOperationRunning = signal(false, ...(ngDevMode ? [{ debugName: "isOperationRunning" }] : [])); this.destroyRef = inject(DestroyRef); this.focusStatus = new EventEmitter(); this.moId = ''; } async ngOnInit() { this.filterLabel = gettext('Filter…'); this.moId = this.opcuaService.getMoId(); this.operation = await this.getRunningScanAddressSpaceOperation(); this.isOperationRunning.set(!!this.operation); this.operationRealtimeService .onAll$(this.moId) .pipe(map(({ data }) => data), filter(operation => operation.c8y_ua_command_ScanAddressSpace), takeUntilDestroyed(this.destroyRef)) .subscribe(operation => { this.operation = operation; this.isOperationRunning.set(operation.status == OperationStatus.EXECUTING || operation.status == OperationStatus.PENDING); if (operation.status == OperationStatus.SUCCESSFUL) { this.addressSpaceService.resetTreeToRootNode(); } }); } async getRunningScanAddressSpaceOperation() { const filter = { deviceId: this.moId, fragmentType: 'c8y_ua_command_ScanAddressSpace', dateFrom: new Date(0).toISOString(), revert: true, pageSize: 1 }; const [operation] = (await this.operationService.list(filter)).data ?? []; return operation?.status == OperationStatus.EXECUTING || operation?.status == OperationStatus.PENDING ? operation : undefined; } ngOnDestroy() { // The BehaviourSubject will store the last array of ancestorNodes from the previous search // this would cause the component while subscribing in the init-phase to the subject to travers // to the last searched node again. From user perspective it does not make sense, because the user // left the Address space (tab) and should loose the context and just request a new search or // browse the tree manually. this.addressSpaceService.resetTreeToRootNode(); } async searchNodes() { this.searchInProgress = true; this.clearNodeListAndCheckSearchString(); if (this.isSearch) { this.currentNode = undefined; this.nodeList = await this.addressSpaceService.getSearchedNodes(this.searchKey, this.moId); this.searchInProgress = false; this.nodeList.resultLabel = gettext('Results found'); } } clearNodeListAndCheckSearchString() { this.isSearch = this.searchKey !== undefined && this.searchKey !== '' ? true : false; if (!this.isSearch) { this.searchInProgress = false; } } clearSearch() { this.isSearch = false; this.searchKey = ''; this.currentNode = undefined; } getIcon(nodeClassName) { return this.addressSpaceService.getIcon(nodeClassName); } async selectNode(node) { if (node && node.nodeId && node.nodeId.length > 0) { const res = await this.addressSpaceService.getNodeById(this.moId, node.nodeId); this.toggleCurrentNode((await res.json())); } } toggleCurrentNode(node) { this.currentNode = this.isNodeSet(node) ? undefined : node; } backHandler(node) { this.isSearch = false; this.focusStatus.emit(node); this.toggleCurrentNode(node); } isNodeSet(node) { if (this.currentNode !== undefined && this.currentNode.nodeId === node.nodeId) { return true; } return false; } async rescanAddressSpace() { const serverResponse = await this.opcuaService.getServer(this.moId); const server = (await serverResponse.json()); let warning = gettext('Rescanning address space from root node might take several hours. Do you want to proceed?'); if (server) { if (server.config.partialAddressScan && !isEmpty(server.config.partialAddressScanNodeIds)) { warning = gettext('Rescanning from nodes ' + server.config.partialAddressScanNodeIds?.join(';') + '. Address space rescan might take several hours. Do you want to proceed?'); } } const doit = await this.modalService.confirm(gettext('Rescan address space'), warning, 'warning', { ok: gettext('Rescan'), cancel: gettext('Cancel') }); if (!doit) { return; } const operation = await this.createConfigurationAwareScanAdressSpaceOperation(server.config); try { this.operation = (await this.operationService.create(operation)).data; this.isOperationRunning.set(true); } catch (error) { this.alert.add({ text: gettext('Error creating rescan operation'), detailedData: error, type: 'danger', timeout: 8000 }); this.isOperationRunning.set(false); } } async createConfigurationAwareScanAdressSpaceOperation(config) { const result = { deviceId: this.moId, description: gettext('[RESCAN] Address space import from Root node'), c8y_ua_command_ScanAddressSpace: { skipSync: false } }; if (config) { if (config.partialAddressScan && config.partialAddressScanNodeIds) { const nodeIds = config.partialAddressScanNodeIds; result.c8y_ua_command_ScanAddressSpace.nodeIds = config.partialAddressScanNodeIds; result.description = gettext(`[RESCAN] Address space from node[s] ${nodeIds.join(';')}`); } } return result; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: OpcuaAddressSpaceComponent, deps: [{ token: AddressSpaceService }, { token: OpcuaService }, { token: i1.OperationService }, { token: i2$1.OperationRealtimeService }, { token: i2$1.AlertService }, { token: i2$1.ModalService }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: OpcuaAddressSpaceComponent, isStandalone: true, selector: "opcua-address-space", outputs: { focusStatus: "focusStatus" }, ngImport: i0, template: "<div class=\"row split-scroll\">\n <div class=\"col-md-5 col-xs-12 scroll-column no-gutter-r\">\n <div class=\"card bg-level-2 split-scroll overflow-auto\">\n <div class=\"flex-grow\">\n <fieldset\n class=\"card-block large-padding bg-level-2 p-0\"\n id=\"operation-block\"\n *ngIf=\"!!operation\"\n >\n <c8y-operation-details [operation]=\"operation\"></c8y-operation-details>\n </fieldset>\n </div>\n <div class=\"card-block separator sticky-top\">\n <div class=\"input-group input-group-search\">\n <input\n class=\"form-control\"\n placeholder=\"{{ filterLabel | translate }}\"\n type=\"search\"\n (keydown.enter)=\"searchNodes()\"\n [(ngModel)]=\"searchKey\"\n />\n <span class=\"input-group-btn\">\n <button\n class=\"btn btn-dot\"\n title=\"{{ 'Search' | translate }}\"\n type=\"submit\"\n *ngIf=\"!isSearch\"\n (click)=\"searchNodes()\"\n >\n <i c8yIcon=\"search\"></i>\n </button>\n <button\n class=\"btn btn-dot\"\n title=\"{{ 'Clear`input`' | translate }}\"\n type=\"button\"\n *ngIf=\"isSearch\"\n (click)=\"clearSearch()\"\n >\n <i c8yIcon=\"times\"></i>\n </button>\n </span>\n </div>\n <div\n class=\"p-t-16\"\n *ngIf=\"isSearch &&