Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 7 additions & 47 deletions workflow-trace-viewer/api/workflow-trace-viewer.api
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,13 @@ public final class com/squareup/workflow1/traceviewer/MainKt {
public static synthetic fun main ([Ljava/lang/String;)V
}

public final class com/squareup/workflow1/traceviewer/model/Node {
public static final field $stable I
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/List;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getChildren ()Ljava/util/List;
public final fun getId ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
public final fun getParent ()Ljava/lang/String;
public final fun getParentId ()Ljava/lang/String;
public final fun getProps ()Ljava/lang/Object;
public final fun getRenderings ()Ljava/lang/Object;
public final fun getState ()Ljava/lang/Object;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/squareup/workflow1/traceviewer/ui/FrameSelectTabKt {
public static final fun FrameSelectTab (Ljava/util/List;ILkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
}

public final class com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanelKt {
public static final fun RightInfoPanel (Lcom/squareup/workflow1/traceviewer/model/Node;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
}

public final class com/squareup/workflow1/traceviewer/ui/WorkflowTreeKt {
public static final fun RenderDiagram (Lio/github/vinceglb/filekit/PlatformFile;ILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
public final class com/squareup/workflow1/traceviewer/ui/ComposableSingletons$WorkflowInfoPanelKt {
public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/ui/ComposableSingletons$WorkflowInfoPanelKt;
public static field lambda-1 Lkotlin/jvm/functions/Function3;
public static field lambda-2 Lkotlin/jvm/functions/Function3;
public fun <init> ()V
public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-2$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3;
}

public final class com/squareup/workflow1/traceviewer/util/ComposableSingletons$UploadFileKt {
Expand All @@ -52,26 +32,6 @@ public final class com/squareup/workflow1/traceviewer/util/ComposableSingletons$

public final class com/squareup/workflow1/traceviewer/util/JsonParserKt {
public static final field ROOT_ID Ljava/lang/String;
public static final fun parseTrace (Lio/github/vinceglb/filekit/PlatformFile;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public abstract interface class com/squareup/workflow1/traceviewer/util/ParseResult {
}

public final class com/squareup/workflow1/traceviewer/util/ParseResult$Failure : com/squareup/workflow1/traceviewer/util/ParseResult {
public static final field $stable I
public fun <init> (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 <init> (Ljava/util/List;)V
public final fun getTrace ()Ljava/util/List;
}

public final class com/squareup/workflow1/traceviewer/util/SandboxBackgroundKt {
public static final fun SandboxBackground (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
}

public final class com/squareup/workflow1/traceviewer/util/UploadFileKt {
Expand Down
10 changes: 10 additions & 0 deletions workflow-trace-viewer/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ kotlin {
implementation(libs.filekit.dialogs.compose)
}
}
jvmTest {
dependencies {
implementation(kotlin("test"))
implementation(kotlin("test-junit5"))
}
}
}
}

Expand All @@ -50,3 +56,7 @@ compose {
}
}
}

tasks.named<Test>("jvmTest") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wonder if this is not needed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 - do we know why this is needed? if not, lets remove.

Copy link
Contributor Author

@wenli-cai wenli-cai Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems like it's needed for registering and running junit tests in a multiplatform app?
https://kotlinlang.org/docs/jvm-test-using-junit.html#add-dependencies

But I havent removed the logging parts afterwards, those aren't necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. sounds good.

useJUnitPlatform()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ 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.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import com.squareup.workflow1.traceviewer.model.Node
import com.squareup.workflow1.traceviewer.model.NodeUpdate
import com.squareup.workflow1.traceviewer.ui.FrameSelectTab
import com.squareup.workflow1.traceviewer.ui.RenderDiagram
import com.squareup.workflow1.traceviewer.ui.RightInfoPanel
Expand All @@ -25,21 +30,32 @@ public fun App(
modifier: Modifier = Modifier
) {
var selectedTraceFile by remember { mutableStateOf<PlatformFile?>(null) }
var selectedNode by remember { mutableStateOf<Node?>(null) }
var selectedNode by remember { mutableStateOf<NodeUpdate?>(null) }
var workflowFrames by remember { mutableStateOf<List<Node>>(emptyList()) }
var frameIndex by remember { mutableIntStateOf(0) }
val sandboxState = remember { SandboxState() }

LaunchedEffect(sandboxState) {
snapshotFlow { frameIndex }.collect {
sandboxState.reset()
}
}

Box(
modifier = modifier
) {
Comment on lines 44 to 46
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in PR scope, but you probably want propagateMinConstraints here.

Copy link
Contributor Author

@wenli-cai wenli-cai Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain this more? As far as I understand, this would be telling that all children containers need to be a specific size within the Box?

// Main content
if (selectedTraceFile != null) {
SandboxBackground {
SandboxBackground(
sandboxState = sandboxState,
) {
RenderDiagram(
traceFile = selectedTraceFile!!,
frameInd = frameIndex,
onFileParse = { workflowFrames = it },
onNodeSelect = { selectedNode = it }
onNodeSelect = { node, prevNode ->
selectedNode = NodeUpdate(node, prevNode)
}
)
}
}
Expand All @@ -51,16 +67,31 @@ public fun App(
modifier = Modifier.align(Alignment.TopCenter)
)

RightInfoPanel(selectedNode)
RightInfoPanel(
selectedNode = selectedNode,
modifier = Modifier
.align(Alignment.TopEnd)
)

// The states are reset when a new file is selected.
UploadFile(
onFileSelect = {
resetOnFileSelect = {
selectedTraceFile = it
selectedNode = null
frameIndex = 0
workflowFrames = emptyList()
},
modifier = Modifier.align(Alignment.BottomStart)
)
}
}

internal class SandboxState {
var offset by mutableStateOf(Offset.Zero)
var scale by mutableFloatStateOf(1f)

fun reset() {
offset = Offset.Zero
scale = 1f
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,52 @@ package com.squareup.workflow1.traceviewer.model
*
* TBD what more metadata should be involved with each node, e.g. (props, states, # of render passes)
*/
public class Node(
internal data class Node(
val name: String,
val id: String,
val parent: String,
val parentId: String,
val props: Any? = null,
val state: Any? = null,
val renderings: Any? = null,
val children: List<Node>,
val props: String,
val state: String,
val rendering: String = "",
@Transient val children: LinkedHashMap<String, Node> = LinkedHashMap()
) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class looks like it's intended to basically be a database table, where the properties are referenced dynamically by the UI. Can these properties just be stored in a map, with keys defined as constants? No need to use reflection.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These properties are all here for the purpose of moshi being able to parse a file into these objects. Unless there is a way to use moshi in the way you're describing it.

Even then, I'm not sure what the difference is. Could you help me explain the use case of having a data class like this vs using a map and treating it like a database table?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The map woudl be just key : value where both are strings. Since everything but children here is a string, the idea would be that essentially we are using this data class to store a bunch of String properties. Since they are all strings, we could just make it a nodeProperties = mapOf<String, String>() for everything but the children. once that is a map then you are able to easily iterate both the 'properties' keys (with nodeProperties.keys.) and the values (with nodeProperties.values) without needing to use reflection etc.

As for Moshi I imagine they have a default adapter that just maps JSON properties to a map.

Where it gets slightly more complicated is with the children property here, since that is not a String and is why we have this as a full class.

You could do something like:

data class Node ( val properties: ImmutableMap<String, String>, val children: LinkedHashMap<String, Node>) {} 

and then you'd have to change the parsing. ultimately probably that would be the 'best' fit for the data (and you'd be able to avoid using reflection as the properties list change since you'd always iterate over all the keys).

As for whether you do that now, I'm not too worried either way 🤷🏻

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

understood, this makes sense. However, I still don't understand the distinction that @zach-klippenstein made originally about classes and "database table classes". Are data classes not meant to be used in a way that just holds data?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ya they are meant for that. I think the distinction comes down to the type of data. if you have properties of different strongly types vs if you just have a set of String properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah gotcha, I understand now, thanks for clarifying that.


override fun toString(): String {
return "Node(name='$name', parent='$parent', children=${children.size})"
return "Node(name='$name', parent='$parent', children=$children)"
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Node) return false
return this.id == other.id
}

override fun hashCode(): Int {
return id.hashCode()
}

companion object {
fun getNodeField(node: Node, field: String): String {
return when (field.lowercase()) {
"name" -> node.name
"id" -> node.id
"parent" -> node.parent
"parentid" -> node.parentId
"props" -> node.props
"state" -> node.state
"rendering" -> node.rendering
"children" -> node.children.toString()
else -> throw IllegalArgumentException("Unknown field: $field")
}
}
}
}

internal fun Node.addChild(child: Node): Node {
return copy(children = LinkedHashMap(this.children.plus(child.id to child)))
}

internal fun Node.replaceChild(child: Node): Node {
return addChild(child)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.squareup.workflow1.traceviewer.model

/**
* Represents the difference between the current and previous state of a node in the workflow trace.
* This will be what is passed as a state between UI to display the diff.
*
* If it's the first node in the frame, [previous] will be null and there is no difference to show.
*/
internal class NodeUpdate(
val current: Node,
val previous: Node?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import com.squareup.workflow1.traceviewer.model.Node
* A trace tab selector that allows devs to switch between different states within the provided trace.
*/
@Composable
public fun FrameSelectTab(
internal fun FrameSelectTab(
frames: List<Node>,
currentIndex: Int,
onIndexChange: (Int) -> Unit,
Expand Down
Loading
Loading