-
Notifications
You must be signed in to change notification settings - Fork 104
Workflow visualizer prototype #1335
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
dceb9e4
759e3e4
efdaa92
56de1f6
f2ec953
6dbd1ff
d192bab
46f2f20
e964c42
97f8e77
e947472
35567b1
761c987
e212736
03d7784
0f8e334
a1099dc
f9a64ff
e36ca77
a778895
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 |
---|---|---|
|
@@ -37,3 +37,6 @@ local.properties | |
*.iml | ||
.idea/ | ||
captures/ | ||
|
||
# Kotlin Metadata | ||
.kotlin/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,46 @@ | ||
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.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.setValue | ||
import androidx.compose.ui.Modifier | ||
import io.github.vinceglb.filekit.PlatformFile | ||
import io.github.vinceglb.filekit.readString | ||
|
||
/** | ||
* Main composable that provides the different layers of UI. | ||
*/ | ||
@Composable | ||
fun App() { | ||
Text("Hello world!") | ||
public fun App( | ||
modifier: Modifier = Modifier | ||
) { | ||
Box { | ||
var selectedFile by remember { mutableStateOf<PlatformFile?>(null) } | ||
|
||
if (selectedFile != null) { | ||
SandboxBackground { WorkflowContent(selectedFile!!) } | ||
} | ||
|
||
UploadFile(onFileSelect = { selectedFile = it }) | ||
} | ||
} | ||
|
||
@Composable | ||
private fun WorkflowContent(file: PlatformFile) { | ||
var jsonString by remember { mutableStateOf<String?>(null) } | ||
LaunchedEffect(file) { | ||
jsonString = file.readString() | ||
} | ||
val root = jsonString?.let { parseTrace(it) } | ||
|
||
if (root != null) { | ||
DrawWorkflowTree(root) | ||
} else { | ||
Text("Empty data or failed to parse data") // TODO: proper handling of error | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package com.squareup.workflow1.traceviewer | ||
|
||
import androidx.compose.foundation.gestures.awaitEachGesture | ||
import androidx.compose.foundation.gestures.detectDragGestures | ||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.wrapContentSize | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.mutableFloatStateOf | ||
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.geometry.Offset | ||
import androidx.compose.ui.graphics.graphicsLayer | ||
import androidx.compose.ui.input.pointer.PointerEventType | ||
import androidx.compose.ui.input.pointer.pointerInput | ||
|
||
/** | ||
* This is the backdrop for the whole app. Since there can be hundreds of modules at a time, there | ||
* is not realistic way to fit everything on the screen at once. Having the liberty to pan across | ||
* the whole tree as well as zoom into specific subtrees means there's a lot more control when | ||
* analyzing the traces. | ||
* | ||
*/ | ||
@Composable | ||
public fun SandboxBackground( | ||
modifier: Modifier = Modifier, | ||
content: @Composable () -> Unit, | ||
) { | ||
var scale by remember { mutableFloatStateOf(1f) } | ||
var offset by remember { mutableStateOf(Offset.Zero) } | ||
|
||
Box( | ||
modifier | ||
.fillMaxSize() | ||
.pointerInput(Unit) { | ||
// Panning capabilities: watches for drag gestures and applies the translation | ||
detectDragGestures { _, translation-> | ||
offset += translation | ||
} | ||
} | ||
.pointerInput(Unit) { | ||
// Zooming capabilities: watches for any scroll events and immediately consumes changes. | ||
// - This is AI generated. | ||
awaitEachGesture { | ||
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. Is this from a blog post or ref article or something? If so, it can be valuable to post the link in a comment as then we can follow up during maintenance. 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. Added note that code is AI generated. |
||
val event = awaitPointerEvent() | ||
if (event.type == PointerEventType.Scroll) { | ||
val scrollDelta = event.changes.first().scrollDelta.y | ||
scale *= if (scrollDelta < 0) 1.1f else 0.9f | ||
scale = scale.coerceIn(0.1f, 10f) | ||
event.changes.forEach { it.consume() } | ||
} | ||
} | ||
} | ||
) { | ||
Box( | ||
modifier = Modifier | ||
.wrapContentSize(unbounded = true, align = Alignment.Center) | ||
.graphicsLayer { | ||
translationX = offset.x | ||
translationY = offset.y | ||
scaleX = scale | ||
scaleY = scale | ||
} | ||
) { | ||
content() | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package com.squareup.workflow1.traceviewer | ||
|
||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.foundation.shape.CircleShape | ||
import androidx.compose.material.Button | ||
import androidx.compose.material.ButtonDefaults.buttonColors | ||
import androidx.compose.material.Text | ||
import androidx.compose.runtime.Composable | ||
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 io.github.vinceglb.filekit.PlatformFile | ||
import io.github.vinceglb.filekit.dialogs.FileKitType | ||
import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher | ||
|
||
/** | ||
* Provides functionality for user to upload a JSON or .txt file from their local devices, which | ||
* contains information pulled from workflow traces | ||
*/ | ||
@Composable | ||
public fun UploadFile( | ||
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. Needs kdoc. 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. Done |
||
onFileSelect: (PlatformFile?) -> Unit, | ||
modifier: Modifier = Modifier, | ||
) { | ||
Box( | ||
modifier | ||
.padding(16.dp) | ||
.fillMaxSize() | ||
) { | ||
val launcher = rememberFilePickerLauncher( | ||
type = FileKitType.File(listOf("json", "txt")), | ||
title = "Select Workflow Trace File" | ||
) { | ||
onFileSelect(it) | ||
} | ||
Button( | ||
onClick = { launcher.launch() }, | ||
modifier = Modifier | ||
.align(Alignment.BottomEnd), | ||
shape = CircleShape, | ||
colors = buttonColors(Color.Black) | ||
) { | ||
Text( | ||
text = "+", | ||
color = Color.White, | ||
fontSize = 24.sp, | ||
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold | ||
) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package com.squareup.workflow1.traceviewer | ||
|
||
import com.squareup.moshi.JsonDataException | ||
import com.squareup.moshi.Moshi | ||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory | ||
import java.io.IOException | ||
|
||
/** | ||
* Parses a JSON string into [WorkflowNode] with Moshi adapters. | ||
* | ||
* All the caught exceptions should be handled by the caller, and appropriate UI feedback should be | ||
* provided to user. | ||
*/ | ||
public fun parseTrace( | ||
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. JSON parsing can be long running. This would be better as a That would make @Composable
private fun WorkflowContent(file: PlatformFile?) {
var rootNode by remember { mutableStateOf<WorkflowNode?>(null) }
LaunchedEffect(file) {
if(file != null) {
rootNode = parseTrace(file?.readString()) // in the future, a try-catch here to switch to an error state!
}
}
if (rootNode != null) {
DrawWorkflowTree(root)
} else {
Text("Empty data or failed to parse data") // TODO: proper handling of error
}
} For the future, when you get to the point where you might wanna show an error on parse failures, you'd instead just let this throw and expect it to be caught in the 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. Working on this in another branch! |
||
json: String | ||
): WorkflowNode? { | ||
return try { | ||
val moshi = Moshi.Builder() | ||
.add(KotlinJsonAdapterFactory()) | ||
.build() | ||
val workflowAdapter = moshi.adapter(WorkflowNode::class.java) | ||
val root = workflowAdapter.fromJson(json) | ||
root | ||
} 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.
Nice!