From b30a565d837341d61e3ef162f9fd9d343c0f1b7f Mon Sep 17 00:00:00 2001 From: Wenli Cai Date: Fri, 6 Jun 2025 15:53:44 -0400 Subject: [PATCH 01/14] 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 667e0bdd3..70f2ec602 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 5cfd1d308..1e71131bf 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/14] 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 70f2ec602..2528090ae 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 73dbe9073..e69de29bb 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 000000000..23bd7aacf --- /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 000000000..b723d0d31 --- /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 536d72f9a..95dc0b81f 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 cbbdb1255..3082713cd 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 1e71131bf..585c62811 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 000000000..b06465d9b --- /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/14] 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 2528090ae..d1bdec52f 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/14] 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/14] 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 d1bdec52f..a9f03c204 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 95dc0b81f..f632b09dd 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 b06465d9b..b44a0919e 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 08ad530c6..241524796 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 9733c6e23..2f9a2df11 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 000000000..3dc1d0d05 --- /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/14] 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 a9f03c204..ee1b4f712 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 e69de29bb..000000000 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 000000000..2db256fa7 --- /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 f632b09dd..d8bde6c3e 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 3082713cd..2b51c8342 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/14] 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 ee1b4f712..879138423 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 2db256fa7..ef5c0efeb 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 b723d0d31..424420d76 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 585c62811..00dd69ea6 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 3dc1d0d05..4c1b0b77b 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/14] 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 ef5c0efeb..f8abb5f14 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/14] 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 dbddaaaa5..4c9c2068d 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/14] 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 9f989983a..57735624a 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 879138423..9f00f1268 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 23bd7aacf..412eb5a7c 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 f8abb5f14..c2eab9d9d 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 424420d76..2fea8d4eb 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 d8bde6c3e..140f85b28 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 000000000..7c438770a --- /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 2b51c8342..822584c8e 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 00dd69ea6..58ab25fc3 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 b44a0919e..000000000 --- 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/14] 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 4c9c2068d..555d18744 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/14] 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 9f00f1268..17336c6ab 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 b455d0435..485c98c12 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 2fea8d4eb..f7c959abe 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 140f85b28..9817226d8 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 7c438770a..9fab8655f 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 58ab25fc3..4a197e8f4 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/14] 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 9817226d8..9f3e6678d 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 9fab8655f..6c1b7e257 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/14] 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 555d18744..ff87380f8 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 }