diff --git a/src/Eagle.ts b/src/Eagle.ts index 6dd94165a..6f0737667 100644 --- a/src/Eagle.ts +++ b/src/Eagle.ts @@ -104,7 +104,7 @@ export class Eagle { draggingNode : ko.Observable; draggingPaletteNode : boolean; - errorsMode : ko.Observable; + errorsMode : ko.Observable; graphWarnings : ko.ObservableArray; graphErrors : ko.ObservableArray; loadingWarnings : ko.ObservableArray; @@ -194,7 +194,7 @@ export class Eagle { this.isDragging = ko.observable(false); this.draggingNode = ko.observable(null); this.draggingPaletteNode = false; - this.errorsMode = ko.observable(Setting.ErrorsMode.Loading); + this.errorsMode = ko.observable(Errors.Mode.Loading); this.graphWarnings = ko.observableArray([]); this.graphErrors = ko.observableArray([]); this.loadingWarnings = ko.observableArray([]); @@ -652,6 +652,7 @@ export class Eagle { setSelection = (rightWindowMode : Eagle.RightWindowMode, selection : Node | Edge, selectedLocation: Eagle.FileType) : void => { Eagle.selectedLocation(selectedLocation); GraphRenderer.clearPortPeek() + if (selection === null){ this.selectedObjects([]); this.rightWindow().mode(rightWindowMode); @@ -947,7 +948,7 @@ export class Eagle { this.loadingErrors(errorsWarnings.errors); this.loadingWarnings(errorsWarnings.warnings); - this.errorsMode(Setting.ErrorsMode.Loading); + this.errorsMode(Errors.Mode.Loading); Utils.showErrorsModal("Loading File"); } } else { @@ -1873,7 +1874,7 @@ export class Eagle { this.loadingErrors(errorsWarnings.errors); this.loadingWarnings(errorsWarnings.warnings); - this.errorsMode(Setting.ErrorsMode.Loading); + this.errorsMode(Errors.Mode.Loading); Utils.showErrorsModal("Loading File"); } @@ -2852,8 +2853,8 @@ export class Eagle { } // validate edge - const isValid: Edge.Validity = Edge.isValid(this, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.isLoopAware(), edge.isClosesLoop(), false, true, null); - if (isValid === Edge.Validity.Impossible || isValid === Edge.Validity.Invalid || isValid === Edge.Validity.Unknown){ + const isValid: Errors.Validity = Edge.isValid(this, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.isLoopAware(), edge.isClosesLoop(), false, true, null); + if (isValid === Errors.Validity.Impossible || isValid === Errors.Validity.Error || isValid === Errors.Validity.Unknown){ Utils.showUserMessage("Error", "Invalid edge"); return; } @@ -2897,8 +2898,8 @@ export class Eagle { } // validate edge - const isValid: Edge.Validity = Edge.isValid(this, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.isLoopAware(), edge.isClosesLoop(), false, true, null); - if (isValid === Edge.Validity.Impossible || isValid === Edge.Validity.Invalid || isValid === Edge.Validity.Unknown){ + const isValid: Errors.Validity = Edge.isValid(this, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.isLoopAware(), edge.isClosesLoop(), false, true, null); + if (isValid === Errors.Validity.Impossible || isValid === Errors.Validity.Error || isValid === Errors.Validity.Unknown){ Utils.showUserMessage("Error", "Invalid edge"); return; } @@ -4564,17 +4565,18 @@ export class Eagle { } checkGraph = (): void => { - const checkResult = Utils.checkGraph(this); - - this.graphWarnings(checkResult.warnings); - this.graphErrors(checkResult.errors); + Utils.checkGraph(this);//validate the graph + const graphErrors = Utils.gatherGraphErrors() //gather all the errors from all of the components + + this.graphWarnings(graphErrors.warnings); + this.graphErrors(graphErrors.errors); }; showGraphErrors = (): void => { if (this.graphWarnings().length > 0 || this.graphErrors().length > 0){ // switch to graph errors mode - this.errorsMode(Setting.ErrorsMode.Graph); + this.errorsMode(Errors.Mode.Graph); // show graph modal this.smartToggleModal('errorsModal') diff --git a/src/Edge.ts b/src/Edge.ts index bcba7f167..a02c3525c 100644 --- a/src/Edge.ts +++ b/src/Edge.ts @@ -31,7 +31,6 @@ import { Field } from './Field'; import { Utils } from './Utils'; import { Errors } from './Errors'; import * as ko from "knockout"; -import { CategoryData } from './CategoryData'; export class Edge { private _id : string @@ -43,6 +42,7 @@ export class Edge { private closesLoop : boolean; // indicates that this is a special type of edge that can be drawn in eagle to specify the start/end of groups. private selectionRelative : boolean // indicates if the edge is either selected or attached to a selected node private isShortEdge : ko.Observable; + private issues : ko.ObservableArray<{issue:Errors.Issue, validity:Errors.Validity}> //keeps track of edge errors constructor(srcNodeKey : number, srcPortId : string, destNodeKey : number, destPortId : string, loopAware: boolean, closesLoop: boolean, selectionRelative : boolean){ this._id = Utils.uuidv4(); @@ -56,6 +56,7 @@ export class Edge { this.closesLoop = closesLoop; this.selectionRelative = selectionRelative; this.isShortEdge = ko.observable(false) + this.issues = ko.observableArray([]); } getId = () : string => { @@ -168,12 +169,23 @@ export class Edge { return result; } - getErrorsWarnings = (eagle: Eagle): Errors.ErrorsWarnings => { - const result: {warnings: Errors.Issue[], errors: Errors.Issue[]} = {warnings: [], errors: []}; + getErrorsWarnings = (): Errors.ErrorsWarnings => { + const errorsWarnings : Errors.ErrorsWarnings = {warnings: [], errors: []}; + + this.getIssues().forEach(function(error){ + if(error.validity === Errors.Validity.Error || error.validity === Errors.Validity.Unknown){ + errorsWarnings.errors.push(error.issue) + }else{ + errorsWarnings.warnings.push(error.issue) + } + }) - Edge.isValid(eagle, this._id, this.srcNodeKey, this.srcPortId, this.destNodeKey, this.destPortId, this.loopAware, this.closesLoop, false, false, result); + return errorsWarnings; - return result; + } + + getIssues = () : {issue:Errors.Issue, validity:Errors.Validity}[] => { + return this.issues(); } static toOJSJson(edge : Edge) : object { @@ -277,68 +289,68 @@ export class Edge { return new Edge(edgeData.from, edgeData.fromPort, edgeData.to, edgeData.toPort, edgeData.loopAware, edgeData.closesLoop, false); } - static isValid(eagle: Eagle, edgeId: string, sourceNodeKey : number, sourcePortId : string, destinationNodeKey : number, destinationPortId : string, loopAware: boolean, closesLoop: boolean, showNotification : boolean, showConsole : boolean, errorsWarnings: Errors.ErrorsWarnings) : Edge.Validity { + static isValid(eagle: Eagle, edgeId: string, sourceNodeKey : number, sourcePortId : string, destinationNodeKey : number, destinationPortId : string, loopAware: boolean, closesLoop: boolean, showNotification : boolean, showConsole : boolean, errorsWarnings: Errors.ErrorsWarnings) : Errors.Validity { + let impossibleEdge : boolean = false; + const edge = eagle.logicalGraph().findEdgeById(edgeId) + if(edge){ + edge.issues([]) //clear old issues + } + // check for problems if (isNaN(sourceNodeKey)){ - return Edge.Validity.Unknown; + return Errors.Validity.Unknown; } if (isNaN(destinationNodeKey)){ - return Edge.Validity.Unknown; + return Errors.Validity.Unknown; } if (sourcePortId === ""){ const issue = Errors.Fix("source port has no id", function(){Utils.fixNodeFieldIds(eagle, sourceNodeKey)}, "Generate ids for ports on source node"); - Edge.isValidLog(edgeId, Edge.Validity.Invalid, issue, showNotification, showConsole, errorsWarnings); - return Edge.Validity.Invalid; + Edge.isValidLog(edge, Errors.Validity.Impossible, issue, showNotification, showConsole, errorsWarnings); + return Errors.Validity.Impossible; } if (destinationPortId === ""){ const issue = Errors.Fix("destination port has no id", function(){Utils.fixNodeFieldIds(eagle, sourceNodeKey)}, "Generate ids for ports on destination node"); - Edge.isValidLog(edgeId, Edge.Validity.Invalid, issue, showNotification, showConsole, errorsWarnings); - return Edge.Validity.Invalid; + Edge.isValidLog(edge, Errors.Validity.Impossible, issue, showNotification, showConsole, errorsWarnings); + return Errors.Validity.Impossible; } if (sourcePortId === null){ const issue = Errors.Fix("source port id is null", function(){Utils.fixNodeFieldIds(eagle, sourceNodeKey)}, "Generate ids for ports on source node"); - Edge.isValidLog(edgeId, Edge.Validity.Invalid, issue, showNotification, showConsole, errorsWarnings); - return Edge.Validity.Invalid; + Edge.isValidLog(edge, Errors.Validity.Impossible, issue, showNotification, showConsole, errorsWarnings); + return Errors.Validity.Impossible; } if (destinationPortId === null){ const issue = Errors.Fix("destination port id is null", function(){Utils.fixNodeFieldIds(eagle, sourceNodeKey)}, "Generate ids for ports on destination node"); - Edge.isValidLog(edgeId, Edge.Validity.Invalid, issue, showNotification, showConsole, errorsWarnings); - return Edge.Validity.Invalid; + Edge.isValidLog(edge, Errors.Validity.Impossible, issue, showNotification, showConsole, errorsWarnings); + return Errors.Validity.Impossible; } // get references to actual source and destination nodes (from the keys) const sourceNode : Node = eagle.logicalGraph().findNodeByKey(sourceNodeKey); const destinationNode : Node = eagle.logicalGraph().findNodeByKey(destinationNodeKey); - // check that we are not connecting two ports within the same node - if (sourceNodeKey === destinationNodeKey){ - Edge.isValidLog(edgeId, Edge.Validity.Impossible, Errors.Show("sourceNodeKey and destinationNodeKey are the same", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); - return Edge.Validity.Impossible; - } - if (sourceNode === null || typeof sourceNode === "undefined" || destinationNode === null || typeof destinationNode === "undefined"){ - return Edge.Validity.Unknown; + return Errors.Validity.Unknown; } // check that we are not connecting a Data component to a Data component, that is not supported if (sourceNode.getCategoryType() === Category.Type.Data && destinationNode.getCategoryType() === Category.Type.Data){ - Edge.isValidLog(edgeId, Edge.Validity.Invalid, Errors.Show("Data nodes may not be connected directly to other Data nodes", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); - return Edge.Validity.Invalid; + Edge.isValidLog(edge, Errors.Validity.Error, Errors.Show("Data nodes may not be connected directly to other Data nodes", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); } // if source node or destination node is a construct, then something is wrong, constructs should not have ports if (sourceNode.getCategoryType() === Category.Type.Construct){ const issue: Errors.Issue = Errors.ShowFix("Edge (" + edgeId + ") cannot have a source node (" + sourceNode.getName() + ") that is a construct", function(){Utils.showEdge(eagle, edgeId)}, function(){Utils.fixMoveEdgeToEmbeddedApplication(eagle, edgeId)}, "Move edge to embedded application"); - Edge.isValidLog(edgeId, Edge.Validity.Invalid, issue, showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edge, Errors.Validity.Error, issue, showNotification, showConsole, errorsWarnings); } + if (destinationNode.getCategoryType() === Category.Type.Construct){ const issue: Errors.Issue = Errors.ShowFix("Edge (" + edgeId + ") cannot have a destination node (" + destinationNode.getName() + ") that is a construct", function(){Utils.showEdge(eagle, edgeId)}, function(){Utils.fixMoveEdgeToEmbeddedApplication(eagle, edgeId)}, "Move edge to embedded application"); - Edge.isValidLog(edgeId, Edge.Validity.Invalid, issue, showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edge, Errors.Validity.Error, issue, showNotification, showConsole, errorsWarnings); } // if source node is a memory, and destination is a BashShellApp, OR @@ -347,7 +359,7 @@ export class Edge { if ((sourceNode.getCategory() === Category.Memory && destinationNode.getCategory() === Category.BashShellApp) || (sourceNode.getCategory() === Category.Memory && destinationNode.isGroup() && destinationNode.getInputApplication() !== undefined && destinationNode.hasInputApplication() && destinationNode.getInputApplication().getCategory() === Category.BashShellApp)){ const issue: Errors.Issue = Errors.ShowFix("output from Memory Node cannot be input into a BashShellApp or input into a Group Node with a BashShellApp inputApplicationType", function(){Utils.showNode(eagle, sourceNode.getId())}, function(){Utils.fixNodeCategory(eagle, sourceNode, Category.File, Category.Type.Data)}, "Change data component type to File"); - Edge.isValidLog(edgeId, Edge.Validity.Invalid, issue, showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edge, Errors.Validity.Error, issue, showNotification, showConsole, errorsWarnings); } const sourcePort : Field = sourceNode.findFieldById(sourcePortId); @@ -356,41 +368,41 @@ export class Edge { // check if source port was found if (sourcePort === null) { const issue: Errors.Issue = Errors.ShowFix("Source port (" + sourcePortId + ") doesn't exist on source node (" + sourceNode.getName() + ")", function(){Utils.showEdge(eagle, edgeId)}, function(){Utils.addSourcePortToSourceNode(eagle, edgeId)}, "Add source port to source node"); - Edge.isValidLog(edgeId, Edge.Validity.Impossible, issue, showNotification, showConsole, errorsWarnings); - return Edge.Validity.Impossible; + Edge.isValidLog(edge, Errors.Validity.Impossible, issue, showNotification, showConsole, errorsWarnings); + impossibleEdge = true; } // check if destination port was found if (destinationPort === null){ const issue: Errors.Issue = Errors.ShowFix("Destination port (" + destinationPortId + ") doesn't exist on destination node (" + destinationNode.getName() + ")", function(){Utils.showEdge(eagle, edgeId)}, function(){Utils.addDestinationPortToDestinationNode(eagle, edgeId)}, "Add destination port to destination node"); - Edge.isValidLog(edgeId, Edge.Validity.Impossible, issue, showNotification, showConsole, errorsWarnings); - return Edge.Validity.Impossible; + Edge.isValidLog(edge, Errors.Validity.Impossible, issue, showNotification, showConsole, errorsWarnings); + impossibleEdge = true; } // check that we are not connecting a port to itself - if (sourcePortId === destinationPortId){ - Edge.isValidLog(edgeId, Edge.Validity.Impossible, Errors.Show("Source port and destination port are the same", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); - return Edge.Validity.Impossible; + if (sourceNodeKey === destinationNodeKey){ + Edge.isValidLog(edge, Errors.Validity.Impossible, Errors.Show("Source port and destination port are the same", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); + impossibleEdge = true; } // check that source is output if (!sourcePort.isOutputPort()){ const issue: Errors.Issue = Errors.ShowFix("Source port is not output port (" + sourcePort.getUsage() + ")", function(){Utils.showEdge(eagle, edgeId);}, function(){Utils.fixFieldUsage(eagle, sourcePort, Daliuge.FieldUsage.OutputPort)}, "Add output usage to source port"); - Edge.isValidLog(edgeId, Edge.Validity.Impossible, issue, showNotification, showConsole, errorsWarnings); - return Edge.Validity.Impossible; + Edge.isValidLog(edge, Errors.Validity.Impossible, issue, showNotification, showConsole, errorsWarnings); + impossibleEdge = true; } // check that destination in input if (!destinationPort.isInputPort()){ const issue: Errors.Issue = Errors.ShowFix("Destination port is not input port (" + destinationPort.getUsage() + ")", function(){Utils.showEdge(eagle, edgeId);}, function(){Utils.fixFieldUsage(eagle, destinationPort, Daliuge.FieldUsage.InputPort)}, "Add input usage to destination port"); - Edge.isValidLog(edgeId, Edge.Validity.Impossible, issue, showNotification, showConsole, errorsWarnings); - return Edge.Validity.Impossible; + Edge.isValidLog(edge, Errors.Validity.Impossible, issue, showNotification, showConsole, errorsWarnings); + impossibleEdge = true; } if (sourcePort !== null && destinationPort !== null){ // check that source and destination port are both event, or both not event if ((sourcePort.getIsEvent() && !destinationPort.getIsEvent()) || (!sourcePort.getIsEvent() && destinationPort.getIsEvent())){ - Edge.isValidLog(edgeId, Edge.Validity.Invalid, Errors.Show("Source port and destination port are mix of event and non-event ports", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edge, Errors.Validity.Error, Errors.Show("Source port and destination port are mix of event and non-event ports", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); } } @@ -420,7 +432,7 @@ export class Edge { // abort if source port and destination port have different data types if (!Utils.portsMatch(sourcePort, destinationPort)){ const x = Errors.ShowFix("Source and destination ports don't match data types: sourcePort (" + sourcePort.getDisplayText() + ":" + sourcePort.getType() + ") destinationPort (" + destinationPort.getDisplayText() + ":" + destinationPort.getType() + ")", function(){Utils.showEdge(eagle, edgeId);}, function(){Utils.fixPortType(eagle, sourcePort, destinationPort);}, "Overwrite destination port type with source port type"); - Edge.isValidLog(edgeId, Edge.Validity.Invalid, x, showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edge, Errors.Validity.Error, x, showNotification, showConsole, errorsWarnings); } } @@ -433,12 +445,12 @@ export class Edge { || associatedConstructType !== Category.Loop && loopAware ){ const x = Errors.ShowFix("An edge between two siblings should not be loop aware", function(){Utils.showEdge(eagle, edgeId);}, function(){Utils.fixDisableEdgeLoopAware(eagle, edgeId);}, "Disable loop aware on the edge."); - Edge.isValidLog(edgeId, Edge.Validity.Warning, x, showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edge, Errors.Validity.Warning, x, showNotification, showConsole, errorsWarnings); } // if link is not a parent, child or sibling, then warn user if (associatedConstructType !== Category.ExclusiveForceNode && associatedConstructType !== Category.Loop && !isSibling && !isParentOfConstruct && !isChildOfConstruct && !destPortIsEmbeddedAppOfSibling && !srcPortIsEmbeddedAppOfSibling){ - Edge.isValidLog(edgeId, Edge.Validity.Warning, Errors.Show("Edge is not between siblings, or between a child and its parent's embedded Application. It could be incorrect or computationally expensive", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edge, Errors.Validity.Warning, Errors.Show("Edge is not between siblings, or between a child and its parent's embedded Application. It could be incorrect or computationally expensive", function(){Utils.showEdge(eagle, edgeId);}), showNotification, showConsole, errorsWarnings); } // check if the edge already exists in the graph, there is no point in a duplicate @@ -448,7 +460,7 @@ export class Edge { if ( isSrcMatch && isDestMatch && edge.getId() !== edgeId){ const x = Errors.ShowFix("Edge is a duplicate. Another edge with the same source port and destination port already exists", function(){Utils.showEdge(eagle, edgeId);}, function(){Utils.fixDeleteEdge(eagle, edgeId);}, "Delete edge"); - Edge.isValidLog(edgeId, Edge.Validity.Invalid, x, showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edge, Errors.Validity.Error, x, showNotification, showConsole, errorsWarnings); } } @@ -460,52 +472,57 @@ export class Edge { if (closesLoop){ if (!sourceNode.isData()){ const x = Errors.Show("Closes Loop Edge (" + edgeId + ") does not start from a Data component.", function(){Utils.showEdge(eagle, edgeId);}); - Edge.isValidLog(edgeId, Edge.Validity.Invalid, x, showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edge, Errors.Validity.Error, x, showNotification, showConsole, errorsWarnings); } if (!destinationNode.isApplication()){ const x = Errors.Show("Closes Loop Edge (" + edgeId + ") does not end at an Application component.", function(){Utils.showEdge(eagle, edgeId);}); - Edge.isValidLog(edgeId, Edge.Validity.Invalid, x, showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edge, Errors.Validity.Error, x, showNotification, showConsole, errorsWarnings); } if (!sourceNode.hasFieldWithDisplayText(Daliuge.FieldName.GROUP_END) || !Utils.asBool(sourceNode.getFieldByDisplayText(Daliuge.FieldName.GROUP_END).getValue())){ const x = Errors.ShowFix("'Closes Loop' Edge (" + edgeId + ") start node (" + sourceNode.getName() + ") does not have 'group_end' set to true.", function(){Utils.showEdge(eagle, edgeId);}, function(){Utils.fixFieldValue(eagle, sourceNode, Daliuge.groupEndField, "true")}, "Set 'group_end' to true"); - Edge.isValidLog(edgeId, Edge.Validity.Invalid, x, showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edge, Errors.Validity.Error, x, showNotification, showConsole, errorsWarnings); } if (!destinationNode.hasFieldWithDisplayText(Daliuge.FieldName.GROUP_START) || !Utils.asBool(destinationNode.getFieldByDisplayText(Daliuge.FieldName.GROUP_START).getValue())){ const x = Errors.ShowFix("'Closes Loop' Edge (" + edgeId + ") end node (" + destinationNode.getName() + ") does not have 'group_start' set to true.", function(){Utils.showEdge(eagle, edgeId);}, function(){Utils.fixFieldValue(eagle, destinationNode, Daliuge.groupStartField, "true")}, "Set 'group_start' to true"); - Edge.isValidLog(edgeId, Edge.Validity.Invalid, x, showNotification, showConsole, errorsWarnings); + Edge.isValidLog(edge, Errors.Validity.Error, x, showNotification, showConsole, errorsWarnings); } } - return Utils.worstEdgeError(errorsWarnings); + //the worst edge errror function can only check for entries in errors or warnings, it isnt able to distinguish impossible from invalid + if(impossibleEdge){ + return Errors.Validity.Impossible + }else{ + return Utils.worstEdgeError(errorsWarnings); + } } - private static isValidLog(edgeId : string, linkValid : Edge.Validity, issue: Errors.Issue, showNotification : boolean, showConsole : boolean, errorsWarnings: Errors.ErrorsWarnings) : void { + private static isValidLog(edge : Edge, linkValid : Errors.Validity, issue: Errors.Issue, showNotification : boolean, showConsole : boolean, errorsWarnings: Errors.ErrorsWarnings) : void { // determine correct title let title = "Edge Valid"; let type : "success" | "info" | "warning" | "danger" = "success"; let message = ""; switch (linkValid){ - case Edge.Validity.Warning: + case Errors.Validity.Warning: title = "Edge Warning"; type = "warning"; break; - case Edge.Validity.Impossible: + case Errors.Validity.Impossible: title = "Edge Impossible"; type = "danger"; break; - case Edge.Validity.Invalid: + case Errors.Validity.Error: title = "Edge Invalid"; type = "danger"; break; } // add edge id to message, if id is known - if (edgeId !== null){ - message = "Edge (" + edgeId + ") " + issue.message; + if (edge !== null){ + message = "Edge (" + edge.getId() + ") " + issue.message; } else { message = issue.message; } @@ -521,15 +538,6 @@ export class Edge { if (type === "warning" && errorsWarnings !== null){ errorsWarnings.warnings.push(issue); } - } -} - -export namespace Edge { - export enum Validity { - Unknown = "Unknown", // validity of the edge is unknown - Impossible = "Impossible", // never useful or valid - Invalid = "Invalid", // invalid, but possibly useful for expert users? - Warning = "Warning", // valid, but some issue that the user should be aware of - Valid = "Valid" // fine + edge.issues().push({issue:issue, validity:linkValid}) } } diff --git a/src/Errors.ts b/src/Errors.ts index 45851b8c4..3833ce668 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -1,7 +1,6 @@ import * as ko from "knockout"; import {Eagle} from './Eagle'; -import {Setting} from './Setting'; import {Utils} from './Utils'; export class Errors { @@ -69,9 +68,9 @@ export class Errors { const eagle: Eagle = Eagle.getInstance(); switch (eagle.errorsMode()){ - case Setting.ErrorsMode.Loading: + case Errors.Mode.Loading: return eagle.loadingWarnings(); - case Setting.ErrorsMode.Graph: + case Errors.Mode.Graph: return eagle.graphWarnings(); default: console.warn("Unknown errorsMode (" + eagle.errorsMode() + "). Unable to getWarnings()"); @@ -83,9 +82,9 @@ export class Errors { const eagle: Eagle = Eagle.getInstance(); switch (eagle.errorsMode()){ - case Setting.ErrorsMode.Loading: + case Errors.Mode.Loading: return eagle.loadingErrors(); - case Setting.ErrorsMode.Graph: + case Errors.Mode.Graph: return eagle.graphErrors(); default: console.warn("Unknown errorsMode (" + eagle.errorsMode() + "). Unable to getErrors()"); @@ -120,4 +119,22 @@ export namespace Errors { export type Issue = {message: string, show: () => void, fix: () => void, fixDescription: string}; export type ErrorsWarnings = {warnings: Issue[], errors: Issue[]}; + + export enum Validity { + Unknown = "Unknown", // validity of the edge is unknown + Impossible = "Impossible", // never useful or valid + Error = "Error", // invalid, but possibly useful for expert users? change to error + Warning = "Warning", // valid, but some issue that the user should be aware of + Fixable = "Fixable", // there is an issue with the connection but for drawing edges eagle will fix this for you + Valid = "Valid" // fine + } + + export enum Mode { + Loading = "Loading", + Graph = "Graph" + } } + + +//change data structure of each nodes, fields and edges and new logicalgraph is valid function to be {issue, validity}[] +// move to errors, use for nodes,fields and edges diff --git a/src/Field.ts b/src/Field.ts index 1c73e9121..cd69e57d0 100644 --- a/src/Field.ts +++ b/src/Field.ts @@ -41,7 +41,7 @@ export class Field { private inputAngle : number; private outputAngle : number; - private errorsWarnings : ko.Observable; + private issues : ko.ObservableArray<{issue:Errors.Issue, validity:Errors.Validity}>//keeps track of issues on the field constructor(id: string, displayText: string, value: string, defaultValue: string, description: string, readonly: boolean, type: string, precious: boolean, options: string[], positional: boolean, parameterType: Daliuge.FieldType, usage: Daliuge.FieldUsage, keyAttribute: boolean){ this.displayText = ko.observable(displayText); @@ -73,7 +73,7 @@ export class Field { this.inputAngle = 0; this.outputAngle = 0; - this.errorsWarnings = ko.observable({warnings: [], errors: []}); + this.issues = ko.observableArray([]) } getId = () : string => { @@ -344,22 +344,34 @@ export class Field { return this.nodeKey(); } - getErrorsWarnings = (): Errors.ErrorsWarnings => { - return this.errorsWarnings(); + getErrorsWarnings : ko.PureComputed = ko.pureComputed(() => { + const errorsWarnings : Errors.ErrorsWarnings = {warnings: [], errors: []}; + + this.getIssues().forEach(function(error){ + if(error.validity === Errors.Validity.Error || error.validity === Errors.Validity.Unknown){ + errorsWarnings.errors.push(error.issue) + }else{ + errorsWarnings.warnings.push(error.issue) + } + }) + + return errorsWarnings; + }, this); + + getIssues = (): {issue:Errors.Issue, validity:Errors.Validity}[] => { + return this.issues(); } - addErrorsWarnings(issue:Errors.Issue, issueType:string) : void { - if(issueType === 'error'){ - this.errorsWarnings().errors.push(issue) - }else{ - this.errorsWarnings().warnings.push(issue) - } + addError(issue:Errors.Issue, validity:Errors.Validity){ + this.issues().push({issue:issue,validity:validity}) } getBackgroundColor : ko.PureComputed = ko.pureComputed(() => { - if(this.errorsWarnings().errors.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) != Setting.ShowErrorsMode.None){ + const errorsWarnings = this.getErrorsWarnings() + + if(errorsWarnings.errors.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) != Setting.ShowErrorsMode.None){ return '#ea2727' - }else if(this.errorsWarnings().warnings.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) === Setting.ShowErrorsMode.Warnings){ + }else if(errorsWarnings.warnings.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) === Setting.ShowErrorsMode.Warnings){ return '#ffa500' }else{ return '' @@ -367,11 +379,13 @@ export class Field { }, this); getHasErrors = () : boolean => { - return this.errorsWarnings().errors.length>0; + const errorsWarnings = this.getErrorsWarnings() + return errorsWarnings.errors.length>0; } getHasOnlyWarnings = () : boolean => { - return this.errorsWarnings().warnings.length>0 && this.errorsWarnings().errors.length === 0; + const errorsWarnings = this.getErrorsWarnings() + return errorsWarnings.warnings.length>0 && errorsWarnings.errors.length === 0; } setNodeKey = (key : number) : void => { @@ -803,7 +817,7 @@ export class Field { static isValid(node:Node, field:Field, selectedLocation:Eagle.FileType, fieldIndex:number){ const eagle = Eagle.getInstance() - const errorsWarnings : Errors.ErrorsWarnings = {warnings: [], errors: []}; + field.issues([]) //clear old issues //checks for input ports if(field.isInputPort()){ @@ -827,7 +841,7 @@ export class Field { issue = Errors.ShowFix("Node " + node.getKey() + " (" + parentNode.getName() + ") has output application (" + node.getName() + ") with input port (" + field.getDisplayText() + ") whose type is not specified", function(){Utils.showField(eagle, node.getId(),field);}, function(){Utils.fixFieldType(eagle, field)}, ""); } } - errorsWarnings.warnings.push(issue); + field.issues().push({issue:issue,validity:Errors.Validity.Warning}) } @@ -855,7 +869,7 @@ export class Field { issue = Errors.ShowFix("Node " + node.getKey() + " (" + parentNode.getName() + ") has output application (" + node.getName() + ") with output port (" + field.getDisplayText() + ") whose type is not specified", function(){Utils.showField(eagle, node.getId(),field);}, function(){Utils.fixFieldType(eagle, field)}, ""); } } - errorsWarnings.warnings.push(issue); + field.issues().push({issue:issue,validity:Errors.Validity.Warning}) } @@ -864,25 +878,25 @@ export class Field { //check that the field has an id if (field.getId() === "" || field.getId() === null){ const issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has field (" + field.getDisplayText() + ") with no id", function(){Utils.showField(eagle, node.getId(),field);}, function(){Utils.fixFieldId(eagle, field)}, "Generate id for field"); - errorsWarnings.errors.push(issue); + field.issues().push({issue:issue,validity:Errors.Validity.Error}) } // check that the field has a default value if (field.getDefaultValue() === "" && !field.isType(Daliuge.DataType.String) && !field.isType(Daliuge.DataType.Password) && !field.isType(Daliuge.DataType.Object) && !field.isType(Daliuge.DataType.Unknown)) { const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has a component parameter (" + field.getDisplayText() + ") whose default value is not specified", function(){Utils.showField(eagle, node.getId(),field)}, function(){Utils.fixFieldDefaultValue(eagle, field)}, "Generate default value for parameter"); - errorsWarnings.warnings.push(issue); + field.issues().push({issue:issue,validity:Errors.Validity.Warning}) } // check that the field has a known type if (!Utils.validateType(field.getType())) { const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has a component parameter (" + field.getDisplayText() + ") whose type (" + field.getType() + ") is unknown", function(){Utils.showField(eagle, node.getId(),field)}, function(){Utils.fixFieldType(eagle, field)}, "Prepend existing type (" + field.getType() + ") with 'Object.'"); - errorsWarnings.warnings.push(issue); + field.issues().push({issue:issue,validity:Errors.Validity.Warning}) } // check that the fields "key" is the same as the key of the node it belongs to if (field.getNodeKey() !== node.getKey()) { const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has a field (" + field.getDisplayText() + ") whose key (" + field.getNodeKey() + ") doesn't match the node (" + node.getKey() + ")", function(){Utils.showField(eagle, node.getId(),field)}, function(){Utils.fixFieldKey(eagle, node, field)}, "Set field node key correctly"); - errorsWarnings.errors.push(issue); + field.issues().push({issue:issue,validity:Errors.Validity.Error}) } // check that the field has a unique display text on the node @@ -895,10 +909,12 @@ export class Field { if (field.getDisplayText() === field1.getDisplayText() && field.getParameterType() === field1.getParameterType()){ if (field.getId() === field1.getId()){ const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has multiple attributes with the same display text and id (" + field.getDisplayText() + ").", function(){Utils.showField(eagle, node.getId(),field);}, function(){Utils.fixNodeMergeFieldsByIndex(eagle, node, fieldIndex, j)}, "Merge fields"); - errorsWarnings.warnings.push(issue); + field.issues().push({issue:issue,validity:Errors.Validity.Warning}) + // errorsWarnings.warnings.push(issue); } else { const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has multiple attributes with the same display text (" + field.getDisplayText() + ").", function(){Utils.showField(eagle, node.getId(),field);}, function(){Utils.fixNodeMergeFields(eagle, node, field, field1)}, "Merge fields"); - errorsWarnings.warnings.push(issue); + field.issues().push({issue:issue,validity:Errors.Validity.Warning}) + // errorsWarnings.warnings.push(issue); } } } @@ -913,7 +929,8 @@ export class Field { } if (numSelfPortConnections > 1){ - errorsWarnings.errors.push(Errors.Message("Port " + field.getDisplayText() + " on node " + node.getName() + " cannot have multiple inputs.")); + const issue: Errors.Issue = Errors.Message("Port " + field.getDisplayText() + " on node " + node.getName() + " cannot have multiple inputs.") + field.issues().push({issue:issue,validity:Errors.Validity.Error}) } } @@ -943,13 +960,9 @@ export class Field { const message = "Node " + node.getKey() + " (" + node.getName() + ") with category " + node.getCategory() + " contains field (" + field.getDisplayText() + ") with unsuitable type (" + field.getParameterType() + ")."; const issue: Errors.Issue = Errors.ShowFix(message, function(){Utils.showField(eagle, node.getId(),field);}, function(){Utils.fixFieldParameterType(eagle, node, field, suitableType)}, "Switch to suitable type, or remove if no suitable type"); - errorsWarnings.warnings.push(issue); + field.issues().push({issue:issue,validity:Errors.Validity.Warning}) } } - - field.errorsWarnings(errorsWarnings) - - return errorsWarnings } public static sortFunc(a: Field, b: Field) : number { diff --git a/src/GraphRenderer.ts b/src/GraphRenderer.ts index 7d544590c..79a22b92c 100644 --- a/src/GraphRenderer.ts +++ b/src/GraphRenderer.ts @@ -27,13 +27,13 @@ import * as ko from "knockout"; import { Category } from './Category'; import { Daliuge } from "./Daliuge"; import { Eagle } from './Eagle'; +import { Errors } from './Errors'; import { EagleConfig } from "./EagleConfig"; import { Edge } from "./Edge"; import { Field } from './Field'; import { LogicalGraph } from './LogicalGraph'; import { Node } from './Node'; import { Utils } from './Utils'; -import { CategoryData} from './CategoryData'; import { Setting } from './Setting'; import { RightClick } from "./RightClick"; @@ -287,7 +287,7 @@ export class GraphRenderer { //port drag handler globals static draggingPort : boolean = false; - static isDraggingPortValid: ko.Observable = ko.observable(Edge.Validity.Unknown); + static isDraggingPortValid: ko.Observable = ko.observable(Errors.Validity.Unknown); static destinationNode : Node = null; static destinationPort : Field = null; @@ -1107,7 +1107,6 @@ export class GraphRenderer { const oldParent: Node = eagle.logicalGraph().findNodeByKeyQuiet(outerMostNode.getParentKey()); // keep track of whether we would update any node parents - const updated = {parent: false}; const allowGraphEditing = Setting.findValue(Setting.ALLOW_GRAPH_EDITING); //construct resizing if(outerMostNode.getParentKey() != null){ @@ -1125,13 +1124,13 @@ export class GraphRenderer { // if a parent was found, update if (parent !== null && outerMostNode.getParentKey() !== parent.getKey() && outerMostNode.getKey() !== parent.getKey() && !ancestorOfParent && !outerMostNode.isEmbedded()){ - GraphRenderer._updateNodeParent(outerMostNode, parent.getKey(), updated, allowGraphEditing); + GraphRenderer.updateNodeParent(outerMostNode, parent.getKey(), allowGraphEditing); GraphRenderer.NodeParentRadiusPreDrag = eagle.logicalGraph().findNodeByKeyQuiet(parent.getKey()).getRadius() } // if no parent found, update if (parent === null && outerMostNode.getParentKey() !== null && !outerMostNode.isEmbedded()){ - GraphRenderer._updateNodeParent(outerMostNode, null, updated, allowGraphEditing); + GraphRenderer.updateNodeParent(outerMostNode, null, allowGraphEditing); } if (oldParent !== null){ @@ -1579,14 +1578,11 @@ export class GraphRenderer { } // update the parent of the given node - // however, if allGraphEditing is false, then don't update - // always keep track of whether an update would have happened, sp we can warn user - static _updateNodeParent(node: Node, parentKey: number, updated: {parent: boolean}, allowGraphEditing: boolean): void { - if (node.getParentKey() !== parentKey){ - if (allowGraphEditing){ - node.setParentKey(parentKey); - } - updated.parent = true; + // however, if allowGraphEditing is false, then don't update + static updateNodeParent(node: Node, parentKey: number, allowGraphEditing: boolean): void { + if (node.getParentKey() !== parentKey && allowGraphEditing){ + node.setParentKey(parentKey); + Eagle.getInstance().checkGraph() } } @@ -1647,6 +1643,7 @@ export class GraphRenderer { //setting up the port event listeners $('#logicalGraphParent').on('mouseup.portDrag',function(){GraphRenderer.portDragEnd()}) $('.node .body').on('mouseup.portDrag',function(){GraphRenderer.portDragEnd()}) + port.setPeek(true) // build the list of all ports in the graph that are a valid end-point for an edge starting at this port GraphRenderer.matchingPortList = GraphRenderer.findMatchingPorts(GraphRenderer.portDragSourceNode(), GraphRenderer.portDragSourcePort()); @@ -1670,7 +1667,6 @@ export class GraphRenderer { static portDragEnd() : void { const eagle = Eagle.getInstance(); - GraphRenderer.draggingPort = false; // cleaning up the port drag event listeners $('#logicalGraphParent').off('mouseup.portDrag') @@ -1719,6 +1715,7 @@ export class GraphRenderer { GraphRenderer.clearEdgeVars(); } } + GraphRenderer.portDragSourcePort().setPeek(false) } //resetting some global cached variables @@ -1747,11 +1744,11 @@ export class GraphRenderer { } // check if link is valid - const linkValid : Edge.Validity = Edge.isValid(eagle, null, realSourceNode.getKey(), realSourcePort.getId(), realDestinationNode.getKey(), realDestinationPort.getId(), false, false, true, true, {errors:[], warnings:[]}); + const linkValid : Errors.Validity = Edge.isValid(eagle, null, realSourceNode.getKey(), realSourcePort.getId(), realDestinationNode.getKey(), realDestinationPort.getId(), false, false, true, true, {errors:[], warnings:[]}); // abort if edge is invalid - if ((Setting.findValue(Setting.ALLOW_INVALID_EDGES) && linkValid === Edge.Validity.Invalid) || linkValid === Edge.Validity.Valid || linkValid === Edge.Validity.Warning){ - if (linkValid === Edge.Validity.Warning){ + if ((Setting.findValue(Setting.ALLOW_INVALID_EDGES) && linkValid === Errors.Validity.Error) || linkValid === Errors.Validity.Valid || linkValid === Errors.Validity.Warning){ + if (linkValid === Errors.Validity.Warning){ GraphRenderer.addEdge(realSourceNode, realSourcePort, realDestinationNode, realDestinationPort, true, false); } else { GraphRenderer.addEdge(realSourceNode, realSourcePort, realDestinationNode, realDestinationPort, false, false); @@ -2006,16 +2003,34 @@ export class GraphRenderer { const eagle = Eagle.getInstance(); const result: {node: Node, field: Field}[] = []; - const minValidity: Edge.Validity = Setting.findValue(Setting.AUTO_COMPLETE_EDGES_LEVEL); - const minValidityIndex: number = Object.values(Edge.Validity).indexOf(minValidity); + const minValidity: Errors.Validity = Setting.findValue(Setting.AUTO_COMPLETE_EDGES_LEVEL); + const minValidityIndex: number = Object.values(Errors.Validity).indexOf(minValidity); + + const potentialNodes :Node[] = [] for (const node of eagle.logicalGraph().getNodes()){ + potentialNodes.push(node) + if(node.isConstruct && node.getInputApplication()){ + potentialNodes.push(node.getInputApplication()) + } + if(node.isConstruct && node.getOutputApplication()){ + potentialNodes.push(node.getOutputApplication()) + } + } + + for(const node of potentialNodes){ for (const port of node.getPorts()){ - const isValid: Edge.Validity = Edge.isValid(eagle, "", sourceNode.getKey(), sourcePort.getId(), node.getKey(), port.getId(), false, false, false, false, {errors:[], warnings:[]}); - const isValidIndex: number = Object.values(Edge.Validity).indexOf(isValid); + let isValid: Errors.Validity + if(!GraphRenderer.portDragSourcePortIsInput){ + isValid = Edge.isValid(eagle, "", sourceNode.getKey(), sourcePort.getId(), node.getKey(), port.getId(), false, false, false, false, {errors:[], warnings:[]}); + }else{ + isValid = Edge.isValid(eagle, "", node.getKey(), port.getId(), sourceNode.getKey(), sourcePort.getId(), false, false, false, false, {errors:[], warnings:[]}); + } + const isValidIndex: number = Object.values(Errors.Validity).indexOf(isValid); if (isValidIndex >= minValidityIndex){ result.push({node: node, field: port}); + port.setPeek(true) } } } @@ -2085,19 +2100,19 @@ export class GraphRenderer { GraphRenderer.destinationPort = null; GraphRenderer.destinationNode = null; - GraphRenderer.isDraggingPortValid(Edge.Validity.Unknown); + GraphRenderer.isDraggingPortValid(Errors.Validity.Unknown); } static draggingEdgeGetStrokeColor: ko.PureComputed = ko.pureComputed(() => { switch (GraphRenderer.isDraggingPortValid()){ - case Edge.Validity.Unknown: + case Errors.Validity.Unknown: return "black"; - case Edge.Validity.Impossible: - case Edge.Validity.Invalid: + case Errors.Validity.Impossible: + case Errors.Validity.Error: return EagleConfig.getColor("edgeInvalid"); - case Edge.Validity.Warning: + case Errors.Validity.Warning: return EagleConfig.getColor("edgeWarning"); - case Edge.Validity.Valid: + case Errors.Validity.Valid: return EagleConfig.getColor("edgeValid"); default: return EagleConfig.getColor("edgeDefault"); @@ -2217,14 +2232,15 @@ export class GraphRenderer { } // check if link has a warning or is invalid - const linkValid : Edge.Validity = Edge.isValid(eagle, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.isLoopAware(), edge.isClosesLoop(), false, false, {errors:[], warnings:[]}); + // const linkValid : Errors.Validity = Edge.isValid(eagle,false, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.isLoopAware(), edge.isClosesLoop(), false, false, {errors:[], warnings:[]}); + const linkValid : Errors.Validity = Utils.worstEdgeError(edge.getErrorsWarnings()); - if (linkValid === Edge.Validity.Invalid || linkValid === Edge.Validity.Impossible){ + if (linkValid === Errors.Validity.Error || linkValid === Errors.Validity.Impossible){ normalColor = EagleConfig.getColor('edgeInvalid'); selectedColor = EagleConfig.getColor('edgeInvalidSelected'); } - if (linkValid === Edge.Validity.Warning){ + if (linkValid === Errors.Validity.Warning){ normalColor = EagleConfig.getColor('edgeWarning'); selectedColor = EagleConfig.getColor('edgeWarningSelected'); } diff --git a/src/GraphUpdater.ts b/src/GraphUpdater.ts index bf1f49dcb..815278f09 100644 --- a/src/GraphUpdater.ts +++ b/src/GraphUpdater.ts @@ -200,7 +200,8 @@ export class GraphUpdater { row.lastModified = date.toLocaleDateString() + " " + date.toLocaleTimeString() // check the graph once loaded - const results: Errors.ErrorsWarnings = Utils.checkGraph(eagle); + Utils.checkGraph(eagle); + const results: Errors.ErrorsWarnings = Utils.gatherGraphErrors(); row.numCheckWarnings = results.warnings.length; row.numCheckErrors = results.errors.length; } diff --git a/src/LogicalGraph.ts b/src/LogicalGraph.ts index d27b2420e..b08c84a67 100644 --- a/src/LogicalGraph.ts +++ b/src/LogicalGraph.ts @@ -42,12 +42,14 @@ export class LogicalGraph { fileInfo : ko.Observable; private nodes : ko.ObservableArray; private edges : ko.ObservableArray; + private issues : ko.ObservableArray<{issue:Errors.Issue, validity:Errors.Validity}> //keeps track of higher level errors on the graph constructor(){ this.fileInfo = ko.observable(new FileInfo()); this.fileInfo().type = Eagle.FileType.Graph; this.nodes = ko.observableArray([]); this.edges = ko.observableArray([]); + this.issues = ko.observableArray([]) } static toOJSJson(graph : LogicalGraph, forTranslation : boolean) : object { @@ -313,6 +315,10 @@ export class LogicalGraph { return result; } + getIssues = (): {issue:Errors.Issue, validity:Errors.Validity}[] => { + return this.issues(); + } + /** * Opens a dialog for selecting a data component type. */ @@ -782,4 +788,60 @@ export class LogicalGraph { return radius; } + + static isValid () : void { + //here should be the higher level graph wide checks for graph validity + const eagle = Eagle.getInstance() + const graph = eagle.logicalGraph() + + // check that all node, edge, field ids are unique + // { + const ids : string[] = []; + + // loop over graph nodes + for (const node of graph.getNodes()){ + //check for unique ids + if (ids.includes(node.getId())){ + const issue: Errors.Issue = Errors.ShowFix( + "Node (" + node.getName() + ") does not have a unique id", + function(){Utils.showNode(eagle, node.getId())}, + function(){Utils.newId(node)}, + "Assign node a new id" + ); + graph.issues.push({issue : issue, validity : Errors.Validity.Error}) + // errorsWarnings.errors.push(issue); + } + ids.push(node.getId()); + + for (const field of node.getFields()){ + if (ids.includes(field.getId())){ + const issue: Errors.Issue = Errors.ShowFix( + "Field (" + field.getDisplayText() + ") on node (" + node.getName() + ") does not have a unique id", + function(){Utils.showNode(eagle, node.getId())}, + function(){Utils.newFieldId(eagle, node, field)}, + "Assign field a new id" + ); + graph.issues.push({issue : issue, validity : Errors.Validity.Error}) + // errorsWarnings.errors.push(issue); + } + ids.push(field.getId()); + } + } + + // loop over graph edges + for (const edge of graph.getEdges()){ + if (ids.includes(edge.getId())){ + const issue: Errors.Issue = Errors.ShowFix( + "Edge (" + edge.getId() + ") does not have a unique id", + function(){Utils.showEdge(eagle, edge.getId())}, + function(){Utils.newId(edge)}, + "Assign edge a new id" + ); + graph.issues.push({issue : issue, validity : Errors.Validity.Error}) + // errorsWarnings.errors.push(issue); + } + ids.push(edge.getId()); + } + // } + } } diff --git a/src/Node.ts b/src/Node.ts index bb20bd27f..c3d6847eb 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -62,7 +62,7 @@ export class Node { private paletteDownloadUrl : ko.Observable; private dataHash : ko.Observable; - private errorsWarnings : ko.Observable; + private issues : ko.ObservableArray<{issue:Errors.Issue, validity:Errors.Validity}>//keeps track of node level errors public static readonly DEFAULT_COLOR : string = "ffffff"; @@ -112,7 +112,7 @@ export class Node { this.paletteDownloadUrl = ko.observable(""); this.dataHash = ko.observable(""); - this.errorsWarnings = ko.observable({warnings: [], errors: []}); + this.issues = ko.observableArray([]); //graph related things this.expanded = ko.observable(true); @@ -1131,29 +1131,59 @@ export class Node { return result; } - getErrorsWarnings = (): Errors.ErrorsWarnings => { - return this.errorsWarnings(); + getIssues = (): {issue:Errors.Issue, validity:Errors.Validity}[] => { + return this.issues(); } - getAllErrorsWarnings = (): Errors.ErrorsWarnings => { + getAllErrors = () : {issue:Errors.Issue, validity:Errors.Validity}[] => { + const allNodeErrors : {issue:Errors.Issue, validity:Errors.Validity}[] = [] + + allNodeErrors.push(...this.getIssues()) + this.getFields().forEach(function(field){ + allNodeErrors.push(...field.getIssues()) + }) + + return allNodeErrors + } + + getErrorsWarnings : ko.PureComputed = ko.pureComputed(() => { const errorsWarnings : Errors.ErrorsWarnings = {warnings: [], errors: []}; - errorsWarnings.errors.push(...this.errorsWarnings().errors) - errorsWarnings.warnings.push(...this.errorsWarnings().warnings) + + this.getIssues().forEach(function(error){ + if(error.validity === Errors.Validity.Error || error.validity === Errors.Validity.Unknown){ + errorsWarnings.errors.push(error.issue) + }else{ + errorsWarnings.warnings.push(error.issue) + } + }) + + return errorsWarnings; + }, this); + + getAllErrorsWarnings : ko.PureComputed = ko.pureComputed(() => { + const errorsWarnings : Errors.ErrorsWarnings = {warnings: [], errors: []}; + const nodeErrors = this.getErrorsWarnings() + + errorsWarnings.errors.push(...nodeErrors.errors) + errorsWarnings.warnings.push(...nodeErrors.warnings) this.getFields().forEach((field) =>{ - errorsWarnings.errors.push(...field.getErrorsWarnings().errors) - errorsWarnings.warnings.push(...field.getErrorsWarnings().warnings) + const fieldErrors = field.getErrorsWarnings() + errorsWarnings.errors.push(...fieldErrors.errors) + errorsWarnings.warnings.push(...fieldErrors.warnings) }) return errorsWarnings - } + }, this); getBorderColor : ko.PureComputed = ko.pureComputed(() => { + const errorsWarnings = this.getErrorsWarnings() + if(this.isEmbedded()){ return '' //returning nothing lets the means we are not over writing the default css behaviour - }else if(this.errorsWarnings().errors.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) != Setting.ShowErrorsMode.None){ + }else if(errorsWarnings.errors.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) != Setting.ShowErrorsMode.None){ return '#ea2727' - }else if(this.errorsWarnings().warnings.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) === Setting.ShowErrorsMode.Warnings){ + }else if(errorsWarnings.warnings.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) === Setting.ShowErrorsMode.Warnings){ return '#ffa500' }else{ return '#2e3192' @@ -1161,10 +1191,12 @@ export class Node { }, this); getBackgroundColor : ko.PureComputed = ko.pureComputed(() => { + const errorsWarnings = this.getErrorsWarnings() const eagle = Eagle.getInstance() - if(this.errorsWarnings().errors.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) != Setting.ShowErrorsMode.None){ + + if(errorsWarnings.errors.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) != Setting.ShowErrorsMode.None){ return '#ffdcdc' - }else if(this.errorsWarnings().warnings.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) === Setting.ShowErrorsMode.Warnings){ + }else if(errorsWarnings.warnings.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) === Setting.ShowErrorsMode.Warnings){ return '#ffeac4' }else if(this.isBranch()){ //for some reason branch nodes dont want to behave like other nodes, i need to return their background or selected color manually @@ -1941,43 +1973,56 @@ export class Node { return x } - static isValid(node: Node, selectedLocation: Eagle.FileType) : Errors.ErrorsWarnings { + static isValid(node: Node, selectedLocation: Eagle.FileType) : void { const eagle = Eagle.getInstance() - const errorsWarnings : Errors.ErrorsWarnings = {warnings: [], errors: []}; + node.issues([])//clear old issues // check that node has modern (not legacy) category if (node.getCategory() === Category.Component){ const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has legacy category (" + node.getCategory() + ")", function(){Utils.showNode(eagle, node.getId());}, function(){Utils.fixNodeCategory(eagle, node, Category.PythonApp, Category.Type.Application)}, ""); - errorsWarnings.warnings.push(issue); + node.issues().push({issue:issue,validity:Errors.Validity.Warning}) } + // looping through and checking all the fields on the node for (let i = 0 ; i < node.getFields().length ; i++){ const field:Field = node.getFields()[i] - const fieldErrorsWarnings = Field.isValid(node,field,selectedLocation,i) + Field.isValid(node,field,selectedLocation,i) } + if(node.isConstruct()){ + //checking the input application if one is present + if(node.hasInputApplication()){ + Node.isValid(node.getInputApplication(),selectedLocation) + } + + //checking the output application if one is present + if(node.hasOutputApplication()){ + Node.isValid(node.getOutputApplication(),selectedLocation) + } + } + // check that all nodes have correct numbers of inputs and outputs const cData: Category.CategoryData = CategoryData.getCategoryData(node.getCategory()); if (node.getInputPorts().length < cData.minInputs){ const message: string = "Node " + node.getKey() + " (" + node.getName() + ") may have too few input ports. A " + node.getCategory() + " component would typically have at least " + cData.minInputs; const issue: Errors.Issue = Errors.ShowFix(message, function(){Utils.showNode(eagle, node.getId())}, null, ""); - errorsWarnings.warnings.push(issue); + node.issues().push({issue:issue,validity:Errors.Validity.Warning}) } if ((node.getInputPorts().length - node.getInputEventPorts().length) > cData.maxInputs){ const message: string = "Node " + node.getKey() + " (" + node.getName() + ") has too many input ports. Should have at most " + cData.maxInputs; const issue: Errors.Issue = Errors.ShowFix(message, function(){Utils.showNode(eagle, node.getId())}, null, ""); - errorsWarnings.warnings.push(issue); + node.issues().push({issue:issue,validity:Errors.Validity.Warning}) } if (node.getOutputPorts().length < cData.minOutputs){ const message: string = "Node " + node.getKey() + " (" + node.getName() + ") may have too few output ports. A " + node.getCategory() + " component would typically have at least " + cData.minOutputs; const issue: Errors.Issue = Errors.ShowFix(message, function(){Utils.showNode(eagle, node.getId())}, null, ""); - errorsWarnings.warnings.push(issue); + node.issues().push({issue:issue,validity:Errors.Validity.Warning}) } if ((node.getOutputPorts().length - node.getOutputEventPorts().length) > cData.maxOutputs){ const message: string = "Node " + node.getKey() + " (" + node.getName() + ") may have too many output ports. Should have at most " + cData.maxOutputs; const issue: Errors.Issue = Errors.ShowFix(message, function(){Utils.showNode(eagle, node.getId())}, null, ""); - errorsWarnings.warnings.push(issue); + node.issues().push({issue:issue,validity:Errors.Validity.Warning}) } // check that all nodes should have at least one connected edge, otherwise what purpose do they serve? @@ -1993,27 +2038,30 @@ export class Node { // only check this if the component has been selected in the graph. If it was selected from the palette, it doesn't make sense to complain that it is not connected. if (!isConnected && !(cData.maxInputs === 0 && cData.maxOutputs === 0) && selectedLocation === Eagle.FileType.Graph){ const issue: Errors.Issue = Errors.ShowFix("Node " + node.getKey() + " (" + node.getName() + ") has no connected edges. It should be connected to the graph in some way", function(){Utils.showNode(eagle, node.getId())}, null, ""); - errorsWarnings.warnings.push(issue); + node.issues().push({issue:issue,validity:Errors.Validity.Warning}) } // check embedded application categories are not 'None' if (node.hasInputApplication() && node.getInputApplication().getCategory() === Category.None){ - errorsWarnings.errors.push(Errors.Message("Node " + node.getKey() + " (" + node.getName() + ") has input application with category 'None'.")); + const issue: Errors.Issue = Errors.Message("Node " + node.getKey() + " (" + node.getName() + ") has input application with category 'None'.") + node.issues().push({issue:issue,validity:Errors.Validity.Error}); } if (node.hasOutputApplication() && node.getOutputApplication().getCategory() === Category.None){ - errorsWarnings.errors.push(Errors.Message("Node " + node.getKey() + " (" + node.getName() + ") has output application with category 'None'.")); + const issue : Errors.Issue = Errors.Message("Node " + node.getKey() + " (" + node.getName() + ") has output application with category 'None'.") + node.issues().push({issue:issue,validity:Errors.Validity.Error}); } // check that Service nodes have inputApplications with no output ports! if (node.getCategory() === Category.Service && node.hasInputApplication() && node.getInputApplication().getOutputPorts().length > 0){ - errorsWarnings.errors.push(Errors.Message("Node " + node.getKey() + " (" + node.getName() + ") is a Service node, but has an input application with at least one output.")); + const issue : Errors.Issue = Errors.Message("Node " + node.getKey() + " (" + node.getName() + ") is a Service node, but has an input application with at least one output.") + node.issues().push({issue:issue,validity:Errors.Validity.Error}); } // check that this category of node contains all the fields it requires for (const requirement of Daliuge.categoryFieldsRequired){ if (requirement.categories.includes(node.getCategory())){ for (const requiredField of requirement.fields){ - Node._checkForField(eagle, node, requiredField, errorsWarnings); + Node._checkForField(eagle, node, requiredField); } } } @@ -2022,17 +2070,13 @@ export class Node { for (const requirement of Daliuge.categoryTypeFieldsRequired){ if (requirement.categoryTypes.includes(node.getCategoryType())){ for (const requiredField of requirement.fields){ - Node._checkForField(eagle, node, requiredField, errorsWarnings); + Node._checkForField(eagle, node, requiredField); } } } - - node.errorsWarnings(errorsWarnings) - - return errorsWarnings } - private static _checkForField(eagle: Eagle, node: Node, field: Field, errorsWarnings: Errors.ErrorsWarnings) : void { + private static _checkForField(eagle: Eagle, node: Node, field: Field) : void { // check if the node already has this field const existingField = node.getFieldByDisplayText(field.getDisplayText()); @@ -2040,12 +2084,12 @@ export class Node { // if so, check the attributes of the field match if (existingField === null){ const message = "Node " + node.getKey() + " (" + node.getName() + ":" + node.category() + ":" + node.categoryType() + ") does not have the required '" + field.getDisplayText() + "' field"; - errorsWarnings.errors.push(Errors.ShowFix(message, function(){Utils.showNode(eagle, node.getId());}, function(){Utils.addMissingRequiredField(eagle, node, field);}, "Add missing " + field.getDisplayText() + " field.")); - } else { - if (existingField.getParameterType() !== field.getParameterType()){ - const message = "Node " + node.getKey() + " (" + node.getName() + ") has a '" + field.getDisplayText() + "' field with the wrong parameter type (" + existingField.getParameterType() + "), should be a " + field.getParameterType(); - existingField.addErrorsWarnings(Errors.ShowFix(message, function(){Utils.showField(eagle, node.getId(),existingField);}, function(){Utils.fixFieldParameterType(eagle, node, existingField, field.getParameterType())}, "Switch type of field to '" + field.getParameterType()),'error'); - } + const issue : Errors.Issue = Errors.ShowFix(message, function(){Utils.showNode(eagle, node.getId());}, function(){Utils.addMissingRequiredField(eagle, node, field);}, "Add missing " + field.getDisplayText() + " field.") + node.issues().push({issue:issue,validity:Errors.Validity.Error}); + } else if (existingField.getParameterType() !== field.getParameterType()){ + const message = "Node " + node.getKey() + " (" + node.getName() + ") has a '" + field.getDisplayText() + "' field with the wrong parameter type (" + existingField.getParameterType() + "), should be a " + field.getParameterType(); + const issue : Errors.Issue = Errors.ShowFix(message, function(){Utils.showField(eagle, node.getId(),existingField);}, function(){Utils.fixFieldParameterType(eagle, node, existingField, field.getParameterType())}, "Switch type of field to '" + field.getParameterType()) + existingField.addError(issue,Errors.Validity.Error) } } } diff --git a/src/Setting.ts b/src/Setting.ts index 2dd5d7c8b..df718e86c 100644 --- a/src/Setting.ts +++ b/src/Setting.ts @@ -1,7 +1,7 @@ import * as ko from "knockout"; import { Eagle } from './Eagle'; -import { Edge } from "./Edge"; +import { Errors } from './Errors'; import { Repository } from "./Repository"; import { UiModeSystem } from './UiModes'; import { Utils } from './Utils'; @@ -342,11 +342,6 @@ export namespace Setting { ReadOnly = "Readonly" } - export enum ErrorsMode { - Loading = "Loading", - Graph = "Graph" - } - export enum TranslatorMode { Minimal = "minimal", Normal = "normal", @@ -402,7 +397,7 @@ const settings : SettingsGroup[] = [ new Setting(true, "Filter Node Suggestions", Setting.FILTER_NODE_SUGGESTIONS, "Filter Node Options When Drawing Edges Into Empty Space", false, Setting.Type.Boolean,true,true,true,true,false), new Setting(false, "STUDENT_SETTINGS_MODE", Setting.STUDENT_SETTINGS_MODE, "Mode disabling setting editing for students.", false, Setting.Type.Boolean, true, false,false, false, false), new Setting(true, "Value Editing", Setting.VALUE_EDITING_PERMS, "Set which values are allowed to be edited.", false, Setting.Type.Select, Setting.valueEditingPerms.KeyOnly,Setting.valueEditingPerms.Normal,Setting.valueEditingPerms.Normal,Setting.valueEditingPerms.ReadOnly,Setting.valueEditingPerms.ReadOnly, Object.values(Setting.valueEditingPerms)), - new Setting(true, "Auto-complete edges level", Setting.AUTO_COMPLETE_EDGES_LEVEL, "Specifies the minimum validity level of auto-complete edges displayed when dragging a new edge", false, Setting.Type.Select, Edge.Validity.Valid, Edge.Validity.Valid, Edge.Validity.Warning, Edge.Validity.Warning, Edge.Validity.Invalid, [Edge.Validity.Invalid, Edge.Validity.Warning, Edge.Validity.Valid]), + new Setting(true, "Auto-complete edges level", Setting.AUTO_COMPLETE_EDGES_LEVEL, "Specifies the minimum validity level of auto-complete edges displayed when dragging a new edge", false, Setting.Type.Select, Errors.Validity.Valid, Errors.Validity.Valid, Errors.Validity.Warning, Errors.Validity.Warning, Errors.Validity.Error, [Errors.Validity.Error, Errors.Validity.Warning, Errors.Validity.Valid]), ] ), new SettingsGroup( diff --git a/src/Utils.ts b/src/Utils.ts index 85756d390..a4f63bbee 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1379,7 +1379,7 @@ export class Utils { static checkPalette(palette: Palette): Errors.ErrorsWarnings { const errorsWarnings: Errors.ErrorsWarnings = {warnings: [], errors: []}; - + const paletteIssues : {issue:Errors.Issue, validity:Errors.Validity}[]=[] // check for duplicate keys const keys: number[] = []; @@ -1394,105 +1394,86 @@ export class Utils { // check all nodes are valid for (const node of palette.getNodes()){ - const nodeErrorsWarnings = Node.isValid(node, Eagle.FileType.Palette); - errorsWarnings.errors.push(...nodeErrorsWarnings.errors) - errorsWarnings.warnings.push(...nodeErrorsWarnings.warnings) + Node.isValid(node, Eagle.FileType.Palette); + paletteIssues.push(...node.getIssues()) + // errorsWarnings.errors.push(...nodeErrorsWarnings.errors) + // errorsWarnings.warnings.push(...nodeErrorsWarnings.warnings) + } + + for(const error of paletteIssues){ + if(error.validity === Errors.Validity.Error){ + errorsWarnings.errors.push(error.issue) + }else{ + errorsWarnings.warnings.push(error.issue) + } } return errorsWarnings; } - static checkGraph(eagle: Eagle): Errors.ErrorsWarnings { - const errorsWarnings: Errors.ErrorsWarnings = {warnings: [], errors: []}; - + static checkGraph(eagle: Eagle): void { const graph: LogicalGraph = eagle.logicalGraph(); // check all nodes are valid for (const node of graph.getNodes()){ - const nodeErrorsWarnings = Node.isValid(node, Eagle.FileType.Graph); - errorsWarnings.errors.push(...nodeErrorsWarnings.errors) - errorsWarnings.warnings.push(...nodeErrorsWarnings.warnings) + Node.isValid(node, Eagle.FileType.Graph); + } - //get the node's field errorswarnings - for(const field of node.getFields()){ - errorsWarnings.errors.push(...field.getErrorsWarnings().errors) - errorsWarnings.warnings.push(...field.getErrorsWarnings().warnings) + // check all edges are valid + for (const edge of graph.getEdges()){ + Edge.isValid(eagle, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.isLoopAware(), edge.isClosesLoop(), false, false, {warnings: [], errors: []}); + } + } + + static gatherGraphErrors(): Errors.ErrorsWarnings { + const eagle = Eagle.getInstance() + const errorsWarnings: Errors.ErrorsWarnings = {warnings: [], errors: []}; + const graphIssues : {issue:Errors.Issue, validity:Errors.Validity}[] = [] + const graph : LogicalGraph = eagle.logicalGraph() + + //gather all the errors + //from nodes + for(const node of graph.getNodes()){ + graphIssues.push(...node.getIssues()) + + //from fields + for( const field of node.getFields()){ + graphIssues.push(...field.getIssues()) } - // check the embedded applications - if (node.hasInputApplication()){ - const inputNodeErrorsWarnings = Node.isValid(node.getInputApplication(),Eagle.FileType.Graph) - errorsWarnings.errors.push(...inputNodeErrorsWarnings.errors) - errorsWarnings.warnings.push(...inputNodeErrorsWarnings.warnings) - - //get the input application's field errorswarnings - for(const field of node.getInputApplication().getFields()){ - errorsWarnings.errors.push(...field.getErrorsWarnings().errors) - errorsWarnings.warnings.push(...field.getErrorsWarnings().warnings) + //embedded input applications and their fields + if(node.hasInputApplication()){ + graphIssues.push(...node.getInputApplication().getIssues()) + + for( const field of node.getInputApplication().getFields()){ + graphIssues.push(...field.getIssues()) } } - if (node.hasOutputApplication()){ - const outputNodeErrorsWarnings = Node.isValid(node.getOutputApplication(),Eagle.FileType.Graph) - errorsWarnings.errors.push(...outputNodeErrorsWarnings.errors) - errorsWarnings.warnings.push(...outputNodeErrorsWarnings.warnings) + + //embedded output applications and their fields + if(node.hasOutputApplication()){ + graphIssues.push(...node.getOutputApplication().getIssues()) - //get the output application's field errorswarnings - for(const field of node.getOutputApplication().getFields()){ - errorsWarnings.errors.push(...field.getErrorsWarnings().errors) - errorsWarnings.warnings.push(...field.getErrorsWarnings().warnings) + for( const field of node.getOutputApplication().getFields()){ + graphIssues.push(...field.getIssues()) } } } - // check all edges are valid + // from edges for (const edge of graph.getEdges()){ - Edge.isValid(eagle, edge.getId(), edge.getSrcNodeKey(), edge.getSrcPortId(), edge.getDestNodeKey(), edge.getDestPortId(), edge.isLoopAware(), edge.isClosesLoop(), false, false, errorsWarnings); - } - - // check that all node, edge, field ids are unique - { - const ids : string[] = []; - - // loop over graph nodes - for (const node of graph.getNodes()){ - //check for unique ids - if (ids.includes(node.getId())){ - const issue: Errors.Issue = Errors.ShowFix( - "Node (" + node.getName() + ") does not have a unique id", - function(){Utils.showNode(eagle, node.getId())}, - function(){Utils.newId(node)}, - "Assign node a new id" - ); - errorsWarnings.errors.push(issue); - } - ids.push(node.getId()); - - for (const field of node.getFields()){ - if (ids.includes(field.getId())){ - const issue: Errors.Issue = Errors.ShowFix( - "Field (" + field.getDisplayText() + ") on node (" + node.getName() + ") does not have a unique id", - function(){Utils.showNode(eagle, node.getId())}, - function(){Utils.newFieldId(eagle, node, field)}, - "Assign field a new id" - ); - errorsWarnings.errors.push(issue); - } - ids.push(field.getId()); - } - } + graphIssues.push(...edge.getIssues()) + } - // loop over graph edges - for (const edge of graph.getEdges()){ - if (ids.includes(edge.getId())){ - const issue: Errors.Issue = Errors.ShowFix( - "Edge (" + edge.getId() + ") does not have a unique id", - function(){Utils.showEdge(eagle, edge.getId())}, - function(){Utils.newId(edge)}, - "Assign edge a new id" - ); - errorsWarnings.errors.push(issue); - } - ids.push(edge.getId()); + //from logical graph + graphIssues.push(...graph.getIssues()) + + //sort all issues into warnings or errors + for(const error of graphIssues){ + if(error.validity === Errors.Validity.Error || error.validity === Errors.Validity.Impossible || error.validity === Errors.Validity.Unknown){ + errorsWarnings.errors.push(error.issue) + }else{ + errorsWarnings.warnings.push(error.issue) } } @@ -2178,22 +2159,21 @@ export class Utils { } // only update result if it is worse that current result - static worstEdgeError(errorsWarnings: Errors.ErrorsWarnings) : Edge.Validity { + static worstEdgeError(errorsWarnings: Errors.ErrorsWarnings) : Errors.Validity { if (errorsWarnings === null){ console.warn("errorsWarnings is null"); - return Edge.Validity.Valid; + return Errors.Validity.Valid; } if (errorsWarnings.warnings.length === 0 && errorsWarnings.errors.length === 0){ - return Edge.Validity.Valid; + return Errors.Validity.Valid; } if (errorsWarnings.errors.length !== 0){ - // TODO: this actually has no way of knowing whether the errors are of type Invalid or Impossible - return Edge.Validity.Invalid; + return Errors.Validity.Error; } - return Edge.Validity.Warning; + return Errors.Validity.Warning; } static printCategories() : void {