blob: d64ca385bba97929cd552fa4ba8516341914e206 [file] [log] [blame]
/********************************************************************************
* Copyright (c) 2015-2018 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
********************************************************************************/
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DatePipe } from '@angular/common';
import { TreeNode } from 'primeng/api';
import { TreeTable } from 'primeng/primeng';
import { Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { Node, Attribute, ContextGroup } from '@navigator/node';
import { NavigatorService } from '@navigator/navigator.service';
import { MDMNotificationService } from '@core/mdm-notification.service';
import { TRANSLATE } from '@core/mdm-core.module';
import { FileService } from '@file-explorer/services/file.service';
import { FileLinkContextWrapper } from '@file-explorer/model/file-explorer.model';
import { AuthenticationService } from '../../../authentication/authentication.service';
import { Sensor, Components, Context, ContextAttributeIdentifier} from '../../model/details.model';
import { ContextService } from '../../services/context.service';
import { Role } from 'src/app/authentication/authentication.service';
@Component({
selector: 'mdm-detail-context',
templateUrl: 'mdm-detail-descriptive-data.component.html',
styleUrls: ['mdm-detail-descriptive-data.component.css'],
})
export class MDMDescriptiveDataComponent implements OnInit {
private readonly StatusLoading = TRANSLATE('details.mdm-detail-descriptive-data.status-loading');
private readonly StatusSaving = TRANSLATE('details.mdm-detail-descriptive-data.status-saving');
private readonly StatusNoNodes = TRANSLATE('details.mdm-detail-descriptive-data.status-no-nodes-available');
private readonly StatusNoDescriptiveData = TRANSLATE('details.mdm-detail-descriptive-data.status-no-descriptive-data-available');
public treeNodes: {[key: string]: TreeNode[]};
// this holds the type dependant original context data in case of cancelling the edit mode
private tmpTreeNodes: Node[];
public selectedNode: Node;
public contextType: string;
public contextComponents: Components;
public sensors: Sensor[];
public status: string;
// reference to be able to use enum in html template
public contextGroupEnum = ContextGroup;
public editMode: boolean;
public isTreeTableExpanded = true;
constructor(private route: ActivatedRoute,
private _contextService: ContextService,
private navigatorService: NavigatorService,
private notificationService: MDMNotificationService,
private translateService: TranslateService,
private datePipe: DatePipe,
private authenticationService: AuthenticationService,
private fileService: FileService) {
}
public canEdit: boolean;
ngOnInit() {
this.authenticationService.isUserInRole([Role.Admin.toString(), Role.User.toString()]).subscribe(b => this.canEdit = b);
this.status = this.StatusLoading;
this.editMode = false;
this.refreshDetailData(this.navigatorService.getSelectedNode());
this.route.params
.subscribe(
params => this.refreshContextData(params['context']),
error => this.notificationService.notifyError(
this.translateService.instant('details.mdm-detail-descriptive-data.err-cannot-load-scope'), error));
// node changed from the navigation tree
this.navigatorService.selectedNodeChanged
.subscribe(
node => this.refreshDetailData(node),
error => this.notificationService.notifyError(
this.translateService.instant('details.mdm-detail-descriptive-data.err-cannot-load-data'), error));
// file changed
this.fileService.onFileChanged().subscribe(link => this.handleFileChanged(link));
}
handleFileChanged(fileLinkContextWrapper: FileLinkContextWrapper) {
if (this.treeNodes != undefined) {
this.treeNodes[fileLinkContextWrapper.contextType]
.filter(tnode => tnode.label === fileLinkContextWrapper.contextComponent.name)
.map(tnode => tnode.children)
.reduce((a, b) => a.concat(b), [])
.filter(tnode => tnode.data.attribute.name === fileLinkContextWrapper.attribute.name)
.forEach(tnode => tnode.data.attribute = fileLinkContextWrapper.attribute);
// spread is necessary for treeTable changeDetection
this.treeNodes[fileLinkContextWrapper.contextType] = [...
this.treeNodes[fileLinkContextWrapper.contextType]];
}
}
setContext(context: string) {
let contextType: string;
switch (context) {
case 'uut':
contextType = 'UNITUNDERTEST';
break;
case 'te':
contextType = 'TESTEQUIPMENT';
break;
case 'ts':
contextType = 'TESTSEQUENCE';
break;
}
this.contextType = contextType;
}
/**
* Listener method to change the context tab
*
* @param contextType
*/
refreshContextData(contextType: string) {
// revert before changing the context
this.undoEditChanges(undefined, this.editMode);
this.setContext(contextType);
this.editMode = false;
}
/**
* Listener method to change the node
*
* @param node
*/
refreshDetailData(node: Node) {
if (node != undefined) {
this.selectedNode = node;
this.status = this.StatusLoading;
this.editMode = false;
this.contextComponents = undefined;
this.treeNodes = undefined;
if (node.type.toLowerCase() === 'measurement' || node.type.toLowerCase() === 'teststep' || node.type.toLowerCase() === 'test') {
this.loadContext(node);
} else {
this.status = this.StatusNoDescriptiveData;
}
} else {
this.status = this.StatusNoNodes;
}
}
/**
* Load the context data for the provided node
*
* @param node
*/
private loadContext(node: Node) {
this._contextService.getContext(node).subscribe(
components => {
if (components['UNITUNDERTEST'] != undefined
|| components['TESTEQUIPMENT'] != undefined
|| components['TESTSEQUENCE'] != undefined) {
this.contextComponents = components;
this.treeNodes = this.mapComponents(components);
} else {
this.status = this.StatusNoDescriptiveData;
}
},
error => this.notificationService.notifyError(
this.translateService.instant('details.mdm-detail-descriptive-data.err-cannot-load-context'), error)
);
}
getIdentifier(contextDescribable: Node, contextComponent: Node, attribute: Attribute, contextGroup?: ContextGroup, contextType?: string) {
return new ContextAttributeIdentifier(contextDescribable, contextComponent, attribute, contextGroup, contextType);
}
getNodeClass(item: Node) {
return 'icon ' + item.type.toLowerCase();
}
/**
* Change the tree state to expanded or collapsed
*
* @param type
* @param expand
*/
toggleTreeNodeState(tt: TreeTable) {
this.isTreeTableExpanded = !this.isTreeTableExpanded;
const nodeArray = this.treeNodes[this.contextType];
if (nodeArray != undefined) {
for (let node of nodeArray) {
this.recursiveChangeNodes(node, this.isTreeTableExpanded);
}
}
// invoke table update when pressing the plus or minus buttons from the table header
tt.updateSerializedValue();
tt.tableService.onUIUpdate(tt.value);
}
/**
* Change the tree node state recursively
*
* @param node
* @param expand
*/
recursiveChangeNodes(node: TreeNode, expand: boolean) {
node.expanded = expand;
if (node.children != undefined && node.children.length > 0) {
for (let child of node.children) {
this.recursiveChangeNodes(child, expand);
}
}
}
/**
* Get the nodes from the current context
*
* @param nodes
* @param parentId optional parent id which will get the child nodes
*/
getNodes(nodes: Node[], parentId: string) {
return nodes.filter(n => this.nodeIsInCurrentContext(n, parentId));
}
nodeIsInCurrentContext(node: Node, parentId: string) {
let parentNodeId = node.relations != null && node.relations.length > 0 ? node.relations[0].parentId : null;
return parentId == null && parentNodeId == null
|| parentId != null && parentNodeId != null && parentId === parentNodeId;
}
/**
* Create a tree node based on the mdm entity
*
* @param node
* @param context
*/
createTreeNode(node: Node, children: Node[]) {
return <TreeNode>{
label: node.name,
data: {
'name': node.name,
'attribute': node,
'header': true
},
children: this.createTreeChildren(node, children),
icon: this.getNodeClass(node),
expanded: true
};
}
/**
* Create the tree children nodes recursive based on the mdm attributes and subsequent mdm entities
*
* @param node the current node
* @param contexts the complete contexts
*/
createTreeChildren(node: Node, children: Node[]) {
let list = [];
for (let attribute of node.attributes) {
let tmp = <TreeNode>{
data: {
'name': attribute.name,
'attribute': this.patchAttributeForDate(attribute),
'header': false
},
expanded: true
};
list.push(tmp);
}
if (node.relations != null && node.relations.length > 0) {
for (let relation of node.relations) {
if (relation.ids != null && relation.ids.length > 0) {
for (let id of relation.ids) {
let nodes = this.getNodes(children, id);
for (let n of nodes) {
list.push(this.createTreeNode(n, children));
}
}
}
}
}
return list;
}
/**
* Method to create a tree structure from the flat context entity and attribute map
*
* @param components
*/
mapComponents(components: Components) {
const list: {[key: string]: TreeNode[]} = {};
if (components != undefined ) {
for (let key of Object.keys(components)) {
const nodes: Node[] = this.getNodes(components[key], null);
if (nodes != undefined) {
list[key] = nodes.map(node => this.createTreeNode(node, components[key]));
}
}
}
return list;
}
/**
* Convert the date for UI or backend
*
* @param attribute
*/
patchAttributeForDate(attribute: Attribute) {
if (attribute.dataType != null && attribute.dataType.length > 0 && 'DATE' === attribute.dataType) {
const val = attribute.value as any[];
if (val != null && val.length > 0) {
if (val[0] != null && val[0].length > 0) {
val[0] = this.convertFixedDateStr(val[0], true);
}
if (val.length > 1 && val[1] != null && val[1].length > 0) {
val[1] = this.convertFixedDateStr(val[1], true);
}
}
}
return attribute;
}
/** *********************
* Edit functions start
********************** */
convertFixedDateStr(dt: string, convertForUI: boolean) {
let newDt = dt;
let sourceFormat = '';
/**
* Get the translation immediately
*/
sourceFormat = this.translateService.instant('details.mdm-detail-descriptive-data.transform-dateformat');
if (dt != null && dt.length > 0 && convertForUI && dt.indexOf('T') > -1) {
// for display purpose
let tmpDt = this.parseISOString(dt);
newDt = this.datePipe.transform(tmpDt, sourceFormat, '+0000');
} else if (dt != null && dt.length > 0 && !convertForUI && dt.indexOf('T') === -1) {
// re-convert UI date to server date for persistence
if (newDt.indexOf('-') === -1) {
// find the date patterns dd, MM and yyyy and grab the according index positions
let startDay = sourceFormat.indexOf('d');
let endDay = sourceFormat.lastIndexOf('d');
let startMonth = sourceFormat.indexOf('M');
let endMonth = sourceFormat.lastIndexOf('M');
let startYear = sourceFormat.indexOf('y');
let endYear = sourceFormat.lastIndexOf('y');
// manually attach the time as toISOString() will not properly transform the winter/summer time
newDt = dt.substring(startYear, endYear + 1) + '-' + dt.substring(startMonth, endMonth + 1)
+ '-' + dt.substring(startDay, endDay + 1);
if (dt.indexOf(' ') > -1) {
// use the provided timestamp
newDt = newDt + 'T' + dt.substring(dt.indexOf(' ') + 1) + ':00Z';
} else {
newDt = newDt + 'T00:00:00Z';
}
if (newDt.length !== 20) {
newDt = '';
}
}
}
return newDt;
}
/**
* Fixed parsing for ISO date string
*
* @param s
*/
parseISOString(s: string) {
let b: any[] = s.split(/\D+/);
return new Date(Date.UTC(b[0], --b[1], b[2], b[3], b[4], b[5], b[6]));
}
/**
* Send the changed data to the backend
*
* @param node
* @param type
*/
private putContext(node: Node, type: string) {
if (type == null) {
return;
}
this.status = this.StatusSaving;
this.treeNodes = undefined;
const data = new Context();
data.ordered = new Components();
data.ordered[type] = this.getNodesWithUpdatedContent(this.contextComponents[type] as Node[], true);
data.measured = new Components();
data.measured[type] = this.getNodesWithUpdatedContent(this.contextComponents[type] as Node[], false);
// clear for status display
this.contextComponents = undefined;
this._contextService.putContext(node, data).subscribe(
components => {
if (components.hasOwnProperty('UNITUNDERTEST')
|| components.hasOwnProperty('TESTEQUIPMENT')
|| components.hasOwnProperty('TESTSEQUENCE')) {
this.contextComponents = components;
this.treeNodes = this.mapComponents(this.contextComponents);
} else {
this.status = this.StatusNoDescriptiveData;
}
},
error => this.notificationService.notifyError(
this.translateService.instant('details.mdm-detail-descriptive-data.err-cannot-load-context'), error)
);
}
onEdit(event: Event) {
event.stopPropagation();
this.editMode = true;
this.tmpTreeNodes = JSON.parse(JSON.stringify(this.contextComponents[this.contextType]));
}
onCancelEdit(event: Event) {
event.stopPropagation();
this.editMode = false;
this.isTreeTableExpanded = true;
this.undoEditChanges(this.contextType, true);
}
onSaveChanges(event: Event) {
event.stopPropagation();
this.editMode = false;
this.isTreeTableExpanded = true;
this.putContext(this.selectedNode, this.contextType);
this.tmpTreeNodes = undefined;
}
/**
* Method to revert table changes back to the original state
*
* @param type
*/
undoEditChanges(type: string, editMode: boolean) {
this.refreshDetailData(this.navigatorService.getSelectedNode());
// if (this.contextComponents != undefined && editMode && this.tmpTreeNodes != undefined) {
// if (type == undefined) {
// type = this.contextType;
// }
// // contexts is the origin, so we revert this back as the tree attributes are just references
// this.contextComponents[type] = this.tmpTreeNodes;
// // revert back
// this.treeNodes = this.mapComponents(this.contextComponents);
// this.tmpTreeNodes = null;
// }
}
/**
* Get the updated nodes from the current context
*
* @param contextComponents
* @param type the context type
* @param ordered true if ordered data, false if measured data
*/
getNodesWithUpdatedContent(contextComponents: Node[], ordered: boolean) {
let list = [];
for (let component of contextComponents) {
let parentNodeId = component.relations != null && component.relations.length > 0 ? component.relations[0].parentId : null;
if (parentNodeId == null) {
let attrs = [];
for (let cAttribute of component.attributes) {
let attr = new Attribute();
let addAttr = true;
attr.dataType = cAttribute.dataType;
attr.name = cAttribute.name;
attr.unit = cAttribute.unit;
attr.value = '';
if (ordered && cAttribute.value instanceof Array && cAttribute.value.length > 0) {
attr.value = cAttribute.value[0];
} else if (!ordered && cAttribute.value instanceof Array && cAttribute.value.length > 1) {
attr.value = cAttribute.value[1];
}
// lookup new value from treenodes
if (attr.dataType === 'BOOLEAN' && attr.value != null && attr.value.toString().length > 0) {
attr.value = attr.value.toString() === 'true' ? '1' : '0';
}
// BUG: don't add if non-string as this throws an parsing error in the middleware
if (attr.dataType !== 'STRING' && (attr.value == null || attr.value.toString().length === 0)) {
addAttr = false;
}
if (addAttr && attr.dataType === 'DATE') {
attr.value = this.convertFixedDateStr(attr.value as string, false);
}
if (addAttr) {
if (this.isAttributeValueModified(attr, component, ordered)) {
attrs.push(attr);
}
}
}
// un-merged list
if (attrs.length > 0) {
let c = JSON.parse(JSON.stringify(component));
c.attributes = attrs;
list.push(c);
}
}
}
return list;
}
private isAttributeValueModified(attr: Attribute, component: Node, ordered: boolean) {
for (let tmpNode of this.tmpTreeNodes) {
if (tmpNode.name === component.name) {
for (let attribute of tmpNode.attributes) {
if (attribute.name === attr.name && attribute.value instanceof Array) {
let orgValue = ordered ? attribute.value[0] :
attribute.value.length > 1 ? attribute.value[1] : undefined;
if (orgValue != undefined) {
if (attr.dataType === 'BOOLEAN') {
// server value = true or false, UI value = 1 or 0
return attr.value === '0' && orgValue === 'true'
|| attr.value === '1' && orgValue === 'false'
|| attr.value !== '' && orgValue === '';
} else {
// plain comparison
return attr.value !== orgValue;
}
}
return false;
}
}
}
}
}
/** *********************
* Edit functions end
********************** */
}