From f433c41240f0c0b51d452219a8459b874dc59651 Mon Sep 17 00:00:00 2001 From: Ryan McNally Date: Wed, 12 Jul 2023 14:46:35 +0100 Subject: [PATCH] Diagram controls (#459) * Added controls * Added testing * avoided headless failure --- .../flow/report/index/ServedIndexTest.java | 40 ++++++++++ .../test/flow/report/seq/IndexSequence.java | 79 +++++++++++++++++++ .../projects/report/src/app/app.module.ts | 2 + .../report/src/app/icon-embed.service.ts | 4 + .../system-diagram.component.css | 13 ++- .../system-diagram.component.html | 31 +++++++- .../system-diagram.component.ts | 49 ++++++++++-- 7 files changed, 209 insertions(+), 9 deletions(-) diff --git a/report/report-core/src/test/java/com/mastercard/test/flow/report/index/ServedIndexTest.java b/report/report-core/src/test/java/com/mastercard/test/flow/report/index/ServedIndexTest.java index 60be64319e..b9349b80be 100644 --- a/report/report-core/src/test/java/com/mastercard/test/flow/report/index/ServedIndexTest.java +++ b/report/report-core/src/test/java/com/mastercard/test/flow/report/index/ServedIndexTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; import org.junit.jupiter.api.extension.ExtendWith; import com.mastercard.test.flow.report.seq.Browser; @@ -58,6 +59,45 @@ void filteredInteractions() { "Edges:", " AVA normal solid BEN", " AVA normal solid CHE " ); + + iseq.toggleFilteredActorHide() + .hasInteractions( + "Nodes:", + " AVA", + " BEN", + "Edges:", + " AVA normal solid BEN" ); + } + + /** + * Checks that the user can extract the mermaid markup + */ + @Test + @DisabledIf(value = "java.awt.GraphicsEnvironment#isHeadless", + disabledReason = "no clipboard") + void mermaidMarkup() { + iseq.expandInteractions() + .hasMermaidMarkup( + "graph LR", + " AVA --> BEN", + " AVA --> CHE" ); + + iseq.diagramOrientation( "TD" ) + .hasMermaidMarkup( + "graph TD", + " AVA --> BEN", + " AVA --> CHE" ); + + iseq.clickTag( "PASS" ) + .hasMermaidMarkup( + "graph TD", + " AVA --> BEN", + " AVA ~~~ CHE" ); + + iseq.toggleFilteredActorHide() + .hasMermaidMarkup( + "graph TD", + " AVA --> BEN" ); } /** diff --git a/report/report-core/src/test/java/com/mastercard/test/flow/report/seq/IndexSequence.java b/report/report-core/src/test/java/com/mastercard/test/flow/report/seq/IndexSequence.java index 60bc358d16..ec1c3a9e79 100644 --- a/report/report-core/src/test/java/com/mastercard/test/flow/report/seq/IndexSequence.java +++ b/report/report-core/src/test/java/com/mastercard/test/flow/report/seq/IndexSequence.java @@ -7,6 +7,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.openqa.selenium.support.ui.ExpectedConditions.elementToBeClickable; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; import java.io.StringReader; import java.util.ArrayList; import java.util.List; @@ -382,6 +386,81 @@ public IndexSequence expandInteractions() { return self(); } + /** + * Clicks on the "Copy mermaid" button, then asserts on the clipboard contents. + * Tries to restore the clipboard state to what it was before the test. + * + * @param expected Expected mermaid content + * @return this + */ + public IndexSequence hasMermaidMarkup( String... expected ) { + trace( "hasMermaidMarkup", (Object[]) expected ); + + Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard(); + Transferable before = cb.getContents( this ); + + try { + driver.findElement( By.id( "copy_mermaid_button" ) ) + .click(); + + Transferable after = cb.getContents( this ); + + assertEquals( + copypasta( expected ), + copypasta( after.getTransferData( DataFlavor.stringFlavor ).toString() ), + "mermaid markup" ); + } + catch( Exception e ) { + throw new IllegalStateException( "failed to extract mermaid", e ); + } + finally { + cb.setContents( before, ( clipboard, contents ) -> { + // we don't care about losing clipboard ownership + } ); + } + + return self(); + } + + /** + * Clicks on the diagram orientation toggles + * + * @param orientation The new orientation + * @return this + */ + public IndexSequence diagramOrientation( String orientation ) { + trace( "diagramOrientation", orientation ); + + List toggles = driver.findElement( By.id( "interactions_orientation_group" ) ) + .findElements( By.tagName( "mat-button-toggle" ) ); + + toggles.stream() + .filter( e -> orientation.equals( e.getAttribute( "value" ) ) ) + .findFirst() + .orElseThrow( () -> new IllegalStateException( + String.format( "Failed to find diagram orientation value '%s' in %s", + orientation, toggles.stream() + .map( e -> e.getAttribute( "value" ) ) + .collect( toSet() ) ) ) ) + .click(); + + return self(); + } + + /** + * Clicks the "hide filtered actors" toggle + * + * @return this + */ + public IndexSequence toggleFilteredActorHide() { + trace( "toggleFilteredActorHide" ); + + driver.findElement( By.id( "hide_filtered_actors_toggle" ) ) + .click(); + + return self(); + } + /** * Asserts on the displayed interaction diagram. * diff --git a/report/report-ng/projects/report/src/app/app.module.ts b/report/report-ng/projects/report/src/app/app.module.ts index d705d7a2d7..e720ff86f9 100644 --- a/report/report-ng/projects/report/src/app/app.module.ts +++ b/report/report-ng/projects/report/src/app/app.module.ts @@ -59,6 +59,7 @@ import { MatRippleModule } from '@angular/material/core'; import { HighlightedTextComponent } from './highlighted-text/highlighted-text.component'; import { TextDiffComponent } from './text-diff/text-diff.component'; import { SystemDiagramComponent } from './system-diagram/system-diagram.component'; +import { ClipboardModule } from '@angular/cdk/clipboard'; const routes: Routes = [ { path: "diff", component: ModelDiffComponent }, @@ -106,6 +107,7 @@ const routes: Routes = [ imports: [ BrowserAnimationsModule, BrowserModule, + ClipboardModule, DragDropModule, FormsModule, HttpClientModule, diff --git a/report/report-ng/projects/report/src/app/icon-embed.service.ts b/report/report-ng/projects/report/src/app/icon-embed.service.ts index b26eabbe0f..680e8e64b4 100644 --- a/report/report-ng/projects/report/src/app/icon-embed.service.ts +++ b/report/report-ng/projects/report/src/app/icon-embed.service.ts @@ -26,9 +26,11 @@ export class IconEmbedService { clear: ``, close: ``, compare: ``, + content_copy: ``, difference: ``, error_outline: ``, expand_less: ``, + filter_alt: ``, format_list_bulleted: ``, foundation: ``, groups: ``, @@ -45,6 +47,8 @@ export class IconEmbedService { navigate_before: ``, navigate_next: ``, new_releases: ``, + panorama_horizontal: ``, + panorama_vertical: ``, person: ``, psychology: ``, remove: ``, diff --git a/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.css b/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.css index 1f27f37a99..53057c2f56 100644 --- a/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.css +++ b/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.css @@ -1,3 +1,14 @@ -#container { +#interaction_root { + display: flex; + flex-direction: column; +} + +#interaction_controls_diagram { + display: flex; + flex-direction: row; +} + +#interactions_diagram { text-align: center; + flex-grow: 1; } \ No newline at end of file diff --git a/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.html b/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.html index a7b72ea911..6eb234dcbd 100644 --- a/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.html +++ b/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.html @@ -3,8 +3,35 @@ Interactions {{summary}} -
+
-

+        
+
+
+ + + + + + + + +
+
+ + + +
+
+ + + +
+
+

+        
\ No newline at end of file diff --git a/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.ts b/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.ts index 6bf12b8eb1..d8108125f8 100644 --- a/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.ts +++ b/report/report-ng/projects/report/src/app/system-diagram/system-diagram.component.ts @@ -4,6 +4,9 @@ import { ModelDiffDataService } from '../model-diff-data.service'; import { Entry, Flow, Interaction } from '../types'; import { FlowFilterService } from '../flow-filter.service'; import { EntryHoverService } from '../entry-hover.service'; +import { MatButtonToggleChange } from '@angular/material/button-toggle'; +import { ClipboardModule } from '@angular/cdk/clipboard'; +import { IconEmbedService } from '../icon-embed.service'; interface Edge { from: string; @@ -32,6 +35,9 @@ export class SystemDiagramComponent implements OnInit { edges: Edge[] = []; renderedEdgeCount: number = 0; hovered: Entry | null = null; + orientation: string = "LR"; + hideFilteredActors: boolean = false; + mermaidMarkup: string = ""; @ViewChild('myTestDiv') containerElRef: ElementRef | null = null; @@ -39,18 +45,29 @@ export class SystemDiagramComponent implements OnInit { private modelData: ModelDiffDataService, private filter: FlowFilterService, private hover: EntryHoverService, + icons: IconEmbedService, ) { modelData.onFlow(this.modelLabel, (label: string, entry: Entry, flow: Flow) => { this.loadProgress = modelData.flowLoadProgress(this.modelLabel); this.refreshEdges(); }); filter.onUpdate(() => { - this.refilterEdges(); + console.log("filter.onUpdate", this.hideFilteredActors); + if (this.hideFilteredActors) { + this.refreshEdges(); + } + else { + this.refilterEdges(); + } }); hover.onHover((entry: Entry | null) => { this.hovered = entry; this.rehoverFlow(); }); + + icons.register( + "panorama_vertical", "panorama_horizontal", + "filter_alt", "content_copy"); } ngOnInit(): void { @@ -63,14 +80,33 @@ export class SystemDiagramComponent implements OnInit { } } + forceRerender(): void { + this.renderedEdgeCount = -1; + this.refilterEdges(); + } + + updateFilterHide(event: MatButtonToggleChange) { + this.hideFilteredActors = event.source.checked; + this.renderedEdgeCount = -1; + this.refreshEdges(); + } + + copyContent(event: MatButtonToggleChange): void { + // we're abusing a toggle button for the visuals, so keep it unchecked + event.source.checked = false; + } + /** * Called when a flow has been loaded, recalculates all the edges in the system */ private refreshEdges(): void { + console.log("refreshEdges", this.hideFilteredActors); + let requests: { [key: string]: { [key: string]: number } } = {}; this.edges = []; this.modelData.index(this.modelLabel)?.index?.entries + .filter(e => !this.hideFilteredActors || this.filter.passes(e)) .map(e => this.modelData.flowFor(this.modelLabel, e)) .filter(f => f != null) .forEach(f => this.extractEdges( @@ -180,9 +216,13 @@ export class SystemDiagramComponent implements OnInit { let ac = actors.size; this.summary = ic + " interactions between " + ac + " actors"; + this.mermaidMarkup = "graph " + this.orientation + "\n" + this.edges + .map(e => " " + e.from + " " + e.edge + " " + e.to) + .join("\n"); + if (this.containerElRef != null) { if (this.edges.length != this.renderedEdgeCount) { - // we 've got a new edge - we have to rerender the diagram completely + // we've got a new edge - we have to rerender the diagram completely // it looks like mermaid doesn't have great support for refreshing // an existing diagram - we have to clear an attribute and manually @@ -195,14 +235,11 @@ export class SystemDiagramComponent implements OnInit { .innerHTML = ""; if (this.edges.length > 0) { - let diagram = "graph LR\n" + this.edges - .map(e => " " + e.from + " " + e.edge + " " + e.to) - .join("\n"); // now we know we have something to draw, put that text into // the dom and trigger mermaid this.containerElRef.nativeElement .querySelector("pre") - .innerHTML = diagram; + .innerHTML = this.mermaidMarkup; mermaid.init(); this.renderedEdgeCount = this.edges.length;