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

Conversation

wenli-cai
Copy link
Collaborator

@wenli-cai wenli-cai commented Jun 11, 2025

Changed project structure and included new directories.

Two notable for added functionality

  1. Instead of "opening" each node to provide more workflow-specific information, a side bar will open instead. This allows for much more information to be presented and will help to avoid cluttering up the main state diagram (but more basic information can still be tacked on later)

  2. Since traces will have different traces to see how render passes affected the responsible workflows, a trace selection bar that allows you to jump between different snapshots will help in visualization.

Screen.Recording.2025-06-11.at.12.05.51.PM.mov

- This will provide a lot more information about each workflow node that's clicked, especially when it could contain a lot of information
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
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
*
* TBD what more metadata should be involved with each node, e.g. (props, states, # of render passes)
*/
public data class WorkflowNode(
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. Let's avoid reusing the term WorkflowNode, that's a very key object in the workflow runtime. Maybe just Node? Or ViewerNode?

  2. Avoid data class in library APIs

  3. A package with a single member is odd, unless you're very confident that there will be other classes here soon. Perhaps you should promote this class and just call it Model or NodeModel.

Be interested to hear @steve-the-edwards's take.

Copy link
Collaborator Author

@wenli-cai wenli-cai Jun 11, 2025

Choose a reason for hiding this comment

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

Could you explain (2)? Do you mean replace it with a normal class like a POJO (I guess POKO) ?

For (3), I'm thinking of taking all the mutable states in App.kt out and having them in a wrapper class, which would be inside model. Would it be better if it was also by itself, like a StatesModel?

Copy link
Contributor

Choose a reason for hiding this comment

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

I just mean don't use the word data. It generates a bunch of code that makes API evolution really difficult / impossible. If you need them go ahead and implement hash / equals manualy.

I don't have the context to answer WRT App.kt, and really the issue I raised is a small one. If you think more models are coming might as well keep the package.

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 see, I've never looked deeply into data class, so thanks for pointing that out.

@@ -0,0 +1,48 @@
package com.squareup.workflow1.traceviewer.utils
Copy link
Contributor

Choose a reason for hiding this comment

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

util rather than utils for the package name.

Comment on lines 43 to 47
} catch (e: JsonDataException) {
throw JsonDataException("Failed to parse JSON: ${e.message}", e)
} catch (e: IOException) {
throw IOException("Malformed JSON: ${e.message}", e)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This wrap and rethrow behavior isn't really providing any value. I think you just want to let them fly and add an @throws line or two in the kdoc.

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

Comment on lines 28 to 29
* All the caught exceptions should be handled by the caller, and appropriate UI feedback should be
* provided to user
Copy link
Contributor

Choose a reason for hiding this comment

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

Implicit, especially if you use @throws here 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.

Done

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

*/
@Composable
public fun StateSelectTab(
trace: List<WorkflowNode>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's name this traces to make it clear that this is a list.

But also, the name traces is kinda confusing since this Composable is named StateSelectTab. Those should be consistent. I don't think trace is super clear, though. I think @rjrjr and @steve-the-edwards might have a better idea there.

Personally, I'm thinking something like snapshots? But snapshot also has a defined meaning in Workflow land so maybe not. My thought there is that since all a trace is is an export of a snapshot of a Workflow's state tree over time, naming this snapshots helps make it clear that this is effectively a list of all of the "root nodes" that the user has loaded. rootNodes is also a potential option there.

Regardless, let's name the Composable and this field the same.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changed the naming schemes, let me know if this seems better. See:
422b94b#r2141079704

Surface(
modifier = modifier
.padding(4.dp),
color = Color.White,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: And this is really a nit but consider using the pre-existing MaterialTheme (that might require a new dependency on Material though...) for pulling colors like this. Just so things can be consistent going forward.

https://developer.android.com/develop/ui/compose/designsystems/material3

I'm saying this is totally optional but if you do, do it in a follow-up PR. In a perfect world, we'd be using existing Square design but someone (....me) still hasn't ported Market (our most recent Square design system) to be usable in Compose for Desktop so...

return@Column
}

Column(
Copy link
Collaborator

Choose a reason for hiding this comment

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

A column with a single column child is a bit weird. These can probably be combined.

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

onNodeSelect: (WorkflowNode) -> Unit,
) {
var workflowNodes by remember { mutableStateOf<List<WorkflowNode>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Either put file in the key block for remember or set isLoading to true in your LaunchedEffect. Otherwise, it'll still think it's not loading when you start loading a new file.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, thanks!

var selectedFile by remember { mutableStateOf<PlatformFile?>(null) }
var selectedFile by remember { mutableStateOf<PlatformFile?>(null) }
var selectedNode by remember { mutableStateOf<WorkflowNode?>(null) }
var workflowTrace by remember { mutableStateOf<List<WorkflowNode>>(emptyList()) }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Kind of the same comment I make below here. snapshotIndex implies this is the index of a list of snapshots but it's really the index for workflowTrace, so those should be named similarly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

**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!

1. Made terminology and variable naming consistent (see README changes)
2. Fixed caught bugs in PR review
3. Right aligned inspection tab
@wenli-cai wenli-cai force-pushed the wenli/workflow-viewer-open-nodes branch from e55dc85 to 6bdc3d4 Compare June 12, 2025 01:32
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
}
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.

)
}

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

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.

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.

* 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

Button(
onClick = { launcher.launch() },
modifier = Modifier
.align(Alignment.BottomEnd),
.align(Alignment.BottomStart),
Copy link

Choose a reason for hiding this comment

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

I noticed that there is a comment in your App method saying that this is a bottom left button, but if you instead passed set the alignment of the button in the modifier you're already passing in then the comment wouldn't be required. I also think it's more typical that the parent would specify the position of a button rather than the button itself specifying where it is on screen.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Makes sense, also a lot more self-explanatory when looking at parent containers. Fixed

@@ -23,6 +23,7 @@ import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher
*/
@Composable
public fun UploadFile(
onReset: () -> Unit,
Copy link

Choose a reason for hiding this comment

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

Can onReset be an implicit step executed every time onFileSelect is called instead of a separate param?

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

* Parses the data from the given file and initiates the workflow tree
*/
public suspend fun fetchTrace(
file: PlatformFile?,
Copy link

Choose a reason for hiding this comment

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

Why allow a nullable PlatformFile?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Mistake, fixed now.

/**
* Parses the data from the given file and initiates the workflow tree
*/
public suspend fun fetchTrace(
Copy link

@japplin japplin Jun 12, 2025

Choose a reason for hiding this comment

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

This method isn't doing much, and it's comment is saying that it parses the file which is what the parseTrace method is doing. Also parseTrace throws exceptions which is commented on parseTrace method but not here if they were one function that wouldn't be an issue.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

True, I've combined them now


val workflowList = Types.newParameterizedType(List::class.java, Node::class.java)
val workflowAdapter: JsonAdapter<List<Node>> = moshi.adapter(workflowList)
val root = workflowAdapter.fromJson(json) ?: emptyList()
Copy link

Choose a reason for hiding this comment

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

Let's return null on an error loading the list and then show an error message to the user that loading the trace was unsuccessful. That would also allow us to handle all of the exceptions here instead of throwing them.

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

wenli-cai and others added 3 commits June 14, 2025 12:11
1. Clarified component positions with modifiers
2. Removed unnecessary comments
3. Simplified JsonParser
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants