Skip to content

Improve workflow visualizer functionality #1343

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 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
8 changes: 8 additions & 0 deletions workflow-trace-viewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Set some standard for naming terminology in this project. Open to changing it though!

### 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.
60 changes: 31 additions & 29 deletions workflow-trace-viewer/api/workflow-trace-viewer.api
Original file line number Diff line number Diff line change
Expand Up @@ -9,47 +9,49 @@ 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 <init> ()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/model/Node {
public static final field $stable I
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V
public final fun getChildren ()Ljava/util/List;
public final fun getId ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
}

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/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/WorkflowJsonParserKt {
public static final fun parseTrace (Ljava/lang/String;)Lcom/squareup/workflow1/traceviewer/WorkflowNode;
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 final class com/squareup/workflow1/traceviewer/WorkflowNode {
public static final field $stable I
public fun <init> (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 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/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/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 <init> ()V
public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3;
}

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/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/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/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
}

Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package com.squareup.workflow1.traceviewer

import androidx.compose.foundation.layout.Box
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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.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.util.SandboxBackground
import com.squareup.workflow1.traceviewer.util.UploadFile
import io.github.vinceglb.filekit.PlatformFile
import io.github.vinceglb.filekit.readString

/**
* Main composable that provides the different layers of UI.
Expand All @@ -19,28 +24,43 @@ import io.github.vinceglb.filekit.readString
public fun App(
modifier: Modifier = Modifier
) {
Box {
var selectedFile by remember { mutableStateOf<PlatformFile?>(null) }
var selectedTraceFile by remember { mutableStateOf<PlatformFile?>(null) }
var selectedNode by remember { mutableStateOf<Node?>(null) }
var workflowFrames by remember { mutableStateOf<List<Node>>(emptyList()) }
var frameIndex by remember { mutableIntStateOf(0) }

if (selectedFile != null) {
SandboxBackground { WorkflowContent(selectedFile!!) }
Box {
// Main content
if (selectedTraceFile != null) {
SandboxBackground {
RenderDiagram(
traceFile = selectedTraceFile!!,
traceInd = frameIndex,
onFileParse = { workflowFrames = it },
onNodeSelect = { selectedNode = it }
)
}
}

UploadFile(onFileSelect = { selectedFile = it })
}
}
// Top trace selector row
StateSelectTab(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Considering how many traces there are likely to be, you might consider using a dropdown instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed, maybe even a combination of the 2, where with a dropdown you can instantly jump to a frame, and a scrolling bar could let you traverse locally. I think, though, this could be something I implement down the line as iterative improvements.

frames = workflowFrames,
currentIndex = frameIndex,
onIndexChange = { frameIndex = it },
modifier = Modifier.align(Alignment.TopCenter)
)

@Composable
private fun WorkflowContent(file: PlatformFile) {
var jsonString by remember { mutableStateOf<String?>(null) }
LaunchedEffect(file) {
jsonString = file.readString()
}
val root = jsonString?.let { parseTrace(it) }
// Right side information panel
InfoPanel(selectedNode)
Copy link

Choose a reason for hiding this comment

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

nit: instead of a comment here rename InfoPanel to RightInfoPanel.


if (root != null) {
DrawWorkflowTree(root)
} else {
Text("Empty data or failed to parse data") // TODO: proper handling of error
// Bottom left upload button
val onReset = {
selectedNode = null
frameIndex = 0
}
UploadFile(
onReset = onReset,
onFileSelect = { selectedTraceFile = it }
)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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 class Node(
val id: String,
val name: String,
val children: List<Node>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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.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.Node

/**
* A trace tab selector that allows devs to switch between different states within the provided trace.
*/
@Composable
public fun StateSelectTab(
frames: List<Node>,
currentIndex: Int,
onIndexChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val state = rememberLazyListState()

Surface(
modifier = modifier
.padding(4.dp),
color = Color.White,
) {
LazyRow(
modifier = Modifier
.padding(8.dp),
state = state
) {
items(frames.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)
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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
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.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
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.squareup.workflow1.traceviewer.model.Node

/**
* 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: Node?,
modifier: Modifier = Modifier
) {
// This row is ordered RTL
Copy link

Choose a reason for hiding this comment

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

What is causing this row to be ordered right to left?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think RTL was the wrong description, I've changed it now to say This row is aligned to the right of the screen.

The full-width spacer pushes everything to the end.

Row {
Spacer(modifier = Modifier.weight(1f))

var panelOpen by remember { mutableStateOf(false) }

IconButton(
onClick = { panelOpen = !panelOpen },
modifier = Modifier
.padding(8.dp)
.size(30.dp)
.align(Alignment.Top)
) {
Icon(
imageVector = if (panelOpen) Filled.KeyboardArrowLeft else Filled.KeyboardArrowRight,
contentDescription = if (panelOpen) "Close Panel" else "Open Panel",
modifier = Modifier
)
}

// based on open/close, display the node details (Column)
Copy link

Choose a reason for hiding this comment

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

This code was already really easy to understand so good job! No need for the comment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Understood!

if (panelOpen) {
PanelDetails(
selectedNode,
Modifier.fillMaxWidth(.35f)
)
}
}
}

/**
* 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(
Copy link

Choose a reason for hiding this comment

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

nit: Instead of the comment perhaps NodeDetailsPanel

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

node: Node?,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxHeight()
.background(Color.LightGray)
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (node == null) {
Text("No node selected")
return@Column
}

Text("only visible with a node selected")
Copy link

Choose a reason for hiding this comment

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

Did you mean to leave this Text here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah just for now, it would be first to go once actual data starts coming in.

Text(
text = "This is a node panel for ${node.name}",
fontSize = 20.sp,
modifier = Modifier.padding(8.dp)
)
}
}
Loading