Skip to content

Commit f4e1297

Browse files
authored
Merge pull request #1367 from square/wenli/visualizer-ux
Workflow Visualizer UX improvements
2 parents 314c0b3 + e9a03f6 commit f4e1297

File tree

4 files changed

+103
-45
lines changed

4 files changed

+103
-45
lines changed

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,6 @@ internal data class Node(
3131
override fun hashCode(): Int {
3232
return id.hashCode()
3333
}
34-
35-
companion object {
36-
fun getNodeField(node: Node, field: String): String {
37-
return when (field.lowercase()) {
38-
"name" -> node.name
39-
"id" -> node.id
40-
"parent" -> node.parent
41-
"parentid" -> node.parentId
42-
"props" -> node.props
43-
"state" -> node.state
44-
"rendering" -> node.rendering
45-
"children" -> node.children.toString()
46-
else -> throw IllegalArgumentException("Unknown field: $field")
47-
}
48-
}
49-
}
5034
}
5135

5236
internal fun Node.addChild(child: Node): Node {

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.squareup.workflow1.traceviewer.ui
22

3+
import androidx.compose.foundation.MutatePriority
34
import androidx.compose.foundation.clickable
5+
import androidx.compose.foundation.gestures.awaitEachGesture
46
import androidx.compose.foundation.layout.padding
57
import androidx.compose.foundation.lazy.LazyRow
68
import androidx.compose.foundation.lazy.rememberLazyListState
@@ -11,8 +13,12 @@ import androidx.compose.runtime.Composable
1113
import androidx.compose.ui.Modifier
1214
import androidx.compose.ui.draw.clip
1315
import androidx.compose.ui.graphics.Color
16+
import androidx.compose.ui.input.pointer.PointerEventType
17+
import androidx.compose.ui.input.pointer.pointerInput
1418
import androidx.compose.ui.unit.dp
1519
import com.squareup.workflow1.traceviewer.model.Node
20+
import kotlinx.coroutines.coroutineScope
21+
import kotlinx.coroutines.launch
1622

1723
/**
1824
* A trace tab selector that allows devs to switch between different states within the provided trace.
@@ -24,17 +30,31 @@ internal fun FrameSelectTab(
2430
onIndexChange: (Int) -> Unit,
2531
modifier: Modifier = Modifier
2632
) {
27-
val state = rememberLazyListState()
33+
val lazyListState = rememberLazyListState()
2834

2935
Surface(
30-
modifier = modifier
31-
.padding(4.dp),
36+
modifier = modifier,
3237
color = Color.White,
3338
) {
3439
LazyRow(
40+
state = lazyListState,
3541
modifier = Modifier
36-
.padding(8.dp),
37-
state = state
42+
.padding(8.dp)
43+
.pointerInput(Unit) {
44+
coroutineScope {
45+
awaitEachGesture {
46+
val event = awaitPointerEvent()
47+
if (event.type == PointerEventType.Scroll) {
48+
val scrollDeltaY = event.changes.first().scrollDelta.y
49+
launch {
50+
lazyListState.scroll(MutatePriority.Default) {
51+
scrollBy(scrollDeltaY * 10f)
52+
}
53+
}
54+
}
55+
}
56+
}
57+
},
3858
) {
3959
items(frames.size) { index ->
4060
Text(

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.height
1212
import androidx.compose.foundation.layout.padding
1313
import androidx.compose.foundation.layout.size
1414
import androidx.compose.foundation.lazy.LazyColumn
15-
import androidx.compose.foundation.lazy.items
1615
import androidx.compose.material.Card
1716
import androidx.compose.material.Icon
1817
import androidx.compose.material.IconButton
@@ -35,6 +34,7 @@ import androidx.compose.ui.text.font.FontWeight
3534
import androidx.compose.ui.unit.dp
3635
import com.squareup.workflow1.traceviewer.model.Node
3736
import com.squareup.workflow1.traceviewer.model.NodeUpdate
37+
import kotlin.reflect.full.memberProperties
3838

3939
/**
4040
* A panel that displays information about the selected workflow node.
@@ -103,14 +103,18 @@ private fun NodePanelDetails(
103103
modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)
104104
)
105105
}
106-
val fields = listOf("Name", "Id", "Props", "State", "Rendering")
107106

108-
items(fields) { field ->
109-
DetailCard(
110-
label = field,
111-
currValue = Node.getNodeField(node.current, field),
112-
pastValue = if (node.previous != null) Node.getNodeField(node.previous, field) else null
113-
)
107+
val fields = Node::class.memberProperties
108+
for (field in fields) {
109+
val currVal = field.get(node.current).toString()
110+
val pastVal = if (node.previous != null) field.get(node.previous).toString() else null
111+
item {
112+
DetailCard(
113+
label = field.name,
114+
currValue = currVal,
115+
pastValue = pastVal
116+
)
117+
}
114118
}
115119
}
116120
}

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.squareup.workflow1.traceviewer.ui
22

33
import androidx.compose.foundation.background
44
import androidx.compose.foundation.border
5-
import androidx.compose.foundation.clickable
65
import androidx.compose.foundation.layout.Arrangement
76
import androidx.compose.foundation.layout.Box
87
import androidx.compose.foundation.layout.Column
@@ -14,12 +13,18 @@ import androidx.compose.runtime.Composable
1413
import androidx.compose.runtime.LaunchedEffect
1514
import androidx.compose.runtime.getValue
1615
import androidx.compose.runtime.mutableStateListOf
16+
import androidx.compose.runtime.mutableStateMapOf
1717
import androidx.compose.runtime.mutableStateOf
1818
import androidx.compose.runtime.remember
1919
import androidx.compose.runtime.setValue
2020
import androidx.compose.ui.Alignment
21+
import androidx.compose.ui.ExperimentalComposeUiApi
2122
import androidx.compose.ui.Modifier
2223
import androidx.compose.ui.graphics.Color
24+
import androidx.compose.ui.input.pointer.PointerEventType
25+
import androidx.compose.ui.input.pointer.isPrimaryPressed
26+
import androidx.compose.ui.input.pointer.isSecondaryPressed
27+
import androidx.compose.ui.input.pointer.onPointerEvent
2328
import androidx.compose.ui.unit.dp
2429
import com.squareup.workflow1.traceviewer.model.Node
2530
import com.squareup.workflow1.traceviewer.util.ParseResult
@@ -69,19 +74,29 @@ internal fun RenderDiagram(
6974

7075
if (!isLoading) {
7176
val previousFrame = if (frameInd > 0) fullTree[frameInd - 1] else null
72-
DrawTree(fullTree[frameInd], previousFrame, affectedNodes[frameInd], onNodeSelect)
77+
DrawTree(
78+
node = fullTree[frameInd],
79+
previousNode = previousFrame,
80+
affectedNodes = affectedNodes[frameInd],
81+
expandedNodes = remember(frameInd) { mutableStateMapOf() },
82+
onNodeSelect = onNodeSelect,
83+
)
7384
}
7485
}
7586

7687
/**
7788
* Since the workflow nodes present a tree structure, we utilize a recursive function to draw the tree
7889
* The Column holds a subtree of nodes, and the Row holds all the children of the current node
90+
*
91+
* A mutable map is used to persist the expansion state of the nodes, allowing them to be open and
92+
* closed from user clicks.
7993
*/
8094
@Composable
8195
private fun DrawTree(
8296
node: Node,
8397
previousNode: Node?,
8498
affectedNodes: Set<Node>,
99+
expandedNodes: MutableMap<String, Boolean>,
85100
onNodeSelect: (Node, Node?) -> Unit,
86101
modifier: Modifier = Modifier,
87102
) {
@@ -93,21 +108,42 @@ private fun DrawTree(
93108
horizontalAlignment = Alignment.CenterHorizontally,
94109
) {
95110
val isAffected = affectedNodes.contains(node)
96-
DrawNode(node, previousNode, isAffected, onNodeSelect)
111+
// By default, nodes that relevant to this specific frame are expanded. All others are closed.
112+
LaunchedEffect(expandedNodes) {
113+
expandedNodes[node.id] = isAffected
114+
}
115+
val isExpanded = expandedNodes[node.id] == true
116+
117+
DrawNode(
118+
node,
119+
previousNode,
120+
isAffected,
121+
isExpanded,
122+
onNodeSelect,
123+
onExpandToggle = { expandedNodes[node.id] = !expandedNodes[node.id]!! }
124+
)
97125

98126
// Draws the node's children recursively.
99-
Row(
100-
horizontalArrangement = Arrangement.Center,
101-
verticalAlignment = Alignment.Top
102-
) {
103-
/*
127+
if (isExpanded) {
128+
Row(
129+
horizontalArrangement = Arrangement.Center,
130+
verticalAlignment = Alignment.Top
131+
) {
132+
/*
104133
We pair up the current node's children with previous frame's children.
105134
In the edge case that the current frame has additional children compared to the previous
106135
frame, we replace with null and will check before next recursive call.
107-
*/
108-
node.children.forEach { (index, childNode) ->
109-
val prevChildNode = previousNode?.children?.get(index)
110-
DrawTree(childNode, prevChildNode, affectedNodes, onNodeSelect)
136+
*/
137+
node.children.forEach { (index, childNode) ->
138+
val prevChildNode = previousNode?.children?.get(index)
139+
DrawTree(
140+
node = childNode,
141+
previousNode = prevChildNode,
142+
affectedNodes = affectedNodes,
143+
expandedNodes = expandedNodes,
144+
onNodeSelect = onNodeSelect
145+
)
146+
}
111147
}
112148
}
113149
}
@@ -116,24 +152,38 @@ private fun DrawTree(
116152
/**
117153
* A basic box that represents a workflow node
118154
*/
155+
@OptIn(ExperimentalComposeUiApi::class)
119156
@Composable
120157
private fun DrawNode(
121158
node: Node,
122159
previousNode: Node?,
123160
isAffected: Boolean,
161+
isExpanded: Boolean,
124162
onNodeSelect: (Node, Node?) -> Unit,
163+
onExpandToggle: (Node) -> Unit,
125164
) {
126165
Box(
127166
modifier = Modifier
128167
.background(if (isAffected) Color.Green else Color.Transparent)
129-
.clickable {
130-
// Selecting a node will bubble back up to the main view to handle the selection
131-
onNodeSelect(node, previousNode)
168+
.onPointerEvent(PointerEventType.Press) {
169+
if (it.buttons.isPrimaryPressed) {
170+
onNodeSelect(node, previousNode)
171+
} else if (it.buttons.isSecondaryPressed) {
172+
onExpandToggle(node)
173+
}
132174
}
133175
.padding(10.dp)
134176
) {
135177
Column(horizontalAlignment = Alignment.CenterHorizontally) {
136-
Text(text = node.name)
178+
Row(
179+
verticalAlignment = Alignment.CenterVertically,
180+
horizontalArrangement = Arrangement.spacedBy(4.dp)
181+
) {
182+
if (node.children.isNotEmpty()) {
183+
Text(text = if (isExpanded) "" else "")
184+
}
185+
Text(text = node.name)
186+
}
137187
Text(text = "ID: ${node.id}")
138188
}
139189
}

0 commit comments

Comments
 (0)