Skip to content

Commit 784e757

Browse files
authored
Merge pull request #1335 from square/wenli/workflow-viewer
Workflow visualizer prototype
2 parents 9c6504f + a778895 commit 784e757

File tree

13 files changed

+3042
-3
lines changed

13 files changed

+3042
-3
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ local.properties
3737
*.iml
3838
.idea/
3939
captures/
40+
41+
# Kotlin Metadata
42+
.kotlin/

gradle/libs.versions.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ androidx-viewbinding = "8.1.2"
3939
detekt = "1.19.0"
4040
dokka = "2.0.0"
4141
dependencyGuard = "0.5.0"
42+
# Any version above 0.10.0-beta03 requires Compose 1.8.0 or higher, beta03 is 1.7.3 or higher.
43+
filekit-dialogs-compose = "0.10.0-beta03"
4244

4345
google-accompanist = "0.18.0"
4446
google-dagger = "2.40.5"
@@ -78,6 +80,7 @@ squareup-curtains = "1.2.5"
7880
squareup-cycler = "0.1.9"
7981
squareup-leakcanary = "3.0-alpha-8"
8082
squareup-moshi = "1.15.0"
83+
squareup-moshi-kotlin = "1.15.2"
8184
squareup-okhttp = "4.9.1"
8285
squareup-okio = "3.3.0"
8386
squareup-radiography = "2.4.1"
@@ -185,6 +188,7 @@ dokka-gradle-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", vers
185188

186189
dropbox-dependencyGuard = { module = "com.dropbox.dependency-guard:dependency-guard", version.ref = "dependencyGuard" }
187190

191+
filekit-dialogs-compose = { module = "io.github.vinceglb:filekit-dialogs-compose", version.ref = "filekit-dialogs-compose" }
188192
google-android-material = { module = "com.google.android.material:material", version.ref = "material" }
189193
google-ksp = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "google-ksp" }
190194

@@ -250,6 +254,7 @@ squareup-leakcanary-objectwatcher-android = { module = "com.squareup.leakcanary:
250254
squareup-moshi = { module = "com.squareup.moshi:moshi", version.ref = "squareup-moshi" }
251255
squareup-moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "squareup-moshi" }
252256
squareup-moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "squareup-moshi" }
257+
squareup-moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "squareup-moshi-kotlin" }
253258

254259
squareup-okio = { module = "com.squareup.okio:okio", version.ref = "squareup-okio" }
255260

workflow-trace-viewer/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ It can be run via Gradle using:
99
```shell
1010
./gradlew :workflow-trace-viewer:run
1111
```
12+
13+
### External Libraries
14+
15+
[FileKit](https://github.com/vinceglb/FileKit) is an external library made to apply file operations on Kotlin and KMP projects. It's purpose in this app is to allow developers to upload their own json trace files. The motivation for its use is to quickly implement a file picker.

workflow-trace-viewer/api/workflow-trace-viewer.api

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
public final class com/squareup/workflow1/traceviewer/AppKt {
2-
public static final fun App (Landroidx/compose/runtime/Composer;I)V
2+
public static final fun App (Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
33
}
44

55
public final class com/squareup/workflow1/traceviewer/ComposableSingletons$MainKt {
@@ -9,8 +9,47 @@ public final class com/squareup/workflow1/traceviewer/ComposableSingletons$MainK
99
public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3;
1010
}
1111

12+
public final class com/squareup/workflow1/traceviewer/ComposableSingletons$UploadFileKt {
13+
public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/ComposableSingletons$UploadFileKt;
14+
public static field lambda-1 Lkotlin/jvm/functions/Function3;
15+
public fun <init> ()V
16+
public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3;
17+
}
18+
1219
public final class com/squareup/workflow1/traceviewer/MainKt {
1320
public static final fun main ()V
1421
public static synthetic fun main ([Ljava/lang/String;)V
1522
}
1623

24+
public final class com/squareup/workflow1/traceviewer/SandboxBackgroundKt {
25+
public static final fun SandboxBackground (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
26+
}
27+
28+
public final class com/squareup/workflow1/traceviewer/UploadFileKt {
29+
public static final fun UploadFile (Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
30+
}
31+
32+
public final class com/squareup/workflow1/traceviewer/WorkflowJsonParserKt {
33+
public static final fun parseTrace (Ljava/lang/String;)Lcom/squareup/workflow1/traceviewer/WorkflowNode;
34+
}
35+
36+
public final class com/squareup/workflow1/traceviewer/WorkflowNode {
37+
public static final field $stable I
38+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V
39+
public final fun component1 ()Ljava/lang/String;
40+
public final fun component2 ()Ljava/lang/String;
41+
public final fun component3 ()Ljava/util/List;
42+
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lcom/squareup/workflow1/traceviewer/WorkflowNode;
43+
public static synthetic fun copy$default (Lcom/squareup/workflow1/traceviewer/WorkflowNode;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lcom/squareup/workflow1/traceviewer/WorkflowNode;
44+
public fun equals (Ljava/lang/Object;)Z
45+
public final fun getChildren ()Ljava/util/List;
46+
public final fun getId ()Ljava/lang/String;
47+
public final fun getName ()Ljava/lang/String;
48+
public fun hashCode ()I
49+
public fun toString ()Ljava/lang/String;
50+
}
51+
52+
public final class com/squareup/workflow1/traceviewer/WorkflowTreeKt {
53+
public static final fun DrawWorkflowTree (Lcom/squareup/workflow1/traceviewer/WorkflowNode;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
54+
}
55+

workflow-trace-viewer/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ kotlin {
2323
implementation(compose.desktop.currentOs)
2424
implementation(libs.kotlinx.coroutines.swing)
2525
implementation(compose.materialIconsExtended)
26+
implementation(libs.squareup.moshi.kotlin)
27+
implementation(libs.filekit.dialogs.compose)
2628
}
2729
}
2830
}
Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,46 @@
11
package com.squareup.workflow1.traceviewer
22

3+
import androidx.compose.foundation.layout.Box
34
import androidx.compose.material.Text
45
import androidx.compose.runtime.Composable
6+
import androidx.compose.runtime.LaunchedEffect
7+
import androidx.compose.runtime.getValue
8+
import androidx.compose.runtime.mutableStateOf
9+
import androidx.compose.runtime.remember
10+
import androidx.compose.runtime.setValue
11+
import androidx.compose.ui.Modifier
12+
import io.github.vinceglb.filekit.PlatformFile
13+
import io.github.vinceglb.filekit.readString
514

15+
/**
16+
* Main composable that provides the different layers of UI.
17+
*/
618
@Composable
7-
fun App() {
8-
Text("Hello world!")
19+
public fun App(
20+
modifier: Modifier = Modifier
21+
) {
22+
Box {
23+
var selectedFile by remember { mutableStateOf<PlatformFile?>(null) }
24+
25+
if (selectedFile != null) {
26+
SandboxBackground { WorkflowContent(selectedFile!!) }
27+
}
28+
29+
UploadFile(onFileSelect = { selectedFile = it })
30+
}
31+
}
32+
33+
@Composable
34+
private fun WorkflowContent(file: PlatformFile) {
35+
var jsonString by remember { mutableStateOf<String?>(null) }
36+
LaunchedEffect(file) {
37+
jsonString = file.readString()
38+
}
39+
val root = jsonString?.let { parseTrace(it) }
40+
41+
if (root != null) {
42+
DrawWorkflowTree(root)
43+
} else {
44+
Text("Empty data or failed to parse data") // TODO: proper handling of error
45+
}
946
}

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package com.squareup.workflow1.traceviewer
22

33
import androidx.compose.ui.window.singleWindowApplication
44

5+
/**
6+
* Main entry point for the desktop application, see [README.md] for more details.
7+
*/
58
fun main() {
69
singleWindowApplication(title = "Workflow Trace Viewer") {
710
App()
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.squareup.workflow1.traceviewer
2+
3+
import androidx.compose.foundation.gestures.awaitEachGesture
4+
import androidx.compose.foundation.gestures.detectDragGestures
5+
import androidx.compose.foundation.layout.Box
6+
import androidx.compose.foundation.layout.fillMaxSize
7+
import androidx.compose.foundation.layout.wrapContentSize
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.runtime.getValue
10+
import androidx.compose.runtime.mutableFloatStateOf
11+
import androidx.compose.runtime.mutableStateOf
12+
import androidx.compose.runtime.remember
13+
import androidx.compose.runtime.setValue
14+
import androidx.compose.ui.Alignment
15+
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.geometry.Offset
17+
import androidx.compose.ui.graphics.graphicsLayer
18+
import androidx.compose.ui.input.pointer.PointerEventType
19+
import androidx.compose.ui.input.pointer.pointerInput
20+
21+
/**
22+
* This is the backdrop for the whole app. Since there can be hundreds of modules at a time, there
23+
* is not realistic way to fit everything on the screen at once. Having the liberty to pan across
24+
* the whole tree as well as zoom into specific subtrees means there's a lot more control when
25+
* analyzing the traces.
26+
*
27+
*/
28+
@Composable
29+
public fun SandboxBackground(
30+
modifier: Modifier = Modifier,
31+
content: @Composable () -> Unit,
32+
) {
33+
var scale by remember { mutableFloatStateOf(1f) }
34+
var offset by remember { mutableStateOf(Offset.Zero) }
35+
36+
Box(
37+
modifier
38+
.fillMaxSize()
39+
.pointerInput(Unit) {
40+
// Panning capabilities: watches for drag gestures and applies the translation
41+
detectDragGestures { _, translation->
42+
offset += translation
43+
}
44+
}
45+
.pointerInput(Unit) {
46+
// Zooming capabilities: watches for any scroll events and immediately consumes changes.
47+
// - This is AI generated.
48+
awaitEachGesture {
49+
val event = awaitPointerEvent()
50+
if (event.type == PointerEventType.Scroll) {
51+
val scrollDelta = event.changes.first().scrollDelta.y
52+
scale *= if (scrollDelta < 0) 1.1f else 0.9f
53+
scale = scale.coerceIn(0.1f, 10f)
54+
event.changes.forEach { it.consume() }
55+
}
56+
}
57+
}
58+
) {
59+
Box(
60+
modifier = Modifier
61+
.wrapContentSize(unbounded = true, align = Alignment.Center)
62+
.graphicsLayer {
63+
translationX = offset.x
64+
translationY = offset.y
65+
scaleX = scale
66+
scaleY = scale
67+
}
68+
) {
69+
content()
70+
}
71+
}
72+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.squareup.workflow1.traceviewer
2+
3+
import androidx.compose.foundation.layout.Box
4+
import androidx.compose.foundation.layout.fillMaxSize
5+
import androidx.compose.foundation.layout.padding
6+
import androidx.compose.foundation.shape.CircleShape
7+
import androidx.compose.material.Button
8+
import androidx.compose.material.ButtonDefaults.buttonColors
9+
import androidx.compose.material.Text
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.Alignment
12+
import androidx.compose.ui.Modifier
13+
import androidx.compose.ui.graphics.Color
14+
import androidx.compose.ui.unit.dp
15+
import androidx.compose.ui.unit.sp
16+
import io.github.vinceglb.filekit.PlatformFile
17+
import io.github.vinceglb.filekit.dialogs.FileKitType
18+
import io.github.vinceglb.filekit.dialogs.compose.rememberFilePickerLauncher
19+
20+
/**
21+
* Provides functionality for user to upload a JSON or .txt file from their local devices, which
22+
* contains information pulled from workflow traces
23+
*/
24+
@Composable
25+
public fun UploadFile(
26+
onFileSelect: (PlatformFile?) -> Unit,
27+
modifier: Modifier = Modifier,
28+
) {
29+
Box(
30+
modifier
31+
.padding(16.dp)
32+
.fillMaxSize()
33+
) {
34+
val launcher = rememberFilePickerLauncher(
35+
type = FileKitType.File(listOf("json", "txt")),
36+
title = "Select Workflow Trace File"
37+
) {
38+
onFileSelect(it)
39+
}
40+
Button(
41+
onClick = { launcher.launch() },
42+
modifier = Modifier
43+
.align(Alignment.BottomEnd),
44+
shape = CircleShape,
45+
colors = buttonColors(Color.Black)
46+
) {
47+
Text(
48+
text = "+",
49+
color = Color.White,
50+
fontSize = 24.sp,
51+
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
52+
)
53+
}
54+
}
55+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.squareup.workflow1.traceviewer
2+
3+
import com.squareup.moshi.JsonDataException
4+
import com.squareup.moshi.Moshi
5+
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
6+
import java.io.IOException
7+
8+
/**
9+
* Parses a JSON string into [WorkflowNode] with Moshi adapters.
10+
*
11+
* All the caught exceptions should be handled by the caller, and appropriate UI feedback should be
12+
* provided to user.
13+
*/
14+
public fun parseTrace(
15+
json: String
16+
): WorkflowNode? {
17+
return try {
18+
val moshi = Moshi.Builder()
19+
.add(KotlinJsonAdapterFactory())
20+
.build()
21+
val workflowAdapter = moshi.adapter(WorkflowNode::class.java)
22+
val root = workflowAdapter.fromJson(json)
23+
root
24+
} catch (e: JsonDataException) {
25+
throw JsonDataException("Failed to parse JSON: ${e.message}", e)
26+
} catch (e: IOException) {
27+
throw IOException("Malformed JSON: ${e.message}", e)
28+
}
29+
}

0 commit comments

Comments
 (0)