From 0f3a375110bb01a1c37e9450e196ac3ca5a9d7ff Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Thu, 10 Jul 2025 17:08:12 -0400 Subject: [PATCH 1/6] Add opening and closing workflow node box function --- .../workflow1/traceviewer/ui/WorkflowTree.kt | 80 +++++++++++++++---- 1 file changed, 65 insertions(+), 15 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 f8524670bb..c3a5442273 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 @@ -2,7 +2,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 import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -13,12 +12,18 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.isPrimaryPressed +import androidx.compose.ui.input.pointer.isSecondaryPressed +import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.unit.dp import com.squareup.workflow1.traceviewer.model.Node import com.squareup.workflow1.traceviewer.util.ParseResult @@ -68,19 +73,29 @@ internal fun RenderDiagram( if (!isLoading) { val previousFrame = if (frameInd > 0) fullTree[frameInd - 1] else null - DrawTree(fullTree[frameInd], previousFrame, affectedNodes[frameInd], onNodeSelect) + DrawTree( + node = fullTree[frameInd], + previousNode = previousFrame, + affectedNodes = affectedNodes[frameInd], + expandedNodes = remember(frameInd) { mutableStateMapOf() }, + onNodeSelect = onNodeSelect, + ) } } /** * Since the workflow nodes present a tree structure, we utilize a recursive function to draw the tree * The Column holds a subtree of nodes, and the Row holds all the children of the current node + * + * A mutable map is used to persist the expansion state of the nodes, allowing them to be open and + * closed from user clicks. */ @Composable private fun DrawTree( node: Node, previousNode: Node?, affectedNodes: Set, + expandedNodes: MutableMap, onNodeSelect: (Node, Node?) -> Unit, modifier: Modifier = Modifier, ) { @@ -92,21 +107,42 @@ private fun DrawTree( horizontalAlignment = Alignment.CenterHorizontally, ) { val isAffected = affectedNodes.contains(node) - DrawNode(node, previousNode, isAffected, onNodeSelect) + // By default, nodes that relevant to this specific frame are expanded. All others are closed. + LaunchedEffect(expandedNodes) { + expandedNodes[node.id] = isAffected + } + val isExpanded = expandedNodes[node.id] == true + + DrawNode( + node, + previousNode, + isAffected, + isExpanded, + onNodeSelect, + onExpandToggle = { expandedNodes[node.id] = !expandedNodes[node.id]!! } + ) // Draws the node's children recursively. - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.Top - ) { - /* + if (isExpanded) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Top + ) { + /* 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) + node.children.forEachIndexed { index, childNode -> + val prevChildNode = previousNode?.children?.getOrNull(index) + DrawTree( + node = childNode, + previousNode = prevChildNode, + affectedNodes = affectedNodes, + expandedNodes = expandedNodes, + onNodeSelect = onNodeSelect + ) + } } } } @@ -115,24 +151,38 @@ private fun DrawTree( /** * A basic box that represents a workflow node */ +@OptIn(ExperimentalComposeUiApi::class) @Composable private fun DrawNode( node: Node, previousNode: Node?, isAffected: Boolean, + isExpanded: Boolean, onNodeSelect: (Node, Node?) -> Unit, + onExpandToggle: (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, previousNode) + .onPointerEvent(PointerEventType.Press) { + if (it.buttons.isPrimaryPressed) { + onNodeSelect(node, previousNode) + } else if (it.buttons.isSecondaryPressed) { + onExpandToggle(node) + } } .padding(10.dp) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = node.name) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (node.children.isNotEmpty()) { + Text(text = if (isExpanded) "▼" else "▶") + } + Text(text = node.name) + } Text(text = "ID: ${node.id}") } } From c6cc245fb0d442997572575713530de33b936d4f Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Fri, 11 Jul 2025 09:47:36 -0400 Subject: [PATCH 2/6] Add functionality to use vertical mouse scroll to travel through row of frames --- .../traceviewer/ui/FrameSelectTab.kt | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) 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 c811e7168e..be0af00ea9 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 @@ -1,6 +1,9 @@ package com.squareup.workflow1.traceviewer.ui +import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState @@ -8,11 +11,15 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import com.squareup.workflow1.traceviewer.model.Node +import kotlinx.coroutines.launch /** * A trace tab selector that allows devs to switch between different states within the provided trace. @@ -24,17 +31,30 @@ internal fun FrameSelectTab( onIndexChange: (Int) -> Unit, modifier: Modifier = Modifier ) { - val state = rememberLazyListState() + val lazyListState = rememberLazyListState() + val coroutineScope = rememberCoroutineScope() Surface( - modifier = modifier - .padding(4.dp), + modifier = modifier, color = Color.White, ) { LazyRow( + state = lazyListState, modifier = Modifier - .padding(8.dp), - state = state + .padding(8.dp) + .pointerInput(Unit) { + awaitEachGesture { + val event = awaitPointerEvent() + if (event.type == PointerEventType.Scroll) { + val scrollDeltaY = event.changes.first().scrollDelta.y + coroutineScope.launch { + lazyListState.scroll(MutatePriority.Default) { + scrollBy(scrollDeltaY * 10f) + } + } + } + } + }, ) { items(frames.size) { index -> Text( From b2a4fa928be6213383053e3ac789267dcd4ab073 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Fri, 11 Jul 2025 16:57:38 -0400 Subject: [PATCH 3/6] Fix compose lint violations --- .../com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c3a5442273..0ba8015b1b 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 @@ -132,7 +132,7 @@ private fun DrawTree( 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( From 77c3d1352b3aef8d9032c638345eafa71ca0f82b Mon Sep 17 00:00:00 2001 From: wenli-cai <213805610+wenli-cai@users.noreply.github.com> Date: Fri, 11 Jul 2025 21:04:59 +0000 Subject: [PATCH 4/6] 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 5ee0c41f03d10659fba1105c6344541fa7a4e449 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Mon, 14 Jul 2025 09:47:51 -0400 Subject: [PATCH 5/6] Use reflection to iterate over Node's properties This had no visible affect on UI performance --- .../workflow1/traceviewer/model/Node.kt | 16 --------------- .../traceviewer/ui/WorkflowInfoPanel.kt | 20 +++++++++++-------- 2 files changed, 12 insertions(+), 24 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 a114ea3d19..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 @@ -31,22 +31,6 @@ 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/ui/WorkflowInfoPanel.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt index 38b6bd8273..a5bd148882 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 @@ -12,7 +12,6 @@ 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 @@ -35,6 +34,7 @@ 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 kotlin.reflect.full.memberProperties /** * A panel that displays information about the selected workflow node. @@ -103,14 +103,18 @@ private fun NodePanelDetails( modifier = Modifier.padding(top = 8.dp, bottom = 8.dp) ) } - val fields = listOf("Name", "Id", "Props", "State", "Rendering") - items(fields) { field -> - DetailCard( - label = field, - currValue = Node.getNodeField(node.current, field), - pastValue = if (node.previous != null) Node.getNodeField(node.previous, field) else null - ) + val fields = Node::class.memberProperties + for (field in fields) { + val currVal = field.get(node.current).toString() + val pastVal = if (node.previous != null) field.get(node.previous).toString() else null + item { + DetailCard( + label = field.name, + currValue = currVal, + pastValue = pastVal + ) + } } } } From 57dbae22404560a8c0eb561ae4a5c3e8539009b6 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Mon, 14 Jul 2025 18:30:52 -0400 Subject: [PATCH 6/6] Change coroutine scope --- .../traceviewer/ui/FrameSelectTab.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 be0af00ea9..1cdcc013b8 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 @@ -3,7 +3,6 @@ package com.squareup.workflow1.traceviewer.ui import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.awaitEachGesture -import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState @@ -11,7 +10,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -19,6 +17,7 @@ import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import com.squareup.workflow1.traceviewer.model.Node +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch /** @@ -32,7 +31,6 @@ internal fun FrameSelectTab( modifier: Modifier = Modifier ) { val lazyListState = rememberLazyListState() - val coroutineScope = rememberCoroutineScope() Surface( modifier = modifier, @@ -43,13 +41,15 @@ internal fun FrameSelectTab( modifier = Modifier .padding(8.dp) .pointerInput(Unit) { - awaitEachGesture { - val event = awaitPointerEvent() - if (event.type == PointerEventType.Scroll) { - val scrollDeltaY = event.changes.first().scrollDelta.y - coroutineScope.launch { - lazyListState.scroll(MutatePriority.Default) { - scrollBy(scrollDeltaY * 10f) + coroutineScope { + awaitEachGesture { + val event = awaitPointerEvent() + if (event.type == PointerEventType.Scroll) { + val scrollDeltaY = event.changes.first().scrollDelta.y + launch { + lazyListState.scroll(MutatePriority.Default) { + scrollBy(scrollDeltaY * 10f) + } } } }