From f946bcf2c03e5a10395d49a90d78fc771a43ea91 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Tue, 8 Jul 2025 11:40:34 -0400 Subject: [PATCH 01/20] Use wrapper around sandbox visual states Hoists the UI state up to App.kt to allow for manual reset each time a different frame is selected --- .../com/squareup/workflow1/traceviewer/App.kt | 21 ++++++++++++++++++- .../traceviewer/util/SandboxBackground.kt | 19 ++++++++--------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt index 2bcdb7a8cd..797f263625 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt @@ -2,6 +2,7 @@ package com.squareup.workflow1.traceviewer import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -9,6 +10,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import com.squareup.workflow1.traceviewer.model.Node import com.squareup.workflow1.traceviewer.ui.FrameSelectTab import com.squareup.workflow1.traceviewer.ui.RenderDiagram @@ -28,13 +30,20 @@ public fun App( var selectedNode by remember { mutableStateOf(null) } var workflowFrames by remember { mutableStateOf>(emptyList()) } var frameIndex by remember { mutableIntStateOf(0) } + val sandboxState = remember { SandboxState() } + + LaunchedEffect(frameIndex) { + sandboxState.reset() + } Box( modifier = modifier ) { // Main content if (selectedTraceFile != null) { - SandboxBackground { + SandboxBackground( + sandboxState = sandboxState, + ) { RenderDiagram( traceFile = selectedTraceFile!!, frameInd = frameIndex, @@ -64,3 +73,13 @@ public fun App( ) } } + +class SandboxState { + var offset by mutableStateOf(Offset.Zero) + var scale by mutableStateOf(1f) + + fun reset() { + offset = Offset.Zero + scale = 1f + } +} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt index 822584c8e8..a4ee07d5c4 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput +import com.squareup.workflow1.traceviewer.SandboxState /** * This is the backdrop for the whole app. Since there can be hundreds of modules at a time, there @@ -27,19 +28,17 @@ import androidx.compose.ui.input.pointer.pointerInput */ @Composable public fun SandboxBackground( + sandboxState: SandboxState, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { - var scale by remember { mutableFloatStateOf(1f) } - var offset by remember { mutableStateOf(Offset.Zero) } - Box( modifier .fillMaxSize() .pointerInput(Unit) { // Panning capabilities: watches for drag gestures and applies the translation detectDragGestures { _, translation -> - offset += translation + sandboxState.offset += translation } } .pointerInput(Unit) { @@ -49,8 +48,8 @@ public fun SandboxBackground( val event = awaitPointerEvent() if (event.type == PointerEventType.Scroll) { val scrollDelta = event.changes.first().scrollDelta.y - scale *= if (scrollDelta < 0) 1.1f else 0.9f - scale = scale.coerceIn(0.1f, 10f) + sandboxState.scale = (sandboxState.scale * if (scrollDelta < 0) 1.1f else 0.9f) + .coerceIn(0.1f, 10f) event.changes.forEach { it.consume() } } } @@ -60,10 +59,10 @@ public fun SandboxBackground( modifier = Modifier .wrapContentSize(unbounded = true, align = Alignment.Center) .graphicsLayer { - translationX = offset.x - translationY = offset.y - scaleX = scale - scaleY = scale + translationX = sandboxState.offset.x + translationY = sandboxState.offset.y + scaleX = sandboxState.scale + scaleY = sandboxState.scale } ) { content() From 7285e687a92da7cac4d9b50ad94f5ed1c9c0ebbf Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Tue, 8 Jul 2025 12:15:47 -0400 Subject: [PATCH 02/20] Change sidebar functionality 1. Force it to consume all pointerInput, meaning there cannot be interactino with any objects shadowed by it. 2. Hoist alignment into App.kt instead of forcing it to the right with a full Spacer --- .../com/squareup/workflow1/traceviewer/App.kt | 6 +++++- .../traceviewer/ui/WorkflowInfoPanel.kt | 16 ++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt index 797f263625..f08d2fa1d6 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt @@ -60,7 +60,11 @@ public fun App( modifier = Modifier.align(Alignment.TopCenter) ) - RightInfoPanel(selectedNode) + RightInfoPanel( + selectedNode = selectedNode, + modifier = Modifier + .align(Alignment.TopEnd) + ) // The states are reset when a new file is selected. UploadFile( diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt index 82317b00e2..3fee91ed6d 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt @@ -1,9 +1,9 @@ package com.squareup.workflow1.traceviewer.ui import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -22,6 +22,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -40,9 +41,9 @@ public fun RightInfoPanel( modifier: Modifier = Modifier ) { // This row is aligned to the right of the screen. - Row { - Spacer(modifier = Modifier.weight(1f)) - + Row( + modifier = modifier + ) { var panelOpen by remember { mutableStateOf(false) } IconButton( @@ -62,7 +63,7 @@ public fun RightInfoPanel( if (panelOpen) { NodePanelDetails( selectedNode, - Modifier.fillMaxWidth(.35f) + Modifier.fillMaxWidth(.30f) ) } } @@ -77,7 +78,10 @@ private fun NodePanelDetails( modifier = modifier .fillMaxHeight() .background(Color.LightGray) - .padding(8.dp), + .padding(8.dp) + .pointerInput(Unit) { + detectTapGestures { } + }, horizontalAlignment = Alignment.CenterHorizontally ) { if (node == null) { From 7070d83df91a6973bf1fa34caec7c3f86a2318ea Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Tue, 8 Jul 2025 17:05:07 -0400 Subject: [PATCH 03/20] Build one main workflow runtime tree Currently this builds one tree which is an amalgamation of all the frames. It uses a recursive algorithm to fill in missing children nodes after each renderpass has been parsed --- .../com/squareup/workflow1/traceviewer/App.kt | 1 + .../workflow1/traceviewer/model/Node.kt | 4 +- .../workflow1/traceviewer/ui/WorkflowTree.kt | 5 ++- .../workflow1/traceviewer/util/JsonParser.kt | 38 ++++++++++++++++--- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt index f08d2fa1d6..8531d9e33e 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt @@ -72,6 +72,7 @@ public fun App( selectedTraceFile = it selectedNode = null frameIndex = 0 + workflowFrames = emptyList() }, modifier = Modifier.align(Alignment.BottomStart) ) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt index 2e5b7a7fcb..4c6973c437 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt @@ -17,8 +17,9 @@ public class Node( val renderings: Any? = null, val children: List, ) { + override fun toString(): String { - return "Node(name='$name', parent='$parent', children=${children.size})" + return "Node(name='$name', parent='$parent', children=${children})" } override fun equals(other: Any?): Boolean { @@ -26,6 +27,7 @@ public class Node( if (other !is Node) return false return this.id == other.id } + override fun hashCode(): Int { return id.hashCode() } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt index e2b12817f4..5cbf630315 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt @@ -39,6 +39,7 @@ public fun RenderDiagram( var frames by remember { mutableStateOf>(emptyList()) } var isLoading by remember(traceFile) { mutableStateOf(true) } var error by remember(traceFile) { mutableStateOf(null) } + var mainTree by remember { mutableStateOf(null) } LaunchedEffect(traceFile) { val parseResult = parseTrace(traceFile) @@ -50,6 +51,7 @@ public fun RenderDiagram( is ParseResult.Success -> { val parsedFrames = parseResult.trace ?: emptyList() frames = parsedFrames + mainTree = parseResult.mainTree onFileParse(parsedFrames) isLoading = false } @@ -62,7 +64,8 @@ public fun RenderDiagram( } if (!isLoading) { - DrawTree(frames[frameInd], onNodeSelect) + // DrawTree(frames[frameInd], onNodeSelect) + DrawTree(mainTree!!, onNodeSelect) } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt index 9c2aea472c..f30b3f9c3a 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt @@ -29,13 +29,23 @@ public suspend fun parseTrace( val workflowAdapter = createMoshiAdapter() val parsedRenderPasses = workflowAdapter.fromJson(jsonString) - val parsedFrames = mutableListOf() + var mainWorkflowTree: Node? = null + val parsedFrame = mutableListOf() parsedRenderPasses?.forEach { renderPass -> val parsed = getFrameFromRenderPass(renderPass) - parsedFrames.add(parsed) + if (mainWorkflowTree == null) { + mainWorkflowTree = parsed + } else { + mergeFrameIntoMainTree(parsed, mainWorkflowTree!!) + } + parsedFrame.add(parsed) } - - ParseResult.Success(parsedFrames) + /* + this parsing method can never be called without a provided file, so we can assume that there + will always be at least one render pass in the trace. If not, then Moshi would catch any + malformed JSON and throw an error beforehand. + */ + ParseResult.Success(parsedFrame, mainWorkflowTree!!) } catch (e: Exception) { ParseResult.Failure(e) } @@ -83,7 +93,25 @@ private fun buildTree(node: Node, childrenByParent: Map>): No ) } +/** + * Every new frame starts with the same roots as the main tree, so we can do a simple traversal to + * add any missing child nodes from the frame. + */ +private fun mergeFrameIntoMainTree( + frame: Node, + main: Node +) { + val children = frame.children + children.forEach { child -> + if (child in main.children) { + mergeFrameIntoMainTree(child, main.children.find { it.id == child.id }!!) + } else { + main.children.add(child) + } + } +} + sealed interface ParseResult { - class Success(val trace: List?) : ParseResult + class Success(val trace: List?, val mainTree: Node) : ParseResult class Failure(val error: Throwable) : ParseResult } From c6264da423143ecbe1bf59438fe77768ac61e979 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Tue, 8 Jul 2025 17:37:33 -0400 Subject: [PATCH 04/20] Normalize scroll zoom sensitivity for mouse vs touchpad --- .../squareup/workflow1/traceviewer/util/SandboxBackground.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt index a4ee07d5c4..96d321ecbb 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt @@ -48,8 +48,8 @@ public fun SandboxBackground( val event = awaitPointerEvent() if (event.type == PointerEventType.Scroll) { val scrollDelta = event.changes.first().scrollDelta.y - sandboxState.scale = (sandboxState.scale * if (scrollDelta < 0) 1.1f else 0.9f) - .coerceIn(0.1f, 10f) + val factor = 1f + (-scrollDelta * 0.1f) + sandboxState.scale = (sandboxState.scale * factor).coerceIn(0.1f, 10f) event.changes.forEach { it.consume() } } } From 193901d17acfbb6ace3b1c4bcbd223b4da694aa6 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Wed, 9 Jul 2025 09:55:29 -0400 Subject: [PATCH 05/20] Include a list of trees that pairs with parsed frames These can be viewed as pairs: for each frame, there is the full workflow tree that is slowly built up with the new nodes. Add a copy method onto Node class to store a current copy of the workflow tree. --- .../workflow1/traceviewer/model/Node.kt | 17 ++++++++++++++--- .../traceviewer/ui/WorkflowInfoPanel.kt | 6 +++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt index 4c6973c437..a4a7ecb21f 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt @@ -9,15 +9,26 @@ package com.squareup.workflow1.traceviewer.model */ public class Node( val name: String, - val id: String, val parent: String, - val parentId: String, val props: Any? = null, val state: Any? = null, val renderings: Any? = null, - val children: List, + val children: MutableList, + val id: String ) { + fun copy(): Node { + return Node( + name = name, + parent = parent, + props = props, + state = state, + rendering = rendering, + children = children.map { it.copy() }.toMutableList(), + id = id + ) + } + override fun toString(): String { return "Node(name='$name', parent='$parent', children=${children})" } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt index 3fee91ed6d..e425658641 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt @@ -94,9 +94,9 @@ private fun NodePanelDetails( val fields = mapOf( "Name" to node.name, "ID" to node.id, - "Props" to node.props.toString(), - "State" to node.state.toString(), - "Renderings" to node.renderings.toString() + "Props" to node.props, + "State" to node.state, + "Rendering" to node.rendering ) fields.forEach { (label, value) -> From a262f7b5dc6860707bb13f487a1bda5670883d4b Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Wed, 9 Jul 2025 12:50:10 -0400 Subject: [PATCH 06/20] WIP --- .../api/workflow-trace-viewer.api | 55 ++------- workflow-trace-viewer/build.gradle.kts | 15 +++ .../com/squareup/workflow1/traceviewer/App.kt | 2 +- .../workflow1/traceviewer/model/Node.kt | 26 +++-- .../traceviewer/ui/FrameSelectTab.kt | 2 +- .../traceviewer/ui/WorkflowInfoPanel.kt | 2 +- .../workflow1/traceviewer/ui/WorkflowTree.kt | 4 +- .../workflow1/traceviewer/util/JsonParser.kt | 87 ++++++++++----- .../traceviewer/util/SandboxBackground.kt | 2 +- .../traceviewer/util/JsonParserTest.kt | 105 ++++++++++++++++++ 10 files changed, 209 insertions(+), 91 deletions(-) create mode 100644 workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt diff --git a/workflow-trace-viewer/api/workflow-trace-viewer.api b/workflow-trace-viewer/api/workflow-trace-viewer.api index ee30c6ebaa..f05b8b6742 100644 --- a/workflow-trace-viewer/api/workflow-trace-viewer.api +++ b/workflow-trace-viewer/api/workflow-trace-viewer.api @@ -14,33 +14,14 @@ public final class com/squareup/workflow1/traceviewer/MainKt { public static synthetic fun main ([Ljava/lang/String;)V } -public final class com/squareup/workflow1/traceviewer/model/Node { +public final class com/squareup/workflow1/traceviewer/SandboxState { public static final field $stable I - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/List;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun equals (Ljava/lang/Object;)Z - public final fun getChildren ()Ljava/util/List; - public final fun getId ()Ljava/lang/String; - public final fun getName ()Ljava/lang/String; - public final fun getParent ()Ljava/lang/String; - public final fun getParentId ()Ljava/lang/String; - public final fun getProps ()Ljava/lang/Object; - public final fun getRenderings ()Ljava/lang/Object; - public final fun getState ()Ljava/lang/Object; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class com/squareup/workflow1/traceviewer/ui/FrameSelectTabKt { - public static final fun FrameSelectTab (Ljava/util/List;ILkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V -} - -public final class com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanelKt { - public static final fun RightInfoPanel (Lcom/squareup/workflow1/traceviewer/model/Node;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V -} - -public final class com/squareup/workflow1/traceviewer/ui/WorkflowTreeKt { - public static final fun RenderDiagram (Lio/github/vinceglb/filekit/PlatformFile;ILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V + public fun ()V + public final fun getOffset-F1C5BW0 ()J + public final fun getScale ()F + public final fun reset ()V + public final fun setOffset-k-4lQ0M (J)V + public final fun setScale (F)V } public final class com/squareup/workflow1/traceviewer/util/ComposableSingletons$UploadFileKt { @@ -50,28 +31,8 @@ public final class com/squareup/workflow1/traceviewer/util/ComposableSingletons$ public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; } -public final class com/squareup/workflow1/traceviewer/util/JsonParserKt { - public static final field ROOT_ID Ljava/lang/String; - public static final fun parseTrace (Lio/github/vinceglb/filekit/PlatformFile;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; -} - -public abstract interface class com/squareup/workflow1/traceviewer/util/ParseResult { -} - -public final class com/squareup/workflow1/traceviewer/util/ParseResult$Failure : com/squareup/workflow1/traceviewer/util/ParseResult { - public static final field $stable I - public fun (Ljava/lang/Throwable;)V - public final fun getError ()Ljava/lang/Throwable; -} - -public final class com/squareup/workflow1/traceviewer/util/ParseResult$Success : com/squareup/workflow1/traceviewer/util/ParseResult { - public static final field $stable I - public fun (Ljava/util/List;)V - public final fun getTrace ()Ljava/util/List; -} - public final class com/squareup/workflow1/traceviewer/util/SandboxBackgroundKt { - public static final fun SandboxBackground (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V + public static final fun SandboxBackground (Lcom/squareup/workflow1/traceviewer/SandboxState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V } public final class com/squareup/workflow1/traceviewer/util/UploadFileKt { diff --git a/workflow-trace-viewer/build.gradle.kts b/workflow-trace-viewer/build.gradle.kts index c0a63cc870..87c6759d3b 100644 --- a/workflow-trace-viewer/build.gradle.kts +++ b/workflow-trace-viewer/build.gradle.kts @@ -27,6 +27,14 @@ kotlin { implementation(libs.filekit.dialogs.compose) } } + + jvmTest { + dependencies { + implementation(kotlin("test")) + implementation(kotlin("test-junit5")) + implementation("org.junit.jupiter:junit-jupiter:5.9.2") + } + } } } @@ -50,3 +58,10 @@ compose { } } } + +tasks.named("jvmTest") { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } +} \ No newline at end of file diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt index 8531d9e33e..14c8e13c5a 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt @@ -79,7 +79,7 @@ public fun App( } } -class SandboxState { +internal class SandboxState { var offset by mutableStateOf(Offset.Zero) var scale by mutableStateOf(1f) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt index a4a7ecb21f..3096df4d6a 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt @@ -7,25 +7,27 @@ package com.squareup.workflow1.traceviewer.model * * TBD what more metadata should be involved with each node, e.g. (props, states, # of render passes) */ -public class Node( +internal data class Node( val name: String, + val id: String, val parent: String, - val props: Any? = null, - val state: Any? = null, - val renderings: Any? = null, - val children: MutableList, - val id: String + val parentId: String, + val props: String, + val state: String, + val rendering: String = "", + val children: List, ) { fun copy(): Node { return Node( name = name, + id = id, parent = parent, + parentId = parentId, props = props, state = state, rendering = rendering, - children = children.map { it.copy() }.toMutableList(), - id = id + children = children, ) } @@ -43,3 +45,11 @@ public class Node( return id.hashCode() } } + +internal fun Node.addChild(child: Node): Node { + return copy( children = this.children + child ) +} + +internal fun Node.replaceChild(child: Node): Node { + return copy(children = this.children.map { if (it.id == child.id) child else it }) +} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt index fbc8a08a4e..c811e7168e 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt @@ -18,7 +18,7 @@ import com.squareup.workflow1.traceviewer.model.Node * A trace tab selector that allows devs to switch between different states within the provided trace. */ @Composable -public fun FrameSelectTab( +internal fun FrameSelectTab( frames: List, currentIndex: Int, onIndexChange: (Int) -> Unit, diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt index e425658641..1a89820ee7 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt @@ -36,7 +36,7 @@ import com.squareup.workflow1.traceviewer.model.Node * @param selectedNode The currently selected workflow node, or null if no node is selected. */ @Composable -public fun RightInfoPanel( +internal fun RightInfoPanel( selectedNode: Node?, modifier: Modifier = Modifier ) { diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt index 5cbf630315..8d2b8c9115 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt @@ -29,7 +29,7 @@ import io.github.vinceglb.filekit.PlatformFile * tabs. This will also all errors related to errors parsing a given trace JSON file. */ @Composable -public fun RenderDiagram( +internal fun RenderDiagram( traceFile: PlatformFile, frameInd: Int, onFileParse: (List) -> Unit, @@ -51,7 +51,7 @@ public fun RenderDiagram( is ParseResult.Success -> { val parsedFrames = parseResult.trace ?: emptyList() frames = parsedFrames - mainTree = parseResult.mainTree + mainTree = parseResult.trees.first() onFileParse(parsedFrames) isLoading = false } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt index f30b3f9c3a..ca438bcde6 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt @@ -5,6 +5,8 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.squareup.workflow1.traceviewer.model.Node +import com.squareup.workflow1.traceviewer.model.addChild +import com.squareup.workflow1.traceviewer.model.replaceChild import io.github.vinceglb.filekit.PlatformFile import io.github.vinceglb.filekit.readString @@ -21,34 +23,50 @@ const val ROOT_ID: String = "-1" * @return A [ParseResult] representing result of parsing, either an error related to the * format of the JSON, or a success and a parsed trace. */ -public suspend fun parseTrace( +internal suspend fun parseTrace( file: PlatformFile, ): ParseResult { - return try { - val jsonString = file.readString() - val workflowAdapter = createMoshiAdapter() - val parsedRenderPasses = workflowAdapter.fromJson(jsonString) - - var mainWorkflowTree: Node? = null - val parsedFrame = mutableListOf() - parsedRenderPasses?.forEach { renderPass -> - val parsed = getFrameFromRenderPass(renderPass) - if (mainWorkflowTree == null) { - mainWorkflowTree = parsed - } else { - mergeFrameIntoMainTree(parsed, mainWorkflowTree!!) - } - parsedFrame.add(parsed) - } + val jsonString = file.readString() + val workflowAdapter = createMoshiAdapter() + val parsedRenderPasses = try { + workflowAdapter.fromJson(jsonString) ?: return ParseResult.Failure( + IllegalArgumentException("The provided file does not contain a valid trace.") + ) /* this parsing method can never be called without a provided file, so we can assume that there will always be at least one render pass in the trace. If not, then Moshi would catch any malformed JSON and throw an error beforehand. */ - ParseResult.Success(parsedFrame, mainWorkflowTree!!) } catch (e: Exception) { - ParseResult.Failure(e) + return ParseResult.Failure(e) + } + + var mainWorkflowTree: Node? = null + // var parsedTrace = mutableListOf() + val frameTrees = mutableListOf() + // unParsedTrace.forEach { renderPass -> + // val parsed = getFrameFromRenderPass(renderPass) + // if (mainWorkflowTree == null) { + // mainWorkflowTree = parsed + // } else { + // mergeFrameIntoMainTree(parsed, mainWorkflowTree!!) + // } + // parsedTrace.add(parsed) + // frameTrees.add(mainWorkflowTree!!.copy()) + // } + + val parsedTrace = parsedRenderPasses.map { renderPass -> getFrameFromRenderPass(renderPass)} + + parsedTrace.fold(parsedTrace[0]) { tree, frame -> + // We assume that the first render pass is the main workflow tree. + // val parsedFrame = getFrameFromRenderPass(unParsedRenderPass) + val mergedTree = mergeFrameIntoMainTree(frame, tree) + // parsedTrace.add(parsedFrame) + frameTrees.add(mergedTree) + mergedTree } + + return ParseResult.Success(parsedTrace, frameTrees) } /** @@ -97,21 +115,30 @@ private fun buildTree(node: Node, childrenByParent: Map>): No * Every new frame starts with the same roots as the main tree, so we can do a simple traversal to * add any missing child nodes from the frame. */ -private fun mergeFrameIntoMainTree( +internal fun mergeFrameIntoMainTree( frame: Node, main: Node -) { - val children = frame.children - children.forEach { child -> - if (child in main.children) { - mergeFrameIntoMainTree(child, main.children.find { it.id == child.id }!!) - } else { - main.children.add(child) - } +) : Node { + if (frame.id != main.id) { + throw IllegalArgumentException("Frame root ID does not match main tree root ID.") + } + + return frame.children.fold(main) { mergedTree, child -> + // println(mergedTree) + val parent = mergedTree.children.singleOrNull() { it.id == child.id } + + if (parent != null) { + // println("Merging child ${child.id} into parent ${parent.id}") + mergedTree.replaceChild(mergeFrameIntoMainTree(child, parent)) + } else { + // println("Adding child ${child.id} to merged tree ${mergedTree.id}") + mergedTree.addChild(child) + } } + // println("Merged tree: $it")} } -sealed interface ParseResult { - class Success(val trace: List?, val mainTree: Node) : ParseResult +internal sealed interface ParseResult { + class Success(val trace: List?, val trees: List) : ParseResult class Failure(val error: Throwable) : ParseResult } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt index 96d321ecbb..90dbcf5670 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt @@ -27,7 +27,7 @@ import com.squareup.workflow1.traceviewer.SandboxState * */ @Composable -public fun SandboxBackground( +internal fun SandboxBackground( sandboxState: SandboxState, modifier: Modifier = Modifier, content: @Composable () -> Unit, diff --git a/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt b/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt new file mode 100644 index 0000000000..d72f2c2550 --- /dev/null +++ b/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt @@ -0,0 +1,105 @@ +package com.squareup.workflow1.traceviewer.util + +import com.squareup.workflow1.traceviewer.model.Node +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class JsonParserTest { + + @Test + fun `test mergeFrameIntoMainTree with new children`() { + // Create main tree with one child + val mainChild = createNode("child1", "root", "1") + val mainTree = createNode("root", "root", "0", mutableListOf(mainChild)) + + // Create frame with a new child + val frameChild1 = createNode("child1", "root", "1") + val frameChild2 = createNode("child2", "root", "2") + val frame = createNode("root", "root", "0", mutableListOf(frameChild1, frameChild2)) + + // Merge frame into main tree + val mergedTree = mergeFrameIntoMainTree(frame, mainTree) + + // Verify results + assertEquals(2, mergedTree.children.size) + assertTrue(mergedTree.children.any { it.id == "1" }) + assertTrue(mergedTree.children.any { it.id == "2" }) + } + + @Test + fun `test mergeFrameIntoMainTree with nested children`() { + // Create main tree with nested structure + val nestedChild = createNode("nested1", "child1", "2") + val mainChild = createNode("child1", "root", "1", mutableListOf(nestedChild)) + val mainTree = createNode("root", "root", "0", mutableListOf(mainChild)) + + // Create frame with new nested child + val frameNestedChild1 = createNode("nested1", "child1", "2") + val frameNestedChild2 = createNode("nested2", "child1", "3") + val frameChild = createNode("child1", "root", "1", + mutableListOf(frameNestedChild1, frameNestedChild2)) + val frame = createNode("root", "root", "0", mutableListOf(frameChild)) + + // Merge frame into main tree + val mergedTree = mergeFrameIntoMainTree(frame, mainTree) + + // Verify results + assertEquals(1, mergedTree.children.size) + val updatedChild = mergedTree.children.first() + assertEquals(2, updatedChild.children.size) + assertTrue(updatedChild.children.any { it.id == "2" }) + assertTrue(updatedChild.children.any { it.id == "3" }) + } + + @Test + fun `test mergeFrameIntoMainTree with empty main tree children`() { + // Create empty main tree + val mainTree = createNode("root", "root", "0") + + // Create frame with children + val frameChild = createNode("child1", "root", "1") + val frame = createNode("root", "root", "0", mutableListOf(frameChild)) + + // Merge frame into main tree + val mergedTree = mergeFrameIntoMainTree(frame, mainTree) + + // Verify results + assertEquals(1, mergedTree.children.size) + assertEquals("child1", mergedTree.children.first().name) + } + + @Test + fun `test mergeFrameIntoMainTree with empty frame children`() { + // Create main tree with children + val mainChild = createNode("child1", "root", "1") + val mainTree = createNode("root", "root", "0", mutableListOf(mainChild)) + + // Create empty frame + val frame = createNode("root", "root", "0") + + // Merge frame into main tree + mergeFrameIntoMainTree(frame, mainTree) + + // Verify results + assertEquals(1, mainTree.children.size) + assertEquals("child1", mainTree.children.first().name) + } + + private fun createNode( + name: String, + parent: String, + id: String, + children: MutableList = mutableListOf() + ): Node { + return Node( + name = name, + parent = parent, + props = "", + state = "", + rendering = "", + children = children, + id = id + ) + } +} From ef6ba80630385de781db88a24e7ec79773fa4f18 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Wed, 9 Jul 2025 13:39:53 -0400 Subject: [PATCH 07/20] Generate full tree diagrams --- .../workflow1/traceviewer/model/Node.kt | 13 ------- .../workflow1/traceviewer/ui/WorkflowTree.kt | 4 +- .../workflow1/traceviewer/util/JsonParser.kt | 37 ++++--------------- 3 files changed, 10 insertions(+), 44 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt index 3096df4d6a..759494ab7c 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt @@ -18,19 +18,6 @@ internal data class Node( val children: List, ) { - fun copy(): Node { - return Node( - name = name, - id = id, - parent = parent, - parentId = parentId, - props = props, - state = state, - rendering = rendering, - children = children, - ) - } - override fun toString(): String { return "Node(name='$name', parent='$parent', children=${children})" } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt index 8d2b8c9115..a3baab10d0 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt @@ -40,6 +40,7 @@ internal fun RenderDiagram( var isLoading by remember(traceFile) { mutableStateOf(true) } var error by remember(traceFile) { mutableStateOf(null) } var mainTree by remember { mutableStateOf(null) } + var fullTree by remember { mutableStateOf>(emptyList()) } LaunchedEffect(traceFile) { val parseResult = parseTrace(traceFile) @@ -52,6 +53,7 @@ internal fun RenderDiagram( val parsedFrames = parseResult.trace ?: emptyList() frames = parsedFrames mainTree = parseResult.trees.first() + fullTree = parseResult.trees onFileParse(parsedFrames) isLoading = false } @@ -65,7 +67,7 @@ internal fun RenderDiagram( if (!isLoading) { // DrawTree(frames[frameInd], onNodeSelect) - DrawTree(mainTree!!, onNodeSelect) + DrawTree(fullTree[frameInd], onNodeSelect) } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt index ca438bcde6..16f6e703ba 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt @@ -41,31 +41,13 @@ internal suspend fun parseTrace( return ParseResult.Failure(e) } - var mainWorkflowTree: Node? = null - // var parsedTrace = mutableListOf() - val frameTrees = mutableListOf() - // unParsedTrace.forEach { renderPass -> - // val parsed = getFrameFromRenderPass(renderPass) - // if (mainWorkflowTree == null) { - // mainWorkflowTree = parsed - // } else { - // mergeFrameIntoMainTree(parsed, mainWorkflowTree!!) - // } - // parsedTrace.add(parsed) - // frameTrees.add(mainWorkflowTree!!.copy()) - // } - val parsedTrace = parsedRenderPasses.map { renderPass -> getFrameFromRenderPass(renderPass)} - + val frameTrees = mutableListOf() parsedTrace.fold(parsedTrace[0]) { tree, frame -> - // We assume that the first render pass is the main workflow tree. - // val parsedFrame = getFrameFromRenderPass(unParsedRenderPass) val mergedTree = mergeFrameIntoMainTree(frame, tree) - // parsedTrace.add(parsedFrame) frameTrees.add(mergedTree) mergedTree } - return ParseResult.Success(parsedTrace, frameTrees) } @@ -124,18 +106,13 @@ internal fun mergeFrameIntoMainTree( } return frame.children.fold(main) { mergedTree, child -> - // println(mergedTree) - val parent = mergedTree.children.singleOrNull() { it.id == child.id } - - if (parent != null) { - // println("Merging child ${child.id} into parent ${parent.id}") - mergedTree.replaceChild(mergeFrameIntoMainTree(child, parent)) - } else { - // println("Adding child ${child.id} to merged tree ${mergedTree.id}") - mergedTree.addChild(child) - } + val parent = mergedTree.children.singleOrNull { it.id == child.id } + if (parent != null) { + mergedTree.replaceChild(mergeFrameIntoMainTree(child, parent)) + } else { + mergedTree.addChild(child) + } } - // println("Merged tree: $it")} } internal sealed interface ParseResult { From c24329248e30ed3e1ded6448b7482c599e7c3559 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Wed, 9 Jul 2025 13:53:11 -0400 Subject: [PATCH 08/20] Highlight each render pass detail within the full tree --- .../workflow1/traceviewer/ui/WorkflowTree.kt | 19 ++++++++++++------- .../workflow1/traceviewer/util/JsonParser.kt | 6 ++++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt index a3baab10d0..7a47f3c8a6 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt @@ -1,5 +1,6 @@ package com.squareup.workflow1.traceviewer.ui +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -36,11 +37,11 @@ internal fun RenderDiagram( onNodeSelect: (Node) -> Unit, modifier: Modifier = Modifier ) { - var frames by remember { mutableStateOf>(emptyList()) } var isLoading by remember(traceFile) { mutableStateOf(true) } var error by remember(traceFile) { mutableStateOf(null) } - var mainTree by remember { mutableStateOf(null) } + var frames by remember { mutableStateOf>(emptyList()) } var fullTree by remember { mutableStateOf>(emptyList()) } + var affectedNodes by remember { mutableStateOf>>(emptyList()) } LaunchedEffect(traceFile) { val parseResult = parseTrace(traceFile) @@ -52,8 +53,8 @@ internal fun RenderDiagram( is ParseResult.Success -> { val parsedFrames = parseResult.trace ?: emptyList() frames = parsedFrames - mainTree = parseResult.trees.first() fullTree = parseResult.trees + affectedNodes = parseResult.affectedNodes onFileParse(parsedFrames) isLoading = false } @@ -66,8 +67,8 @@ internal fun RenderDiagram( } if (!isLoading) { - // DrawTree(frames[frameInd], onNodeSelect) - DrawTree(fullTree[frameInd], onNodeSelect) + // DrawTree(frames[frameInd], affectedNodes[frameInd], onNodeSelect) + DrawTree(fullTree[frameInd], affectedNodes[frameInd], onNodeSelect) } } @@ -78,6 +79,7 @@ internal fun RenderDiagram( @Composable private fun DrawTree( node: Node, + affectedNodes: Set, onNodeSelect: (Node) -> Unit, modifier: Modifier = Modifier, ) { @@ -88,7 +90,8 @@ private fun DrawTree( .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { - DrawNode(node, onNodeSelect) + val isAffected = affectedNodes.contains(node) + DrawNode(node, isAffected, onNodeSelect) // Draws the node's children recursively. Row( @@ -96,7 +99,7 @@ private fun DrawTree( verticalAlignment = Alignment.Top ) { node.children.forEach { childNode -> - DrawTree(childNode, onNodeSelect) + DrawTree(childNode, affectedNodes, onNodeSelect) } } } @@ -108,10 +111,12 @@ private fun DrawTree( @Composable private fun DrawNode( node: Node, + isAffected: Boolean, onNodeSelect: (Node) -> Unit, ) { Box( modifier = Modifier + .background(if (isAffected) Color.Green else Color.Transparent) .clickable { // Selecting a node will bubble back up to the main view to handle the selection onNodeSelect(node) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt index 16f6e703ba..c2988c3118 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt @@ -48,7 +48,7 @@ internal suspend fun parseTrace( frameTrees.add(mergedTree) mergedTree } - return ParseResult.Success(parsedTrace, frameTrees) + return ParseResult.Success(parsedTrace, frameTrees, parsedRenderPasses) } /** @@ -116,6 +116,8 @@ internal fun mergeFrameIntoMainTree( } internal sealed interface ParseResult { - class Success(val trace: List?, val trees: List) : ParseResult + class Success(val trace: List?, val trees: List, affectedNodes: List>) : ParseResult { + val affectedNodes = affectedNodes.map { it.toSet() } + } class Failure(val error: Throwable) : ParseResult } From 7481f369986c3a99a4fdb1cd5175d07a388a86e7 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Thu, 10 Jul 2025 09:55:16 -0400 Subject: [PATCH 09/20] Change junit dependency to library catalog --- gradle/libs.versions.toml | 2 ++ workflow-trace-viewer/build.gradle.kts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 03264b2c87..d386e9c112 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,6 +51,7 @@ google-material = "1.4.0" groovy = "3.0.9" jUnit = "4.13.2" +junitJupiter = "5.10.1" java-diff-utils = "4.12" javaParser = "3.24.0" jetbrains-compose-plugin = "1.7.3" @@ -205,6 +206,7 @@ java-diff-utils = { module = "io.github.java-diff-utils:java-diff-utils", versio jetbrains-annotations = "org.jetbrains:annotations:24.0.1" junit = { module = "junit:junit", version.ref = "jUnit" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junitJupiter" } kgx = { module = "com.rickbusarow.kgx:kotlin-gradle-extensions", version.ref = "kgx" } diff --git a/workflow-trace-viewer/build.gradle.kts b/workflow-trace-viewer/build.gradle.kts index 87c6759d3b..100a0f4b5b 100644 --- a/workflow-trace-viewer/build.gradle.kts +++ b/workflow-trace-viewer/build.gradle.kts @@ -32,7 +32,7 @@ kotlin { dependencies { implementation(kotlin("test")) implementation(kotlin("test-junit5")) - implementation("org.junit.jupiter:junit-jupiter:5.9.2") + implementation(libs.junit.jupiter) } } } @@ -64,4 +64,4 @@ tasks.named("jvmTest") { testLogging { events("passed", "skipped", "failed") } -} \ No newline at end of file +} From 13fa318ba061ef0d519efb3487b58acfeb0db39f Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Thu, 10 Jul 2025 10:18:15 -0400 Subject: [PATCH 10/20] Clean up comments and fix build errors --- .../api/workflow-trace-viewer.api | 14 ++--------- workflow-trace-viewer/build.gradle.kts | 9 ++++---- .../com/squareup/workflow1/traceviewer/App.kt | 2 +- .../traceviewer/ui/WorkflowInfoPanel.kt | 1 - .../workflow1/traceviewer/ui/WorkflowTree.kt | 1 - .../workflow1/traceviewer/util/JsonParser.kt | 10 +++++--- .../workflow1/traceviewer/util/UploadFile.kt | 4 ++-- .../traceviewer/util/JsonParserTest.kt | 23 +++++++++++-------- 8 files changed, 29 insertions(+), 35 deletions(-) diff --git a/workflow-trace-viewer/api/workflow-trace-viewer.api b/workflow-trace-viewer/api/workflow-trace-viewer.api index f05b8b6742..abc17e5ffc 100644 --- a/workflow-trace-viewer/api/workflow-trace-viewer.api +++ b/workflow-trace-viewer/api/workflow-trace-viewer.api @@ -14,16 +14,6 @@ public final class com/squareup/workflow1/traceviewer/MainKt { public static synthetic fun main ([Ljava/lang/String;)V } -public final class com/squareup/workflow1/traceviewer/SandboxState { - public static final field $stable I - public fun ()V - public final fun getOffset-F1C5BW0 ()J - public final fun getScale ()F - public final fun reset ()V - public final fun setOffset-k-4lQ0M (J)V - public final fun setScale (F)V -} - public final class com/squareup/workflow1/traceviewer/util/ComposableSingletons$UploadFileKt { public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/util/ComposableSingletons$UploadFileKt; public static field lambda-1 Lkotlin/jvm/functions/Function3; @@ -31,8 +21,8 @@ public final class com/squareup/workflow1/traceviewer/util/ComposableSingletons$ public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; } -public final class com/squareup/workflow1/traceviewer/util/SandboxBackgroundKt { - public static final fun SandboxBackground (Lcom/squareup/workflow1/traceviewer/SandboxState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V +public final class com/squareup/workflow1/traceviewer/util/JsonParserKt { + public static final field ROOT_ID Ljava/lang/String; } public final class com/squareup/workflow1/traceviewer/util/UploadFileKt { diff --git a/workflow-trace-viewer/build.gradle.kts b/workflow-trace-viewer/build.gradle.kts index 100a0f4b5b..b7c4461dee 100644 --- a/workflow-trace-viewer/build.gradle.kts +++ b/workflow-trace-viewer/build.gradle.kts @@ -27,7 +27,6 @@ kotlin { implementation(libs.filekit.dialogs.compose) } } - jvmTest { dependencies { implementation(kotlin("test")) @@ -60,8 +59,8 @@ compose { } tasks.named("jvmTest") { - useJUnitPlatform() - testLogging { - events("passed", "skipped", "failed") - } + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt index 14c8e13c5a..3c01aa1411 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt @@ -68,7 +68,7 @@ public fun App( // The states are reset when a new file is selected. UploadFile( - onFileSelect = { + resetOnFileSelect = { selectedTraceFile = it selectedNode = null frameIndex = 0 diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt index 1a89820ee7..6b8544e065 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt @@ -40,7 +40,6 @@ internal fun RightInfoPanel( selectedNode: Node?, modifier: Modifier = Modifier ) { - // This row is aligned to the right of the screen. Row( modifier = modifier ) { diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt index 7a47f3c8a6..5a5eb5f018 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt @@ -67,7 +67,6 @@ internal fun RenderDiagram( } if (!isLoading) { - // DrawTree(frames[frameInd], affectedNodes[frameInd], onNodeSelect) DrawTree(fullTree[frameInd], affectedNodes[frameInd], onNodeSelect) } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt index c2988c3118..2984700539 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt @@ -30,7 +30,7 @@ internal suspend fun parseTrace( val workflowAdapter = createMoshiAdapter() val parsedRenderPasses = try { workflowAdapter.fromJson(jsonString) ?: return ParseResult.Failure( - IllegalArgumentException("The provided file does not contain a valid trace.") + IllegalArgumentException("Provided trace file is empty or malformed.") ) /* this parsing method can never be called without a provided file, so we can assume that there @@ -73,6 +73,7 @@ private fun createMoshiAdapter(): JsonAdapter>> { */ private fun getFrameFromRenderPass(renderPass: List): Node { val childrenByParent: Map> = renderPass.groupBy { it.parentId } + println(childrenByParent) val root = childrenByParent[ROOT_ID]?.single() return buildTree(root!!, childrenByParent) } @@ -94,8 +95,11 @@ private fun buildTree(node: Node, childrenByParent: Map>): No } /** - * Every new frame starts with the same roots as the main tree, so we can do a simple traversal to - * add any missing child nodes from the frame. + * Every new frame starts with the same roots as the main tree, so we can fold each frame into the + * current tree, add all the missing children or replace any new ones, and then store the newly + * merged tree. + * + * @return Node the newly formed tree with the frame merged into it. */ internal fun mergeFrameIntoMainTree( frame: Node, diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/UploadFile.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/UploadFile.kt index 4a197e8f44..6daae36917 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/UploadFile.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/UploadFile.kt @@ -20,14 +20,14 @@ import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher */ @Composable public fun UploadFile( - onFileSelect: (PlatformFile?) -> Unit, + resetOnFileSelect: (PlatformFile?) -> Unit, modifier: Modifier = Modifier, ) { val launcher = rememberFilePickerLauncher( type = FileKitType.File(listOf("json", "txt")), title = "Select Workflow Trace File" ) { - onFileSelect(it) + resetOnFileSelect(it) } Button( diff --git a/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt b/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt index d72f2c2550..9f0997bb3b 100644 --- a/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt +++ b/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt @@ -1,6 +1,7 @@ package com.squareup.workflow1.traceviewer.util import com.squareup.workflow1.traceviewer.model.Node +import com.squareup.workflow1.traceviewer.util.ROOT_ID import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -11,12 +12,12 @@ class JsonParserTest { fun `test mergeFrameIntoMainTree with new children`() { // Create main tree with one child val mainChild = createNode("child1", "root", "1") - val mainTree = createNode("root", "root", "0", mutableListOf(mainChild)) + val mainTree = createNode("root", "root", "0", "0", listOf(mainChild)) // Create frame with a new child val frameChild1 = createNode("child1", "root", "1") val frameChild2 = createNode("child2", "root", "2") - val frame = createNode("root", "root", "0", mutableListOf(frameChild1, frameChild2)) + val frame = createNode("root", "root", "0", "0", listOf(frameChild1, frameChild2)) // Merge frame into main tree val mergedTree = mergeFrameIntoMainTree(frame, mainTree) @@ -31,15 +32,15 @@ class JsonParserTest { fun `test mergeFrameIntoMainTree with nested children`() { // Create main tree with nested structure val nestedChild = createNode("nested1", "child1", "2") - val mainChild = createNode("child1", "root", "1", mutableListOf(nestedChild)) - val mainTree = createNode("root", "root", "0", mutableListOf(mainChild)) + val mainChild = createNode("child1", "root", "1", "0", listOf(nestedChild)) + val mainTree = createNode("root", "root", "0", "0", listOf(mainChild)) // Create frame with new nested child val frameNestedChild1 = createNode("nested1", "child1", "2") val frameNestedChild2 = createNode("nested2", "child1", "3") - val frameChild = createNode("child1", "root", "1", + val frameChild = createNode("child1", "root", "1", "0", mutableListOf(frameNestedChild1, frameNestedChild2)) - val frame = createNode("root", "root", "0", mutableListOf(frameChild)) + val frame = createNode("root", "root", "0", "0", listOf(frameChild)) // Merge frame into main tree val mergedTree = mergeFrameIntoMainTree(frame, mainTree) @@ -59,7 +60,7 @@ class JsonParserTest { // Create frame with children val frameChild = createNode("child1", "root", "1") - val frame = createNode("root", "root", "0", mutableListOf(frameChild)) + val frame = createNode("root", "root", "0", "0", listOf(frameChild)) // Merge frame into main tree val mergedTree = mergeFrameIntoMainTree(frame, mainTree) @@ -73,7 +74,7 @@ class JsonParserTest { fun `test mergeFrameIntoMainTree with empty frame children`() { // Create main tree with children val mainChild = createNode("child1", "root", "1") - val mainTree = createNode("root", "root", "0", mutableListOf(mainChild)) + val mainTree = createNode("root", "root", "0", "0", listOf(mainChild)) // Create empty frame val frame = createNode("root", "root", "0") @@ -90,16 +91,18 @@ class JsonParserTest { name: String, parent: String, id: String, - children: MutableList = mutableListOf() + parentId: String = "0", + children: List = emptyList() ): Node { return Node( name = name, + id = id, parent = parent, + parentId = parentId, props = "", state = "", rendering = "", children = children, - id = id ) } } From 8f5514d3edd3157864557530b881f930261d0a0f Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Thu, 10 Jul 2025 11:31:31 -0400 Subject: [PATCH 11/20] Fix compose lint violations --- .../com/squareup/workflow1/traceviewer/App.kt | 3 +- .../workflow1/traceviewer/model/Node.kt | 4 +- .../workflow1/traceviewer/util/JsonParser.kt | 4 +- .../traceviewer/util/SandboxBackground.kt | 6 - .../traceviewer/util/JsonParserTest.kt | 193 +++++++++--------- 5 files changed, 102 insertions(+), 108 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt index 3c01aa1411..e5aa6959b9 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -81,7 +82,7 @@ public fun App( internal class SandboxState { var offset by mutableStateOf(Offset.Zero) - var scale by mutableStateOf(1f) + var scale by mutableFloatStateOf(1f) fun reset() { offset = Offset.Zero diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt index 759494ab7c..445c2fe4d6 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt @@ -19,7 +19,7 @@ internal data class Node( ) { override fun toString(): String { - return "Node(name='$name', parent='$parent', children=${children})" + return "Node(name='$name', parent='$parent', children=$children)" } override fun equals(other: Any?): Boolean { @@ -34,7 +34,7 @@ internal data class Node( } internal fun Node.addChild(child: Node): Node { - return copy( children = this.children + child ) + return copy(children = this.children + child) } internal fun Node.replaceChild(child: Node): Node { diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt index 2984700539..ecea8087c2 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt @@ -41,7 +41,7 @@ internal suspend fun parseTrace( return ParseResult.Failure(e) } - val parsedTrace = parsedRenderPasses.map { renderPass -> getFrameFromRenderPass(renderPass)} + val parsedTrace = parsedRenderPasses.map { renderPass -> getFrameFromRenderPass(renderPass) } val frameTrees = mutableListOf() parsedTrace.fold(parsedTrace[0]) { tree, frame -> val mergedTree = mergeFrameIntoMainTree(frame, tree) @@ -104,7 +104,7 @@ private fun buildTree(node: Node, childrenByParent: Map>): No internal fun mergeFrameIntoMainTree( frame: Node, main: Node -) : Node { +): Node { if (frame.id != main.id) { throw IllegalArgumentException("Frame root ID does not match main tree root ID.") } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt index 90dbcf5670..302e96346f 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt @@ -6,14 +6,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput diff --git a/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt b/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt index 9f0997bb3b..57be442972 100644 --- a/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt +++ b/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt @@ -8,101 +8,100 @@ import kotlin.test.assertTrue class JsonParserTest { - @Test - fun `test mergeFrameIntoMainTree with new children`() { - // Create main tree with one child - val mainChild = createNode("child1", "root", "1") - val mainTree = createNode("root", "root", "0", "0", listOf(mainChild)) - - // Create frame with a new child - val frameChild1 = createNode("child1", "root", "1") - val frameChild2 = createNode("child2", "root", "2") - val frame = createNode("root", "root", "0", "0", listOf(frameChild1, frameChild2)) - - // Merge frame into main tree - val mergedTree = mergeFrameIntoMainTree(frame, mainTree) - - // Verify results - assertEquals(2, mergedTree.children.size) - assertTrue(mergedTree.children.any { it.id == "1" }) - assertTrue(mergedTree.children.any { it.id == "2" }) - } - - @Test - fun `test mergeFrameIntoMainTree with nested children`() { - // Create main tree with nested structure - val nestedChild = createNode("nested1", "child1", "2") - val mainChild = createNode("child1", "root", "1", "0", listOf(nestedChild)) - val mainTree = createNode("root", "root", "0", "0", listOf(mainChild)) - - // Create frame with new nested child - val frameNestedChild1 = createNode("nested1", "child1", "2") - val frameNestedChild2 = createNode("nested2", "child1", "3") - val frameChild = createNode("child1", "root", "1", "0", - mutableListOf(frameNestedChild1, frameNestedChild2)) - val frame = createNode("root", "root", "0", "0", listOf(frameChild)) - - // Merge frame into main tree - val mergedTree = mergeFrameIntoMainTree(frame, mainTree) - - // Verify results - assertEquals(1, mergedTree.children.size) - val updatedChild = mergedTree.children.first() - assertEquals(2, updatedChild.children.size) - assertTrue(updatedChild.children.any { it.id == "2" }) - assertTrue(updatedChild.children.any { it.id == "3" }) - } - - @Test - fun `test mergeFrameIntoMainTree with empty main tree children`() { - // Create empty main tree - val mainTree = createNode("root", "root", "0") - - // Create frame with children - val frameChild = createNode("child1", "root", "1") - val frame = createNode("root", "root", "0", "0", listOf(frameChild)) - - // Merge frame into main tree - val mergedTree = mergeFrameIntoMainTree(frame, mainTree) - - // Verify results - assertEquals(1, mergedTree.children.size) - assertEquals("child1", mergedTree.children.first().name) - } - - @Test - fun `test mergeFrameIntoMainTree with empty frame children`() { - // Create main tree with children - val mainChild = createNode("child1", "root", "1") - val mainTree = createNode("root", "root", "0", "0", listOf(mainChild)) - - // Create empty frame - val frame = createNode("root", "root", "0") - - // Merge frame into main tree - mergeFrameIntoMainTree(frame, mainTree) - - // Verify results - assertEquals(1, mainTree.children.size) - assertEquals("child1", mainTree.children.first().name) - } - - private fun createNode( - name: String, - parent: String, - id: String, - parentId: String = "0", - children: List = emptyList() - ): Node { - return Node( - name = name, - id = id, - parent = parent, - parentId = parentId, - props = "", - state = "", - rendering = "", - children = children, - ) - } + @Test + fun `test mergeFrameIntoMainTree with new children`() { + // Create main tree with one child + val mainChild = createNode("child1", "root", "1") + val mainTree = createNode("root", "root", "0", "0", listOf(mainChild)) + + // Create frame with a new child + val frameChild1 = createNode("child1", "root", "1") + val frameChild2 = createNode("child2", "root", "2") + val frame = createNode("root", "root", "0", "0", listOf(frameChild1, frameChild2)) + + // Merge frame into main tree + val mergedTree = mergeFrameIntoMainTree(frame, mainTree) + + // Verify results + assertEquals(2, mergedTree.children.size) + assertTrue(mergedTree.children.any { it.id == "1" }) + assertTrue(mergedTree.children.any { it.id == "2" }) + } + + @Test + fun `test mergeFrameIntoMainTree with nested children`() { + // Create main tree with nested structure + val nestedChild = createNode("nested1", "child1", "2") + val mainChild = createNode("child1", "root", "1", "0", listOf(nestedChild)) + val mainTree = createNode("root", "root", "0", "0", listOf(mainChild)) + + // Create frame with new nested child + val frameNestedChild1 = createNode("nested1", "child1", "2") + val frameNestedChild2 = createNode("nested2", "child1", "3") + val frameChild = createNode("child1", "root", "1", "0", mutableListOf(frameNestedChild1, frameNestedChild2)) + val frame = createNode("root", "root", "0", "0", listOf(frameChild)) + + // Merge frame into main tree + val mergedTree = mergeFrameIntoMainTree(frame, mainTree) + + // Verify results + assertEquals(1, mergedTree.children.size) + val updatedChild = mergedTree.children.first() + assertEquals(2, updatedChild.children.size) + assertTrue(updatedChild.children.any { it.id == "2" }) + assertTrue(updatedChild.children.any { it.id == "3" }) + } + + @Test + fun `test mergeFrameIntoMainTree with empty main tree children`() { + // Create empty main tree + val mainTree = createNode("root", "root", "0") + + // Create frame with children + val frameChild = createNode("child1", "root", "1") + val frame = createNode("root", "root", "0", "0", listOf(frameChild)) + + // Merge frame into main tree + val mergedTree = mergeFrameIntoMainTree(frame, mainTree) + + // Verify results + assertEquals(1, mergedTree.children.size) + assertEquals("child1", mergedTree.children.first().name) + } + + @Test + fun `test mergeFrameIntoMainTree with empty frame children`() { + // Create main tree with children + val mainChild = createNode("child1", "root", "1") + val mainTree = createNode("root", "root", "0", "0", listOf(mainChild)) + + // Create empty frame + val frame = createNode("root", "root", "0") + + // Merge frame into main tree + mergeFrameIntoMainTree(frame, mainTree) + + // Verify results + assertEquals(1, mainTree.children.size) + assertEquals("child1", mainTree.children.first().name) + } + + private fun createNode( + name: String, + parent: String, + id: String, + parentId: String = "0", + children: List = emptyList() + ): Node { + return Node( + name = name, + id = id, + parent = parent, + parentId = parentId, + props = "", + state = "", + rendering = "", + children = children, + ) + } } From 69d9c27d68f207eef33906131cafbaf859ec18d8 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Fri, 11 Jul 2025 10:22:21 -0400 Subject: [PATCH 12/20] Clean up --- .../workflow1/traceviewer/util/JsonParser.kt | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt index ecea8087c2..b0e5b4bbd7 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt @@ -32,23 +32,18 @@ internal suspend fun parseTrace( workflowAdapter.fromJson(jsonString) ?: return ParseResult.Failure( IllegalArgumentException("Provided trace file is empty or malformed.") ) - /* - this parsing method can never be called without a provided file, so we can assume that there - will always be at least one render pass in the trace. If not, then Moshi would catch any - malformed JSON and throw an error beforehand. - */ } catch (e: Exception) { return ParseResult.Failure(e) } - val parsedTrace = parsedRenderPasses.map { renderPass -> getFrameFromRenderPass(renderPass) } + val parsedFrames = parsedRenderPasses.map { renderPass -> getFrameFromRenderPass(renderPass) } val frameTrees = mutableListOf() - parsedTrace.fold(parsedTrace[0]) { tree, frame -> + parsedFrames.fold(parsedFrames[0]) { tree, frame -> val mergedTree = mergeFrameIntoMainTree(frame, tree) frameTrees.add(mergedTree) mergedTree } - return ParseResult.Success(parsedTrace, frameTrees, parsedRenderPasses) + return ParseResult.Success(parsedFrames, frameTrees, parsedRenderPasses) } /** @@ -109,12 +104,12 @@ internal fun mergeFrameIntoMainTree( throw IllegalArgumentException("Frame root ID does not match main tree root ID.") } - return frame.children.fold(main) { mergedTree, child -> - val parent = mergedTree.children.singleOrNull { it.id == child.id } - if (parent != null) { - mergedTree.replaceChild(mergeFrameIntoMainTree(child, parent)) + return frame.children.fold(main) { mergedTree, frameChild -> + val mainTreeChild = mergedTree.children.singleOrNull { it.id == frameChild.id } + if (mainTreeChild != null) { + mergedTree.replaceChild(mergeFrameIntoMainTree(frameChild, mainTreeChild)) } else { - mergedTree.addChild(child) + mergedTree.addChild(frameChild) } } } From 32c5b9f732500002d30a4831add3000b1e1bb042 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Fri, 11 Jul 2025 11:21:49 -0400 Subject: [PATCH 13/20] Improve UI for detailed node panel --- .../traceviewer/ui/WorkflowInfoPanel.kt | 96 ++++++++++++++----- 1 file changed, 74 insertions(+), 22 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt index 6b8544e065..6f4235180d 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt @@ -1,15 +1,22 @@ package com.squareup.workflow1.traceviewer.ui import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Card import androidx.compose.material.Icon import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons.AutoMirrored.Filled import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft @@ -22,11 +29,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.squareup.workflow1.traceviewer.model.Node /** @@ -49,7 +53,7 @@ internal fun RightInfoPanel( onClick = { panelOpen = !panelOpen }, modifier = Modifier .padding(8.dp) - .size(30.dp) + .size(40.dp) .align(Alignment.Top) ) { Icon( @@ -68,41 +72,89 @@ internal fun RightInfoPanel( } } +/** + * Displays specific details about the opened workflow node. + */ @Composable private fun NodePanelDetails( node: Node?, modifier: Modifier = Modifier ) { - Column( + LazyColumn( modifier = modifier .fillMaxHeight() - .background(Color.LightGray) - .padding(8.dp) - .pointerInput(Unit) { - detectTapGestures { } - }, - horizontalAlignment = Alignment.CenterHorizontally + .background(Color.White) + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { if (node == null) { - Text("No node selected") - return@Column + item { + Text("Select a node to view details") + } + return@LazyColumn } - val textModifier = Modifier.padding(8.dp) - val textStyle = TextStyle(fontSize = 16.sp, textAlign = TextAlign.Center) - val fields = mapOf( + val fields = listOf( "Name" to node.name, "ID" to node.id, "Props" to node.props, "State" to node.state, "Rendering" to node.rendering ) + item { + Text( + text = "Workflow Details", + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(top = 8.dp, bottom = 8.dp) + ) + } + + items(fields) { (label, value) -> + DetailCard( + label = label, + value = value + ) + } + } +} + +/** + * Card component that represents each item for the nodes. + * + * Can be open/closed to show/hide details. + */ +@Composable +private fun DetailCard( + label: String, + value: String, +) { + var open by remember { mutableStateOf(true) } + Card( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .clickable { + open = !open + }, + elevation = 3.dp, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = label, + style = MaterialTheme.typography.subtitle1, + color = Color.Black, + fontWeight = FontWeight.Medium + ) + if (!open) { return@Card } - fields.forEach { (label, value) -> + Spacer(modifier = Modifier.height(4.dp)) Text( - text = "$label: $value", - modifier = textModifier, - style = textStyle + text = value, + style = MaterialTheme.typography.body1 ) } } From 81b0da7c8e27530ff9f6454fd790cb6688d47132 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Fri, 11 Jul 2025 16:30:26 -0400 Subject: [PATCH 14/20] Show before and after of node states when selecting them This commit introduces the idea of a NodeDiff, which stores the node's current and previous state - from the previous frame - in order to better represent what is actually changing through the UI. If there is no previous frame (only on the first frame of the trace), there is no need to show a diff, so we plainly depict the node's states. --- .../com/squareup/workflow1/traceviewer/App.kt | 7 +- .../workflow1/traceviewer/model/Node.kt | 16 +++++ .../workflow1/traceviewer/model/NodeDiff.kt | 12 ++++ .../traceviewer/ui/WorkflowInfoPanel.kt | 65 +++++++++++++------ .../workflow1/traceviewer/ui/WorkflowTree.kt | 30 ++++++--- .../workflow1/traceviewer/util/JsonParser.kt | 5 +- 6 files changed, 104 insertions(+), 31 deletions(-) create mode 100644 workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/NodeDiff.kt diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt index e5aa6959b9..b43df5e436 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import com.squareup.workflow1.traceviewer.model.Node +import com.squareup.workflow1.traceviewer.model.NodeDiff import com.squareup.workflow1.traceviewer.ui.FrameSelectTab import com.squareup.workflow1.traceviewer.ui.RenderDiagram import com.squareup.workflow1.traceviewer.ui.RightInfoPanel @@ -28,7 +29,7 @@ public fun App( modifier: Modifier = Modifier ) { var selectedTraceFile by remember { mutableStateOf(null) } - var selectedNode by remember { mutableStateOf(null) } + var selectedNode by remember { mutableStateOf(null) } var workflowFrames by remember { mutableStateOf>(emptyList()) } var frameIndex by remember { mutableIntStateOf(0) } val sandboxState = remember { SandboxState() } @@ -49,7 +50,9 @@ public fun App( traceFile = selectedTraceFile!!, frameInd = frameIndex, onFileParse = { workflowFrames = it }, - onNodeSelect = { selectedNode = it } + onNodeSelect = { node, prevNode -> + selectedNode = NodeDiff(node, prevNode) + } ) } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt index 445c2fe4d6..c4da5a7050 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt @@ -31,6 +31,22 @@ internal data class Node( override fun hashCode(): Int { return id.hashCode() } + + companion object { + fun getNodeField(node: Node, field: String): String { + return when(field.lowercase()) { + "name" -> node.name + "id" -> node.id + "parent" -> node.parent + "parentid" -> node.parentId + "props" -> node.props + "state" -> node.state + "rendering" -> node.rendering + "children" -> node.children.toString() + else -> throw IllegalArgumentException("Unknown field: $field") + } + } + } } internal fun Node.addChild(child: Node): Node { diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/NodeDiff.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/NodeDiff.kt new file mode 100644 index 0000000000..026d6bc11b --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/NodeDiff.kt @@ -0,0 +1,12 @@ +package com.squareup.workflow1.traceviewer.model + +/** + * Represents the difference between the current and previous state of a node in the workflow trace. + * This will be what is passed as a state between UI to display the diff. + * + * If it's the first node in the frame, [previous] will be null and there is no difference to show. + */ +internal class NodeDiff( + val current: Node, + val previous: Node?, +) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt index 6f4235180d..ef2bb8408d 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt @@ -29,9 +29,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.squareup.workflow1.traceviewer.model.Node +import com.squareup.workflow1.traceviewer.model.NodeDiff /** * A panel that displays information about the selected workflow node. @@ -41,7 +44,7 @@ import com.squareup.workflow1.traceviewer.model.Node */ @Composable internal fun RightInfoPanel( - selectedNode: Node?, + selectedNode: NodeDiff?, modifier: Modifier = Modifier ) { Row( @@ -77,7 +80,7 @@ internal fun RightInfoPanel( */ @Composable private fun NodePanelDetails( - node: Node?, + node: NodeDiff?, modifier: Modifier = Modifier ) { LazyColumn( @@ -93,14 +96,6 @@ private fun NodePanelDetails( } return@LazyColumn } - - val fields = listOf( - "Name" to node.name, - "ID" to node.id, - "Props" to node.props, - "State" to node.state, - "Rendering" to node.rendering - ) item { Text( text = "Workflow Details", @@ -108,11 +103,13 @@ private fun NodePanelDetails( modifier = Modifier.padding(top = 8.dp, bottom = 8.dp) ) } + val fields = listOf("Name", "Id", "Props", "State", "Rendering") - items(fields) { (label, value) -> + items(fields) { field -> DetailCard( - label = label, - value = value + label = field, + currValue = Node.getNodeField(node.current, field), + pastValue = if (node.previous != null) Node.getNodeField(node.previous, field) else null ) } } @@ -126,7 +123,8 @@ private fun NodePanelDetails( @Composable private fun DetailCard( label: String, - value: String, + currValue: String, + pastValue: String? ) { var open by remember { mutableStateOf(true) } Card( @@ -145,17 +143,46 @@ private fun DetailCard( ) { Text( text = label, - style = MaterialTheme.typography.subtitle1, + style = MaterialTheme.typography.h6, color = Color.Black, fontWeight = FontWeight.Medium ) if (!open) { return@Card } Spacer(modifier = Modifier.height(4.dp)) - Text( - text = value, - style = MaterialTheme.typography.body1 - ) + if (pastValue != null) { + Column { + Text( + text = "Before:", + style = TextStyle(fontStyle = FontStyle.Italic), + color = Color.Black, + fontWeight = FontWeight.Medium + ) + Text( + text = pastValue, + style = MaterialTheme.typography.body2, + color = Color.Black + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "After:", + style = TextStyle(fontStyle = FontStyle.Italic), + color = Color.Black, + fontWeight = FontWeight.Medium + ) + Text( + text = currValue, + style = MaterialTheme.typography.body2, + color = Color.Black + ) + } + } else { + Text( + text = currValue, + style = MaterialTheme.typography.body1, + color = Color.Black + ) + } } } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt index 5a5eb5f018..897245d51b 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.squareup.workflow1.traceviewer.model.Node +import com.squareup.workflow1.traceviewer.model.NodeDiff import com.squareup.workflow1.traceviewer.util.ParseResult import com.squareup.workflow1.traceviewer.util.parseTrace import io.github.vinceglb.filekit.PlatformFile @@ -34,7 +35,7 @@ internal fun RenderDiagram( traceFile: PlatformFile, frameInd: Int, onFileParse: (List) -> Unit, - onNodeSelect: (Node) -> Unit, + onNodeSelect: (Node, Node?) -> Unit, modifier: Modifier = Modifier ) { var isLoading by remember(traceFile) { mutableStateOf(true) } @@ -67,7 +68,8 @@ internal fun RenderDiagram( } if (!isLoading) { - DrawTree(fullTree[frameInd], affectedNodes[frameInd], onNodeSelect) + val previousFrame = if (frameInd > 0) fullTree[frameInd - 1] else null + DrawTree(fullTree[frameInd], previousFrame, affectedNodes[frameInd], onNodeSelect) } } @@ -78,10 +80,12 @@ internal fun RenderDiagram( @Composable private fun DrawTree( node: Node, + previousNode: Node?, affectedNodes: Set, - onNodeSelect: (Node) -> Unit, + onNodeSelect: (Node, Node?) -> Unit, modifier: Modifier = Modifier, ) { + println("Drawing node: ${node.name} with state: ${node.state}") Column( modifier .padding(5.dp) @@ -90,15 +94,21 @@ private fun DrawTree( horizontalAlignment = Alignment.CenterHorizontally, ) { val isAffected = affectedNodes.contains(node) - DrawNode(node, isAffected, onNodeSelect) + DrawNode(node, previousNode, isAffected, onNodeSelect) // Draws the node's children recursively. Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.Top ) { - node.children.forEach { childNode -> - DrawTree(childNode, affectedNodes, onNodeSelect) + /* + We pair up the current node's children with previous frame's children. + In the edge case that the current frame has additional children compared to the previous + frame, we replace with null and will check before next recursive call. + */ + node.children.forEachIndexed { index, childNode -> + val prevChildNode = previousNode?.children?.getOrNull(index) + DrawTree(childNode, prevChildNode, affectedNodes, onNodeSelect) } } } @@ -110,19 +120,23 @@ private fun DrawTree( @Composable private fun DrawNode( node: Node, + previousNode: Node?, isAffected: Boolean, - onNodeSelect: (Node) -> Unit, + onNodeSelect: (Node, Node?) -> Unit, ) { Box( modifier = Modifier .background(if (isAffected) Color.Green else Color.Transparent) .clickable { // Selecting a node will bubble back up to the main view to handle the selection - onNodeSelect(node) + onNodeSelect(node, previousNode) } .padding(10.dp) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { + if (node.name == "LocationServicesGateKeeper"){ + println(node.state) + } Text(text = node.name) Text(text = "ID: ${node.id}") } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt index b0e5b4bbd7..1382bd2ca4 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt @@ -68,7 +68,6 @@ private fun createMoshiAdapter(): JsonAdapter>> { */ private fun getFrameFromRenderPass(renderPass: List): Node { val childrenByParent: Map> = renderPass.groupBy { it.parentId } - println(childrenByParent) val root = childrenByParent[ROOT_ID]?.single() return buildTree(root!!, childrenByParent) } @@ -104,7 +103,9 @@ internal fun mergeFrameIntoMainTree( throw IllegalArgumentException("Frame root ID does not match main tree root ID.") } - return frame.children.fold(main) { mergedTree, frameChild -> + val updatedNode = frame.copy(children = main.children) + + return frame.children.fold(updatedNode) { mergedTree, frameChild -> val mainTreeChild = mergedTree.children.singleOrNull { it.id == frameChild.id } if (mainTreeChild != null) { mergedTree.replaceChild(mergeFrameIntoMainTree(frameChild, mainTreeChild)) From abe0486f70660dbcdb1741b551a2ce23f10b511f Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Fri, 11 Jul 2025 16:45:15 -0400 Subject: [PATCH 15/20] Fix compose lint violations --- .../jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt | 2 +- .../kotlin/com/squareup/workflow1/traceviewer/model/Node.kt | 2 +- .../squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt | 4 +++- .../com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt | 5 ----- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt index b43df5e436..f5cec10acc 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt @@ -50,7 +50,7 @@ public fun App( traceFile = selectedTraceFile!!, frameInd = frameIndex, onFileParse = { workflowFrames = it }, - onNodeSelect = { node, prevNode -> + onNodeSelect = { node, prevNode -> selectedNode = NodeDiff(node, prevNode) } ) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt index c4da5a7050..a114ea3d19 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt @@ -34,7 +34,7 @@ internal data class Node( companion object { fun getNodeField(node: Node, field: String): String { - return when(field.lowercase()) { + return when (field.lowercase()) { "name" -> node.name "id" -> node.id "parent" -> node.parent diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt index ef2bb8408d..38b6bd8273 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt @@ -147,7 +147,9 @@ private fun DetailCard( color = Color.Black, fontWeight = FontWeight.Medium ) - if (!open) { return@Card } + if (!open) { + return@Card + } Spacer(modifier = Modifier.height(4.dp)) if (pastValue != null) { diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt index 897245d51b..f8524670bb 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.squareup.workflow1.traceviewer.model.Node -import com.squareup.workflow1.traceviewer.model.NodeDiff import com.squareup.workflow1.traceviewer.util.ParseResult import com.squareup.workflow1.traceviewer.util.parseTrace import io.github.vinceglb.filekit.PlatformFile @@ -85,7 +84,6 @@ private fun DrawTree( onNodeSelect: (Node, Node?) -> Unit, modifier: Modifier = Modifier, ) { - println("Drawing node: ${node.name} with state: ${node.state}") Column( modifier .padding(5.dp) @@ -134,9 +132,6 @@ private fun DrawNode( .padding(10.dp) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - if (node.name == "LocationServicesGateKeeper"){ - println(node.state) - } Text(text = node.name) Text(text = "ID: ${node.id}") } From 841fee222f01436658af35a57f9361f2120fafb4 Mon Sep 17 00:00:00 2001 From: wenli-cai <213805610+wenli-cai@users.noreply.github.com> Date: Fri, 11 Jul 2025 20:56:08 +0000 Subject: [PATCH 16/20] Apply changes from apiDump Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- workflow-trace-viewer/api/workflow-trace-viewer.api | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/workflow-trace-viewer/api/workflow-trace-viewer.api b/workflow-trace-viewer/api/workflow-trace-viewer.api index abc17e5ffc..698647f649 100644 --- a/workflow-trace-viewer/api/workflow-trace-viewer.api +++ b/workflow-trace-viewer/api/workflow-trace-viewer.api @@ -14,6 +14,15 @@ public final class com/squareup/workflow1/traceviewer/MainKt { public static synthetic fun main ([Ljava/lang/String;)V } +public final class com/squareup/workflow1/traceviewer/ui/ComposableSingletons$WorkflowInfoPanelKt { + public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/ui/ComposableSingletons$WorkflowInfoPanelKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public static field lambda-2 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-2$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; +} + public final class com/squareup/workflow1/traceviewer/util/ComposableSingletons$UploadFileKt { public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/util/ComposableSingletons$UploadFileKt; public static field lambda-1 Lkotlin/jvm/functions/Function3; From eceef59a4c5a5ae434c0f65a0df496d59084077c Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Mon, 14 Jul 2025 17:16:27 -0400 Subject: [PATCH 17/20] Fix PR comments --- gradle/libs.versions.toml | 2 -- workflow-trace-viewer/build.gradle.kts | 1 - .../com/squareup/workflow1/traceviewer/App.kt | 13 ++++++++----- .../workflow1/traceviewer/model/Node.kt | 8 +++++--- .../model/{NodeDiff.kt => NodeUpdate.kt} | 2 +- .../traceviewer/ui/WorkflowInfoPanel.kt | 6 +++--- .../workflow1/traceviewer/ui/WorkflowTree.kt | 18 ++++++++++-------- .../workflow1/traceviewer/util/JsonParser.kt | 12 ++++++------ .../traceviewer/util/SandboxBackground.kt | 7 ++++++- 9 files changed, 39 insertions(+), 30 deletions(-) rename workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/{NodeDiff.kt => NodeUpdate.kt} (93%) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d386e9c112..03264b2c87 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,7 +51,6 @@ google-material = "1.4.0" groovy = "3.0.9" jUnit = "4.13.2" -junitJupiter = "5.10.1" java-diff-utils = "4.12" javaParser = "3.24.0" jetbrains-compose-plugin = "1.7.3" @@ -206,7 +205,6 @@ java-diff-utils = { module = "io.github.java-diff-utils:java-diff-utils", versio jetbrains-annotations = "org.jetbrains:annotations:24.0.1" junit = { module = "junit:junit", version.ref = "jUnit" } -junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junitJupiter" } kgx = { module = "com.rickbusarow.kgx:kotlin-gradle-extensions", version.ref = "kgx" } diff --git a/workflow-trace-viewer/build.gradle.kts b/workflow-trace-viewer/build.gradle.kts index b7c4461dee..0fe520cb99 100644 --- a/workflow-trace-viewer/build.gradle.kts +++ b/workflow-trace-viewer/build.gradle.kts @@ -31,7 +31,6 @@ kotlin { dependencies { implementation(kotlin("test")) implementation(kotlin("test-junit5")) - implementation(libs.junit.jupiter) } } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt index f5cec10acc..e8b1d19639 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt @@ -9,11 +9,12 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import com.squareup.workflow1.traceviewer.model.Node -import com.squareup.workflow1.traceviewer.model.NodeDiff +import com.squareup.workflow1.traceviewer.model.NodeUpdate import com.squareup.workflow1.traceviewer.ui.FrameSelectTab import com.squareup.workflow1.traceviewer.ui.RenderDiagram import com.squareup.workflow1.traceviewer.ui.RightInfoPanel @@ -29,13 +30,15 @@ public fun App( modifier: Modifier = Modifier ) { var selectedTraceFile by remember { mutableStateOf(null) } - var selectedNode by remember { mutableStateOf(null) } + var selectedNode by remember { mutableStateOf(null) } var workflowFrames by remember { mutableStateOf>(emptyList()) } var frameIndex by remember { mutableIntStateOf(0) } val sandboxState = remember { SandboxState() } - LaunchedEffect(frameIndex) { - sandboxState.reset() + LaunchedEffect(sandboxState) { + snapshotFlow { frameIndex }.collect { + sandboxState.reset() + } } Box( @@ -51,7 +54,7 @@ public fun App( frameInd = frameIndex, onFileParse = { workflowFrames = it }, onNodeSelect = { node, prevNode -> - selectedNode = NodeDiff(node, prevNode) + selectedNode = NodeUpdate(node, prevNode) } ) } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt index a114ea3d19..591ec2cb42 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt @@ -1,5 +1,7 @@ package com.squareup.workflow1.traceviewer.model +import com.squareup.moshi.Json + /** * Since the logic of Workflow is hierarchical (where each workflow may have parent workflows and/or * children workflows, a tree structure is most appropriate for representing the data rather than @@ -15,7 +17,7 @@ internal data class Node( val props: String, val state: String, val rendering: String = "", - val children: List, + @Transient val children: LinkedHashMap = LinkedHashMap() ) { override fun toString(): String { @@ -50,9 +52,9 @@ internal data class Node( } internal fun Node.addChild(child: Node): Node { - return copy(children = this.children + child) + return copy(children = (this.children.plus(child.id to child) as LinkedHashMap)) } internal fun Node.replaceChild(child: Node): Node { - return copy(children = this.children.map { if (it.id == child.id) child else it }) + return copy(children = (this.children.plus(child.id to child) as LinkedHashMap)) } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/NodeDiff.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/NodeUpdate.kt similarity index 93% rename from workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/NodeDiff.kt rename to workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/NodeUpdate.kt index 026d6bc11b..c97557757d 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/NodeDiff.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/NodeUpdate.kt @@ -6,7 +6,7 @@ package com.squareup.workflow1.traceviewer.model * * If it's the first node in the frame, [previous] will be null and there is no difference to show. */ -internal class NodeDiff( +internal class NodeUpdate( val current: Node, val previous: Node?, ) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt index 38b6bd8273..a74b550e50 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.squareup.workflow1.traceviewer.model.Node -import com.squareup.workflow1.traceviewer.model.NodeDiff +import com.squareup.workflow1.traceviewer.model.NodeUpdate /** * A panel that displays information about the selected workflow node. @@ -44,7 +44,7 @@ import com.squareup.workflow1.traceviewer.model.NodeDiff */ @Composable internal fun RightInfoPanel( - selectedNode: NodeDiff?, + selectedNode: NodeUpdate?, modifier: Modifier = Modifier ) { Row( @@ -80,7 +80,7 @@ internal fun RightInfoPanel( */ @Composable private fun NodePanelDetails( - node: NodeDiff?, + node: NodeUpdate?, modifier: Modifier = Modifier ) { LazyColumn( diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt index f8524670bb..bde19be79a 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt @@ -13,9 +13,11 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -39,9 +41,9 @@ internal fun RenderDiagram( ) { var isLoading by remember(traceFile) { mutableStateOf(true) } var error by remember(traceFile) { mutableStateOf(null) } - var frames by remember { mutableStateOf>(emptyList()) } - var fullTree by remember { mutableStateOf>(emptyList()) } - var affectedNodes by remember { mutableStateOf>>(emptyList()) } + var frames = remember { mutableStateListOf() } + var fullTree = remember { mutableStateListOf() } + var affectedNodes = remember { mutableStateListOf>() } LaunchedEffect(traceFile) { val parseResult = parseTrace(traceFile) @@ -52,9 +54,9 @@ internal fun RenderDiagram( } is ParseResult.Success -> { val parsedFrames = parseResult.trace ?: emptyList() - frames = parsedFrames - fullTree = parseResult.trees - affectedNodes = parseResult.affectedNodes + frames.addAll(parsedFrames) + fullTree.addAll(parseResult.trees) + affectedNodes.addAll(parseResult.affectedNodes) onFileParse(parsedFrames) isLoading = false } @@ -104,8 +106,8 @@ private fun DrawTree( In the edge case that the current frame has additional children compared to the previous frame, we replace with null and will check before next recursive call. */ - node.children.forEachIndexed { index, childNode -> - val prevChildNode = previousNode?.children?.getOrNull(index) + node.children.forEach { (index, childNode) -> + val prevChildNode = previousNode?.children?.get(index) DrawTree(childNode, prevChildNode, affectedNodes, onNodeSelect) } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt index 1382bd2ca4..334ea59a01 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt @@ -9,6 +9,7 @@ import com.squareup.workflow1.traceviewer.model.addChild import com.squareup.workflow1.traceviewer.model.replaceChild import io.github.vinceglb.filekit.PlatformFile import io.github.vinceglb.filekit.readString +import java.util.LinkedHashMap /* The root workflow Node uses an ID of 0, and since we are filtering childrenByParent by the @@ -77,6 +78,7 @@ private fun getFrameFromRenderPass(renderPass: List): Node { */ private fun buildTree(node: Node, childrenByParent: Map>): Node { val children = (childrenByParent[node.id] ?: emptyList()) + .map { buildTree(it, childrenByParent) } return Node( name = node.name, id = node.id, @@ -84,7 +86,7 @@ private fun buildTree(node: Node, childrenByParent: Map>): No parentId = node.parentId, props = node.props, state = node.state, - children = children.map { buildTree(it, childrenByParent) }, + children = LinkedHashMap(children.associateBy { it.id }), ) } @@ -99,14 +101,12 @@ internal fun mergeFrameIntoMainTree( frame: Node, main: Node ): Node { - if (frame.id != main.id) { - throw IllegalArgumentException("Frame root ID does not match main tree root ID.") - } + require(frame.id == main.id) val updatedNode = frame.copy(children = main.children) - return frame.children.fold(updatedNode) { mergedTree, frameChild -> - val mainTreeChild = mergedTree.children.singleOrNull { it.id == frameChild.id } + return frame.children.values.fold(updatedNode) { mergedTree, frameChild -> + val mainTreeChild = mergedTree.children[frameChild.id] if (mainTreeChild != null) { mergedTree.replaceChild(mergeFrameIntoMainTree(frameChild, mainTreeChild)) } else { diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt index 302e96346f..a0534a3425 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt @@ -42,8 +42,13 @@ internal fun SandboxBackground( val event = awaitPointerEvent() if (event.type == PointerEventType.Scroll) { val scrollDelta = event.changes.first().scrollDelta.y + // Applies zoom factor based on the actual delta change rather than just the act of scrolling + // This helps to normalize mouse scrolling and touchpad scrolling, since touchpad will + // fire a lot more scroll events. val factor = 1f + (-scrollDelta * 0.1f) - sandboxState.scale = (sandboxState.scale * factor).coerceIn(0.1f, 10f) + val minWindowSize = 0.1f + val maxWindowSize = 10f + sandboxState.scale = (sandboxState.scale * factor).coerceIn(minWindowSize, maxWindowSize) event.changes.forEach { it.consume() } } } From 46765cbf1878865f90637aada529b034e597ddc4 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Mon, 14 Jul 2025 18:24:54 -0400 Subject: [PATCH 18/20] Fix unit tests --- .../workflow1/traceviewer/model/Node.kt | 4 ++-- .../traceviewer/util/JsonParserTest.kt | 20 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt index 591ec2cb42..26a528a1f3 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt @@ -52,9 +52,9 @@ internal data class Node( } internal fun Node.addChild(child: Node): Node { - return copy(children = (this.children.plus(child.id to child) as LinkedHashMap)) + return copy(children = LinkedHashMap(this.children.plus(child.id to child))) } internal fun Node.replaceChild(child: Node): Node { - return copy(children = (this.children.plus(child.id to child) as LinkedHashMap)) + return copy(children = LinkedHashMap(this.children.plus(child.id to child))) } diff --git a/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt b/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt index 57be442972..434aac6ba4 100644 --- a/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt +++ b/workflow-trace-viewer/src/jvmTest/kotlin/com/squareup/workflow1/traceviewer/util/JsonParserTest.kt @@ -1,7 +1,7 @@ package com.squareup.workflow1.traceviewer.util import com.squareup.workflow1.traceviewer.model.Node -import com.squareup.workflow1.traceviewer.util.ROOT_ID +import java.util.LinkedHashMap import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -24,8 +24,8 @@ class JsonParserTest { // Verify results assertEquals(2, mergedTree.children.size) - assertTrue(mergedTree.children.any { it.id == "1" }) - assertTrue(mergedTree.children.any { it.id == "2" }) + assertTrue(mergedTree.children.containsKey("1")) + assertTrue(mergedTree.children.containsKey("2")) } @Test @@ -46,10 +46,10 @@ class JsonParserTest { // Verify results assertEquals(1, mergedTree.children.size) - val updatedChild = mergedTree.children.first() + val updatedChild = mergedTree.children["1"]!! assertEquals(2, updatedChild.children.size) - assertTrue(updatedChild.children.any { it.id == "2" }) - assertTrue(updatedChild.children.any { it.id == "3" }) + assertTrue(updatedChild.children.containsKey("2")) + assertTrue(updatedChild.children.containsKey("3")) } @Test @@ -66,7 +66,7 @@ class JsonParserTest { // Verify results assertEquals(1, mergedTree.children.size) - assertEquals("child1", mergedTree.children.first().name) + assertEquals("child1", mergedTree.children["1"]?.name) } @Test @@ -83,7 +83,7 @@ class JsonParserTest { // Verify results assertEquals(1, mainTree.children.size) - assertEquals("child1", mainTree.children.first().name) + assertEquals("child1", mainTree.children["1"]?.name) } private fun createNode( @@ -101,7 +101,9 @@ class JsonParserTest { props = "", state = "", rendering = "", - children = children, + children = LinkedHashMap().apply { + children.forEach { put(it.id, it) } + } ) } } From dc8fae60ac2fb8e0a70e9dd571b87233173eb747 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Mon, 14 Jul 2025 18:32:17 -0400 Subject: [PATCH 19/20] Fix compose lint violations --- .../kotlin/com/squareup/workflow1/traceviewer/model/Node.kt | 2 -- .../com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt | 1 - 2 files changed, 3 deletions(-) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt index 26a528a1f3..4cbc66c68f 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt @@ -1,7 +1,5 @@ package com.squareup.workflow1.traceviewer.model -import com.squareup.moshi.Json - /** * Since the logic of Workflow is hierarchical (where each workflow may have parent workflows and/or * children workflows, a tree structure is most appropriate for representing the data rather than diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt index bde19be79a..bd2c21f8a6 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt @@ -17,7 +17,6 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color From 4fd32ef7744fa65e172f62b39eb98ab3e68101b6 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Fri, 18 Jul 2025 11:50:17 -0400 Subject: [PATCH 20/20] Fix more PR comments --- workflow-trace-viewer/build.gradle.kts | 3 --- .../kotlin/com/squareup/workflow1/traceviewer/model/Node.kt | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/workflow-trace-viewer/build.gradle.kts b/workflow-trace-viewer/build.gradle.kts index 0fe520cb99..cee4e4e65e 100644 --- a/workflow-trace-viewer/build.gradle.kts +++ b/workflow-trace-viewer/build.gradle.kts @@ -59,7 +59,4 @@ compose { tasks.named("jvmTest") { useJUnitPlatform() - testLogging { - events("passed", "skipped", "failed") - } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt index 4cbc66c68f..365f5a8216 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt @@ -54,5 +54,5 @@ internal fun Node.addChild(child: Node): Node { } internal fun Node.replaceChild(child: Node): Node { - return copy(children = LinkedHashMap(this.children.plus(child.id to child))) + return addChild(child) }