From 2506178d674be510ccccb5a3acea1ac4d876ca2a Mon Sep 17 00:00:00 2001 From: Jette Petzold Date: Fri, 11 Oct 2024 12:56:09 +0200 Subject: [PATCH] missing feedback edges are highlighted along with source node and labels --- .../stpa/diagram/diagram-controlStructure.ts | 14 ++++++-- .../stpa/diagram/stpa-interfaces.ts | 1 + .../stpa/diagram/stpa-model.ts | 1 + extension/src-webview/css/stpa-diagram.css | 22 +++++++++++- extension/src-webview/di.config.ts | 5 +-- extension/src-webview/stpa/stpa-model.ts | 2 ++ extension/src-webview/stpa/stpa-views.tsx | 35 ++++++++++++++++--- 7 files changed, 69 insertions(+), 11 deletions(-) diff --git a/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts index 91407a4b..1b94b2f7 100644 --- a/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts +++ b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts @@ -264,21 +264,29 @@ export function translateCommandsToEdges( // add missing feedback edges if (addMissing && missingFeedback) { // add feedback edge to each node to which a feedback is missing + let hasMissingFeedback = false; for (const target of missingFeedback.get(source?.name ?? "") ?? []) { if (source && target) { + hasMissingFeedback = true; createEdgeForCommand( source, target, idCache.uniqueId(`${source?.name}_missingFeedback_${target}`), - EdgeType.FEEDBACK, - // TODO: mark edge as missing to render it red - ["MISSING FEEDBACK"], + EdgeType.MISSING_FEEDBACK, + ["MISSING"], idToSNode, idCache, edges ); } } + // set flag for missing feedback to the source node + if (hasMissingFeedback) { + const sourceNode = idToSNode.get(idCache.getId(source) ?? ""); + if (sourceNode && sourceNode.type === CS_NODE_TYPE) { + (sourceNode as CSNode).hasMissingFeedback = true; + } + } } return edges; diff --git a/extension/src-language-server/stpa/diagram/stpa-interfaces.ts b/extension/src-language-server/stpa/diagram/stpa-interfaces.ts index 6408a5d8..d6361900 100644 --- a/extension/src-language-server/stpa/diagram/stpa-interfaces.ts +++ b/extension/src-language-server/stpa/diagram/stpa-interfaces.ts @@ -55,6 +55,7 @@ export interface PastaPort extends SPort { */ export interface CSNode extends SNode { level?: number; + hasMissingFeedback?: boolean; } /** diff --git a/extension/src-language-server/stpa/diagram/stpa-model.ts b/extension/src-language-server/stpa/diagram/stpa-model.ts index d5413520..286688ee 100644 --- a/extension/src-language-server/stpa/diagram/stpa-model.ts +++ b/extension/src-language-server/stpa/diagram/stpa-model.ts @@ -52,6 +52,7 @@ export enum STPAAspect { export enum EdgeType { CONTROL_ACTION, FEEDBACK, + MISSING_FEEDBACK, INPUT, OUTPUT, UNDEFINED diff --git a/extension/src-webview/css/stpa-diagram.css b/extension/src-webview/css/stpa-diagram.css index 6512724e..623b8659 100644 --- a/extension/src-webview/css/stpa-diagram.css +++ b/extension/src-webview/css/stpa-diagram.css @@ -19,7 +19,27 @@ .stpa-node { stroke: black; -} +} + +/* red highlighting for visualization of missing feedback edges */ +.missing-feedback-label { + fill: red !important; +} + +.missing-feedback-node { + stroke: red !important; + stroke-width: 3 !important; +} + +.missing-edge { + stroke: red !important; + stroke-width: 3 !important; +} + +.missing-edge-arrow { + stroke: red !important; + fill: red !important; +} /* Feedback edges */ .feedback-edge { diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index c274d381..ec783bd8 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -95,6 +95,7 @@ import { PolylineArrowEdgeView, PortView, STPAGraphView, + PastaLabelView, STPANodeView, } from "./stpa/stpa-views"; import { snippetModule } from './snippets/snippet-module'; @@ -119,8 +120,8 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = // configure the diagram elements const context = { bind, unbind, isBound, rebind }; - configureModelElement(context, "label", SLabel, SLabelView); - configureModelElement(context, "label:xref", SLabel, SLabelView); + configureModelElement(context, "label", SLabel, PastaLabelView); + configureModelElement(context, "label:xref", SLabel, PastaLabelView); configureModelElement(context, HEADER_LABEL_TYPE, SLabel, HeaderLabelView); configureModelElement(context, "html", HtmlRoot, HtmlRootView); configureModelElement(context, "pre-rendered", PreRenderedElement, PreRenderedView); diff --git a/extension/src-webview/stpa/stpa-model.ts b/extension/src-webview/stpa/stpa-model.ts index 566e6f66..414f2142 100644 --- a/extension/src-webview/stpa/stpa-model.ts +++ b/extension/src-webview/stpa/stpa-model.ts @@ -72,6 +72,7 @@ export class PastaPort extends SPort { */ export class CSNode extends SNode { level?: number; + hasMissingFeedback?: boolean; static readonly DEFAULT_FEATURES = [connectableFeature, selectFeature, layoutContainerFeature, fadeFeature]; } @@ -104,6 +105,7 @@ export enum STPAAspect { export enum EdgeType { CONTROL_ACTION, FEEDBACK, + MISSING_FEEDBACK, INPUT, OUTPUT, UNDEFINED, diff --git a/extension/src-webview/stpa/stpa-views.tsx b/extension/src-webview/stpa/stpa-views.tsx index 31554411..b07b72bc 100644 --- a/extension/src-webview/stpa/stpa-views.tsx +++ b/extension/src-webview/stpa/stpa-views.tsx @@ -24,7 +24,7 @@ import { ColorStyleOption, DifferentFormsOption, RenderOptionsRegistry } from '. import { SendModelRendererAction } from '../snippets/actions'; import { renderDiamond, renderHexagon, renderMirroredTriangle, renderOval, renderPentagon, renderRectangle, renderRoundedRectangle, renderTrapez, renderTriangle } from '../views-rendering'; import { collectAllChildren } from './helper-methods'; -import { CSEdge, CS_EDGE_TYPE, CS_INTERMEDIATE_EDGE_TYPE, EdgeType, STPAAspect, STPAEdge, STPANode, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE } from './stpa-model'; +import { CSEdge, CSNode, CS_EDGE_TYPE, CS_INTERMEDIATE_EDGE_TYPE, CS_NODE_TYPE, EdgeType, STPAAspect, STPAEdge, STPANode, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE } from './stpa-model'; /** Determines if path/aspect highlighting is currently on. */ let highlighting: boolean; @@ -46,6 +46,8 @@ export class PolylineArrowEdgeView extends PolylineEdgeView { const hidden = (edge.type === STPA_EDGE_TYPE || edge.type === STPA_INTERMEDIATE_EDGE_TYPE) && highlighting && !(edge as STPAEdge).highlight; // feedback edges in the control structure should be dashed const feedbackEdge = (edge.type === CS_EDGE_TYPE || edge.type === CS_INTERMEDIATE_EDGE_TYPE) && (edge as CSEdge).edgeType === EdgeType.FEEDBACK; + // edges that represent missing edges should be highlighted + const missing = (edge.type === CS_EDGE_TYPE || edge.type === CS_INTERMEDIATE_EDGE_TYPE) && (edge as CSEdge).edgeType === EdgeType.MISSING_FEEDBACK; const colorStyle = this.renderOptionsRegistry.getValue(ColorStyleOption); const printEdge = colorStyle === "black & white"; @@ -57,7 +59,7 @@ export class PolylineArrowEdgeView extends PolylineEdgeView { aspect = (edge as STPAEdge).aspect % 2 === 0 || !lessColoredEdge ? (edge as STPAEdge).aspect : (edge as STPAEdge).aspect - 1; } return ; + class-feedback-edge={feedbackEdge} class-missing-edge={missing} class-greyed-out={hidden} aspect={aspect} d={path} />; } protected renderAdditionals(edge: SEdge, segments: Point[], context: RenderingContext): VNode[] { @@ -76,8 +78,11 @@ export class PolylineArrowEdgeView extends PolylineEdgeView { if (edge.type === STPA_EDGE_TYPE || edge.type === STPA_INTERMEDIATE_EDGE_TYPE) { aspect = (edge as STPAEdge).aspect % 2 === 0 || !lessColoredEdge ? (edge as STPAEdge).aspect : (edge as STPAEdge).aspect - 1; } + // edges that represent missing edges should be highlighted + const missing = (edge.type === CS_EDGE_TYPE || edge.type === CS_INTERMEDIATE_EDGE_TYPE) && (edge as CSEdge).edgeType === EdgeType.MISSING_FEEDBACK; + return [ - ]; @@ -208,8 +213,10 @@ export class CSNodeView extends RectangularNodeView { const colorStyle = this.renderOptionsRegistry.getValue(ColorStyleOption); const sprottyNode = colorStyle === "standard"; const printNode = !sprottyNode; + const missingFeedback = node.type === CS_NODE_TYPE && (node as CSNode).hasMissingFeedback; return - , context: RenderingContext): VNode | undefined { return {super.render(label, context)} - + ; } } + +@injectable() +export class PastaLabelView extends SLabelView { + render(label: Readonly, context: RenderingContext): VNode | undefined { + // label belongs to a node which may have missing feedback + const nodeMissingFeedback = label.parent.type === CS_NODE_TYPE && (label.parent as CSNode).hasMissingFeedback; + // label belongs to an edge which may be a missing feedback edge + const edgeMissingFeedback = (label.parent.type === CS_EDGE_TYPE || label.parent.type === CS_INTERMEDIATE_EDGE_TYPE) && (label.parent as CSEdge).edgeType === EdgeType.MISSING_FEEDBACK; + const missingFeedbackLabel = nodeMissingFeedback || edgeMissingFeedback; + + const vnode = super.render(label, context); + if (vnode?.data?.class) { + vnode.data.class['missing-feedback-label'] = missingFeedbackLabel ?? false; + } + + return vnode; + } +} \ No newline at end of file