blob: 6402e46af3bf039f80000ac781f59ae89f1bc098 [file] [log] [blame]
/********************************************************************************
* Copyright (c) 2015, 2023 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 { DatePipe } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { TRANSLATE } from "@core/mdm-core.module";
import { MDMItem } from "@core/mdm-item";
import { MDMNotificationService } from "@core/mdm-notification.service";
import { SystemParameterService } from "@details/services/systemparameter.service";
import { FileLinkContextWrapper } from "@file-explorer/model/file-explorer.model";
import { ContextFilesService } from "@file-explorer/services/context-files.service";
import { FileService } from "@file-explorer/services/file.service";
import { NavigatorService } from "@navigator/navigator.service";
import { Attribute, ContextGroup, Node } from "@navigator/node";
import { TranslateService } from "@ngx-translate/core";
import { TreeNode } from "primeng/api";
import { TreeTable } from "primeng/treetable";
import { of, Subscription } from "rxjs";
import { catchError, finalize, skip, take, tap } from "rxjs/operators";
import { Role } from "src/app/authentication/authentication.service";
import { EntityCreatorService } from "src/app/entity-creator/entity-creator.service";
import { MDMEntity } from "src/app/entity-creator/model/mdm-entity";
import { TimezoneService } from "src/app/timezone/timezone-service";
import { AuthenticationService } from "../../../authentication/authentication.service";
import { Components, Context, ContextAttributeIdentifier, Sensor } from "../../model/details.model";
import { ContextService } from "../../services/context.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, OnDestroy {
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 StatusVirtualNodes = TRANSLATE("details.mdm-detail-descriptive-data.status-virtual-node-selected");
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;
private sub = new Subscription();
public tplComponents: MDMEntity[];
public selectedTplComponent: MDMEntity[] = [];
public showTplComponents = false;
public selectedContext: string[] = [];
public showConfirmDelete = false;
public selectedContextComponentName: string;
ctxCompName = { ctxName: "" };
hasOrdered: boolean;
hasMeasured: boolean;
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,
private contextFileService: ContextFilesService,
private systemParameterService: SystemParameterService,
private entityCreatorService: EntityCreatorService,
private timezoneService: TimezoneService,
) {}
public canEdit: boolean;
public isTest: boolean;
public canEdit4Env: boolean;
private editAllowed: { [key: string]: boolean } = {};
ngOnInit() {
this.authenticationService.isUserInRole([Role.Admin.toString(), Role.User.toString()]).subscribe((b) => (this.canEdit = b));
this.status = this.StatusNoNodes;
this.editMode = false;
this.timezoneService.timezoneChanged.subscribe(() => {
this.reloadData();
});
this.sub.add(
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.sub.add(
this.navigatorService
.onSelectionChanged()
.pipe(
skip(1), // skip first to avoid double loading, since path param also changes for sure on init.
tap((obj) => this.handleSelectionChanged(obj)),
finalize(() => (this.status = this.StatusNoDescriptiveData)),
catchError((error) =>
of(
this.notificationService.notifyError(
this.translateService.instant("details.mdm-detail-descriptive-data.err-cannot-load-data"),
error,
),
),
),
)
.subscribe(),
);
// file changed
this.fileService.onFileChanged().subscribe((link) => this.handleFileChanged(link));
}
ngOnDestroy() {
this.sub.unsubscribe();
}
private handleSelectionChanged(obj: Node | MDMItem) {
this.isTest = false;
if (obj instanceof MDMItem) {
this.selectedNode = undefined;
this.status = this.StatusVirtualNodes;
} else {
this.refreshDetailData(obj);
this.handleEditMode(obj);
}
}
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) {
switch (context) {
case "uut":
this.contextType = "UNITUNDERTEST";
break;
case "te":
this.contextType = "TESTEQUIPMENT";
break;
case "ts":
this.contextType = "TESTSEQUENCE";
break;
}
}
/**
* Listener method to change the context tab
*
* @param contextType
*/
refreshContextData(contextType: string) {
// revert before changing the context
this.reloadData();
this.setContext(contextType);
this.editMode = false;
}
private reloadData() {
this.sub.add(
this.navigatorService
.onSelectionChanged()
.pipe(
take(1),
tap((object) => this.handleSelectionChanged(object)),
)
.subscribe(),
);
}
/**
* Listener method to change the node
*
* @param node
*/
refreshDetailData(node: Node) {
this.status = this.StatusLoading;
if (node != undefined) {
this.selectedNode = node;
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;
}
if (node.type.toLowerCase() === "test") {
this.isTest = true;
}
}
}
/**
* Load the context data for the provided node
*
* @param node
*/
private loadContext(node: Node) {
this._contextService.getContext(node).subscribe((components) => this.setComponents(components));
}
getContextAttribute(
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 (const 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 (const 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) {
const 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[], contextType: string) {
const data = {
name: node.name,
// 'attribute': node,
header: true,
mimeType: this.extractMimeType(node),
optional: node.optional,
id: node.id,
};
return <TreeNode>{
label: node.name,
data: data,
children: this.createTreeChildren(node, children, data, contextType),
icon: this.getNodeClass(node),
expanded: true,
optional: node.optional,
defaultActive: node.defaultActive,
testStepSeriesVariable: node.testStepSeriesVariable,
};
}
/**
* 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[], parentData: any, contextType: string) {
const list = node.attributes
.filter((attr) => attr.name !== "MimeType" && attr.name !== "Name" && attr.name !== "Sortindex")
.map((attribute) => {
const patchedAttribute = this.patchAttribute(node, attribute, contextType);
return {
data: {
name: attribute.name,
attribute: patchedAttribute,
header: false,
mimeType: this.extractMimeType(node),
orderedContextAttribute: this.getContextAttribute(
parentData,
this.selectedNode,
patchedAttribute,
ContextGroup.ORDERED,
contextType,
),
measuredContextAttribute: this.getContextAttribute(
parentData,
this.selectedNode,
patchedAttribute,
ContextGroup.MEASURED,
contextType,
),
},
expanded: true,
} as TreeNode;
});
const tplCompRel = node.relations.find((r) => r.entityType === "TemplateComponent");
if (tplCompRel && tplCompRel.ids.length > 0) {
const nodes = this.getNodesWithTplCompId(children, tplCompRel.ids[0]);
for (const n of nodes) {
list.push(this.createTreeNode(n, children, contextType));
}
}
return list;
}
/**
* Searches for nodes which have a relation to the given TemplateComponent ID.
* @param nodes List with nodes to search
* @param tplCompId TemplateComponent ID
*/
getNodesWithTplCompId(nodes: Node[], tplCompId: string) {
return nodes.filter((n) => n.relations[0].parentId === tplCompId);
}
private extractMimeType(node: Node) {
if (node != undefined) {
const attr = node.attributes.find((attr) => attr.name === "MimeType");
if (attr) {
if (typeof attr.value === "string") {
return attr.value;
} else if (Array.isArray(attr.value) && attr.value.length === 1 && typeof attr.value[0] === "string") {
return attr.value[0];
} else if (Array.isArray(attr.value) && attr.value.length > 1 && typeof attr.value[1] === "string") {
return attr.value[1];
}
}
}
}
/**
* 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 (const 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], key));
}
}
}
return list;
}
/**
* Convert, complement or load attribute values
*
* @param attribute
*/
patchAttribute(node: Node, attribute: Attribute, contextType: string) {
if (attribute.dataType != null && attribute.dataType.length > 0) {
const val = attribute.value as any[];
if ("FILE_RELATION" === attribute.dataType) {
if (val.length > 0) {
this.contextFileService
.getMDMFileExts(this.getContextAttribute(node, this.selectedNode, attribute, ContextGroup.ORDERED, contextType))
.subscribe((mdmLinks) => (val[0] = mdmLinks));
}
if (val.length > 1) {
this.contextFileService
.getMDMFileExts(this.getContextAttribute(node, this.selectedNode, attribute, ContextGroup.MEASURED, contextType))
.subscribe((mdmLinks) => (val[1] = mdmLinks));
}
}
}
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
const 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
const startDay = sourceFormat.indexOf("d");
const endDay = sourceFormat.lastIndexOf("d");
const startMonth = sourceFormat.indexOf("M");
const endMonth = sourceFormat.lastIndexOf("M");
const startYear = sourceFormat.indexOf("y");
const 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) {
const 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);
if (data.ordered[type].length === 0 && data.measured[type].length === 0) {
this.treeNodes = this.mapComponents(this.contextComponents);
return;
}
// clear for status display
this.contextComponents = undefined;
this._contextService.putContext(node, data).subscribe(
(components) => {
this.setComponents(components);
},
(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.reloadData();
}
onSaveChanges(event: Event) {
event.stopPropagation();
this.editMode = false;
this.isTreeTableExpanded = true;
this.putContext(this.selectedNode, this.contextType);
this.tmpTreeNodes = undefined;
}
onAddTempComponent(event: Event) {
event.stopPropagation();
this.entityCreatorService.getTemplateTestStep(this.selectedNode).subscribe((r) => {
if (r.length > 0 && r[0].relations.filter((rel) => rel.contextType === this.contextType).length > 0) {
this.entityCreatorService.getTemplateComponents(r[0], this.contextType).subscribe((co) => {
this.setTplComponents(co);
this.showTplComponents = true;
});
} else {
this.notificationService.notifyInfo(
this.translateService.instant("details.mdm-detail-descriptive-data.info-no-tempComp"),
this.translateService.instant("details.mdm-detail-descriptive-data.msg-no_tplComp"),
);
}
});
}
setTplComponents(entities: MDMEntity[]) {
if (entities !== undefined) {
let cIds: string[] = [];
this.contextComponents[this.contextType].forEach((attr) => {
if (attr.attributes[0].value.length === 2 && attr.attributes[0].value[0] !== undefined) {
attr.relations.forEach((rel) => {
if (rel.contextType === this.contextType) {
cIds = cIds.concat(rel.ids);
}
});
}
});
this.tplComponents = [];
entities
.filter(
(ent) =>
cIds.filter((id) => id === ent.id)[0] === undefined &&
ent.attributes.filter((attr) => attr.name === "Optional" && attr.value === "true"),
)
.forEach((entity) => this.tplComponents.push(entity));
}
}
onSaveSelection(event: Event) {
event.stopPropagation();
const compNames: string[] = [];
this.selectedTplComponent.forEach((ent) => compNames.push(ent.name));
this._contextService
.addNewComponent(this.selectedNode.sourceName, this.contextType, this.selectedNode.id, compNames, this.selectedContext)
.subscribe((components) => this.setComponents(components));
this.selectedTplComponent = [];
this.showTplComponents = false;
}
onDeleteComponent(event: Event, name: string) {
event.stopPropagation();
this.hasOrdered = this.contextComponents[this.contextType].filter((cmp) => cmp.name === name)[0].attributes[0].value[0] !== undefined;
this.hasMeasured = this.contextComponents[this.contextType].filter((cmp) => cmp.name === name)[0].attributes[0].value.length === 2;
this.selectedContext = [];
this.selectedContextComponentName = name;
this.ctxCompName = { ctxName: name };
this.showConfirmDelete = true;
}
onNotDeleteSelection(event: Event) {
event.stopPropagation();
this.showConfirmDelete = false;
}
onDeleteSelection(event: Event) {
if (this.selectedContext?.length > 0) {
event.stopPropagation();
this._contextService
.deleteComponent(
this.selectedNode.sourceName,
this.contextType,
this.selectedNode.id,
this.selectedContextComponentName,
this.selectedContext,
)
.subscribe((components) => this.loadContext(this.selectedNode));
this.showConfirmDelete = false;
}
}
private setComponents(components: Components) {
if (components["UNITUNDERTEST"] != undefined || components["TESTEQUIPMENT"] != undefined || components["TESTSEQUENCE"] != undefined) {
this.contextComponents = components;
this.treeNodes = this.mapComponents(components);
} else {
this.status = this.StatusNoDescriptiveData;
}
}
/**
* 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) {
const list = [];
for (const component of contextComponents) {
const attrs = [];
for (const cAttribute of component.attributes) {
const 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";
}
// FILE_LINK_SEQUENCE is saved separately
if (attr.dataType === "FILE_LINK_SEQUENCE" || attr.dataType === "FILE_LINK" || attr.dataType === "FILE_RELATION") {
addAttr = false;
}
if (addAttr) {
if (this.isAttributeValueModified(attr, component, ordered)) {
attrs.push(attr);
}
}
}
// un-merged list
if (attrs.length > 0) {
const c = JSON.parse(JSON.stringify(component));
c.attributes = attrs;
list.push(c);
}
}
return list;
}
private isAttributeValueModified(attr: Attribute, component: Node, ordered: boolean) {
for (const tmpNode of this.tmpTreeNodes) {
if (tmpNode.name === component.name) {
for (const attribute of tmpNode.attributes) {
if (attribute.name === attr.name && attribute.value instanceof Array) {
const 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
********************** */
handleEditMode(node: Node) {
if (node != undefined) {
if (this.editAllowed[node.sourceName] != undefined) {
this.canEdit4Env = this.canEdit && this.editAllowed[node.sourceName];
} else {
this.systemParameterService.findSystemParameter(node.sourceName, "org.eclipse.mdm.detailview.editable").subscribe((values) => {
if (values.length === 1) {
this.editAllowed[node.sourceName] = values[0].attributes.find((attr) => attr.name === "Value").value === "false" ? false : true;
} else {
this.editAllowed[node.sourceName] = true;
}
this.canEdit4Env = this.canEdit && this.editAllowed[node.sourceName];
});
}
}
}
}