From b30a565d837341d61e3ef162f9fd9d343c0f1b7f Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Fri, 6 Jun 2025 15:53:44 -0400 Subject: [PATCH 01/16] Include a detailed view of node via side panel - This will provide a lot more information about each workflow node that's clicked, especially when it could contain a lot of information --- .../com/squareup/workflow1/traceviewer/App.kt | 100 +++++++++++++++--- .../workflow1/traceviewer/UploadFile.kt | 2 + 2 files changed, 89 insertions(+), 13 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 667e0bdd32..70f2ec602b 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 @@ -1,6 +1,18 @@ package com.squareup.workflow1.traceviewer +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -19,28 +31,90 @@ import io.github.vinceglb.filekit.readString public fun App( modifier: Modifier = Modifier ) { - Box { - var selectedFile by remember { mutableStateOf(null) } + var selectedFile by remember { mutableStateOf(null) } + var selectedNode by remember { mutableStateOf(null) } + // Used when user selects a new file in [UploadFile] + val resetSelectedNode = { selectedNode = null } + Box { + // Main content if (selectedFile != null) { - SandboxBackground { WorkflowContent(selectedFile!!) } + SandboxBackground { + LoadWorkflowContent(selectedFile) { + selectedNode = it + } + } } - UploadFile(onFileSelect = { selectedFile = it }) + // Left side information panel + InfoPanel( + selectedNode.value + ) + + // Bottom right upload button + UploadFile(resetSelectedNode, { selectedFile.value = it }) } } + + @Composable -private fun WorkflowContent(file: PlatformFile) { - var jsonString by remember { mutableStateOf(null) } - LaunchedEffect(file) { - jsonString = file.readString() +private fun InfoPanel( + selectedNode: WorkflowNode? +) { + Row { + val panelOpen = remember { mutableStateOf(false) } + + // based on open/close, display the node details (Column) + if (panelOpen.value) { + PanelDetails( + selectedNode, + Modifier.fillMaxWidth(.35f) + ) + } + + IconButton( + onClick = { panelOpen.value = !panelOpen.value }, + modifier = Modifier + .padding(8.dp) + .size(30.dp) + .align(Alignment.Top) + ) { + Icon( + imageVector = if (panelOpen.value) Filled.KeyboardArrowLeft else Filled.KeyboardArrowRight, + contentDescription = if (panelOpen.value) "Close Panel" else "Open Panel", + modifier = Modifier + ) + } } - val root = jsonString?.let { parseTrace(it) } +} + +@Composable +private fun PanelDetails( + node: WorkflowNode?, + modifier: Modifier = Modifier +) { + Column( + modifier + .fillMaxHeight() + .background(Color.LightGray) + ) { + if (node == null) { + Text("No node selected") + return@Column + } - if (root != null) { - DrawWorkflowTree(root) - } else { - Text("Empty data or failed to parse data") // TODO: proper handling of error + Column( + modifier = Modifier + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("only visible with a node selected") + Text( + text = "This is a node panel for ${node.name}", + fontSize = 20.sp, + modifier = Modifier.padding(8.dp) + ) + } } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/UploadFile.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/UploadFile.kt index 5cfd1d308d..1e71131bf2 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/UploadFile.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/UploadFile.kt @@ -23,6 +23,7 @@ import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher */ @Composable public fun UploadFile( + resetSelectedNode: () -> Unit, onFileSelect: (PlatformFile?) -> Unit, modifier: Modifier = Modifier, ) { @@ -35,6 +36,7 @@ public fun UploadFile( type = FileKitType.File(listOf("json", "txt")), title = "Select Workflow Trace File" ) { + resetSelectedNode() onFileSelect(it) } Button( From 42eeef91cc9f114abe8453c895914dbc8ba394a2 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Fri, 6 Jun 2025 16:08:49 -0400 Subject: [PATCH 02/16] Clean up project structure --- .../com/squareup/workflow1/traceviewer/App.kt | 71 ++------------ .../traceviewer/WorkflowJsonParser.kt | 29 ------ .../traceviewer/model/WorkflowNode.kt | 14 +++ .../workflow1/traceviewer/ui/NodeInfoPanel.kt | 96 +++++++++++++++++++ .../traceviewer/{ => ui}/WorkflowTree.kt | 40 ++++---- .../{ => utils}/SandboxBackground.kt | 2 +- .../traceviewer/{ => utils}/UploadFile.kt | 2 +- .../traceviewer/utils/WorkflowTreeLoader.kt | 61 ++++++++++++ 8 files changed, 197 insertions(+), 118 deletions(-) create mode 100644 workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/WorkflowNode.kt create mode 100644 workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/NodeInfoPanel.kt rename workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/{ => ui}/WorkflowTree.kt (69%) rename workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/{ => utils}/SandboxBackground.kt (98%) rename workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/{ => utils}/UploadFile.kt (97%) create mode 100644 workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/WorkflowTreeLoader.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 70f2ec602b..2528090aec 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 @@ -21,6 +21,14 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.squareup.workflow1.traceviewer.model.WorkflowNode +import com.squareup.workflow1.traceviewer.ui.InfoPanel +import com.squareup.workflow1.traceviewer.utils.LoadWorkflowContent +import com.squareup.workflow1.traceviewer.utils.SandboxBackground +import com.squareup.workflow1.traceviewer.utils.UploadFile import io.github.vinceglb.filekit.PlatformFile import io.github.vinceglb.filekit.readString @@ -55,66 +63,3 @@ public fun App( UploadFile(resetSelectedNode, { selectedFile.value = it }) } } - - - -@Composable -private fun InfoPanel( - selectedNode: WorkflowNode? -) { - Row { - val panelOpen = remember { mutableStateOf(false) } - - // based on open/close, display the node details (Column) - if (panelOpen.value) { - PanelDetails( - selectedNode, - Modifier.fillMaxWidth(.35f) - ) - } - - IconButton( - onClick = { panelOpen.value = !panelOpen.value }, - modifier = Modifier - .padding(8.dp) - .size(30.dp) - .align(Alignment.Top) - ) { - Icon( - imageVector = if (panelOpen.value) Filled.KeyboardArrowLeft else Filled.KeyboardArrowRight, - contentDescription = if (panelOpen.value) "Close Panel" else "Open Panel", - modifier = Modifier - ) - } - } -} - -@Composable -private fun PanelDetails( - node: WorkflowNode?, - modifier: Modifier = Modifier -) { - Column( - modifier - .fillMaxHeight() - .background(Color.LightGray) - ) { - if (node == null) { - Text("No node selected") - return@Column - } - - Column( - modifier = Modifier - .padding(8.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text("only visible with a node selected") - Text( - text = "This is a node panel for ${node.name}", - fontSize = 20.sp, - modifier = Modifier.padding(8.dp) - ) - } - } -} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowJsonParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowJsonParser.kt index 73dbe90735..e69de29bb2 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowJsonParser.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowJsonParser.kt @@ -1,29 +0,0 @@ -package com.squareup.workflow1.traceviewer - -import com.squareup.moshi.JsonDataException -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import java.io.IOException - -/** - * Parses a JSON string into [WorkflowNode] with Moshi adapters. - * - * All the caught exceptions should be handled by the caller, and appropriate UI feedback should be - * provided to user. - */ -public fun parseTrace( - json: String -): WorkflowNode? { - return try { - val moshi = Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() - val workflowAdapter = moshi.adapter(WorkflowNode::class.java) - val root = workflowAdapter.fromJson(json) - root - } catch (e: JsonDataException) { - throw JsonDataException("Failed to parse JSON: ${e.message}", e) - } catch (e: IOException) { - throw IOException("Malformed JSON: ${e.message}", e) - } -} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/WorkflowNode.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/WorkflowNode.kt new file mode 100644 index 0000000000..23bd7aacfb --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/WorkflowNode.kt @@ -0,0 +1,14 @@ +package com.squareup.workflow1.traceviewer.model + +/** + * 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 + * using flat data structures like an array. + * + * TBD what more metadata should be involved with each node, e.g. (props, states, # of render passes) + */ +public data class WorkflowNode( + val id: String, + val name: String, + val children: List +) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/NodeInfoPanel.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/NodeInfoPanel.kt new file mode 100644 index 0000000000..b723d0d317 --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/NodeInfoPanel.kt @@ -0,0 +1,96 @@ +package com.squareup.workflow1.traceviewer.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons.AutoMirrored.Filled +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.squareup.workflow1.traceviewer.model.WorkflowNode + +/** + * A panel that displays information about the selected workflow node. + * It can be toggled open or closed, and resets when the user selects a new file + * + * @param selectedNode The currently selected workflow node, or null if no node is selected. + */ +@Composable +public fun InfoPanel( + selectedNode: WorkflowNode?, + modifier: Modifier = Modifier +) { + Row { + val panelOpen = remember { mutableStateOf(false) } + + // based on open/close, display the node details (Column) + if (panelOpen.value) { + PanelDetails( + selectedNode, + Modifier.fillMaxWidth(.35f) + ) + } + + IconButton( + onClick = { panelOpen.value = !panelOpen.value }, + modifier = Modifier + .padding(8.dp) + .size(30.dp) + .align(Alignment.Top) + ) { + Icon( + imageVector = if (panelOpen.value) Filled.KeyboardArrowLeft else Filled.KeyboardArrowRight, + contentDescription = if (panelOpen.value) "Close Panel" else "Open Panel", + modifier = Modifier + ) + } + } +} + +/** + * The text details of the selected node. This should be closely coupled with the [WorkflowNode] + * data class to see what information should be displayed. + */ +@Composable +private fun PanelDetails( + node: WorkflowNode?, + modifier: Modifier = Modifier +) { + Column( + modifier + .fillMaxHeight() + .background(Color.LightGray) + ) { + if (node == null) { + Text("No node selected") + return@Column + } + + Column( + modifier = Modifier + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("only visible with a node selected") + Text( + text = "This is a node panel for ${node.name}", + fontSize = 20.sp, + modifier = Modifier.padding(8.dp) + ) + } + } +} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowTree.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt similarity index 69% rename from workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowTree.kt rename to workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt index 536d72f9a6..95dc0b81f7 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowTree.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt @@ -18,27 +18,16 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import com.squareup.workflow1.traceviewer.model.WorkflowNode /** - * 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 - * using flat data structures like an array. - * - * TBD what more metadata should be involved with each node, e.g. (props, states, # of render passes) - */ -public data class WorkflowNode( - val id: String, - val name: String, - val children: List -) - -/** - * 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. + * 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 */ @Composable public fun DrawWorkflowTree( node: WorkflowNode, + onNodeSelect: (WorkflowNode) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -48,40 +37,43 @@ public fun DrawWorkflowTree( .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { - // draws itself - DrawNode(node) + // draws the node itself + DrawNode(node, onNodeSelect) - // draws children recursively + // draws the node's children recursively Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.Top ) { node.children.forEach { childNode -> - DrawWorkflowTree(childNode) + DrawWorkflowTree(childNode, onNodeSelect) } } } } /** - * A basic box that represents a workflow node. + * A basic box that represents a workflow node */ @Composable private fun DrawNode( node: WorkflowNode, + onNodeSelect: (WorkflowNode) -> Unit, ) { var open by remember { mutableStateOf(false) } Box( modifier = Modifier - .clickable { open = !open } + .clickable { + // open.value = !open.value + + // selection will bubble back up to the main view to handle the selection + onNodeSelect(node) + } .padding(10.dp) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(text = node.name) Text(text = "ID: ${node.id}") - if (open) { - Text("node is opened") - } } } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/SandboxBackground.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/SandboxBackground.kt similarity index 98% rename from workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/SandboxBackground.kt rename to workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/SandboxBackground.kt index cbbdb1255c..3082713cd9 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/SandboxBackground.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/SandboxBackground.kt @@ -1,4 +1,4 @@ -package com.squareup.workflow1.traceviewer +package com.squareup.workflow1.traceviewer.utils import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.detectDragGestures diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/UploadFile.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/UploadFile.kt similarity index 97% rename from workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/UploadFile.kt rename to workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/UploadFile.kt index 1e71131bf2..585c628111 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/UploadFile.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/UploadFile.kt @@ -1,4 +1,4 @@ -package com.squareup.workflow1.traceviewer +package com.squareup.workflow1.traceviewer.utils import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/WorkflowTreeLoader.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/WorkflowTreeLoader.kt new file mode 100644 index 0000000000..b06465d9b7 --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/WorkflowTreeLoader.kt @@ -0,0 +1,61 @@ +package com.squareup.workflow1.traceviewer.utils + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.squareup.moshi.JsonDataException +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.squareup.workflow1.traceviewer.model.WorkflowNode +import com.squareup.workflow1.traceviewer.ui.DrawWorkflowTree +import io.github.vinceglb.filekit.PlatformFile +import io.github.vinceglb.filekit.readString +import java.io.IOException + +/** + * Parses the data from the given file and initiates the workflow tree + */ +@Composable +public fun LoadWorkflowContent( + file: PlatformFile?, + onNodeSelect: (WorkflowNode) -> Unit, + modifier: Modifier = Modifier +) { + val jsonString = remember { mutableStateOf(null) } + LaunchedEffect(file) { + jsonString.value = file?.readString() + } + val root = jsonString.value?.let { fetchRoot(it) } + + if (root != null) { + DrawWorkflowTree(root, onNodeSelect) + } else { + Text("Empty data or failed to parse data") // TODO: proper handling of error + } +} + +/** + * Parses a JSON string into [WorkflowNode] with Moshi adapters + * + * All the caught exceptions should be handled by the caller, and appropriate UI feedback should be + * provided to user + */ +public fun fetchRoot( + json: String +): WorkflowNode? { + return try { + val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + val workflowAdapter = moshi.adapter(WorkflowNode::class.java) + val root = workflowAdapter.fromJson(json) + root + } catch (e: JsonDataException) { + throw JsonDataException("Failed to parse JSON: ${e.message}", e) + } catch (e: IOException) { + throw IOException("Malformed JSON: ${e.message}", e) + } +} From 7db8039e75862ae3ba0cfe77ff49dec5dbc514e6 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Fri, 6 Jun 2025 16:08:49 -0400 Subject: [PATCH 03/16] Clean up project structure --- .../com/squareup/workflow1/traceviewer/App.kt | 21 ++----------------- 1 file changed, 2 insertions(+), 19 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 2528090aec..d1bdec52f5 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 @@ -1,19 +1,6 @@ package com.squareup.workflow1.traceviewer -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -21,16 +8,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.squareup.workflow1.traceviewer.model.WorkflowNode import com.squareup.workflow1.traceviewer.ui.InfoPanel import com.squareup.workflow1.traceviewer.utils.LoadWorkflowContent import com.squareup.workflow1.traceviewer.utils.SandboxBackground import com.squareup.workflow1.traceviewer.utils.UploadFile import io.github.vinceglb.filekit.PlatformFile -import io.github.vinceglb.filekit.readString /** * Main composable that provides the different layers of UI. @@ -56,10 +39,10 @@ public fun App( // Left side information panel InfoPanel( - selectedNode.value + selectedNode ) // Bottom right upload button - UploadFile(resetSelectedNode, { selectedFile.value = it }) + UploadFile(resetSelectedNode, { selectedFile = it }) } } From 6dea47e109760a439ad67749b573d3c852458c20 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Mon, 9 Jun 2025 16:41:09 -0400 Subject: [PATCH 04/16] Change file names. --- .../traceviewer/ui/{NodeInfoPanel.kt => WorkflowInfoPanel.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/{NodeInfoPanel.kt => WorkflowInfoPanel.kt} (100%) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/NodeInfoPanel.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt similarity index 100% rename from workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/NodeInfoPanel.kt rename to workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt From da2da0cdfdf292d46190af983e3fe8c49ff11462 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Tue, 10 Jun 2025 11:30:30 -0400 Subject: [PATCH 05/16] Edit composables to align with their responsibilities The order of the main content being displayed was from parsing -> rendering, which would lead to complexity when adding in tabbing functionality. Having the order be Renderer -> parsing -> rendering was more clear. This commit also introduces the idea of a "trace", where we have input a json array instead of a singular json --- .../com/squareup/workflow1/traceviewer/App.kt | 21 +- .../workflow1/traceviewer/ui/WorkflowTree.kt | 39 +- .../{WorkflowTreeLoader.kt => JsonParser.kt} | 37 +- .../src/jvmMain/resources/workflow-20.json | 4 +- .../src/jvmMain/resources/workflow-300.json | 4 +- .../jvmMain/resources/workflow-traces.json | 1052 +++++++++++++++++ 6 files changed, 1116 insertions(+), 41 deletions(-) rename workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/{WorkflowTreeLoader.kt => JsonParser.kt} (55%) create mode 100644 workflow-trace-viewer/src/jvmMain/resources/workflow-traces.json 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 d1bdec52f5..a9f03c2040 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,13 +4,15 @@ 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.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.squareup.workflow1.traceviewer.model.WorkflowNode import com.squareup.workflow1.traceviewer.ui.InfoPanel -import com.squareup.workflow1.traceviewer.utils.LoadWorkflowContent +import com.squareup.workflow1.traceviewer.ui.RenderDiagram import com.squareup.workflow1.traceviewer.utils.SandboxBackground import com.squareup.workflow1.traceviewer.utils.UploadFile import io.github.vinceglb.filekit.PlatformFile @@ -24,6 +26,8 @@ public fun App( ) { var selectedFile by remember { mutableStateOf(null) } var selectedNode by remember { mutableStateOf(null) } + var snapshotIndex by remember { mutableIntStateOf(0) } + // Used when user selects a new file in [UploadFile] val resetSelectedNode = { selectedNode = null } @@ -31,16 +35,19 @@ public fun App( // Main content if (selectedFile != null) { SandboxBackground { - LoadWorkflowContent(selectedFile) { - selectedNode = it - } + RenderDiagram( + file = selectedFile, + traceInd = snapshotIndex, + onNodeSelect = { selectedNode = it } + ) } } + // Top trace selector row + // TraceSelectRow { snapshotIndex.value = it } + // Left side information panel - InfoPanel( - selectedNode - ) + InfoPanel(selectedNode) // Bottom right upload button UploadFile(resetSelectedNode, { selectedFile = it }) 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 95dc0b81f7..f632b09dd2 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,4 +1,4 @@ -package com.squareup.workflow1.traceviewer +package com.squareup.workflow1.traceviewer.ui import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -14,18 +14,46 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.squareup.workflow1.traceviewer.model.WorkflowNode +import com.squareup.workflow1.traceviewer.utils.fetchTrace +import io.github.vinceglb.filekit.PlatformFile + +/** + * Access point for drawing the main content of the app. It will load the trace for given files and + * tabs. This will also all errors related to errors parsing a given trace JSON file. + */ +@Composable +public fun RenderDiagram( + file: PlatformFile?, + traceInd: Int, + onNodeSelect: (WorkflowNode) -> Unit, +) { + var workflowNodes by remember { mutableStateOf>(emptyList()) } + var isLoading by remember { mutableStateOf(true) } + + LaunchedEffect(file) { + workflowNodes = fetchTrace(file) + isLoading = false + } + + if (!isLoading) { + DrawTree(workflowNodes[traceInd], onNodeSelect) + } + + // TODO: catch errors and display UI here +} /** * 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 */ @Composable -public fun DrawWorkflowTree( +private fun DrawTree( node: WorkflowNode, onNodeSelect: (WorkflowNode) -> Unit, modifier: Modifier = Modifier, @@ -46,7 +74,7 @@ public fun DrawWorkflowTree( verticalAlignment = Alignment.Top ) { node.children.forEach { childNode -> - DrawWorkflowTree(childNode, onNodeSelect) + DrawTree(childNode, onNodeSelect) } } } @@ -60,13 +88,10 @@ private fun DrawNode( node: WorkflowNode, onNodeSelect: (WorkflowNode) -> Unit, ) { - var open by remember { mutableStateOf(false) } Box( modifier = Modifier .clickable { - // open.value = !open.value - - // selection will bubble back up to the main view to handle the selection + // Selecting a node will bubble back up to the main view to handle the selection onNodeSelect(node) } .padding(10.dp) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/WorkflowTreeLoader.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/JsonParser.kt similarity index 55% rename from workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/WorkflowTreeLoader.kt rename to workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/JsonParser.kt index b06465d9b7..b44a0919e9 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/WorkflowTreeLoader.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/JsonParser.kt @@ -1,16 +1,12 @@ package com.squareup.workflow1.traceviewer.utils -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonDataException import com.squareup.moshi.Moshi +import com.squareup.moshi.Types import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.squareup.workflow1.traceviewer.model.WorkflowNode -import com.squareup.workflow1.traceviewer.ui.DrawWorkflowTree import io.github.vinceglb.filekit.PlatformFile import io.github.vinceglb.filekit.readString import java.io.IOException @@ -18,23 +14,12 @@ import java.io.IOException /** * Parses the data from the given file and initiates the workflow tree */ -@Composable -public fun LoadWorkflowContent( +public suspend fun fetchTrace( file: PlatformFile?, - onNodeSelect: (WorkflowNode) -> Unit, modifier: Modifier = Modifier -) { - val jsonString = remember { mutableStateOf(null) } - LaunchedEffect(file) { - jsonString.value = file?.readString() - } - val root = jsonString.value?.let { fetchRoot(it) } - - if (root != null) { - DrawWorkflowTree(root, onNodeSelect) - } else { - Text("Empty data or failed to parse data") // TODO: proper handling of error - } +): List { + val jsonString = file?.readString() + return jsonString?.let { parseTrace(it) } ?: emptyList() } /** @@ -43,15 +28,17 @@ public fun LoadWorkflowContent( * All the caught exceptions should be handled by the caller, and appropriate UI feedback should be * provided to user */ -public fun fetchRoot( +public fun parseTrace( json: String -): WorkflowNode? { +): List { return try { val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() - val workflowAdapter = moshi.adapter(WorkflowNode::class.java) - val root = workflowAdapter.fromJson(json) + + val workflowList = Types.newParameterizedType(List::class.java, WorkflowNode::class.java) + val workflowAdapter: JsonAdapter> = moshi.adapter(workflowList) + val root = workflowAdapter.fromJson(json) ?: emptyList() root } catch (e: JsonDataException) { throw JsonDataException("Failed to parse JSON: ${e.message}", e) diff --git a/workflow-trace-viewer/src/jvmMain/resources/workflow-20.json b/workflow-trace-viewer/src/jvmMain/resources/workflow-20.json index 08ad530c63..2415247969 100644 --- a/workflow-trace-viewer/src/jvmMain/resources/workflow-20.json +++ b/workflow-trace-viewer/src/jvmMain/resources/workflow-20.json @@ -1,3 +1,4 @@ +[ { "id": 1, "name": "root", @@ -102,4 +103,5 @@ ] } ] -} \ No newline at end of file +} +] diff --git a/workflow-trace-viewer/src/jvmMain/resources/workflow-300.json b/workflow-trace-viewer/src/jvmMain/resources/workflow-300.json index 9733c6e233..2f9a2df11f 100644 --- a/workflow-trace-viewer/src/jvmMain/resources/workflow-300.json +++ b/workflow-trace-viewer/src/jvmMain/resources/workflow-300.json @@ -1,3 +1,4 @@ +[ { "id": 1, "name": "root", @@ -2595,4 +2596,5 @@ ] } ] -} \ No newline at end of file +} +] diff --git a/workflow-trace-viewer/src/jvmMain/resources/workflow-traces.json b/workflow-trace-viewer/src/jvmMain/resources/workflow-traces.json new file mode 100644 index 0000000000..3dc1d0d055 --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/resources/workflow-traces.json @@ -0,0 +1,1052 @@ +[ + { + "id": 1, + "name": "root", + "children": [ + { + "id": 2, + "name": "auth-flow", + "children": [ + { + "id": 3, + "name": "login-screen", + "children": [ + { + "id": 4, + "name": "login-form", + "children": [] + }, + { + "id": 5, + "name": "social-login", + "children": [] + } + ] + } + ] + }, + { + "id": 6, + "name": "main-flow", + "children": [ + { + "id": 7, + "name": "dashboard", + "children": [ + { + "id": 8, + "name": "stats-widget", + "children": [] + }, + { + "id": 9, + "name": "recent-activity", + "children": [] + } + ] + } + ] + }, + { + "id": 10, + "name": "background-tasks", + "children": [ + { + "id": 11, + "name": "sync-service", + "children": [ + { + "id": 12, + "name": "profile-sync", + "children": [] + }, + { + "id": 13, + "name": "preferences-sync", + "children": [] + } + ] + }, + { + "id": 14, + "name": "notification-service", + "children": [ + { + "id": 15, + "name": "push-handler", + "children": [] + } + ] + } + ] + }, + { + "id": 16, + "name": "settings-flow", + "children": [ + { + "id": 17, + "name": "settings-screen", + "children": [ + { + "id": 18, + "name": "profile-settings", + "children": [] + }, + { + "id": 19, + "name": "notification-settings", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 20, + "name": "app-root", + "children": [ + { + "id": 21, + "name": "user-flow", + "children": [ + { + "id": 22, + "name": "signup-screen", + "children": [ + { + "id": 23, + "name": "registration-form", + "children": [] + }, + { + "id": 24, + "name": "oauth-signup", + "children": [] + } + ] + } + ] + }, + { + "id": 25, + "name": "content-flow", + "children": [ + { + "id": 26, + "name": "home-screen", + "children": [ + { + "id": 27, + "name": "featured-content", + "children": [] + }, + { + "id": 28, + "name": "user-feed", + "children": [] + } + ] + } + ] + }, + { + "id": 29, + "name": "system-tasks", + "children": [ + { + "id": 30, + "name": "data-service", + "children": [ + { + "id": 31, + "name": "content-sync", + "children": [] + }, + { + "id": 32, + "name": "cache-manager", + "children": [] + } + ] + }, + { + "id": 33, + "name": "analytics-service", + "children": [ + { + "id": 34, + "name": "event-tracker", + "children": [] + } + ] + } + ] + }, + { + "id": 35, + "name": "preferences-flow", + "children": [ + { + "id": 36, + "name": "preferences-screen", + "children": [ + { + "id": 37, + "name": "account-settings", + "children": [] + }, + { + "id": 38, + "name": "privacy-settings", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 39, + "name": "system-root", + "children": [ + { + "id": 40, + "name": "security-flow", + "children": [ + { + "id": 41, + "name": "auth-screen", + "children": [ + { + "id": 42, + "name": "biometric-auth", + "children": [] + }, + { + "id": 43, + "name": "pin-verification", + "children": [] + } + ] + } + ] + }, + { + "id": 44, + "name": "navigation-flow", + "children": [ + { + "id": 45, + "name": "tab-navigator", + "children": [ + { + "id": 46, + "name": "home-tab", + "children": [] + }, + { + "id": 47, + "name": "search-tab", + "children": [] + } + ] + } + ] + }, + { + "id": 48, + "name": "core-services", + "children": [ + { + "id": 49, + "name": "network-service", + "children": [ + { + "id": 50, + "name": "api-client", + "children": [] + }, + { + "id": 51, + "name": "offline-handler", + "children": [] + } + ] + }, + { + "id": 52, + "name": "storage-service", + "children": [ + { + "id": 53, + "name": "database-manager", + "children": [] + } + ] + } + ] + }, + { + "id": 54, + "name": "admin-flow", + "children": [ + { + "id": 55, + "name": "admin-panel", + "children": [ + { + "id": 56, + "name": "user-management", + "children": [] + }, + { + "id": 57, + "name": "system-monitoring", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 58, + "name": "commerce-root", + "children": [ + { + "id": 59, + "name": "payment-flow", + "children": [ + { + "id": 60, + "name": "checkout-screen", + "children": [ + { + "id": 61, + "name": "payment-form", + "children": [] + }, + { + "id": 62, + "name": "card-scanner", + "children": [] + } + ] + } + ] + }, + { + "id": 63, + "name": "catalog-flow", + "children": [ + { + "id": 64, + "name": "product-list", + "children": [ + { + "id": 65, + "name": "product-card", + "children": [] + }, + { + "id": 66, + "name": "filter-panel", + "children": [] + } + ] + } + ] + }, + { + "id": 67, + "name": "order-tasks", + "children": [ + { + "id": 68, + "name": "fulfillment-service", + "children": [ + { + "id": 69, + "name": "inventory-check", + "children": [] + }, + { + "id": 70, + "name": "shipping-calc", + "children": [] + } + ] + }, + { + "id": 71, + "name": "payment-service", + "children": [ + { + "id": 72, + "name": "transaction-processor", + "children": [] + } + ] + } + ] + }, + { + "id": 73, + "name": "merchant-flow", + "children": [ + { + "id": 74, + "name": "seller-dashboard", + "children": [ + { + "id": 75, + "name": "sales-analytics", + "children": [] + }, + { + "id": 76, + "name": "inventory-manager", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 77, + "name": "social-root", + "children": [ + { + "id": 78, + "name": "messaging-flow", + "children": [ + { + "id": 79, + "name": "chat-screen", + "children": [ + { + "id": 80, + "name": "message-input", + "children": [] + }, + { + "id": 81, + "name": "media-picker", + "children": [] + } + ] + } + ] + }, + { + "id": 82, + "name": "social-flow", + "children": [ + { + "id": 83, + "name": "timeline-screen", + "children": [ + { + "id": 84, + "name": "post-composer", + "children": [] + }, + { + "id": 85, + "name": "story-viewer", + "children": [] + } + ] + } + ] + }, + { + "id": 86, + "name": "connection-tasks", + "children": [ + { + "id": 87, + "name": "friend-service", + "children": [ + { + "id": 88, + "name": "contact-sync", + "children": [] + }, + { + "id": 89, + "name": "suggestion-engine", + "children": [] + } + ] + }, + { + "id": 90, + "name": "activity-service", + "children": [ + { + "id": 91, + "name": "feed-generator", + "children": [] + } + ] + } + ] + }, + { + "id": 92, + "name": "privacy-flow", + "children": [ + { + "id": 93, + "name": "privacy-screen", + "children": [ + { + "id": 94, + "name": "visibility-controls", + "children": [] + }, + { + "id": 95, + "name": "block-manager", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 96, + "name": "media-root", + "children": [ + { + "id": 97, + "name": "streaming-flow", + "children": [ + { + "id": 98, + "name": "player-screen", + "children": [ + { + "id": 99, + "name": "video-player", + "children": [] + }, + { + "id": 100, + "name": "audio-controls", + "children": [] + } + ] + } + ] + }, + { + "id": 101, + "name": "library-flow", + "children": [ + { + "id": 102, + "name": "media-browser", + "children": [ + { + "id": 103, + "name": "playlist-view", + "children": [] + }, + { + "id": 104, + "name": "search-results", + "children": [] + } + ] + } + ] + }, + { + "id": 105, + "name": "processing-tasks", + "children": [ + { + "id": 106, + "name": "encoding-service", + "children": [ + { + "id": 107, + "name": "video-encoder", + "children": [] + }, + { + "id": 108, + "name": "thumbnail-generator", + "children": [] + } + ] + }, + { + "id": 109, + "name": "cdn-service", + "children": [ + { + "id": 110, + "name": "content-distributor", + "children": [] + } + ] + } + ] + }, + { + "id": 111, + "name": "creator-flow", + "children": [ + { + "id": 112, + "name": "upload-screen", + "children": [ + { + "id": 113, + "name": "file-uploader", + "children": [] + }, + { + "id": 114, + "name": "metadata-editor", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 115, + "name": "finance-root", + "children": [ + { + "id": 116, + "name": "banking-flow", + "children": [ + { + "id": 117, + "name": "account-screen", + "children": [ + { + "id": 118, + "name": "balance-widget", + "children": [] + }, + { + "id": 119, + "name": "transaction-list", + "children": [] + } + ] + } + ] + }, + { + "id": 120, + "name": "transfer-flow", + "children": [ + { + "id": 121, + "name": "send-money-screen", + "children": [ + { + "id": 122, + "name": "recipient-selector", + "children": [] + }, + { + "id": 123, + "name": "amount-input", + "children": [] + } + ] + } + ] + }, + { + "id": 124, + "name": "financial-tasks", + "children": [ + { + "id": 125, + "name": "fraud-service", + "children": [ + { + "id": 126, + "name": "risk-analyzer", + "children": [] + }, + { + "id": 127, + "name": "pattern-detector", + "children": [] + } + ] + }, + { + "id": 128, + "name": "compliance-service", + "children": [ + { + "id": 129, + "name": "kyc-validator", + "children": [] + } + ] + } + ] + }, + { + "id": 130, + "name": "investment-flow", + "children": [ + { + "id": 131, + "name": "portfolio-screen", + "children": [ + { + "id": 132, + "name": "asset-overview", + "children": [] + }, + { + "id": 133, + "name": "performance-chart", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 134, + "name": "health-root", + "children": [ + { + "id": 135, + "name": "tracking-flow", + "children": [ + { + "id": 136, + "name": "activity-screen", + "children": [ + { + "id": 137, + "name": "step-counter", + "children": [] + }, + { + "id": 138, + "name": "workout-tracker", + "children": [] + } + ] + } + ] + }, + { + "id": 139, + "name": "wellness-flow", + "children": [ + { + "id": 140, + "name": "health-dashboard", + "children": [ + { + "id": 141, + "name": "vital-signs", + "children": [] + }, + { + "id": 142, + "name": "medication-reminder", + "children": [] + } + ] + } + ] + }, + { + "id": 143, + "name": "monitoring-tasks", + "children": [ + { + "id": 144, + "name": "sensor-service", + "children": [ + { + "id": 145, + "name": "heart-rate-monitor", + "children": [] + }, + { + "id": 146, + "name": "sleep-tracker", + "children": [] + } + ] + }, + { + "id": 147, + "name": "analysis-service", + "children": [ + { + "id": 148, + "name": "trend-analyzer", + "children": [] + } + ] + } + ] + }, + { + "id": 149, + "name": "consultation-flow", + "children": [ + { + "id": 150, + "name": "telemedicine-screen", + "children": [ + { + "id": 151, + "name": "video-call", + "children": [] + }, + { + "id": 152, + "name": "symptom-checker", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 153, + "name": "education-root", + "children": [ + { + "id": 154, + "name": "learning-flow", + "children": [ + { + "id": 155, + "name": "course-screen", + "children": [ + { + "id": 156, + "name": "video-lesson", + "children": [] + }, + { + "id": 157, + "name": "quiz-component", + "children": [] + } + ] + } + ] + }, + { + "id": 158, + "name": "progress-flow", + "children": [ + { + "id": 159, + "name": "achievement-screen", + "children": [ + { + "id": 160, + "name": "badge-display", + "children": [] + }, + { + "id": 161, + "name": "progress-tracker", + "children": [] + } + ] + } + ] + }, + { + "id": 162, + "name": "assessment-tasks", + "children": [ + { + "id": 163, + "name": "grading-service", + "children": [ + { + "id": 164, + "name": "auto-grader", + "children": [] + }, + { + "id": 165, + "name": "feedback-generator", + "children": [] + } + ] + }, + { + "id": 166, + "name": "analytics-service", + "children": [ + { + "id": 167, + "name": "learning-analyzer", + "children": [] + } + ] + } + ] + }, + { + "id": 168, + "name": "collaboration-flow", + "children": [ + { + "id": 169, + "name": "study-group-screen", + "children": [ + { + "id": 170, + "name": "whiteboard-tool", + "children": [] + }, + { + "id": 171, + "name": "discussion-forum", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 172, + "name": "travel-root", + "children": [ + { + "id": 173, + "name": "booking-flow", + "children": [ + { + "id": 174, + "name": "search-screen", + "children": [ + { + "id": 175, + "name": "flight-finder", + "children": [] + }, + { + "id": 176, + "name": "hotel-search", + "children": [] + } + ] + } + ] + }, + { + "id": 177, + "name": "itinerary-flow", + "children": [ + { + "id": 178, + "name": "trip-planner", + "children": [ + { + "id": 179, + "name": "schedule-builder", + "children": [] + }, + { + "id": 180, + "name": "map-integration", + "children": [] + } + ] + } + ] + }, + { + "id": 181, + "name": "travel-tasks", + "children": [ + { + "id": 182, + "name": "booking-service", + "children": [ + { + "id": 183, + "name": "reservation-manager", + "children": [] + }, + { + "id": 184, + "name": "price-tracker", + "children": [] + } + ] + }, + { + "id": 185, + "name": "notification-service", + "children": [ + { + "id": 186, + "name": "flight-alerts", + "children": [] + } + ] + } + ] + }, + { + "id": 187, + "name": "companion-flow", + "children": [ + { + "id": 188, + "name": "travel-guide", + "children": [ + { + "id": 189, + "name": "local-recommendations", + "children": [] + }, + { + "id": 190, + "name": "weather-widget", + "children": [] + } + ] + } + ] + } + ] + } +] \ No newline at end of file From d85d0ffd406e4dfc2edaff71a8f2bfbe4ac6097e Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Tue, 10 Jun 2025 16:41:48 -0400 Subject: [PATCH 06/16] Add scroll bar functionality to allow indexing different states within trace --- .../com/squareup/workflow1/traceviewer/App.kt | 20 +++++---- .../traceviewer/WorkflowJsonParser.kt | 0 .../traceviewer/ui/StateSelectTab.kt | 41 +++++++++++++++++++ .../workflow1/traceviewer/ui/WorkflowTree.kt | 6 ++- .../traceviewer/utils/SandboxBackground.kt | 2 +- 5 files changed, 58 insertions(+), 11 deletions(-) delete mode 100644 workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowJsonParser.kt create mode 100644 workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.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 a9f03c2040..ee1b4f712b 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,17 +2,17 @@ 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.mutableIntStateOf 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 com.squareup.workflow1.traceviewer.model.WorkflowNode import com.squareup.workflow1.traceviewer.ui.InfoPanel import com.squareup.workflow1.traceviewer.ui.RenderDiagram +import com.squareup.workflow1.traceviewer.ui.StateSelectTab import com.squareup.workflow1.traceviewer.utils.SandboxBackground import com.squareup.workflow1.traceviewer.utils.UploadFile import io.github.vinceglb.filekit.PlatformFile @@ -26,30 +26,34 @@ public fun App( ) { var selectedFile by remember { mutableStateOf(null) } var selectedNode by remember { mutableStateOf(null) } + var workflowTrace by remember { mutableStateOf>(emptyList()) } var snapshotIndex by remember { mutableIntStateOf(0) } - // Used when user selects a new file in [UploadFile] - val resetSelectedNode = { selectedNode = null } - Box { // Main content if (selectedFile != null) { SandboxBackground { RenderDiagram( - file = selectedFile, + file = selectedFile!!, traceInd = snapshotIndex, + onFileParse = { workflowTrace = it }, onNodeSelect = { selectedNode = it } ) } } // Top trace selector row - // TraceSelectRow { snapshotIndex.value = it } + StateSelectTab( + trace = workflowTrace, + currentIndex = snapshotIndex, + onIndexChange = { snapshotIndex = it }, + modifier = Modifier.align(Alignment.TopCenter) + ) // Left side information panel InfoPanel(selectedNode) // Bottom right upload button - UploadFile(resetSelectedNode, { selectedFile = it }) + UploadFile(resetSelectedNode = { selectedNode = null }, onFileSelect = { selectedFile = it }) } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowJsonParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowJsonParser.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt new file mode 100644 index 0000000000..2db256fa7e --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt @@ -0,0 +1,41 @@ +package com.squareup.workflow1.traceviewer.ui + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.squareup.workflow1.traceviewer.model.WorkflowNode + +@Composable +public fun StateSelectTab( + trace: List, + currentIndex: Int, + onIndexChange: (Int) -> Unit, + modifier: Modifier = Modifier +) { + val state = rememberLazyListState() + + LazyRow( + modifier = modifier, + state = state + ) { + items(trace.size) { index -> + Button( + modifier = Modifier + .padding(10.dp), + onClick = { onIndexChange(index) }, + colors = ButtonDefaults.buttonColors( + backgroundColor = if (index == currentIndex) Color.DarkGray else Color.LightGray + ) + ) { + Text("State ${index + 1}") + } + } + } +} 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 f632b09dd2..d8bde6c3e8 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 @@ -10,11 +10,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -29,8 +29,9 @@ import io.github.vinceglb.filekit.PlatformFile */ @Composable public fun RenderDiagram( - file: PlatformFile?, + file: PlatformFile, traceInd: Int, + onFileParse: (List) -> Unit, onNodeSelect: (WorkflowNode) -> Unit, ) { var workflowNodes by remember { mutableStateOf>(emptyList()) } @@ -38,6 +39,7 @@ public fun RenderDiagram( LaunchedEffect(file) { workflowNodes = fetchTrace(file) + onFileParse(workflowNodes) isLoading = false } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/SandboxBackground.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/SandboxBackground.kt index 3082713cd9..2b51c83420 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/SandboxBackground.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/SandboxBackground.kt @@ -38,7 +38,7 @@ public fun SandboxBackground( .fillMaxSize() .pointerInput(Unit) { // Panning capabilities: watches for drag gestures and applies the translation - detectDragGestures { _, translation-> + detectDragGestures { _, translation -> offset += translation } } From bdbc65fb0e12bce101dd1ef9cb59cf1a3bcf6f37 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Wed, 11 Jun 2025 11:24:28 -0400 Subject: [PATCH 07/16] Bugfix with switching between different files and UI change for top row --- .../com/squareup/workflow1/traceviewer/App.kt | 6 +- .../traceviewer/ui/StateSelectTab.kt | 35 +- .../traceviewer/ui/WorkflowInfoPanel.kt | 12 +- .../workflow1/traceviewer/utils/UploadFile.kt | 4 +- .../jvmMain/resources/workflow-traces.json | 1052 ++++++++++++++++- 5 files changed, 1086 insertions(+), 23 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 ee1b4f712b..8791384235 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 @@ -54,6 +54,10 @@ public fun App( InfoPanel(selectedNode) // Bottom right upload button - UploadFile(resetSelectedNode = { selectedNode = null }, onFileSelect = { selectedFile = it }) + val onReset = { + selectedNode = null + snapshotIndex = 0 + } + UploadFile(onReset = onReset, onFileSelect = { selectedFile = it }) } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt index 2db256fa7e..ef5c0efeb3 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt @@ -1,13 +1,15 @@ package com.squareup.workflow1.traceviewer.ui +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.squareup.workflow1.traceviewer.model.WorkflowNode @@ -21,20 +23,25 @@ public fun StateSelectTab( ) { val state = rememberLazyListState() - LazyRow( - modifier = modifier, - state = state + Surface( + modifier = modifier + .padding(4.dp), + color = Color.White, ) { - items(trace.size) { index -> - Button( - modifier = Modifier - .padding(10.dp), - onClick = { onIndexChange(index) }, - colors = ButtonDefaults.buttonColors( - backgroundColor = if (index == currentIndex) Color.DarkGray else Color.LightGray + LazyRow( + modifier = Modifier + .padding(8.dp), + state = state + ) { + items(trace.size) { index -> + Text( + text = "State ${index + 1}", + color = if (index == currentIndex) Color.Black else Color.LightGray, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { onIndexChange(index) } + .padding(10.dp) ) - ) { - Text("State ${index + 1}") } } } 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 b723d0d317..424420d76c 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 @@ -14,8 +14,10 @@ import androidx.compose.material.icons.Icons.AutoMirrored.Filled import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue 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.graphics.Color @@ -35,10 +37,10 @@ public fun InfoPanel( modifier: Modifier = Modifier ) { Row { - val panelOpen = remember { mutableStateOf(false) } + var panelOpen by remember { mutableStateOf(false) } // based on open/close, display the node details (Column) - if (panelOpen.value) { + if (panelOpen) { PanelDetails( selectedNode, Modifier.fillMaxWidth(.35f) @@ -46,15 +48,15 @@ public fun InfoPanel( } IconButton( - onClick = { panelOpen.value = !panelOpen.value }, + onClick = { panelOpen = !panelOpen }, modifier = Modifier .padding(8.dp) .size(30.dp) .align(Alignment.Top) ) { Icon( - imageVector = if (panelOpen.value) Filled.KeyboardArrowLeft else Filled.KeyboardArrowRight, - contentDescription = if (panelOpen.value) "Close Panel" else "Open Panel", + imageVector = if (panelOpen) Filled.KeyboardArrowLeft else Filled.KeyboardArrowRight, + contentDescription = if (panelOpen) "Close Panel" else "Open Panel", modifier = Modifier ) } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/UploadFile.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/UploadFile.kt index 585c628111..00dd69ea6f 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/UploadFile.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/UploadFile.kt @@ -23,7 +23,7 @@ import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher */ @Composable public fun UploadFile( - resetSelectedNode: () -> Unit, + onReset: () -> Unit, onFileSelect: (PlatformFile?) -> Unit, modifier: Modifier = Modifier, ) { @@ -36,7 +36,7 @@ public fun UploadFile( type = FileKitType.File(listOf("json", "txt")), title = "Select Workflow Trace File" ) { - resetSelectedNode() + onReset() onFileSelect(it) } Button( diff --git a/workflow-trace-viewer/src/jvmMain/resources/workflow-traces.json b/workflow-trace-viewer/src/jvmMain/resources/workflow-traces.json index 3dc1d0d055..4c1b0b77b4 100644 --- a/workflow-trace-viewer/src/jvmMain/resources/workflow-traces.json +++ b/workflow-trace-viewer/src/jvmMain/resources/workflow-traces.json @@ -1048,5 +1048,1055 @@ ] } ] + }, + { + "id": 1, + "name": "root", + "children": [ + { + "id": 2, + "name": "auth-flow", + "children": [ + { + "id": 3, + "name": "login-screen", + "children": [ + { + "id": 4, + "name": "login-form", + "children": [] + }, + { + "id": 5, + "name": "social-login", + "children": [] + } + ] + } + ] + }, + { + "id": 6, + "name": "main-flow", + "children": [ + { + "id": 7, + "name": "dashboard", + "children": [ + { + "id": 8, + "name": "stats-widget", + "children": [] + }, + { + "id": 9, + "name": "recent-activity", + "children": [] + } + ] + } + ] + }, + { + "id": 10, + "name": "background-tasks", + "children": [ + { + "id": 11, + "name": "sync-service", + "children": [ + { + "id": 12, + "name": "profile-sync", + "children": [] + }, + { + "id": 13, + "name": "preferences-sync", + "children": [] + } + ] + }, + { + "id": 14, + "name": "notification-service", + "children": [ + { + "id": 15, + "name": "push-handler", + "children": [] + } + ] + } + ] + }, + { + "id": 16, + "name": "settings-flow", + "children": [ + { + "id": 17, + "name": "settings-screen", + "children": [ + { + "id": 18, + "name": "profile-settings", + "children": [] + }, + { + "id": 19, + "name": "notification-settings", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 20, + "name": "app-root", + "children": [ + { + "id": 21, + "name": "user-flow", + "children": [ + { + "id": 22, + "name": "signup-screen", + "children": [ + { + "id": 23, + "name": "registration-form", + "children": [] + }, + { + "id": 24, + "name": "oauth-signup", + "children": [] + } + ] + } + ] + }, + { + "id": 25, + "name": "content-flow", + "children": [ + { + "id": 26, + "name": "home-screen", + "children": [ + { + "id": 27, + "name": "featured-content", + "children": [] + }, + { + "id": 28, + "name": "user-feed", + "children": [] + } + ] + } + ] + }, + { + "id": 29, + "name": "system-tasks", + "children": [ + { + "id": 30, + "name": "data-service", + "children": [ + { + "id": 31, + "name": "content-sync", + "children": [] + }, + { + "id": 32, + "name": "cache-manager", + "children": [] + } + ] + }, + { + "id": 33, + "name": "analytics-service", + "children": [ + { + "id": 34, + "name": "event-tracker", + "children": [] + } + ] + } + ] + }, + { + "id": 35, + "name": "preferences-flow", + "children": [ + { + "id": 36, + "name": "preferences-screen", + "children": [ + { + "id": 37, + "name": "account-settings", + "children": [] + }, + { + "id": 38, + "name": "privacy-settings", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 39, + "name": "system-root", + "children": [ + { + "id": 40, + "name": "security-flow", + "children": [ + { + "id": 41, + "name": "auth-screen", + "children": [ + { + "id": 42, + "name": "biometric-auth", + "children": [] + }, + { + "id": 43, + "name": "pin-verification", + "children": [] + } + ] + } + ] + }, + { + "id": 44, + "name": "navigation-flow", + "children": [ + { + "id": 45, + "name": "tab-navigator", + "children": [ + { + "id": 46, + "name": "home-tab", + "children": [] + }, + { + "id": 47, + "name": "search-tab", + "children": [] + } + ] + } + ] + }, + { + "id": 48, + "name": "core-services", + "children": [ + { + "id": 49, + "name": "network-service", + "children": [ + { + "id": 50, + "name": "api-client", + "children": [] + }, + { + "id": 51, + "name": "offline-handler", + "children": [] + } + ] + }, + { + "id": 52, + "name": "storage-service", + "children": [ + { + "id": 53, + "name": "database-manager", + "children": [] + } + ] + } + ] + }, + { + "id": 54, + "name": "admin-flow", + "children": [ + { + "id": 55, + "name": "admin-panel", + "children": [ + { + "id": 56, + "name": "user-management", + "children": [] + }, + { + "id": 57, + "name": "system-monitoring", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 58, + "name": "commerce-root", + "children": [ + { + "id": 59, + "name": "payment-flow", + "children": [ + { + "id": 60, + "name": "checkout-screen", + "children": [ + { + "id": 61, + "name": "payment-form", + "children": [] + }, + { + "id": 62, + "name": "card-scanner", + "children": [] + } + ] + } + ] + }, + { + "id": 63, + "name": "catalog-flow", + "children": [ + { + "id": 64, + "name": "product-list", + "children": [ + { + "id": 65, + "name": "product-card", + "children": [] + }, + { + "id": 66, + "name": "filter-panel", + "children": [] + } + ] + } + ] + }, + { + "id": 67, + "name": "order-tasks", + "children": [ + { + "id": 68, + "name": "fulfillment-service", + "children": [ + { + "id": 69, + "name": "inventory-check", + "children": [] + }, + { + "id": 70, + "name": "shipping-calc", + "children": [] + } + ] + }, + { + "id": 71, + "name": "payment-service", + "children": [ + { + "id": 72, + "name": "transaction-processor", + "children": [] + } + ] + } + ] + }, + { + "id": 73, + "name": "merchant-flow", + "children": [ + { + "id": 74, + "name": "seller-dashboard", + "children": [ + { + "id": 75, + "name": "sales-analytics", + "children": [] + }, + { + "id": 76, + "name": "inventory-manager", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 77, + "name": "social-root", + "children": [ + { + "id": 78, + "name": "messaging-flow", + "children": [ + { + "id": 79, + "name": "chat-screen", + "children": [ + { + "id": 80, + "name": "message-input", + "children": [] + }, + { + "id": 81, + "name": "media-picker", + "children": [] + } + ] + } + ] + }, + { + "id": 82, + "name": "social-flow", + "children": [ + { + "id": 83, + "name": "timeline-screen", + "children": [ + { + "id": 84, + "name": "post-composer", + "children": [] + }, + { + "id": 85, + "name": "story-viewer", + "children": [] + } + ] + } + ] + }, + { + "id": 86, + "name": "connection-tasks", + "children": [ + { + "id": 87, + "name": "friend-service", + "children": [ + { + "id": 88, + "name": "contact-sync", + "children": [] + }, + { + "id": 89, + "name": "suggestion-engine", + "children": [] + } + ] + }, + { + "id": 90, + "name": "activity-service", + "children": [ + { + "id": 91, + "name": "feed-generator", + "children": [] + } + ] + } + ] + }, + { + "id": 92, + "name": "privacy-flow", + "children": [ + { + "id": 93, + "name": "privacy-screen", + "children": [ + { + "id": 94, + "name": "visibility-controls", + "children": [] + }, + { + "id": 95, + "name": "block-manager", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 96, + "name": "media-root", + "children": [ + { + "id": 97, + "name": "streaming-flow", + "children": [ + { + "id": 98, + "name": "player-screen", + "children": [ + { + "id": 99, + "name": "video-player", + "children": [] + }, + { + "id": 100, + "name": "audio-controls", + "children": [] + } + ] + } + ] + }, + { + "id": 101, + "name": "library-flow", + "children": [ + { + "id": 102, + "name": "media-browser", + "children": [ + { + "id": 103, + "name": "playlist-view", + "children": [] + }, + { + "id": 104, + "name": "search-results", + "children": [] + } + ] + } + ] + }, + { + "id": 105, + "name": "processing-tasks", + "children": [ + { + "id": 106, + "name": "encoding-service", + "children": [ + { + "id": 107, + "name": "video-encoder", + "children": [] + }, + { + "id": 108, + "name": "thumbnail-generator", + "children": [] + } + ] + }, + { + "id": 109, + "name": "cdn-service", + "children": [ + { + "id": 110, + "name": "content-distributor", + "children": [] + } + ] + } + ] + }, + { + "id": 111, + "name": "creator-flow", + "children": [ + { + "id": 112, + "name": "upload-screen", + "children": [ + { + "id": 113, + "name": "file-uploader", + "children": [] + }, + { + "id": 114, + "name": "metadata-editor", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 115, + "name": "finance-root", + "children": [ + { + "id": 116, + "name": "banking-flow", + "children": [ + { + "id": 117, + "name": "account-screen", + "children": [ + { + "id": 118, + "name": "balance-widget", + "children": [] + }, + { + "id": 119, + "name": "transaction-list", + "children": [] + } + ] + } + ] + }, + { + "id": 120, + "name": "transfer-flow", + "children": [ + { + "id": 121, + "name": "send-money-screen", + "children": [ + { + "id": 122, + "name": "recipient-selector", + "children": [] + }, + { + "id": 123, + "name": "amount-input", + "children": [] + } + ] + } + ] + }, + { + "id": 124, + "name": "financial-tasks", + "children": [ + { + "id": 125, + "name": "fraud-service", + "children": [ + { + "id": 126, + "name": "risk-analyzer", + "children": [] + }, + { + "id": 127, + "name": "pattern-detector", + "children": [] + } + ] + }, + { + "id": 128, + "name": "compliance-service", + "children": [ + { + "id": 129, + "name": "kyc-validator", + "children": [] + } + ] + } + ] + }, + { + "id": 130, + "name": "investment-flow", + "children": [ + { + "id": 131, + "name": "portfolio-screen", + "children": [ + { + "id": 132, + "name": "asset-overview", + "children": [] + }, + { + "id": 133, + "name": "performance-chart", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 134, + "name": "health-root", + "children": [ + { + "id": 135, + "name": "tracking-flow", + "children": [ + { + "id": 136, + "name": "activity-screen", + "children": [ + { + "id": 137, + "name": "step-counter", + "children": [] + }, + { + "id": 138, + "name": "workout-tracker", + "children": [] + } + ] + } + ] + }, + { + "id": 139, + "name": "wellness-flow", + "children": [ + { + "id": 140, + "name": "health-dashboard", + "children": [ + { + "id": 141, + "name": "vital-signs", + "children": [] + }, + { + "id": 142, + "name": "medication-reminder", + "children": [] + } + ] + } + ] + }, + { + "id": 143, + "name": "monitoring-tasks", + "children": [ + { + "id": 144, + "name": "sensor-service", + "children": [ + { + "id": 145, + "name": "heart-rate-monitor", + "children": [] + }, + { + "id": 146, + "name": "sleep-tracker", + "children": [] + } + ] + }, + { + "id": 147, + "name": "analysis-service", + "children": [ + { + "id": 148, + "name": "trend-analyzer", + "children": [] + } + ] + } + ] + }, + { + "id": 149, + "name": "consultation-flow", + "children": [ + { + "id": 150, + "name": "telemedicine-screen", + "children": [ + { + "id": 151, + "name": "video-call", + "children": [] + }, + { + "id": 152, + "name": "symptom-checker", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 153, + "name": "education-root", + "children": [ + { + "id": 154, + "name": "learning-flow", + "children": [ + { + "id": 155, + "name": "course-screen", + "children": [ + { + "id": 156, + "name": "video-lesson", + "children": [] + }, + { + "id": 157, + "name": "quiz-component", + "children": [] + } + ] + } + ] + }, + { + "id": 158, + "name": "progress-flow", + "children": [ + { + "id": 159, + "name": "achievement-screen", + "children": [ + { + "id": 160, + "name": "badge-display", + "children": [] + }, + { + "id": 161, + "name": "progress-tracker", + "children": [] + } + ] + } + ] + }, + { + "id": 162, + "name": "assessment-tasks", + "children": [ + { + "id": 163, + "name": "grading-service", + "children": [ + { + "id": 164, + "name": "auto-grader", + "children": [] + }, + { + "id": 165, + "name": "feedback-generator", + "children": [] + } + ] + }, + { + "id": 166, + "name": "analytics-service", + "children": [ + { + "id": 167, + "name": "learning-analyzer", + "children": [] + } + ] + } + ] + }, + { + "id": 168, + "name": "collaboration-flow", + "children": [ + { + "id": 169, + "name": "study-group-screen", + "children": [ + { + "id": 170, + "name": "whiteboard-tool", + "children": [] + }, + { + "id": 171, + "name": "discussion-forum", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 172, + "name": "travel-root", + "children": [ + { + "id": 173, + "name": "booking-flow", + "children": [ + { + "id": 174, + "name": "search-screen", + "children": [ + { + "id": 175, + "name": "flight-finder", + "children": [] + }, + { + "id": 176, + "name": "hotel-search", + "children": [] + } + ] + } + ] + }, + { + "id": 177, + "name": "itinerary-flow", + "children": [ + { + "id": 178, + "name": "trip-planner", + "children": [ + { + "id": 179, + "name": "schedule-builder", + "children": [] + }, + { + "id": 180, + "name": "map-integration", + "children": [] + } + ] + } + ] + }, + { + "id": 181, + "name": "travel-tasks", + "children": [ + { + "id": 182, + "name": "booking-service", + "children": [ + { + "id": 183, + "name": "reservation-manager", + "children": [] + }, + { + "id": 184, + "name": "price-tracker", + "children": [] + } + ] + }, + { + "id": 185, + "name": "notification-service", + "children": [ + { + "id": 186, + "name": "flight-alerts", + "children": [] + } + ] + } + ] + }, + { + "id": 187, + "name": "companion-flow", + "children": [ + { + "id": 188, + "name": "travel-guide", + "children": [ + { + "id": 189, + "name": "local-recommendations", + "children": [] + }, + { + "id": 190, + "name": "weather-widget", + "children": [] + } + ] + } + ] + } + ] } -] \ No newline at end of file +] From 6831535521a86f4d1d22a69542cfb51137c82fb0 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Wed, 11 Jun 2025 11:32:25 -0400 Subject: [PATCH 08/16] Add missing Kdocs --- .../com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt index ef5c0efeb3..f8abb5f149 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt @@ -14,6 +14,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.squareup.workflow1.traceviewer.model.WorkflowNode +/** + * A trace tab selector that allows devs to switch between different states within the provided trace. + */ @Composable public fun StateSelectTab( trace: List, From d2626f2fe5f7b7d8d800593afa67f6a95bd130d0 Mon Sep 17 00:00:00 2001 From: wenli-cai Date: Wed, 11 Jun 2025 15:39:04 +0000 Subject: [PATCH 09/16] Apply changes from apiDump Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../api/workflow-trace-viewer.api | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/workflow-trace-viewer/api/workflow-trace-viewer.api b/workflow-trace-viewer/api/workflow-trace-viewer.api index dbddaaaa5d..4c9c2068db 100644 --- a/workflow-trace-viewer/api/workflow-trace-viewer.api +++ b/workflow-trace-viewer/api/workflow-trace-viewer.api @@ -9,38 +9,19 @@ public final class com/squareup/workflow1/traceviewer/ComposableSingletons$MainK public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; } -public final class com/squareup/workflow1/traceviewer/ComposableSingletons$UploadFileKt { - public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/ComposableSingletons$UploadFileKt; - public static field lambda-1 Lkotlin/jvm/functions/Function3; - public fun ()V - public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; -} - public final class com/squareup/workflow1/traceviewer/MainKt { public static final fun main ()V public static synthetic fun main ([Ljava/lang/String;)V } -public final class com/squareup/workflow1/traceviewer/SandboxBackgroundKt { - public static final fun SandboxBackground (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V -} - -public final class com/squareup/workflow1/traceviewer/UploadFileKt { - public static final fun UploadFile (Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V -} - -public final class com/squareup/workflow1/traceviewer/WorkflowJsonParserKt { - public static final fun parseTrace (Ljava/lang/String;)Lcom/squareup/workflow1/traceviewer/WorkflowNode; -} - -public final class com/squareup/workflow1/traceviewer/WorkflowNode { +public final class com/squareup/workflow1/traceviewer/model/WorkflowNode { public static final field $stable I public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/util/List; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lcom/squareup/workflow1/traceviewer/WorkflowNode; - public static synthetic fun copy$default (Lcom/squareup/workflow1/traceviewer/WorkflowNode;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lcom/squareup/workflow1/traceviewer/WorkflowNode; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lcom/squareup/workflow1/traceviewer/model/WorkflowNode; + public static synthetic fun copy$default (Lcom/squareup/workflow1/traceviewer/model/WorkflowNode;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lcom/squareup/workflow1/traceviewer/model/WorkflowNode; public fun equals (Ljava/lang/Object;)Z public final fun getChildren ()Ljava/util/List; public final fun getId ()Ljava/lang/String; @@ -49,7 +30,36 @@ public final class com/squareup/workflow1/traceviewer/WorkflowNode { public fun toString ()Ljava/lang/String; } -public final class com/squareup/workflow1/traceviewer/WorkflowTreeKt { - public static final fun DrawWorkflowTree (Lcom/squareup/workflow1/traceviewer/WorkflowNode;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +public final class com/squareup/workflow1/traceviewer/ui/StateSelectTabKt { + public static final fun StateSelectTab (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 InfoPanel (Lcom/squareup/workflow1/traceviewer/model/WorkflowNode;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/runtime/Composer;I)V +} + +public final class com/squareup/workflow1/traceviewer/utils/ComposableSingletons$UploadFileKt { + public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/utils/ComposableSingletons$UploadFileKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; +} + +public final class com/squareup/workflow1/traceviewer/utils/JsonParserKt { + public static final fun fetchTrace (Lio/github/vinceglb/filekit/PlatformFile;Landroidx/compose/ui/Modifier;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun fetchTrace$default (Lio/github/vinceglb/filekit/PlatformFile;Landroidx/compose/ui/Modifier;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public static final fun parseTrace (Ljava/lang/String;)Ljava/util/List; +} + +public final class com/squareup/workflow1/traceviewer/utils/SandboxBackgroundKt { + public static final fun SandboxBackground (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V +} + +public final class com/squareup/workflow1/traceviewer/utils/UploadFileKt { + public static final fun UploadFile (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V } From 6bdc3d4bd76144bc7f8b30d05f67c2578107e01b Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Wed, 11 Jun 2025 16:16:00 -0400 Subject: [PATCH 10/16] Fix for PR comments 1. Made terminology and variable naming consistent (see README changes) 2. Fixed caught bugs in PR review 3. Right aligned inspection tab --- workflow-trace-viewer/README.md | 8 +++ .../com/squareup/workflow1/traceviewer/App.kt | 39 ++++++++------- .../traceviewer/model/WorkflowNode.kt | 4 +- .../{StateSelectTab.kt => FrameSelectTab.kt} | 6 +-- .../traceviewer/ui/WorkflowInfoPanel.kt | 50 +++++++++---------- .../workflow1/traceviewer/ui/WorkflowTree.kt | 29 +++++------ .../workflow1/traceviewer/util/JsonParser.kt | 42 ++++++++++++++++ .../{utils => util}/SandboxBackground.kt | 2 +- .../traceviewer/{utils => util}/UploadFile.kt | 5 +- .../workflow1/traceviewer/utils/JsonParser.kt | 48 ------------------ 10 files changed, 120 insertions(+), 113 deletions(-) rename workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/{StateSelectTab.kt => FrameSelectTab.kt} (91%) create mode 100644 workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt rename workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/{utils => util}/SandboxBackground.kt (98%) rename workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/{utils => util}/UploadFile.kt (94%) delete mode 100644 workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/JsonParser.kt diff --git a/workflow-trace-viewer/README.md b/workflow-trace-viewer/README.md index 9f989983a8..57735624af 100644 --- a/workflow-trace-viewer/README.md +++ b/workflow-trace-viewer/README.md @@ -10,6 +10,14 @@ It can be run via Gradle using: ./gradlew :workflow-trace-viewer:run ``` +### Terminology + +**Trace**: A trace is a file — made up of frames — that contains the execution history of a Workflow. It includes information about render passes, how states have changed within workflows, and the specific props being passed through. The data collected to generate these should be in chronological order, and allows developers to step through the process easily. + +**Frame**: Essentially a "snapshot" of the current "state" of the whole Workflow tree. It contains relevant information about the changes in workflow states and how props are passed throughout. + +- Note that "snapshot" and "state" are different from `snapshotState` and `State`, which are idiomatic to the Workflow library. + ### External Libraries [FileKit](https://github.com/vinceglb/FileKit) is an external library made to apply file operations on Kotlin and KMP projects. It's purpose in this app is to allow developers to upload their own json trace files. The motivation for its use is to quickly implement a file picker. 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 8791384235..9f00f12682 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,12 +9,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import com.squareup.workflow1.traceviewer.model.WorkflowNode +import com.squareup.workflow1.traceviewer.model.Node import com.squareup.workflow1.traceviewer.ui.InfoPanel import com.squareup.workflow1.traceviewer.ui.RenderDiagram import com.squareup.workflow1.traceviewer.ui.StateSelectTab -import com.squareup.workflow1.traceviewer.utils.SandboxBackground -import com.squareup.workflow1.traceviewer.utils.UploadFile +import com.squareup.workflow1.traceviewer.util.SandboxBackground +import com.squareup.workflow1.traceviewer.util.UploadFile import io.github.vinceglb.filekit.PlatformFile /** @@ -24,19 +24,19 @@ import io.github.vinceglb.filekit.PlatformFile public fun App( modifier: Modifier = Modifier ) { - var selectedFile by remember { mutableStateOf(null) } - var selectedNode by remember { mutableStateOf(null) } - var workflowTrace by remember { mutableStateOf>(emptyList()) } - var snapshotIndex by remember { mutableIntStateOf(0) } + var selectedTraceFile by remember { mutableStateOf(null) } + var selectedNode by remember { mutableStateOf(null) } + var workflowFrames by remember { mutableStateOf>(emptyList()) } + var frameIndex by remember { mutableIntStateOf(0) } Box { // Main content - if (selectedFile != null) { + if (selectedTraceFile != null) { SandboxBackground { RenderDiagram( - file = selectedFile!!, - traceInd = snapshotIndex, - onFileParse = { workflowTrace = it }, + traceFile = selectedTraceFile!!, + traceInd = frameIndex, + onFileParse = { workflowFrames = it }, onNodeSelect = { selectedNode = it } ) } @@ -44,20 +44,23 @@ public fun App( // Top trace selector row StateSelectTab( - trace = workflowTrace, - currentIndex = snapshotIndex, - onIndexChange = { snapshotIndex = it }, + frames = workflowFrames, + currentIndex = frameIndex, + onIndexChange = { frameIndex = it }, modifier = Modifier.align(Alignment.TopCenter) ) - // Left side information panel + // Right side information panel InfoPanel(selectedNode) - // Bottom right upload button + // Bottom left upload button val onReset = { selectedNode = null - snapshotIndex = 0 + frameIndex = 0 } - UploadFile(onReset = onReset, onFileSelect = { selectedFile = it }) + UploadFile( + onReset = onReset, + onFileSelect = { selectedTraceFile = it } + ) } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/WorkflowNode.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/WorkflowNode.kt index 23bd7aacfb..412eb5a7c5 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/WorkflowNode.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/WorkflowNode.kt @@ -7,8 +7,8 @@ package com.squareup.workflow1.traceviewer.model * * TBD what more metadata should be involved with each node, e.g. (props, states, # of render passes) */ -public data class WorkflowNode( +public class Node( val id: String, val name: String, - val children: List + val children: List ) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt similarity index 91% rename from workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt rename to workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt index f8abb5f149..c2eab9d9d5 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/StateSelectTab.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt @@ -12,14 +12,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.squareup.workflow1.traceviewer.model.WorkflowNode +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 StateSelectTab( - trace: List, + frames: List, currentIndex: Int, onIndexChange: (Int) -> Unit, modifier: Modifier = Modifier @@ -36,7 +36,7 @@ public fun StateSelectTab( .padding(8.dp), state = state ) { - items(trace.size) { index -> + items(frames.size) { index -> Text( text = "State ${index + 1}", color = if (index == currentIndex) Color.Black else Color.LightGray, 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 424420d76c..2fea8d4eb9 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 @@ -3,6 +3,7 @@ package com.squareup.workflow1.traceviewer.ui import androidx.compose.foundation.background 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 @@ -23,7 +24,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.squareup.workflow1.traceviewer.model.WorkflowNode +import com.squareup.workflow1.traceviewer.model.Node /** * A panel that displays information about the selected workflow node. @@ -33,19 +34,14 @@ import com.squareup.workflow1.traceviewer.model.WorkflowNode */ @Composable public fun InfoPanel( - selectedNode: WorkflowNode?, + selectedNode: Node?, modifier: Modifier = Modifier ) { + // This row is ordered RTL Row { - var panelOpen by remember { mutableStateOf(false) } + Spacer(modifier = Modifier.weight(1f)) - // based on open/close, display the node details (Column) - if (panelOpen) { - PanelDetails( - selectedNode, - Modifier.fillMaxWidth(.35f) - ) - } + var panelOpen by remember { mutableStateOf(false) } IconButton( onClick = { panelOpen = !panelOpen }, @@ -60,39 +56,43 @@ public fun InfoPanel( modifier = Modifier ) } + + // based on open/close, display the node details (Column) + if (panelOpen) { + PanelDetails( + selectedNode, + Modifier.fillMaxWidth(.35f) + ) + } } } /** - * The text details of the selected node. This should be closely coupled with the [WorkflowNode] + * The text details of the selected node. This should be closely coupled with the [Node] * data class to see what information should be displayed. */ @Composable private fun PanelDetails( - node: WorkflowNode?, + node: Node?, modifier: Modifier = Modifier ) { Column( - modifier + modifier = modifier .fillMaxHeight() .background(Color.LightGray) + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { if (node == null) { Text("No node selected") return@Column } - Column( - modifier = Modifier - .padding(8.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text("only visible with a node selected") - Text( - text = "This is a node panel for ${node.name}", - fontSize = 20.sp, - modifier = Modifier.padding(8.dp) - ) - } + Text("only visible with a node selected") + Text( + text = "This is a node panel for ${node.name}", + fontSize = 20.sp, + modifier = Modifier.padding(8.dp) + ) } } 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 d8bde6c3e8..140f85b281 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 @@ -19,8 +19,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.squareup.workflow1.traceviewer.model.WorkflowNode -import com.squareup.workflow1.traceviewer.utils.fetchTrace +import com.squareup.workflow1.traceviewer.model.Node +import com.squareup.workflow1.traceviewer.util.fetchTrace import io.github.vinceglb.filekit.PlatformFile /** @@ -29,22 +29,23 @@ import io.github.vinceglb.filekit.PlatformFile */ @Composable public fun RenderDiagram( - file: PlatformFile, + traceFile: PlatformFile, traceInd: Int, - onFileParse: (List) -> Unit, - onNodeSelect: (WorkflowNode) -> Unit, + onFileParse: (List) -> Unit, + onNodeSelect: (Node) -> Unit, ) { - var workflowNodes by remember { mutableStateOf>(emptyList()) } + var nodes by remember { mutableStateOf>(emptyList()) } var isLoading by remember { mutableStateOf(true) } - LaunchedEffect(file) { - workflowNodes = fetchTrace(file) - onFileParse(workflowNodes) + LaunchedEffect(traceFile) { + isLoading = true + nodes = fetchTrace(traceFile) + onFileParse(nodes) isLoading = false } if (!isLoading) { - DrawTree(workflowNodes[traceInd], onNodeSelect) + DrawTree(nodes[traceInd], onNodeSelect) } // TODO: catch errors and display UI here @@ -56,8 +57,8 @@ public fun RenderDiagram( */ @Composable private fun DrawTree( - node: WorkflowNode, - onNodeSelect: (WorkflowNode) -> Unit, + node: Node, + onNodeSelect: (Node) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -87,8 +88,8 @@ private fun DrawTree( */ @Composable private fun DrawNode( - node: WorkflowNode, - onNodeSelect: (WorkflowNode) -> Unit, + node: Node, + onNodeSelect: (Node) -> Unit, ) { Box( modifier = Modifier 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 new file mode 100644 index 0000000000..7c438770a1 --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt @@ -0,0 +1,42 @@ +package com.squareup.workflow1.traceviewer.util + +import androidx.compose.ui.Modifier +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonDataException +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 io.github.vinceglb.filekit.PlatformFile +import io.github.vinceglb.filekit.readString +import java.io.IOException + +/** + * Parses the data from the given file and initiates the workflow tree + */ +public suspend fun fetchTrace( + file: PlatformFile?, + modifier: Modifier = Modifier +): List { + val jsonString = file?.readString() + return jsonString?.let { parseTrace(it) } ?: emptyList() +} + +/** + * Parses a JSON string into [Node] with Moshi adapters. Moshi automatically throws JsonDataException + * and IOException + * @throws JsonDataException malformed JSON data or an error reading. + * @throws IOException JSON is correct, but mismatch between class and JSON structure. + */ +public fun parseTrace( + json: String +): List { + val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + + val workflowList = Types.newParameterizedType(List::class.java, Node::class.java) + val workflowAdapter: JsonAdapter> = moshi.adapter(workflowList) + val root = workflowAdapter.fromJson(json) ?: emptyList() + return root +} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/SandboxBackground.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt similarity index 98% rename from workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/SandboxBackground.kt rename to workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt index 2b51c83420..822584c8e8 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/SandboxBackground.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt @@ -1,4 +1,4 @@ -package com.squareup.workflow1.traceviewer.utils +package com.squareup.workflow1.traceviewer.util import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.detectDragGestures diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/UploadFile.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/UploadFile.kt similarity index 94% rename from workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/UploadFile.kt rename to workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/UploadFile.kt index 00dd69ea6f..58ab25fc35 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/UploadFile.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/UploadFile.kt @@ -1,4 +1,4 @@ -package com.squareup.workflow1.traceviewer.utils +package com.squareup.workflow1.traceviewer.util import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -39,10 +39,11 @@ public fun UploadFile( onReset() onFileSelect(it) } + Button( onClick = { launcher.launch() }, modifier = Modifier - .align(Alignment.BottomEnd), + .align(Alignment.BottomStart), shape = CircleShape, colors = buttonColors(Color.Black) ) { diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/JsonParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/JsonParser.kt deleted file mode 100644 index b44a0919e9..0000000000 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/utils/JsonParser.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.squareup.workflow1.traceviewer.utils - -import androidx.compose.ui.Modifier -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.JsonDataException -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import com.squareup.workflow1.traceviewer.model.WorkflowNode -import io.github.vinceglb.filekit.PlatformFile -import io.github.vinceglb.filekit.readString -import java.io.IOException - -/** - * Parses the data from the given file and initiates the workflow tree - */ -public suspend fun fetchTrace( - file: PlatformFile?, - modifier: Modifier = Modifier -): List { - val jsonString = file?.readString() - return jsonString?.let { parseTrace(it) } ?: emptyList() -} - -/** - * Parses a JSON string into [WorkflowNode] with Moshi adapters - * - * All the caught exceptions should be handled by the caller, and appropriate UI feedback should be - * provided to user - */ -public fun parseTrace( - json: String -): List { - return try { - val moshi = Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() - - val workflowList = Types.newParameterizedType(List::class.java, WorkflowNode::class.java) - val workflowAdapter: JsonAdapter> = moshi.adapter(workflowList) - val root = workflowAdapter.fromJson(json) ?: emptyList() - root - } catch (e: JsonDataException) { - throw JsonDataException("Failed to parse JSON: ${e.message}", e) - } catch (e: IOException) { - throw IOException("Malformed JSON: ${e.message}", e) - } -} From 1a6d86802c68a53c024cf21b039d115da44c085b Mon Sep 17 00:00:00 2001 From: wenli-cai Date: Thu, 12 Jun 2025 01:37:11 +0000 Subject: [PATCH 11/16] Apply changes from apiDump Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../api/workflow-trace-viewer.api | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/workflow-trace-viewer/api/workflow-trace-viewer.api b/workflow-trace-viewer/api/workflow-trace-viewer.api index 4c9c2068db..555d18744d 100644 --- a/workflow-trace-viewer/api/workflow-trace-viewer.api +++ b/workflow-trace-viewer/api/workflow-trace-viewer.api @@ -14,52 +14,44 @@ 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/WorkflowNode { +public final class com/squareup/workflow1/traceviewer/model/Node { public static final field $stable I public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/util/List; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lcom/squareup/workflow1/traceviewer/model/WorkflowNode; - public static synthetic fun copy$default (Lcom/squareup/workflow1/traceviewer/model/WorkflowNode;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lcom/squareup/workflow1/traceviewer/model/WorkflowNode; - 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 fun hashCode ()I - public fun toString ()Ljava/lang/String; } -public final class com/squareup/workflow1/traceviewer/ui/StateSelectTabKt { +public final class com/squareup/workflow1/traceviewer/ui/FrameSelectTabKt { public static final fun StateSelectTab (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 InfoPanel (Lcom/squareup/workflow1/traceviewer/model/WorkflowNode;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V + public static final fun InfoPanel (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/runtime/Composer;I)V } -public final class com/squareup/workflow1/traceviewer/utils/ComposableSingletons$UploadFileKt { - public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/utils/ComposableSingletons$UploadFileKt; +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; public fun ()V public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; } -public final class com/squareup/workflow1/traceviewer/utils/JsonParserKt { +public final class com/squareup/workflow1/traceviewer/util/JsonParserKt { public static final fun fetchTrace (Lio/github/vinceglb/filekit/PlatformFile;Landroidx/compose/ui/Modifier;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun fetchTrace$default (Lio/github/vinceglb/filekit/PlatformFile;Landroidx/compose/ui/Modifier;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun parseTrace (Ljava/lang/String;)Ljava/util/List; } -public final class com/squareup/workflow1/traceviewer/utils/SandboxBackgroundKt { +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 final class com/squareup/workflow1/traceviewer/utils/UploadFileKt { +public final class com/squareup/workflow1/traceviewer/util/UploadFileKt { public static final fun UploadFile (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V } From 4255a8f1c857c8ce5cc9924aad3ee6865c696191 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Fri, 13 Jun 2025 12:07:33 -0400 Subject: [PATCH 12/16] More fix for PR comments 1. Clarified component positions with modifiers 2. Removed unnecessary comments 3. Simplified JsonParser --- .../com/squareup/workflow1/traceviewer/App.kt | 26 +++++------ .../squareup/workflow1/traceviewer/Main.kt | 4 +- .../traceviewer/ui/WorkflowInfoPanel.kt | 15 ++---- .../workflow1/traceviewer/ui/WorkflowTree.kt | 15 +++--- .../workflow1/traceviewer/util/JsonParser.kt | 27 ++++------- .../workflow1/traceviewer/util/UploadFile.kt | 46 +++++++------------ 6 files changed, 55 insertions(+), 78 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 9f00f12682..17336c6ab1 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 @@ -10,8 +10,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.squareup.workflow1.traceviewer.model.Node -import com.squareup.workflow1.traceviewer.ui.InfoPanel import com.squareup.workflow1.traceviewer.ui.RenderDiagram +import com.squareup.workflow1.traceviewer.ui.RightInfoPanel import com.squareup.workflow1.traceviewer.ui.StateSelectTab import com.squareup.workflow1.traceviewer.util.SandboxBackground import com.squareup.workflow1.traceviewer.util.UploadFile @@ -29,20 +29,21 @@ public fun App( var workflowFrames by remember { mutableStateOf>(emptyList()) } var frameIndex by remember { mutableIntStateOf(0) } - Box { + Box( + modifier = modifier + ) { // Main content if (selectedTraceFile != null) { SandboxBackground { RenderDiagram( traceFile = selectedTraceFile!!, - traceInd = frameIndex, + frameInd = frameIndex, onFileParse = { workflowFrames = it }, onNodeSelect = { selectedNode = it } ) } } - // Top trace selector row StateSelectTab( frames = workflowFrames, currentIndex = frameIndex, @@ -50,17 +51,16 @@ public fun App( modifier = Modifier.align(Alignment.TopCenter) ) - // Right side information panel - InfoPanel(selectedNode) + RightInfoPanel(selectedNode) - // Bottom left upload button - val onReset = { - selectedNode = null - frameIndex = 0 - } + // The states are reset when a new file is selected. UploadFile( - onReset = onReset, - onFileSelect = { selectedTraceFile = it } + onFileSelect = { + selectedTraceFile = it + selectedNode = null + frameIndex = 0 + }, + modifier = Modifier.align(Alignment.BottomStart) ) } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt index b455d04354..485c98c12b 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt @@ -1,5 +1,7 @@ package com.squareup.workflow1.traceviewer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier import androidx.compose.ui.window.singleWindowApplication /** @@ -7,6 +9,6 @@ import androidx.compose.ui.window.singleWindowApplication */ fun main() { singleWindowApplication(title = "Workflow Trace Viewer") { - App() + App(Modifier.fillMaxSize()) } } 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 2fea8d4eb9..f7c959abe2 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 @@ -33,11 +33,11 @@ import com.squareup.workflow1.traceviewer.model.Node * @param selectedNode The currently selected workflow node, or null if no node is selected. */ @Composable -public fun InfoPanel( +public fun RightInfoPanel( selectedNode: Node?, modifier: Modifier = Modifier ) { - // This row is ordered RTL + // This row is aligned to the right of the screen. Row { Spacer(modifier = Modifier.weight(1f)) @@ -51,15 +51,14 @@ public fun InfoPanel( .align(Alignment.Top) ) { Icon( - imageVector = if (panelOpen) Filled.KeyboardArrowLeft else Filled.KeyboardArrowRight, + imageVector = if (panelOpen) Filled.KeyboardArrowRight else Filled.KeyboardArrowLeft, contentDescription = if (panelOpen) "Close Panel" else "Open Panel", modifier = Modifier ) } - // based on open/close, display the node details (Column) if (panelOpen) { - PanelDetails( + NodePanelDetails( selectedNode, Modifier.fillMaxWidth(.35f) ) @@ -67,12 +66,8 @@ public fun InfoPanel( } } -/** - * The text details of the selected node. This should be closely coupled with the [Node] - * data class to see what information should be displayed. - */ @Composable -private fun PanelDetails( +private fun NodePanelDetails( node: 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 140f85b281..9817226d85 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 @@ -20,7 +20,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.util.fetchTrace +import com.squareup.workflow1.traceviewer.util.parseTrace import io.github.vinceglb.filekit.PlatformFile /** @@ -30,22 +30,22 @@ import io.github.vinceglb.filekit.PlatformFile @Composable public fun RenderDiagram( traceFile: PlatformFile, - traceInd: Int, + frameInd: Int, onFileParse: (List) -> Unit, onNodeSelect: (Node) -> Unit, ) { - var nodes by remember { mutableStateOf>(emptyList()) } + var frames by remember { mutableStateOf>(emptyList()) } var isLoading by remember { mutableStateOf(true) } LaunchedEffect(traceFile) { isLoading = true - nodes = fetchTrace(traceFile) - onFileParse(nodes) + frames = parseTrace(traceFile) + onFileParse(frames) isLoading = false } if (!isLoading) { - DrawTree(nodes[traceInd], onNodeSelect) + DrawTree(frames[frameInd], onNodeSelect) } // TODO: catch errors and display UI here @@ -68,10 +68,9 @@ private fun DrawTree( .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { - // draws the node itself DrawNode(node, onNodeSelect) - // draws the node's children recursively + // Draws the node's children recursively. Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.Top 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 7c438770a1..9fab8655f6 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 @@ -1,6 +1,5 @@ package com.squareup.workflow1.traceviewer.util -import androidx.compose.ui.Modifier import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonDataException import com.squareup.moshi.Moshi @@ -12,31 +11,25 @@ import io.github.vinceglb.filekit.readString import java.io.IOException /** - * Parses the data from the given file and initiates the workflow tree - */ -public suspend fun fetchTrace( - file: PlatformFile?, - modifier: Modifier = Modifier -): List { - val jsonString = file?.readString() - return jsonString?.let { parseTrace(it) } ?: emptyList() -} - -/** - * Parses a JSON string into [Node] with Moshi adapters. Moshi automatically throws JsonDataException + * Parses a given file's JSON String into [Node] with Moshi adapters. Moshi automatically throws JsonDataException * and IOException * @throws JsonDataException malformed JSON data or an error reading. * @throws IOException JSON is correct, but mismatch between class and JSON structure. */ -public fun parseTrace( - json: String +public suspend fun parseTrace( + file: PlatformFile, ): List { + val jsonString = file.readString() + val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() val workflowList = Types.newParameterizedType(List::class.java, Node::class.java) val workflowAdapter: JsonAdapter> = moshi.adapter(workflowList) - val root = workflowAdapter.fromJson(json) ?: emptyList() - return root + val trace = workflowAdapter.fromJson(jsonString) + if (trace == null) { + throw JsonDataException("Could not parse JSON") + } + return trace } 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 58ab25fc35..4a197e8f44 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 @@ -1,14 +1,11 @@ package com.squareup.workflow1.traceviewer.util -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults.buttonColors import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @@ -23,36 +20,27 @@ import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher */ @Composable public fun UploadFile( - onReset: () -> Unit, onFileSelect: (PlatformFile?) -> Unit, modifier: Modifier = Modifier, ) { - Box( - modifier - .padding(16.dp) - .fillMaxSize() + val launcher = rememberFilePickerLauncher( + type = FileKitType.File(listOf("json", "txt")), + title = "Select Workflow Trace File" ) { - val launcher = rememberFilePickerLauncher( - type = FileKitType.File(listOf("json", "txt")), - title = "Select Workflow Trace File" - ) { - onReset() - onFileSelect(it) - } + onFileSelect(it) + } - Button( - onClick = { launcher.launch() }, - modifier = Modifier - .align(Alignment.BottomStart), - shape = CircleShape, - colors = buttonColors(Color.Black) - ) { - Text( - text = "+", - color = Color.White, - fontSize = 24.sp, - fontWeight = androidx.compose.ui.text.font.FontWeight.Bold - ) - } + Button( + onClick = { launcher.launch() }, + modifier = modifier.padding(16.dp), + shape = CircleShape, + colors = buttonColors(Color.Black) + ) { + Text( + text = "+", + color = Color.White, + fontSize = 24.sp, + fontWeight = androidx.compose.ui.text.font.FontWeight.Bold + ) } } From ebe259b747230f97ec0348be2ea5ddbc713d53ab Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Sat, 14 Jun 2025 12:06:34 -0400 Subject: [PATCH 13/16] Improve error handling --- .../workflow1/traceviewer/ui/WorkflowTree.kt | 23 +++++++++-- .../workflow1/traceviewer/util/JsonParser.kt | 40 ++++++++++--------- 2 files changed, 40 insertions(+), 23 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 9817226d85..9f3e6678d8 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 @@ -20,6 +20,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.util.ParseResult import com.squareup.workflow1.traceviewer.util.parseTrace import io.github.vinceglb.filekit.PlatformFile @@ -33,17 +34,31 @@ public fun RenderDiagram( frameInd: Int, onFileParse: (List) -> Unit, onNodeSelect: (Node) -> Unit, + modifier: Modifier = Modifier ) { var frames by remember { mutableStateOf>(emptyList()) } - var isLoading by remember { mutableStateOf(true) } + var isLoading by remember(traceFile) { mutableStateOf(true) } + var error by remember(traceFile) { mutableStateOf(null) } LaunchedEffect(traceFile) { - isLoading = true - frames = parseTrace(traceFile) - onFileParse(frames) + val parseResult = parseTrace(traceFile) + + if (parseResult is ParseResult.Failure) { + error = parseResult.error + return@LaunchedEffect + } + + val parsedFrames = (parseResult as ParseResult.Success).trace ?: emptyList() + frames = parsedFrames + onFileParse(parsedFrames) isLoading = false } + if (error != null) { + Text("Error parsing file: ${error?.message}") + return + } + if (!isLoading) { DrawTree(frames[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 9fab8655f6..6c1b7e2575 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 @@ -1,35 +1,37 @@ package com.squareup.workflow1.traceviewer.util import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.JsonDataException 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 io.github.vinceglb.filekit.PlatformFile import io.github.vinceglb.filekit.readString -import java.io.IOException /** - * Parses a given file's JSON String into [Node] with Moshi adapters. Moshi automatically throws JsonDataException - * and IOException - * @throws JsonDataException malformed JSON data or an error reading. - * @throws IOException JSON is correct, but mismatch between class and JSON structure. + * Parses a given file's JSON String into [Node] with Moshi adapters. + * + * @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( file: PlatformFile, -): List { - val jsonString = file.readString() - - val moshi = Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() - - val workflowList = Types.newParameterizedType(List::class.java, Node::class.java) - val workflowAdapter: JsonAdapter> = moshi.adapter(workflowList) - val trace = workflowAdapter.fromJson(jsonString) - if (trace == null) { - throw JsonDataException("Could not parse JSON") +): ParseResult { + return try { + val jsonString = file.readString() + val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + val workflowList = Types.newParameterizedType(List::class.java, Node::class.java) + val workflowAdapter: JsonAdapter> = moshi.adapter(workflowList) + val trace = workflowAdapter.fromJson(jsonString) + ParseResult.Success(trace) + } catch (e: Exception) { + ParseResult.Failure(e) } - return trace +} + +sealed interface ParseResult { + class Success(val trace: List?) : ParseResult + class Failure(val error: Throwable) : ParseResult } From 1beba352078078caaa8a94a48a42cde93a10cfdb Mon Sep 17 00:00:00 2001 From: wenli-cai Date: Sat, 14 Jun 2025 16:31:24 +0000 Subject: [PATCH 14/16] Apply changes from apiDump Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../api/workflow-trace-viewer.api | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/workflow-trace-viewer/api/workflow-trace-viewer.api b/workflow-trace-viewer/api/workflow-trace-viewer.api index 555d18744d..ff87380f88 100644 --- a/workflow-trace-viewer/api/workflow-trace-viewer.api +++ b/workflow-trace-viewer/api/workflow-trace-viewer.api @@ -27,11 +27,11 @@ public final class com/squareup/workflow1/traceviewer/ui/FrameSelectTabKt { } public final class com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanelKt { - public static final fun InfoPanel (Lcom/squareup/workflow1/traceviewer/model/Node;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V + 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/runtime/Composer;I)V + 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 final class com/squareup/workflow1/traceviewer/util/ComposableSingletons$UploadFileKt { @@ -42,9 +42,22 @@ public final class com/squareup/workflow1/traceviewer/util/ComposableSingletons$ } public final class com/squareup/workflow1/traceviewer/util/JsonParserKt { - public static final fun fetchTrace (Lio/github/vinceglb/filekit/PlatformFile;Landroidx/compose/ui/Modifier;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public static synthetic fun fetchTrace$default (Lio/github/vinceglb/filekit/PlatformFile;Landroidx/compose/ui/Modifier;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static final fun parseTrace (Ljava/lang/String;)Ljava/util/List; + 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 { @@ -52,6 +65,6 @@ public final class com/squareup/workflow1/traceviewer/util/SandboxBackgroundKt { } public final class com/squareup/workflow1/traceviewer/util/UploadFileKt { - public static final fun UploadFile (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V + public static final fun UploadFile (Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V } From b7fd015b16159d52512bd29e26040d70a601bd05 Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Mon, 16 Jun 2025 15:57:01 -0400 Subject: [PATCH 15/16] Final fixes for PR comments --- .../com/squareup/workflow1/traceviewer/App.kt | 4 ++-- .../model/{WorkflowNode.kt => Node.kt} | 0 .../workflow1/traceviewer/ui/FrameSelectTab.kt | 2 +- .../workflow1/traceviewer/ui/WorkflowTree.kt | 18 ++++++++++-------- 4 files changed, 13 insertions(+), 11 deletions(-) rename workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/{WorkflowNode.kt => Node.kt} (100%) 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 17336c6ab1..2bcdb7a8cd 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 @@ -10,9 +10,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.squareup.workflow1.traceviewer.model.Node +import com.squareup.workflow1.traceviewer.ui.FrameSelectTab import com.squareup.workflow1.traceviewer.ui.RenderDiagram import com.squareup.workflow1.traceviewer.ui.RightInfoPanel -import com.squareup.workflow1.traceviewer.ui.StateSelectTab import com.squareup.workflow1.traceviewer.util.SandboxBackground import com.squareup.workflow1.traceviewer.util.UploadFile import io.github.vinceglb.filekit.PlatformFile @@ -44,7 +44,7 @@ public fun App( } } - StateSelectTab( + FrameSelectTab( frames = workflowFrames, currentIndex = frameIndex, onIndexChange = { frameIndex = it }, diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/WorkflowNode.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt similarity index 100% rename from workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/WorkflowNode.kt rename to workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/Node.kt 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 c2eab9d9d5..0dccfa4e8b 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 StateSelectTab( +public fun FrameSelectTab( frames: List, currentIndex: Int, onIndexChange: (Int) -> Unit, 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 9f3e6678d8..59277e82f0 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 @@ -43,15 +43,17 @@ public fun RenderDiagram( LaunchedEffect(traceFile) { val parseResult = parseTrace(traceFile) - if (parseResult is ParseResult.Failure) { - error = parseResult.error - return@LaunchedEffect + when (parseResult) { + is ParseResult.Failure -> { + error = parseResult.error + } + is ParseResult.Success -> { + val parsedFrames = parseResult.trace ?: emptyList() + frames = parsedFrames + onFileParse(parsedFrames) + isLoading = false + } } - - val parsedFrames = (parseResult as ParseResult.Success).trace ?: emptyList() - frames = parsedFrames - onFileParse(parsedFrames) - isLoading = false } if (error != null) { From d9b1a192b0cb308fbba58b50124dd65ce6827eca Mon Sep 17 00:00:00 2001 From: wenli-cai Date: Mon, 16 Jun 2025 20:03:16 +0000 Subject: [PATCH 16/16] 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow-trace-viewer/api/workflow-trace-viewer.api b/workflow-trace-viewer/api/workflow-trace-viewer.api index ff87380f88..c06abc9a6b 100644 --- a/workflow-trace-viewer/api/workflow-trace-viewer.api +++ b/workflow-trace-viewer/api/workflow-trace-viewer.api @@ -23,7 +23,7 @@ public final class com/squareup/workflow1/traceviewer/model/Node { } public final class com/squareup/workflow1/traceviewer/ui/FrameSelectTabKt { - public static final fun StateSelectTab (Ljava/util/List;ILkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V + 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 {