-
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 17 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 |
---|---|---|
@@ -1,9 +1,44 @@ | ||
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.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
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 { | ||
val selectedFile = remember { mutableStateOf<PlatformFile?>(null) } | ||
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. Prefer using Kotlin delegation here. var selectedFile by remember { mutableStateOf<PlatformFile?>(null) } That'll let you do simple reassignments without having to do the whole An example of this syntax is here: https://github.com/squareup/android-emulator-runner/blob/92d600d5e9cb215e3509ee215881e55a42fb6fac/desktop-app/src/jvmMain/kotlin/com/wardellbagby/aer/ui/App.kt#L192 The imports you may need to add manually are: import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue 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 |
||
|
||
if (selectedFile.value != null) { | ||
SandboxBackground { WorkflowContent(selectedFile.value) } | ||
} | ||
|
||
UploadFile(onFileSelect = { selectedFile.value = it }) | ||
} | ||
} | ||
|
||
@Composable | ||
private fun WorkflowContent(file: PlatformFile?) { | ||
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. Nit: No need for 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 |
||
val jsonString = remember { mutableStateOf<String?>(null) } | ||
LaunchedEffect(file) { | ||
jsonString.value = file?.readString() | ||
} | ||
val root = jsonString.value?.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 | ||
} | ||
Comment on lines
+41
to
+43
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. Nice! |
||
} | ||
.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.
Let's stick with the same naming convention we have here. Android SDK versions get camel case but dependency versions use kebab-case. In that same vein, dependencies should be named without the
version
suffix.So this would be
filekit-dialogs-compose
, Moshi would bemoshi-kotlin
, and uiGraphicsAndroidVersion would beandroidx-ui-graphics-android
.This is also is roughly alphabetical order, with spacings denoting group separations for when we have many dependencies all related to the same thing, like
androidx
, so this and the other new versions should be listed in that same order.It's ultimately a small thing admittedly, but it's always good to be consistent 'cause then things tend to stay how you'd expect them to be when you come back later!
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