-
Notifications
You must be signed in to change notification settings - Fork 104
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
base: main
Are you sure you want to change the base?
Conversation
- 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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
Let's avoid reusing the term
WorkflowNode
, that's a very key object in the workflow runtime. Maybe justNode
? OrViewerNode
? -
Avoid
data class
in library APIs -
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
orNodeModel
.
Be interested to hear @steve-the-edwards's take.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
} catch (e: JsonDataException) { | ||
throw JsonDataException("Failed to parse JSON: ${e.message}", e) | ||
} catch (e: IOException) { | ||
throw IOException("Malformed JSON: ${e.message}", e) | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
* All the caught exceptions should be handled by the caller, and appropriate UI feedback should be | ||
* provided to user |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()) } |
There was a problem hiding this comment.
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 snapshot
s but it's really the index for workflowTrace
, so those should be named similarly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See:
422b94b#r2141079704
**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. | ||
|
There was a problem hiding this comment.
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
e55dc85
to
6bdc3d4
Compare
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
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>
Changed project structure and included new directories.
Two notable for added functionality
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)
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