@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
851 lines (843 loc) • 245 kB
JavaScript
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 ×\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 ×\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 &&