-
Notifications
You must be signed in to change notification settings - Fork 112
Workflow visualizer UI improvement #1366
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
Changes from all commits
f946bcf
7285e68
7070d83
c6264da
193901d
a262f7b
ef6ba80
c243292
7481f36
13fa318
8f5514d
69d9c27
32c5b9f
81b0da7
abe0486
841fee2
eceef59
46765cb
dc8fae6
4fd32ef
aed9dab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not in PR scope, but you probably want There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| // 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) | ||
| } | ||
| ) | ||
| } | ||
| } | ||
|
|
@@ -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 |
|---|---|---|
|
|
@@ -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() | ||
| ) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 You could do something like: 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 🤷🏻 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?, | ||
| ) |
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 also wonder if this is not needed?
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.
+1 - do we know why this is needed? if not, lets remove.
Uh oh!
There was an error while loading. Please reload this page.
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.
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.
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.
Great. sounds good.