@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
1 lines • 249 kB
Source Map (JSON)
{"version":3,"file":"c8y-ngx-components-protocol-opcua.mjs","sources":["../../protocol-opcua/address-space.service.ts","../../protocol-opcua/opcua-address-space-detail.component.ts","../../protocol-opcua/opcua-address-space-detail.component.html","../../protocol-opcua/opcuaService.ts","../../protocol-opcua/opcua-agent.guard.ts","../../protocol-opcua/opcua-device-protocol-browse-path-validation.directive.ts","../../protocol-opcua/dynamic-data-source.ts","../../protocol-opcua/opcua-address-space-tree.component.ts","../../protocol-opcua/opcua-address-space-tree.component.html","../../protocol-opcua/opcua-address-space.component.ts","../../protocol-opcua/opcua-address-space.component.html","../../protocol-opcua/opcua-auto-apply-settings.component.ts","../../protocol-opcua/opcua-auto-apply-settings.component.html","../../protocol-opcua/opcua-device-protocol-data-reporting.component.ts","../../protocol-opcua/opcua-device-protocol-data-reporting.html","../../protocol-opcua/opcua-device-protocol-description.component.ts","../../protocol-opcua/opcua-device-protocol-description.html","../../protocol-opcua/mappings/custom-action-mapping.ts","../../protocol-opcua/mappings/default-mappings.ts","../../protocol-opcua/opcua-device-protocol-object-mapping-status-icon.component.ts","../../protocol-opcua/opcua-device-protocol-mapping.component.ts","../../protocol-opcua/opcua-device-protocol-mapping.html","../../protocol-opcua/opcua-device-protocol-detail.component.ts","../../protocol-opcua/opcua-device-protocol-detail.html","../../protocol-opcua/opcua-microservice.guard.ts","../../protocol-opcua/opcua-server.interface.ts","../../protocol-opcua/opcua-server-config.component.ts","../../protocol-opcua/opcua-server-config.component.html","../../protocol-opcua/opcua-server-list.component.ts","../../protocol-opcua/opcua-server-list.component.html","../../protocol-opcua/opcua-server.guard.ts","../../protocol-opcua/opcua-servers.component.ts","../../protocol-opcua/opcua-servers.component.html","../../protocol-opcua/opcua-protocol.module.ts","../../protocol-opcua/ng1/downgraded.components.ts","../../protocol-opcua/ng1/index.ts","../../protocol-opcua/c8y-ngx-components-protocol-opcua.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\nimport { FetchClient, IFetchOptions } from '@c8y/client';\nimport { BehaviorSubject, Observable } from 'rxjs';\n\n@Injectable()\nexport class AddressSpaceService {\n private nodeNavigationData$: BehaviorSubject<NodeNavigationData>;\n private client: FetchClient;\n private microserviceUrl: string;\n private header: any;\n\n constructor(fetchClient: FetchClient) {\n this.client = fetchClient;\n this.microserviceUrl = '/service/opcua-mgmt-service/address-space';\n this.header = { 'Content-Type': 'application/json' };\n this.nodeNavigationData$ = new BehaviorSubject<NodeNavigationData>({\n node: undefined,\n selectedAncestorIds: []\n });\n }\n\n resetTreeToRootNode() {\n this.triggerNodeToOpen({ node: undefined, selectedAncestorIds: [] });\n }\n\n triggerNodeToOpen(nodeNavigationData: NodeNavigationData) {\n this.nodeNavigationData$.next(nodeNavigationData);\n }\n\n getNodeNavData$(): Observable<NodeNavigationData> {\n return this.nodeNavigationData$.asObservable();\n }\n\n getNode(serverId: string, nodeId?: string) {\n if (serverId && serverId.length > 0) {\n if (nodeId && nodeId.length > 0) {\n return this.getNodeById(serverId, nodeId);\n }\n return this.getRootNode(serverId);\n }\n }\n\n getRootNode(serverId: string) {\n if (serverId && serverId.length > 0) {\n const options: IFetchOptions = {\n method: 'GET',\n headers: this.header\n };\n return this.client.fetch(`${this.microserviceUrl}/${serverId}`, options);\n }\n }\n\n getNodeById(serverId: string, nodeId: string) {\n if (serverId && nodeId && serverId.length > 0 && nodeId.length > 0) {\n const options: IFetchOptions = {\n method: 'GET',\n headers: this.header\n };\n const param = encodeURIComponent(nodeId);\n return this.client.fetch(`${this.microserviceUrl}/${serverId}?nodeId=${param}`, options);\n }\n }\n\n getChildrenOf(node: AddressSpaceNode, serverId: string) {\n if (serverId && node.nodeId && serverId.length > 0 && node.nodeId.length > 0) {\n const options: IFetchOptions = {\n method: 'GET',\n headers: this.header\n };\n const param = encodeURIComponent(node.nodeId);\n return this.client.fetch(\n `${this.microserviceUrl}/${serverId}/children?nodeId=${param}`,\n options\n );\n }\n }\n\n childrenAvailable(nodeReferences: AdressSpaceNodeReference[]): boolean {\n if (!nodeReferences || nodeReferences.length === 0) {\n return false;\n }\n return nodeReferences.some(ref => !ref.inverse && ref.hierarchical);\n }\n\n async getSearchedNodes(searchKey: string, serverId: string) {\n const url = `service/opcua-mgmt-service/search/${serverId}/`;\n const options: IFetchOptions = {\n headers: this.header,\n params: {\n searchString: '*' + searchKey + '*'\n }\n };\n const res = await this.client.fetch(url, options);\n return res.json();\n }\n\n getIcon(nodeClassName: string) {\n const iconList = {\n Object: 'cube',\n Variable: 'th-list',\n Method: 'random',\n View: 'window-maximize',\n ObjectType: 'c8y-group',\n VariableType: 'c8y-group',\n ReferenceType: 'c8y-group',\n DataType: 'c8y-group'\n };\n return iconList[nodeClassName] || 'circle';\n }\n}\n\nexport interface AddressSpaceNode {\n nodeId: string;\n currentlyLoadingChildren?: boolean;\n nodeClass?: number;\n nodeClassName?: string;\n browseName?: string;\n displayName?: string;\n description?: any;\n references?: AdressSpaceNodeReference[];\n ancestorNodeIds?: [string[]];\n children: AddressSpaceNode[];\n expanded: boolean;\n absolutePaths: [string[]];\n relativePath?: string[];\n parentNode?: AddressSpaceNode;\n}\n\nexport interface AdressSpaceNodeReference {\n referenceId: string;\n targetId: string;\n inverse: boolean;\n hierarchical: boolean;\n}\n\nexport interface SearchedNode {\n absolutePath: string[];\n ancestorNodeIds: [string[]];\n displayName: string;\n nodeClassName: string;\n nodeId: string;\n}\n\nexport interface NodeNavigationData {\n node: AddressSpaceNode;\n selectedAncestorIds: string[];\n}\n","import { Component, Input, Output, EventEmitter } from '@angular/core';\nimport { AddressSpaceNode, AddressSpaceService, NodeNavigationData } from './address-space.service';\nimport { omit } from 'lodash-es';\nimport { NgClass, NgFor, NgIf, JsonPipe, KeyValuePipe } from '@angular/common';\nimport { C8yTranslatePipe } from '@c8y/ngx-components';\n\n@Component({\n selector: 'opcua-address-space-detail',\n templateUrl: './opcua-address-space-detail.component.html',\n imports: [NgClass, NgFor, NgIf, C8yTranslatePipe, JsonPipe, KeyValuePipe]\n})\nexport class OpcuaAddressSpaceDetailComponent {\n @Input() set node(n) {\n this._node = n;\n if (n) {\n this.setNodeData(n);\n } else {\n // remove details from current view\n this.showDetails = false;\n }\n }\n nodeDataAttr: Map<string, string>;\n nodeDataRef: object[];\n selected = false;\n showDetails = false;\n\n @Output() toggleAttrDetail: EventEmitter<AddressSpaceNode> = new EventEmitter<AddressSpaceNode>();\n\n private _node: AddressSpaceNode;\n\n constructor(private addressSpaceService: AddressSpaceService) {}\n setNodeData(nodeData) {\n this.showDetails = true;\n const { attributes, references } = nodeData;\n this.nodeDataRef = references;\n const omitList = [\n 'attributes',\n 'references',\n 'children',\n 'currentlyLoadingChildren',\n 'expanded',\n 'browsePath',\n 'relativePath',\n 'parentNode'\n ];\n this.nodeDataAttr = Object.assign({}, attributes, omit(nodeData, omitList));\n }\n\n toggleDetail(node) {\n this.showDetails = !this.showDetails;\n this.toggleAttrDetail.emit(node);\n }\n\n navigateTo(ancestors: string[]) {\n const nodeNavData: NodeNavigationData = {\n node: this._node,\n selectedAncestorIds: ancestors\n };\n\n this.toggleDetail(this._node);\n this.addressSpaceService.triggerNodeToOpen(nodeNavData);\n }\n}\n","<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 ×\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","import { Injectable } from '@angular/core';\nimport { Router } from '@angular/router';\nimport {\n FetchClient,\n IFetchOptions,\n IFetchResponse,\n InventoryBinaryService,\n InventoryService\n} from '@c8y/client';\nimport { AlertService } from '@c8y/ngx-components';\nimport { OpcuaServer } from './opcua-server.interface';\n\n@Injectable()\nexport class OpcuaService {\n private binaryService: InventoryBinaryService;\n private microserviceUrlDepr: string;\n private microserviceUrl: string;\n private deviceTypeProtocolUrl: string;\n private header: any;\n\n constructor(\n private client: FetchClient,\n private inventoryService: InventoryService,\n private router: Router,\n private alertService: AlertService\n ) {\n this.microserviceUrl = '/service/opcua-mgmt-service';\n this.microserviceUrlDepr = '/service/opcua-mgmt-service/server';\n this.deviceTypeProtocolUrl = '/service/opcua-mgmt-service/deviceTypes';\n this.header = { 'Content-Type': 'application/json' };\n this.binaryService = inventoryService.binary;\n }\n createServer(data: OpcuaServer) {\n if (this.doesGatewayIdExist(data)) {\n this.cleanUpPayload(data);\n const options: IFetchOptions = {\n method: 'POST',\n headers: this.header,\n body: JSON.stringify(data)\n };\n return this.client.fetch(`${this.microserviceUrlDepr}`, options);\n }\n }\n\n async updateServer(server: OpcuaServer) {\n if (this.doesGatewayIdExist(server) && this.doesIdExist(server)) {\n this.cleanUpPayload(server);\n const options: IFetchOptions = {\n method: 'POST',\n headers: this.header,\n body: JSON.stringify(server)\n };\n const res = await this.client.fetch(`${this.microserviceUrlDepr}`, options);\n let data;\n try {\n data = await res.json();\n } catch (e) {\n // nothing\n }\n\n if (res.status !== 200) {\n this.alertService.addServerFailure({ data, res });\n } else {\n return data;\n }\n }\n }\n\n removeServer(data: OpcuaServer) {\n if (this.doesGatewayIdExist(data) && this.doesIdExist(data)) {\n const options: IFetchOptions = {\n method: 'DELETE'\n };\n return this.client.fetch(`${this.microserviceUrlDepr}/${data.gatewayId}/${data.id}`, options);\n }\n }\n\n getKeystore(binaryId: string) {\n if (binaryId && binaryId.length > 0) {\n return this.inventoryService.detail(binaryId);\n }\n return null;\n }\n\n uploadKeystore(file: File) {\n if (file && file.size > 0) {\n return this.binaryService.create(file);\n }\n return Promise.reject('Invalid file');\n }\n\n async updateKeystore(id: string, file: File) {\n if (id && id.length > 0 && file && file.size > 0) {\n const { res } = await this.removeKeystore(id);\n if (res && res.status === 204) {\n return this.uploadKeystore(file);\n }\n }\n return Promise.reject('Invalid file');\n }\n\n removeKeystore(id: string) {\n if (id && id.length > 0) {\n return this.binaryService.delete(id);\n }\n }\n\n getMoId() {\n const currentUrl: string = this.router.routerState.snapshot.url;\n const isDevice: boolean = new RegExp(/device\\/\\d+/).test(currentUrl);\n if (isDevice) {\n return currentUrl.match(/\\d+/)[0];\n }\n return '';\n }\n\n getId() {\n const currentUrl: string = this.router.routerState.snapshot.url;\n const isDeviceprotocol: boolean = new RegExp(/deviceprotocols/).test(currentUrl);\n if (isDeviceprotocol && RegExp(/\\d+$/).test(currentUrl)) {\n return currentUrl.match(/\\d+$/)[0];\n }\n }\n\n async getDeviceProtocol(id: string) {\n const options: IFetchOptions = {\n method: 'GET',\n headers: this.header\n };\n return this.client.fetch(`${this.deviceTypeProtocolUrl}/${id}`, options);\n }\n\n async updateDeviceProtocol(data) {\n const options: IFetchOptions = {\n method: 'PUT',\n headers: this.header,\n body: JSON.stringify(data)\n };\n return this.client.fetch(`${this.deviceTypeProtocolUrl}/${data.id}`, options);\n }\n\n async createDeviceProtocol(data) {\n const options: IFetchOptions = {\n method: 'POST',\n headers: this.header,\n body: JSON.stringify(data)\n };\n return this.client.fetch(`${this.deviceTypeProtocolUrl}`, options);\n }\n\n getServers(id: string) {\n if (id && id.length > 0) {\n const options: IFetchOptions = {\n method: 'GET',\n headers: this.header\n };\n return this.client.fetch(`${this.microserviceUrlDepr}/${id}`, options);\n }\n }\n\n getServer(id: string): Promise<IFetchResponse> {\n if (id && id.length > 0) {\n const options: IFetchOptions = {\n method: 'GET',\n headers: this.header\n };\n return this.client\n .fetch(`${this.microserviceUrl}/servers/${id}`, options)\n .then(this.handleErrorStatusCodes);\n }\n }\n\n /**\n * Checks the response for errors and throws exceptions, otherwise returns the response as is.\n *\n * @param response The response from server.\n *\n * @returns If no errors are detected, it returns the same response.\n *\n * @throws If an error is detected, it throws `{ res, data }`, where `data` contains error details from server.\n */\n\n protected async handleErrorStatusCodes(response: IFetchResponse): Promise<IFetchResponse> {\n if (response.status >= 400) {\n let data = null;\n try {\n data = await response.json();\n } catch (ex) {\n try {\n data = await response.text();\n } catch (ex) {\n // do nothing\n }\n }\n throw { res: response, data };\n }\n return response;\n }\n\n private doesGatewayIdExist(data: OpcuaServer) {\n return data && data.gatewayId && data.gatewayId.length > 0;\n }\n\n private doesIdExist(data: OpcuaServer) {\n return data && data.id && data.id.length > 0 && data.id !== 'new';\n }\n\n private cleanUpPayload(data: OpcuaServer) {\n if (data) {\n if (data.id && data.id === 'new') {\n delete data.id;\n }\n if (data.quickInfo) {\n delete data.quickInfo;\n }\n }\n }\n}\n","import { Injectable } from '@angular/core';\n\n@Injectable()\nexport class OpcuaAgentGuard {\n type = 'c8y_OPCUA_Device_Agent';\n canActivate({ data }) {\n const { contextData } = data;\n return contextData && contextData.type === this.type;\n }\n}\n","import { Directive, forwardRef, ElementRef, Input } from '@angular/core';\nimport { Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms';\nimport { some, isEqual, isEmpty } from 'lodash-es';\n\n@Directive({\n selector: '[c8yBrowsePathValidator][ngModel]',\n providers: [\n {\n provide: NG_VALIDATORS,\n useExisting: forwardRef(() => OpcuaDeviceProtocolBrowsePathValidation),\n multi: true\n }\n ]\n})\nexport class OpcuaDeviceProtocolBrowsePathValidation implements Validator {\n @Input() getMappings: () => [];\n @Input() model: any;\n\n constructor(private el: ElementRef) {}\n\n validate(control: AbstractControl): { [key: string]: any } {\n if (control.value) {\n if (!this.isValidJson(control.value)) {\n return { invalidBrowsePathNotation: true };\n } else {\n if (this.isBrowsePathUnique(control.value)) {\n return { browsePathNotUnique: true };\n }\n }\n }\n return null;\n }\n\n isValidJson(value) {\n try {\n const browsePath = JSON.parse(value);\n return !isEmpty(browsePath);\n } catch (error) {\n return false;\n }\n }\n\n toArray(str) {\n return JSON.parse(str);\n }\n\n isBrowsePathUnique(value): boolean {\n const mappings = this.getMappings();\n const found = some(mappings, item => {\n if (isEqual(item.browsePath, this.toArray(value)) && item.id !== this.model.id) {\n return item;\n }\n });\n return found ? true : false;\n }\n}\n","import { BehaviorSubject, Observable, merge } from 'rxjs';\nimport { NestedTreeControl } from '@angular/cdk/tree';\nimport { CollectionViewer, SelectionChange } from '@angular/cdk/collections';\nimport { AddressSpaceService, AddressSpaceNode } from './address-space.service';\nimport { map } from 'rxjs/operators';\n\nexport class DynamicDataSource {\n dataChange = new BehaviorSubject<AddressSpaceNode[]>([]);\n\n get data(): AddressSpaceNode[] {\n return this.dataChange.value;\n }\n set data(value: AddressSpaceNode[]) {\n this.treeControl.dataNodes = value;\n this.dataChange.next(value);\n }\n\n constructor(\n public treeControl: NestedTreeControl<AddressSpaceNode>,\n private addressSpaceService: AddressSpaceService,\n private serverId: string\n ) {\n this.treeControl.isExpanded = (node: AddressSpaceNode) => node.expanded;\n }\n\n connect(collectionViewer: CollectionViewer): Observable<AddressSpaceNode[]> {\n this.treeControl.expansionModel.changed.subscribe(\n (change: SelectionChange<AddressSpaceNode>) => {\n if (change.added || change.removed) {\n this.handleTreeControl(change);\n }\n }\n );\n return merge(collectionViewer.viewChange, this.dataChange).pipe(map(() => this.data));\n }\n\n /** Handle expand/collapse behaviors */\n handleTreeControl(change: SelectionChange<AddressSpaceNode>) {\n if (change.added) {\n change.added.forEach(node => this.toggleNode(node, true));\n }\n if (change.removed) {\n change.removed\n .slice()\n .reverse()\n .forEach(node => this.toggleNode(node, false));\n }\n }\n\n /**\n * Toggle the node, remove from display list\n */\n async toggleNode(addressSpaceNode: AddressSpaceNode, expand: boolean) {\n if (!addressSpaceNode.children || addressSpaceNode.children.length === 0) {\n addressSpaceNode.currentlyLoadingChildren = true;\n\n const res = await this.addressSpaceService.getChildrenOf(addressSpaceNode, this.serverId);\n const children = (await res.json()) as AddressSpaceNode[];\n\n addressSpaceNode.children = children || [];\n addressSpaceNode.children = addressSpaceNode.children.map((node: AddressSpaceNode) => {\n node.parentNode = addressSpaceNode;\n return node;\n });\n addressSpaceNode.currentlyLoadingChildren = false;\n\n this.treeControl.expand(addressSpaceNode);\n }\n\n addressSpaceNode.expanded = expand && addressSpaceNode.children.length > 0;\n this.refreshNestedTree(this.data);\n\n return Promise.resolve(addressSpaceNode);\n }\n catch() {\n // do nothing\n }\n\n private refreshNestedTree(treeData: AddressSpaceNode[]) {\n // necessary to rerender tree, otherwise new nodes will not\n // appear, but they are added to the list.\n this.data = [];\n this.dataChange.next(treeData);\n this.triggerResize(); // to resize the modal window when creating a new device protocol\n }\n\n private triggerResize() {\n setTimeout(() => {\n try {\n window.dispatchEvent(new Event('resize'));\n } catch (error) {\n // do nothing\n }\n }, 200);\n }\n}\n","import {\n NestedTreeControl,\n CdkTree,\n CdkTreeNodeDef,\n CdkNestedTreeNode,\n CdkTreeNodeToggle,\n CdkTreeNodeOutlet\n} from '@angular/cdk/tree';\nimport {\n Component,\n EventEmitter,\n Input,\n OnChanges,\n OnDestroy,\n OnInit,\n Output,\n SimpleChanges\n} from '@angular/core';\nimport {\n AlertService,\n IconDirective,\n LoadingComponent,\n C8yTranslateDirective,\n C8yTranslatePipe\n} from '@c8y/ngx-components';\nimport { clone } from 'lodash';\nimport { Subject, Subscription } from 'rxjs';\nimport { takeUntil } from 'rxjs/operators';\nimport { AddressSpaceNode, AddressSpaceService, NodeNavigationData } from './address-space.service';\nimport { DynamicDataSource } from './dynamic-data-source';\nimport { OpcuaService } from './opcuaService';\nimport { NgIf, NgClass } from '@angular/common';\n\n@Component({\n selector: 'opcua-address-space-tree',\n templateUrl: './opcua-address-space-tree.component.html',\n imports: [\n NgIf,\n CdkTree,\n CdkTreeNodeDef,\n CdkNestedTreeNode,\n NgClass,\n IconDirective,\n CdkTreeNodeToggle,\n CdkTreeNodeOutlet,\n LoadingComponent,\n C8yTranslateDirective,\n C8yTranslatePipe\n ]\n})\nexport class OpcuaAddressSpaceTreeComponent implements OnInit, OnDestroy, OnChanges {\n @Input()\n set moId(id: string) {\n this._moId = id || undefined;\n }\n\n @Input() node;\n @Input() focusEmitter: EventEmitter<AddressSpaceNode> = new EventEmitter<AddressSpaceNode>();\n @Output() selectedNode: EventEmitter<AddressSpaceNode> = new EventEmitter<AddressSpaceNode>();\n nestedTreeControl: NestedTreeControl<AddressSpaceNode>;\n dataSource: DynamicDataSource = null;\n focused: AddressSpaceNode;\n loading = false;\n subscriptionRef: Subscription;\n nodeNavDataSubscription: Subscription;\n private _moId: string;\n private destroy$: Subject<void> = new Subject<void>();\n\n constructor(\n private addressSpaceService: AddressSpaceService,\n private opcuaService: OpcuaService,\n private alertService: AlertService\n ) {}\n\n getChildren = (node: AddressSpaceNode) => (node.expanded ? node.children : []);\n hasChild = (_: number, _nodeData: AddressSpaceNode) =>\n this.addressSpaceService.childrenAvailable(_nodeData.references);\n\n ngOnInit() {\n this.initializeDataSet();\n }\n\n ngOnChanges(changes: SimpleChanges) {\n if (\n changes.moId &&\n changes.moId.previousValue &&\n changes.moId.currentValue !== changes.moId.previousValue\n ) {\n this.initializeDataSet();\n }\n }\n\n initializeDataSet() {\n this.nodeNavDataSubscription = this.addressSpaceService\n .getNodeNavData$()\n .pipe(takeUntil(this.destroy$))\n .subscribe(nodeNavData => this.openNode(nodeNavData));\n this.subscriptionRef = this.focusEmitter.subscribe(node => {\n this.focused = this.isFocusedNode(node) ? undefined : node;\n });\n }\n\n ngOnDestroy() {\n this.destroy$.next();\n this.destroy$.complete();\n // clean up the address-space-tree\n this.addressSpaceService.resetTreeToRootNode();\n\n if (this.nodeNavDataSubscription && !this.nodeNavDataSubscription.closed) {\n this.nodeNavDataSubscription.unsubscribe();\n }\n\n if (this.subscriptionRef && !this.subscriptionRef.closed) {\n this.subscriptionRef.unsubscribe();\n }\n }\n\n async openNode(nodeNavData: NodeNavigationData) {\n const { node, selectedAncestorIds } = nodeNavData;\n let nodeId;\n\n // We just set the nodeId when the selectedAncestorIds variable an empty array.\n // If selectedAncestorIds contain any id we assume that the tree should be travsersed beginning\n // from the root node.\n if (node && node.nodeId && selectedAncestorIds && selectedAncestorIds.length === 0) {\n nodeId = node.nodeId;\n }\n // Always recreate the tree when routing to a specific nested node,\n // because previous modifications to the tree-structure could cause errors\n // while traversing with 'old' tree-data\n // -----------------\n // setupTree is able to handle nodeId = undefined\n await this.setupTree(nodeId);\n\n if (!selectedAncestorIds || selectedAncestorIds.length === 0) {\n return;\n }\n\n if (nodeNavData && this.dataSource) {\n const clonedAncestors = clone(selectedAncestorIds);\n clonedAncestors.shift();\n\n const n = await this.dataSource.toggleNode(this.dataSource.data[0], true);\n this.setChildNodes(n.children, clonedAncestors);\n\n this.toggleFocusedNode(node);\n }\n }\n\n setChildNodes(nodes: AddressSpaceNode[], ids: string[]) {\n if (nodes) {\n ids.forEach(async id => {\n const match = nodes.find(n => n.nodeId === id);\n if (match && ids.length > 0) {\n const idx = ids.findIndex(value => value === id);\n if (idx >= 0) {\n ids.splice(idx, 1);\n }\n const toggledNode = await this.dataSource.toggleNode(match, true);\n this.setChildNodes(toggledNode.children, ids);\n }\n });\n }\n }\n\n async setupTree(nodeId?: string) {\n this.loading = true;\n\n if (!this._moId || this._moId.length === 0) {\n this._moId = this.opcuaService.getMoId();\n }\n\n // addressSpaceService.getNode returns either the root node of the server (moId)\n // or if nodeId !== undefined the node with given nodeId\n const res = await this.addressSpaceService.getNode(this._moId, nodeId);\n if (res) {\n if (res.status !== 200) {\n const data = res.json ? await res.json() : undefined;\n this.alertService.addServerFailure({ data, res });\n this.dataSource = undefined;\n } else {\n const rootNode = (await res.json()) as AddressSpaceNode;\n this.nestedTreeControl = new NestedTreeControl<AddressSpaceNode>(this.getChildren);\n this.dataSource = new DynamicDataSource(\n this.nestedTreeControl,\n this.addressSpaceService,\n this._moId\n );\n this.dataSource.data = [rootNode];\n }\n this.loading = false;\n } else {\n this.loading = false;\n }\n }\n\n getMoId() {\n if (!this._moId || this._moId.length === 0) {\n return this.opcuaService.getMoId();\n }\n return this._moId;\n }\n\n getIcon(nodeClassName) {\n return this.addressSpaceService.getIcon(nodeClassName);\n }\n\n toggleFocusedNode(node) {\n const relativePath = [];\n this.getRelativePath(node, relativePath);\n node.relativePath = relativePath;\n\n this.selectedNode.emit(node);\n this.focused = this.isFocusedNode(node) ? undefined : node;\n }\n\n isFocusedNode(node: AddressSpaceNode) {\n if (this.focused) {\n return node.nodeId === this.focused.nodeId;\n }\n return false;\n }\n\n private getRelativePath(node: AddressSpaceNode, relativePath: string[]) {\n if (node.parentNode) {\n relativePath.unshift(node.browseName);\n this.getRelativePath(node.parentNode, relativePath);\n }\n }\n}\n","<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","import {\n Component,\n DestroyRef,\n EventEmitter,\n inject,\n OnDestroy,\n OnInit,\n Output,\n signal\n} from '@angular/core';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { IOperation, OperationService, OperationStatus } from '@c8y/client';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport {\n AlertService,\n ModalService,\n OperationRealtimeService,\n IconDirective,\n LoadingComponent,\n C8yTranslatePipe\n} from '@c8y/ngx-components';\nimport { isEmpty } from 'lodash-es';\nimport { filter, map } from 'rxjs/operators';\nimport { AddressSpaceNode, AddressSpaceService } from './address-space.service';\nimport { DynamicDataSource } from './dynamic-data-source';\nimport { OpcuaServer, OpcuaServerConfig } from './opcua-server.interface';\nimport { OpcuaService } from './opcuaService';\nimport { NgIf, NgFor, NgClass } from '@angular/common';\nimport { OperationDetailsComponent } from '@c8y/ngx-components/operations/operation-details';\nimport { FormsModule } from '@angular/forms';\nimport { OpcuaAddressSpaceTreeComponent } from './opcua-address-space-tree.component';\nimport { OpcuaAddressSpaceDetailComponent } from './opcua-address-space-detail.component';\n\n@Component({\n selector: 'opcua-address-space',\n templateUrl: './opcua-address-space.component.html',\n imports: [\n NgIf,\n OperationDetailsComponent,\n FormsModule,\n IconDirective,\n LoadingComponent,\n NgFor,\n NgClass,\n OpcuaAddressSpaceTreeComponent,\n OpcuaAddressSpaceDetailComponent,\n C8yTranslatePipe\n ]\n})\nexport class OpcuaAddressSpaceComponent implements OnInit, OnDestroy {\n currentNode: AddressSpaceNode;\n selectednode = false;\n searchKey: string;\n isSearch: boolean;\n loading = false;\n searchInProgress = false;\n filterLabel: string;\n dataSource: DynamicDataSource;\n nodeList;\n operation?: IOperation;\n isOperationRunning = signal(false);\n destroyRef = inject(DestroyRef);\n\n @Output() focusStatus: EventEmitter<AddressSpaceNode> = new EventEmitter<AddressSpaceNode>();\n private moId = '';\n constructor(\n private addressSpaceService: AddressSpaceService,\n private opcuaService: OpcuaService,\n private operationService: OperationService,\n private operationRealtimeService: OperationRealtimeService,\n private alert: AlertService,\n private modalService: ModalService\n ) {}\n\n async ngOnInit() {\n this.filterLabel = gettext('Filter…');\n this.moId = this.opcuaService.getMoId();\n\n this.operation = await this.getRunningScanAddressSpaceOperation();\n this.isOperationRunning.set(!!this.operation);\n\n this.operationRealtimeService\n .onAll$(this.moId)\n .pipe(\n map(({ data }) => data as IOperation),\n filter(operation => operation.c8y_ua_command_ScanAddressSpace),\n takeUntilDestroyed(this.destroyRef)\n )\n .subscribe(operation => {\n this.operation = operation;\n this.isOperationRunning.set(\n operation.status == OperationStatus.EXECUTING ||\n operation.status == OperationStatus.PENDING\n );\n if (operation.status == OperationStatus.SUCCESSFUL) {\n this.addressSpaceService.resetTreeToRootNode();\n }\n });\n }\n\n async getRunningScanAddressSpaceOperation(): Promise<IOperation | undefined> {\n const filter: object = {\n deviceId: this.moId,\n fragmentType: 'c8y_ua_command_ScanAddressSpace',\n dateFrom: new Date(0).toISOString(),\n revert: true,\n pageSize: 1\n };\n const [operation] = (await this.operationService.list(filter)).data ?? [];\n\n return operation?.status == OperationStatus.EXECUTING ||\n operation?.status == OperationStatus.PENDING\n ? operation\n : undefined;\n }\n\n ngOnDestroy() {\n // The BehaviourSubject will store the last array of ancestorNodes from the previous search\n // this would cause the component while subscribing in the init-phase to the subject to travers\n // to the last searched node again. From user perspective it does not make sense, because the user\n // left the Address space (tab) and should loose the context and just request a new search or\n // browse the tree manually.\n this.addressSpaceService.resetTreeToRootNode();\n }\n\n async searchNodes() {\n this.searchInProgress = true;\n this.clearNodeListAndCheckSearchString();\n if (this.isSearch) {\n this.currentNode = undefined;\n this.nodeList = await this.addressSpaceService.getSearchedNodes(this.searchKey, this.moId);\n this.searchInProgress = false;\n this.nodeList.resultLabel = gettext('Results found');\n }\n }\n\n clearNodeListAndCheckSearchString() {\n this.isSearch = this.searchKey !== undefined && this.searchKey !== '' ? true : false;\n if (!this.isSearch) {\n this.searchInProgress = false;\n }\n }\n\n clearSearch() {\n this.isSearch = false;\n this.searchKey = '';\n this.currentNode = undefined;\n }\n\n getIcon(nodeClassName: string) {\n return this.addressSpaceService.getIcon(nodeClassName);\n }\n\n async selectNode(node) {\n if (node && node.nodeId && node.nodeId.length > 0) {\n const res = await this.addressSpaceService.getNodeById(this.moId, node.nodeId);\n this.toggleCurrentNode((await res.json()) as AddressSpaceNode);\n }\n }\n\n toggleCurrentNode(node: AddressSpaceNode) {\n this.currentNode = this.isNodeSet(node) ? undefined : node;\n }\n\n backHandler(node) {\n this.isSearch = false;\n this.focusStatus.emit(node);\n this.toggleCurrentNode(node);\n }\n\n isNodeSet(node: AddressSpaceNode) {\n if (this.currentNode !== undefined && this.currentNode.nodeId === node.nodeId) {\n return true;\n }\n return false;\n }\n async rescanAddressSpace() {\n const serverResponse = await this.opcuaService.getServer(this.moId);\n const server = (await serverResponse.json()) as OpcuaServer;\n let warning: string = gettext(\n 'Rescanning address space from root node might take several hours. Do you want to proceed?'\n );\n\n if (server) {\n if (server.config.partialAddressScan && !isEmpty(server.config.partialAddressScanNodeIds)) {\n warning = gettext(\n 'Rescanning from nodes ' +\n server.config.partialAddressScanNodeIds?.join(';') +\n '. Address space rescan might take several hours. Do you want to proceed?'\n );\n }\n }\n\n const doit = await this.modalService.confirm(\n gettext('Rescan address space'),\n warning,\n 'warning',\n { ok: gettext('Rescan'), cancel: gettext('Cancel') }\n );\n if (!doit) {\n return;\n }\n\n const operation = await this.createConfigurationAwareScanAdressSpaceOperation(server.config);\n try {\n this.operation = (await this.operationService.create(operation)).data;\n this.isOperationRunning.set(true);\n } catch (error) {\n this.alert.add({\n text: gettext('Error creating rescan operation'),\n detailedData: error,\n type: 'danger',\n timeout: 8000\n });\n this.isOperationRunning.set(false);\n }\n }\n\n async createConfigurationAwareScanAdressSpaceOperation(\n config: OpcuaServerConfig\n ): Promise<IOperation> {\n const result: IOperation = {\n deviceId: this.moId,\n description: gettext('[RESCAN] Address space import from Root node'),\n c8y_ua_command_ScanAddressSpace: {\n skipSync: false\n }\n };\n\n if (config) {\n if (config.partialAddressScan && config.partialAddressScanNodeIds) {\n const nodeIds = config.partialAddressScanNodeIds;\n result.c8y_ua_command_ScanAddressSpace.nodeIds = config.partialAddressScanNodeIds;\n result.description = gettext(`[RESCAN] Address space from node[s] ${nodeIds.join(';')}`);\n }\n }\n return result;\n }\n}\n","<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 && !loading\"\n >\n <p *ngIf=\"!searchInProgress\">\n <em>{{ nodeList.resultLabel | translate }}</em>\n \n <span class=\"badge badge-info\">{{ nodeList?.length }}</span>\n </p>\n </div>\n </div>\n\n <div\n class=\"p-t-8\"\n *ngIf=\"(isSearch && loading) || searchInProgress\"\n >\n <c8y-loading></c8y-loading>\n </div>\n\n <div\n class=\"inner-scroll\"\n *ngIf=\"isSearch && !loading && !searchInProgress\"\n >\n <div\n class=\"list-group list-group-links\"\n *ngIf=\"isSearch && !loading\"\n >\n <button\n *ngFor=\"let nodeItem of nodeList\"\n (click)=\"selectNode(nodeItem)\"\n [ngClass]=\"{ 'list-group-item d-flex': true }\"\n >\n <div class=\"list-group-icon m-r-4\">\n <i\n class=\"m-r-4\"\n [c8yIcon]=\"getIcon(nodeItem.nodeClassName)\"\n ></i>\n </div>\n <div class=\"list-item-body text-truncate\">\n <span tile=\"nodeId\">{{ nodeItem.nodeId }}</span>\n {{ nodeItem.displayName }}\n </div>\n </button>\n </div>\n </div>\n <div\n class=\"inner-scroll\"\n *ngIf=\"!isSearch\"\n >\n <opcua-address-space-tree\n (selectedNode)=\"toggleCurrentNode($event)\"\n [focusEmitter]=\"focusStatus\"\n ></opcua-address-space-tree>\n </div>\n <div class=\"card-footer separator-top\">\n <button\n class=\"btn btn-default\"\n [class.btn-pending]=\"isOperationRunning()\"\n type=\"button\"\n (click)=\"rescanAddressSpace()\"\n [disabled]=\"isOperationRunning()\"\n >\n {{ 'Rescan' | translate }}\n </button>\n </div>\n </div>\n </div>\n <opcua-address-space-detail\n class=\"col-md-7 col-xs-12 scroll-column no-gutter-l no-pointer\"\n [node]=\"currentNode\"\n (toggleAttrDetail)=\"backHandler($event)\"\n ></opcua-address-space-detail>\n</div>\n","import { Component, Input, OnInit, EventEmitter } from '@angular/core';\nimport { AutoApplyConstraints } from './opcua-protocol-device-type.interface';\nimport { IManagedObject, InventoryService } from '@c8y/client';\nimport { gettext } from '@c8y/ngx-components/gettext';\nimport {\n C8yTranslateDirective,\n FormGroupComponent,\n SelectLegacyComponent,\n ListItemComponent,\n FilterInputComponent,\n ListItemBodyComponent,\n ListItemCheckboxComponent,\n InputGroupListContainerDirective,\n InputGroupListComponent,\n RequiredInputPlaceholderDirective,\n C8yTranslatePipe\n} from '@c8y/ngx-components';\nimport { reject } from 'lodash-es';\nimport { CollapseDirective } from 'ngx-bootstrap/collapse';\nimport { NgIf, NgFor } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\n@Component({\n selector: 'opcua-auto-apply',\n templateUrl: './opcua-auto-apply-settings.component.html',\n imports: [\n C8yTranslateDirective,\n CollapseDirective,\n FormGroupComponent,\n SelectLegacyComponent,\n NgIf,\n ListItemComponent,\n FilterInputComponent,\n ListItemBodyComponent,\n NgFor,\n ListItemCheckboxComponent,\n FormsModule,\n InputGroupListContainerDirective,\n InputGroupListComponent,\n RequiredInputPlaceholderDirective,\n C8yTranslatePipe\n ]\n})\nexport class OpcuaAutoApplySettingsComponent implements OnInit {\n opcuaServers: IManagedObject[] = [];\n selectedItems: IManagedObject[] = [];\n filteredList: IManagedObject[] = [];\n checked = {};\n readonly sizeToShowFilter: number = 5;\n constraints: AutoApplyConstraints = {\n browsePathMatchesRegex: '',\n matchesNodeIds: [],\n serverObjectHasFragment: '',\n matchesServerIds: []\n };\n placeholderSelectServerIds: string = gettext('Select server IDs from list');\n showServerIds: boolean;\n showBrowsePath: boolean;\n showServerFragment: boolean;\n showRootNodes: boolean;\n updateSelectedItem: EventEmitter<boolean> = new EventEmitter();\n private _model: IManagedObject;\n\n constructor(private inventoryService: InventoryService) {}\n\n async ngOnInit() {\n const { data } = await this.inventoryService.list({\n pageSize: 1000,\n withTotalPages: true,\n type: 'c8y_OpcuaServer'\n });\n\n this.opcuaServers = data;\n this.filteredList = data;\n this.selectedItems = [];\n\n const { matchesServerIds } = this.constraints;\n\n data.forEach(server => {\n if (\n matchesServerIds &&\n matchesServerIds.length > 0 &&\n matchesServerIds.find(itemId => itemId === server.id)\n ) {\n this.selectedItems.push(server);\n this.checked[server.id] = true;\n this.showServerIds = true;\n }\n });\n }\n\n @Input() set model(model) {\n if (model && model.applyConstraints) {\n this.constraints = model.applyConstraints as AutoApplyConstraints;\n }\n this._model = model;\n }\n\n get model() {\n return this._model;\n }\n\n serverIdsSelected(items: IManagedObject[]) {\n if (this.constraints) {\n this.constraints.matchesServerIds = items.map((item: IManagedObject) => item.id);\n }\n this.selectedItems = items;\n }\n\n onChangeNodeId(event) {\n if (event.target.checked) {\n this.showRootNodes = true;\n this.add();\n } else {\n this.showRootNodes = false;\n this.constraints.matchesNodeIds = [];\n }\n }\n\n onChangeShowServerIds(event) {\n if (!event.target.checked) {\n this.constraints.matchesServerIds = [];\n this.showServerIds = false;\n this.selectedItems = [];\n this.checked = {};\n } else {\n this.showServerIds = true;\n }\n }\n\n onChangeShowBrowsePath(event) {\n if (!event.target.checked) {\n this.constraints.browsePathMatchesRegex = '';\n this.showBrowsePath = false;\n } else {\n this.showBrowsePath = true;\n }\n }\n\n onChangeShowServerFragment(event) {\n if (!event.target.checked) {\n this.constraints.serverObjectHasFragment = '';\n this.showServerFragment = false;\n } else {\n this.showServerFragment = true;\n }\n }\n\n add() {\n this.constraints.matchesNodeIds.push('');\n }\n\n remove(index) {\n this.constraints.matchesNodeIds.splice(index, 1);\n }\n\n trackByFn(index: any, _item: any) {\n return index;\n }\n\n updateConstraints(items) {\n if (this.constraints) {\n this.constraints.matchesServerIds = items.map((item: IManagedObject) => item.id);\n }\n }\n\n filterItems(filterText) {\n if (filterText.length !== 0) {\n const search = new RegExp(filterText, 'i');\n this.filteredList = this.opcuaServers.filter(({ name, id }) => {\n return search.test(name) || search.test(id);\n });\n } else {\n this.filteredList = this.opcuaServers;\n }\n }\n\n isChecked(item) {\n return this.checked[item.id];\n }\n\n onSelect(selected, item) {\n if (!selected) {\n this.selectedItems = reject(this.selectedItems, { id: item.id });\n delete this.checked[item.id];\n } else {\n this.selectedItems.push(item);\n this.checked[item.id] = selected;\n }\n this.updateSelectedItem.emit(true);\n this.updateConstraints(this.selectedItems);\n }\n}\n","<div class=\"row\">\n <div class=\"col-md-4\">\n <p translate>\n Specifying auto-apply constraints allows you to limit the scope where the device protocols are\n applied, for example by specifying a set of possible servers or node IDs.\n </p>\n <p translate>\n If no constraints are set, device protocols are applied at any fitting location on the OPC UA\n server.\n </p>\n </div>\n\n <div class=\"col-md-6\">\n <ul class=\"list-group\" style=\"box-shadow: none;\">\n <!-- Limit device type to a set of servers -->\n <li class=\"list-group-item\">\n <label\n title=\"{{ 'Limit device protocol to a set of servers' | translate }}\"\n class=\"c8y-checkbox\"\n >\n <input\n type=\"checkbox\"\n [checked]=\"\n constraints.matchesServerIds !== null && constraints.matchesServerIds.length > 0\n \"\n (change)=\"onChangeShowServerIds($event)\"\n />\n <span></span>\n <span class=\"m-l-8\">\n {{ 'Limit device protocol to a set of servers' | translate }}\n </span>\n </label>\n <div\n class=\"collapse\"\n [collapse]=\"\n