diff --git a/workflow-trace-viewer/README.md b/workflow-trace-viewer/README.md index 9f989983a..57735624a 100644 --- a/workflow-trace-viewer/README.md +++ b/workflow-trace-viewer/README.md @@ -10,6 +10,14 @@ It can be run via Gradle using: ./gradlew :workflow-trace-viewer:run ``` +### Terminology + +**Trace**: A trace is a file — made up of frames — that contains the execution history of a Workflow. It includes information about render passes, how states have changed within workflows, and the specific props being passed through. The data collected to generate these should be in chronological order, and allows developers to step through the process easily. + +**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. + ### External Libraries [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. diff --git a/workflow-trace-viewer/api/workflow-trace-viewer.api b/workflow-trace-viewer/api/workflow-trace-viewer.api index dbddaaaa5..ff87380f8 100644 --- a/workflow-trace-viewer/api/workflow-trace-viewer.api +++ b/workflow-trace-viewer/api/workflow-trace-viewer.api @@ -9,47 +9,62 @@ public final class com/squareup/workflow1/traceviewer/ComposableSingletons$MainK public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; } -public final class com/squareup/workflow1/traceviewer/ComposableSingletons$UploadFileKt { - public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/ComposableSingletons$UploadFileKt; +public final class com/squareup/workflow1/traceviewer/MainKt { + public static final fun main ()V + public static synthetic fun main ([Ljava/lang/String;)V +} + +public final class com/squareup/workflow1/traceviewer/model/Node { + public static final field $stable I + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V + public final fun getChildren ()Ljava/util/List; + public final fun getId ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/traceviewer/ui/FrameSelectTabKt { + public static final fun StateSelectTab (Ljava/util/List;ILkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +} + +public final class com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanelKt { + public static final fun RightInfoPanel (Lcom/squareup/workflow1/traceviewer/model/Node;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +} + +public final class com/squareup/workflow1/traceviewer/ui/WorkflowTreeKt { + public static final fun RenderDiagram (Lio/github/vinceglb/filekit/PlatformFile;ILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +} + +public final class com/squareup/workflow1/traceviewer/util/ComposableSingletons$UploadFileKt { + public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/util/ComposableSingletons$UploadFileKt; public static field lambda-1 Lkotlin/jvm/functions/Function3; public fun ()V public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3; } -public final class com/squareup/workflow1/traceviewer/MainKt { - public static final fun main ()V - public static synthetic fun main ([Ljava/lang/String;)V +public final class com/squareup/workflow1/traceviewer/util/JsonParserKt { + public static final fun parseTrace (Lio/github/vinceglb/filekit/PlatformFile;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class com/squareup/workflow1/traceviewer/SandboxBackgroundKt { - public static final fun SandboxBackground (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V +public abstract interface class com/squareup/workflow1/traceviewer/util/ParseResult { } -public final class com/squareup/workflow1/traceviewer/UploadFileKt { - public static final fun UploadFile (Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +public final class com/squareup/workflow1/traceviewer/util/ParseResult$Failure : com/squareup/workflow1/traceviewer/util/ParseResult { + public static final field $stable I + public fun (Ljava/lang/Throwable;)V + public final fun getError ()Ljava/lang/Throwable; } -public final class com/squareup/workflow1/traceviewer/WorkflowJsonParserKt { - public static final fun parseTrace (Ljava/lang/String;)Lcom/squareup/workflow1/traceviewer/WorkflowNode; +public final class com/squareup/workflow1/traceviewer/util/ParseResult$Success : com/squareup/workflow1/traceviewer/util/ParseResult { + public static final field $stable I + public fun (Ljava/util/List;)V + public final fun getTrace ()Ljava/util/List; } -public final class com/squareup/workflow1/traceviewer/WorkflowNode { - public static final field $stable I - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/util/List; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lcom/squareup/workflow1/traceviewer/WorkflowNode; - 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; - public fun equals (Ljava/lang/Object;)Z - public final fun getChildren ()Ljava/util/List; - public final fun getId ()Ljava/lang/String; - public final fun getName ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; +public final class com/squareup/workflow1/traceviewer/util/SandboxBackgroundKt { + public static final fun SandboxBackground (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V } -public final class com/squareup/workflow1/traceviewer/WorkflowTreeKt { - public static final fun DrawWorkflowTree (Lcom/squareup/workflow1/traceviewer/WorkflowNode;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +public final class com/squareup/workflow1/traceviewer/util/UploadFileKt { + public static final fun UploadFile (Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt index 667e0bdd3..17336c6ab 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt @@ -1,16 +1,21 @@ 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.mutableIntStateOf 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 com.squareup.workflow1.traceviewer.model.Node +import com.squareup.workflow1.traceviewer.ui.RenderDiagram +import com.squareup.workflow1.traceviewer.ui.RightInfoPanel +import com.squareup.workflow1.traceviewer.ui.StateSelectTab +import com.squareup.workflow1.traceviewer.util.SandboxBackground +import com.squareup.workflow1.traceviewer.util.UploadFile import io.github.vinceglb.filekit.PlatformFile -import io.github.vinceglb.filekit.readString /** * Main composable that provides the different layers of UI. @@ -19,28 +24,43 @@ import io.github.vinceglb.filekit.readString public fun App( modifier: Modifier = Modifier ) { - Box { - var selectedFile by remember { mutableStateOf(null) } + var selectedTraceFile by remember { mutableStateOf(null) } + var selectedNode by remember { mutableStateOf(null) } + var workflowFrames by remember { mutableStateOf>(emptyList()) } + var frameIndex by remember { mutableIntStateOf(0) } - if (selectedFile != null) { - SandboxBackground { WorkflowContent(selectedFile!!) } + Box( + modifier = modifier + ) { + // Main content + if (selectedTraceFile != null) { + SandboxBackground { + RenderDiagram( + traceFile = selectedTraceFile!!, + frameInd = frameIndex, + onFileParse = { workflowFrames = it }, + onNodeSelect = { selectedNode = it } + ) + } } - UploadFile(onFileSelect = { selectedFile = it }) - } -} + StateSelectTab( + frames = workflowFrames, + currentIndex = frameIndex, + onIndexChange = { frameIndex = it }, + modifier = Modifier.align(Alignment.TopCenter) + ) -@Composable -private fun WorkflowContent(file: PlatformFile) { - var jsonString by remember { mutableStateOf(null) } - LaunchedEffect(file) { - jsonString = file.readString() - } - val root = jsonString?.let { parseTrace(it) } + RightInfoPanel(selectedNode) - if (root != null) { - DrawWorkflowTree(root) - } else { - Text("Empty data or failed to parse data") // TODO: proper handling of error + // The states are reset when a new file is selected. + UploadFile( + onFileSelect = { + selectedTraceFile = it + selectedNode = null + frameIndex = 0 + }, + modifier = Modifier.align(Alignment.BottomStart) + ) } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt index b455d0435..485c98c12 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt @@ -1,5 +1,7 @@ package com.squareup.workflow1.traceviewer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier import androidx.compose.ui.window.singleWindowApplication /** @@ -7,6 +9,6 @@ import androidx.compose.ui.window.singleWindowApplication */ fun main() { singleWindowApplication(title = "Workflow Trace Viewer") { - App() + App(Modifier.fillMaxSize()) } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowJsonParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowJsonParser.kt deleted file mode 100644 index 73dbe9073..000000000 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowJsonParser.kt +++ /dev/null @@ -1,29 +0,0 @@ -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( - 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) - } -} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowTree.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowTree.kt deleted file mode 100644 index 536d72f9a..000000000 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/WorkflowTree.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.squareup.workflow1.traceviewer - -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -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.graphics.Color -import androidx.compose.ui.unit.dp - -/** - * Since the logic of Workflow is hierarchical (where each workflow may have parent workflows and/or - * children workflows, a tree structure is most appropriate for representing the data rather than - * using flat data structures like an array. - * - * TBD what more metadata should be involved with each node, e.g. (props, states, # of render passes) - */ -public data class WorkflowNode( - val id: String, - val name: String, - val children: List -) - -/** - * Since the workflow nodes present a tree structure, we utilize a recursive function to draw the tree. - * The Column holds a subtree of nodes, and the Row holds all the children of the current node. - */ -@Composable -public fun DrawWorkflowTree( - node: WorkflowNode, - modifier: Modifier = Modifier, -) { - Column( - modifier - .padding(5.dp) - .border(1.dp, Color.Black) - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - // draws itself - DrawNode(node) - - // draws children recursively - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.Top - ) { - node.children.forEach { childNode -> - DrawWorkflowTree(childNode) - } - } - } -} - -/** - * A basic box that represents a workflow node. - */ -@Composable -private fun DrawNode( - node: WorkflowNode, -) { - var open by remember { mutableStateOf(false) } - Box( - modifier = Modifier - .clickable { open = !open } - .padding(10.dp) - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = node.name) - Text(text = "ID: ${node.id}") - if (open) { - Text("node is opened") - } - } - } -} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/WorkflowNode.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/WorkflowNode.kt new file mode 100644 index 000000000..412eb5a7c --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/model/WorkflowNode.kt @@ -0,0 +1,14 @@ +package com.squareup.workflow1.traceviewer.model + +/** + * Since the logic of Workflow is hierarchical (where each workflow may have parent workflows and/or + * children workflows, a tree structure is most appropriate for representing the data rather than + * using flat data structures like an array. + * + * TBD what more metadata should be involved with each node, e.g. (props, states, # of render passes) + */ +public class Node( + val id: String, + val name: String, + val children: List +) diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt new file mode 100644 index 000000000..c2eab9d9d --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt @@ -0,0 +1,51 @@ +package com.squareup.workflow1.traceviewer.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.squareup.workflow1.traceviewer.model.Node + +/** + * A trace tab selector that allows devs to switch between different states within the provided trace. + */ +@Composable +public fun StateSelectTab( + frames: List, + currentIndex: Int, + onIndexChange: (Int) -> Unit, + modifier: Modifier = Modifier +) { + val state = rememberLazyListState() + + Surface( + modifier = modifier + .padding(4.dp), + color = Color.White, + ) { + LazyRow( + modifier = Modifier + .padding(8.dp), + state = state + ) { + items(frames.size) { index -> + Text( + text = "State ${index + 1}", + color = if (index == currentIndex) Color.Black else Color.LightGray, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { onIndexChange(index) } + .padding(10.dp) + ) + } + } + } +} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt new file mode 100644 index 000000000..f7c959abe --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowInfoPanel.kt @@ -0,0 +1,93 @@ +package com.squareup.workflow1.traceviewer.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons.AutoMirrored.Filled +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.squareup.workflow1.traceviewer.model.Node + +/** + * A panel that displays information about the selected workflow node. + * It can be toggled open or closed, and resets when the user selects a new file + * + * @param selectedNode The currently selected workflow node, or null if no node is selected. + */ +@Composable +public fun RightInfoPanel( + selectedNode: Node?, + modifier: Modifier = Modifier +) { + // This row is aligned to the right of the screen. + Row { + Spacer(modifier = Modifier.weight(1f)) + + var panelOpen by remember { mutableStateOf(false) } + + IconButton( + onClick = { panelOpen = !panelOpen }, + modifier = Modifier + .padding(8.dp) + .size(30.dp) + .align(Alignment.Top) + ) { + Icon( + imageVector = if (panelOpen) Filled.KeyboardArrowRight else Filled.KeyboardArrowLeft, + contentDescription = if (panelOpen) "Close Panel" else "Open Panel", + modifier = Modifier + ) + } + + if (panelOpen) { + NodePanelDetails( + selectedNode, + Modifier.fillMaxWidth(.35f) + ) + } + } +} + +@Composable +private fun NodePanelDetails( + node: Node?, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxHeight() + .background(Color.LightGray) + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (node == null) { + Text("No node selected") + return@Column + } + + Text("only visible with a node selected") + Text( + text = "This is a node panel for ${node.name}", + fontSize = 20.sp, + modifier = Modifier.padding(8.dp) + ) + } +} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt new file mode 100644 index 000000000..9f3e6678d --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt @@ -0,0 +1,121 @@ +package com.squareup.workflow1.traceviewer.ui + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.squareup.workflow1.traceviewer.model.Node +import com.squareup.workflow1.traceviewer.util.ParseResult +import com.squareup.workflow1.traceviewer.util.parseTrace +import io.github.vinceglb.filekit.PlatformFile + +/** + * Access point for drawing the main content of the app. It will load the trace for given files and + * tabs. This will also all errors related to errors parsing a given trace JSON file. + */ +@Composable +public fun RenderDiagram( + traceFile: PlatformFile, + frameInd: Int, + onFileParse: (List) -> Unit, + onNodeSelect: (Node) -> Unit, + modifier: Modifier = Modifier +) { + var frames by remember { mutableStateOf>(emptyList()) } + var isLoading by remember(traceFile) { mutableStateOf(true) } + var error by remember(traceFile) { mutableStateOf(null) } + + LaunchedEffect(traceFile) { + val parseResult = parseTrace(traceFile) + + if (parseResult is ParseResult.Failure) { + error = parseResult.error + return@LaunchedEffect + } + + val parsedFrames = (parseResult as ParseResult.Success).trace ?: emptyList() + frames = parsedFrames + onFileParse(parsedFrames) + isLoading = false + } + + if (error != null) { + Text("Error parsing file: ${error?.message}") + return + } + + if (!isLoading) { + DrawTree(frames[frameInd], onNodeSelect) + } + + // TODO: catch errors and display UI here +} + +/** + * Since the workflow nodes present a tree structure, we utilize a recursive function to draw the tree + * The Column holds a subtree of nodes, and the Row holds all the children of the current node + */ +@Composable +private fun DrawTree( + node: Node, + onNodeSelect: (Node) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier + .padding(5.dp) + .border(1.dp, Color.Black) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + DrawNode(node, onNodeSelect) + + // Draws the node's children recursively. + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.Top + ) { + node.children.forEach { childNode -> + DrawTree(childNode, onNodeSelect) + } + } + } +} + +/** + * A basic box that represents a workflow node + */ +@Composable +private fun DrawNode( + node: Node, + onNodeSelect: (Node) -> Unit, +) { + Box( + modifier = Modifier + .clickable { + // Selecting a node will bubble back up to the main view to handle the selection + onNodeSelect(node) + } + .padding(10.dp) + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = node.name) + Text(text = "ID: ${node.id}") + } + } +} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt new file mode 100644 index 000000000..6c1b7e257 --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/JsonParser.kt @@ -0,0 +1,37 @@ +package com.squareup.workflow1.traceviewer.util + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.squareup.workflow1.traceviewer.model.Node +import io.github.vinceglb.filekit.PlatformFile +import io.github.vinceglb.filekit.readString + +/** + * Parses a given file's JSON String into [Node] with Moshi adapters. + * + * @return A [ParseResult] representing result of parsing, either an error related to the + * format of the JSON, or a success and a parsed trace. + */ +public suspend fun parseTrace( + file: PlatformFile, +): ParseResult { + return try { + val jsonString = file.readString() + val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + val workflowList = Types.newParameterizedType(List::class.java, Node::class.java) + val workflowAdapter: JsonAdapter> = moshi.adapter(workflowList) + val trace = workflowAdapter.fromJson(jsonString) + ParseResult.Success(trace) + } catch (e: Exception) { + ParseResult.Failure(e) + } +} + +sealed interface ParseResult { + class Success(val trace: List?) : ParseResult + class Failure(val error: Throwable) : ParseResult +} diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/SandboxBackground.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt similarity index 96% rename from workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/SandboxBackground.kt rename to workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt index cbbdb1255..822584c8e 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/SandboxBackground.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SandboxBackground.kt @@ -1,4 +1,4 @@ -package com.squareup.workflow1.traceviewer +package com.squareup.workflow1.traceviewer.util import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.detectDragGestures @@ -38,7 +38,7 @@ public fun SandboxBackground( .fillMaxSize() .pointerInput(Unit) { // Panning capabilities: watches for drag gestures and applies the translation - detectDragGestures { _, translation-> + detectDragGestures { _, translation -> offset += translation } } diff --git a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/UploadFile.kt b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/UploadFile.kt similarity index 53% rename from workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/UploadFile.kt rename to workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/UploadFile.kt index 5cfd1d308..4a197e8f4 100644 --- a/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/UploadFile.kt +++ b/workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/UploadFile.kt @@ -1,14 +1,11 @@ -package com.squareup.workflow1.traceviewer +package com.squareup.workflow1.traceviewer.util -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 @@ -26,30 +23,24 @@ public fun UploadFile( 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" ) { - 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 - ) - } + onFileSelect(it) + } + + Button( + onClick = { launcher.launch() }, + modifier = modifier.padding(16.dp), + shape = CircleShape, + colors = buttonColors(Color.Black) + ) { + Text( + text = "+", + color = Color.White, + fontSize = 24.sp, + fontWeight = androidx.compose.ui.text.font.FontWeight.Bold + ) } } diff --git a/workflow-trace-viewer/src/jvmMain/resources/workflow-20.json b/workflow-trace-viewer/src/jvmMain/resources/workflow-20.json index 08ad530c6..241524796 100644 --- a/workflow-trace-viewer/src/jvmMain/resources/workflow-20.json +++ b/workflow-trace-viewer/src/jvmMain/resources/workflow-20.json @@ -1,3 +1,4 @@ +[ { "id": 1, "name": "root", @@ -102,4 +103,5 @@ ] } ] -} \ No newline at end of file +} +] diff --git a/workflow-trace-viewer/src/jvmMain/resources/workflow-300.json b/workflow-trace-viewer/src/jvmMain/resources/workflow-300.json index 9733c6e23..2f9a2df11 100644 --- a/workflow-trace-viewer/src/jvmMain/resources/workflow-300.json +++ b/workflow-trace-viewer/src/jvmMain/resources/workflow-300.json @@ -1,3 +1,4 @@ +[ { "id": 1, "name": "root", @@ -2595,4 +2596,5 @@ ] } ] -} \ No newline at end of file +} +] diff --git a/workflow-trace-viewer/src/jvmMain/resources/workflow-traces.json b/workflow-trace-viewer/src/jvmMain/resources/workflow-traces.json new file mode 100644 index 000000000..4c1b0b77b --- /dev/null +++ b/workflow-trace-viewer/src/jvmMain/resources/workflow-traces.json @@ -0,0 +1,2102 @@ +[ + { + "id": 1, + "name": "root", + "children": [ + { + "id": 2, + "name": "auth-flow", + "children": [ + { + "id": 3, + "name": "login-screen", + "children": [ + { + "id": 4, + "name": "login-form", + "children": [] + }, + { + "id": 5, + "name": "social-login", + "children": [] + } + ] + } + ] + }, + { + "id": 6, + "name": "main-flow", + "children": [ + { + "id": 7, + "name": "dashboard", + "children": [ + { + "id": 8, + "name": "stats-widget", + "children": [] + }, + { + "id": 9, + "name": "recent-activity", + "children": [] + } + ] + } + ] + }, + { + "id": 10, + "name": "background-tasks", + "children": [ + { + "id": 11, + "name": "sync-service", + "children": [ + { + "id": 12, + "name": "profile-sync", + "children": [] + }, + { + "id": 13, + "name": "preferences-sync", + "children": [] + } + ] + }, + { + "id": 14, + "name": "notification-service", + "children": [ + { + "id": 15, + "name": "push-handler", + "children": [] + } + ] + } + ] + }, + { + "id": 16, + "name": "settings-flow", + "children": [ + { + "id": 17, + "name": "settings-screen", + "children": [ + { + "id": 18, + "name": "profile-settings", + "children": [] + }, + { + "id": 19, + "name": "notification-settings", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 20, + "name": "app-root", + "children": [ + { + "id": 21, + "name": "user-flow", + "children": [ + { + "id": 22, + "name": "signup-screen", + "children": [ + { + "id": 23, + "name": "registration-form", + "children": [] + }, + { + "id": 24, + "name": "oauth-signup", + "children": [] + } + ] + } + ] + }, + { + "id": 25, + "name": "content-flow", + "children": [ + { + "id": 26, + "name": "home-screen", + "children": [ + { + "id": 27, + "name": "featured-content", + "children": [] + }, + { + "id": 28, + "name": "user-feed", + "children": [] + } + ] + } + ] + }, + { + "id": 29, + "name": "system-tasks", + "children": [ + { + "id": 30, + "name": "data-service", + "children": [ + { + "id": 31, + "name": "content-sync", + "children": [] + }, + { + "id": 32, + "name": "cache-manager", + "children": [] + } + ] + }, + { + "id": 33, + "name": "analytics-service", + "children": [ + { + "id": 34, + "name": "event-tracker", + "children": [] + } + ] + } + ] + }, + { + "id": 35, + "name": "preferences-flow", + "children": [ + { + "id": 36, + "name": "preferences-screen", + "children": [ + { + "id": 37, + "name": "account-settings", + "children": [] + }, + { + "id": 38, + "name": "privacy-settings", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 39, + "name": "system-root", + "children": [ + { + "id": 40, + "name": "security-flow", + "children": [ + { + "id": 41, + "name": "auth-screen", + "children": [ + { + "id": 42, + "name": "biometric-auth", + "children": [] + }, + { + "id": 43, + "name": "pin-verification", + "children": [] + } + ] + } + ] + }, + { + "id": 44, + "name": "navigation-flow", + "children": [ + { + "id": 45, + "name": "tab-navigator", + "children": [ + { + "id": 46, + "name": "home-tab", + "children": [] + }, + { + "id": 47, + "name": "search-tab", + "children": [] + } + ] + } + ] + }, + { + "id": 48, + "name": "core-services", + "children": [ + { + "id": 49, + "name": "network-service", + "children": [ + { + "id": 50, + "name": "api-client", + "children": [] + }, + { + "id": 51, + "name": "offline-handler", + "children": [] + } + ] + }, + { + "id": 52, + "name": "storage-service", + "children": [ + { + "id": 53, + "name": "database-manager", + "children": [] + } + ] + } + ] + }, + { + "id": 54, + "name": "admin-flow", + "children": [ + { + "id": 55, + "name": "admin-panel", + "children": [ + { + "id": 56, + "name": "user-management", + "children": [] + }, + { + "id": 57, + "name": "system-monitoring", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 58, + "name": "commerce-root", + "children": [ + { + "id": 59, + "name": "payment-flow", + "children": [ + { + "id": 60, + "name": "checkout-screen", + "children": [ + { + "id": 61, + "name": "payment-form", + "children": [] + }, + { + "id": 62, + "name": "card-scanner", + "children": [] + } + ] + } + ] + }, + { + "id": 63, + "name": "catalog-flow", + "children": [ + { + "id": 64, + "name": "product-list", + "children": [ + { + "id": 65, + "name": "product-card", + "children": [] + }, + { + "id": 66, + "name": "filter-panel", + "children": [] + } + ] + } + ] + }, + { + "id": 67, + "name": "order-tasks", + "children": [ + { + "id": 68, + "name": "fulfillment-service", + "children": [ + { + "id": 69, + "name": "inventory-check", + "children": [] + }, + { + "id": 70, + "name": "shipping-calc", + "children": [] + } + ] + }, + { + "id": 71, + "name": "payment-service", + "children": [ + { + "id": 72, + "name": "transaction-processor", + "children": [] + } + ] + } + ] + }, + { + "id": 73, + "name": "merchant-flow", + "children": [ + { + "id": 74, + "name": "seller-dashboard", + "children": [ + { + "id": 75, + "name": "sales-analytics", + "children": [] + }, + { + "id": 76, + "name": "inventory-manager", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 77, + "name": "social-root", + "children": [ + { + "id": 78, + "name": "messaging-flow", + "children": [ + { + "id": 79, + "name": "chat-screen", + "children": [ + { + "id": 80, + "name": "message-input", + "children": [] + }, + { + "id": 81, + "name": "media-picker", + "children": [] + } + ] + } + ] + }, + { + "id": 82, + "name": "social-flow", + "children": [ + { + "id": 83, + "name": "timeline-screen", + "children": [ + { + "id": 84, + "name": "post-composer", + "children": [] + }, + { + "id": 85, + "name": "story-viewer", + "children": [] + } + ] + } + ] + }, + { + "id": 86, + "name": "connection-tasks", + "children": [ + { + "id": 87, + "name": "friend-service", + "children": [ + { + "id": 88, + "name": "contact-sync", + "children": [] + }, + { + "id": 89, + "name": "suggestion-engine", + "children": [] + } + ] + }, + { + "id": 90, + "name": "activity-service", + "children": [ + { + "id": 91, + "name": "feed-generator", + "children": [] + } + ] + } + ] + }, + { + "id": 92, + "name": "privacy-flow", + "children": [ + { + "id": 93, + "name": "privacy-screen", + "children": [ + { + "id": 94, + "name": "visibility-controls", + "children": [] + }, + { + "id": 95, + "name": "block-manager", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 96, + "name": "media-root", + "children": [ + { + "id": 97, + "name": "streaming-flow", + "children": [ + { + "id": 98, + "name": "player-screen", + "children": [ + { + "id": 99, + "name": "video-player", + "children": [] + }, + { + "id": 100, + "name": "audio-controls", + "children": [] + } + ] + } + ] + }, + { + "id": 101, + "name": "library-flow", + "children": [ + { + "id": 102, + "name": "media-browser", + "children": [ + { + "id": 103, + "name": "playlist-view", + "children": [] + }, + { + "id": 104, + "name": "search-results", + "children": [] + } + ] + } + ] + }, + { + "id": 105, + "name": "processing-tasks", + "children": [ + { + "id": 106, + "name": "encoding-service", + "children": [ + { + "id": 107, + "name": "video-encoder", + "children": [] + }, + { + "id": 108, + "name": "thumbnail-generator", + "children": [] + } + ] + }, + { + "id": 109, + "name": "cdn-service", + "children": [ + { + "id": 110, + "name": "content-distributor", + "children": [] + } + ] + } + ] + }, + { + "id": 111, + "name": "creator-flow", + "children": [ + { + "id": 112, + "name": "upload-screen", + "children": [ + { + "id": 113, + "name": "file-uploader", + "children": [] + }, + { + "id": 114, + "name": "metadata-editor", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 115, + "name": "finance-root", + "children": [ + { + "id": 116, + "name": "banking-flow", + "children": [ + { + "id": 117, + "name": "account-screen", + "children": [ + { + "id": 118, + "name": "balance-widget", + "children": [] + }, + { + "id": 119, + "name": "transaction-list", + "children": [] + } + ] + } + ] + }, + { + "id": 120, + "name": "transfer-flow", + "children": [ + { + "id": 121, + "name": "send-money-screen", + "children": [ + { + "id": 122, + "name": "recipient-selector", + "children": [] + }, + { + "id": 123, + "name": "amount-input", + "children": [] + } + ] + } + ] + }, + { + "id": 124, + "name": "financial-tasks", + "children": [ + { + "id": 125, + "name": "fraud-service", + "children": [ + { + "id": 126, + "name": "risk-analyzer", + "children": [] + }, + { + "id": 127, + "name": "pattern-detector", + "children": [] + } + ] + }, + { + "id": 128, + "name": "compliance-service", + "children": [ + { + "id": 129, + "name": "kyc-validator", + "children": [] + } + ] + } + ] + }, + { + "id": 130, + "name": "investment-flow", + "children": [ + { + "id": 131, + "name": "portfolio-screen", + "children": [ + { + "id": 132, + "name": "asset-overview", + "children": [] + }, + { + "id": 133, + "name": "performance-chart", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 134, + "name": "health-root", + "children": [ + { + "id": 135, + "name": "tracking-flow", + "children": [ + { + "id": 136, + "name": "activity-screen", + "children": [ + { + "id": 137, + "name": "step-counter", + "children": [] + }, + { + "id": 138, + "name": "workout-tracker", + "children": [] + } + ] + } + ] + }, + { + "id": 139, + "name": "wellness-flow", + "children": [ + { + "id": 140, + "name": "health-dashboard", + "children": [ + { + "id": 141, + "name": "vital-signs", + "children": [] + }, + { + "id": 142, + "name": "medication-reminder", + "children": [] + } + ] + } + ] + }, + { + "id": 143, + "name": "monitoring-tasks", + "children": [ + { + "id": 144, + "name": "sensor-service", + "children": [ + { + "id": 145, + "name": "heart-rate-monitor", + "children": [] + }, + { + "id": 146, + "name": "sleep-tracker", + "children": [] + } + ] + }, + { + "id": 147, + "name": "analysis-service", + "children": [ + { + "id": 148, + "name": "trend-analyzer", + "children": [] + } + ] + } + ] + }, + { + "id": 149, + "name": "consultation-flow", + "children": [ + { + "id": 150, + "name": "telemedicine-screen", + "children": [ + { + "id": 151, + "name": "video-call", + "children": [] + }, + { + "id": 152, + "name": "symptom-checker", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 153, + "name": "education-root", + "children": [ + { + "id": 154, + "name": "learning-flow", + "children": [ + { + "id": 155, + "name": "course-screen", + "children": [ + { + "id": 156, + "name": "video-lesson", + "children": [] + }, + { + "id": 157, + "name": "quiz-component", + "children": [] + } + ] + } + ] + }, + { + "id": 158, + "name": "progress-flow", + "children": [ + { + "id": 159, + "name": "achievement-screen", + "children": [ + { + "id": 160, + "name": "badge-display", + "children": [] + }, + { + "id": 161, + "name": "progress-tracker", + "children": [] + } + ] + } + ] + }, + { + "id": 162, + "name": "assessment-tasks", + "children": [ + { + "id": 163, + "name": "grading-service", + "children": [ + { + "id": 164, + "name": "auto-grader", + "children": [] + }, + { + "id": 165, + "name": "feedback-generator", + "children": [] + } + ] + }, + { + "id": 166, + "name": "analytics-service", + "children": [ + { + "id": 167, + "name": "learning-analyzer", + "children": [] + } + ] + } + ] + }, + { + "id": 168, + "name": "collaboration-flow", + "children": [ + { + "id": 169, + "name": "study-group-screen", + "children": [ + { + "id": 170, + "name": "whiteboard-tool", + "children": [] + }, + { + "id": 171, + "name": "discussion-forum", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 172, + "name": "travel-root", + "children": [ + { + "id": 173, + "name": "booking-flow", + "children": [ + { + "id": 174, + "name": "search-screen", + "children": [ + { + "id": 175, + "name": "flight-finder", + "children": [] + }, + { + "id": 176, + "name": "hotel-search", + "children": [] + } + ] + } + ] + }, + { + "id": 177, + "name": "itinerary-flow", + "children": [ + { + "id": 178, + "name": "trip-planner", + "children": [ + { + "id": 179, + "name": "schedule-builder", + "children": [] + }, + { + "id": 180, + "name": "map-integration", + "children": [] + } + ] + } + ] + }, + { + "id": 181, + "name": "travel-tasks", + "children": [ + { + "id": 182, + "name": "booking-service", + "children": [ + { + "id": 183, + "name": "reservation-manager", + "children": [] + }, + { + "id": 184, + "name": "price-tracker", + "children": [] + } + ] + }, + { + "id": 185, + "name": "notification-service", + "children": [ + { + "id": 186, + "name": "flight-alerts", + "children": [] + } + ] + } + ] + }, + { + "id": 187, + "name": "companion-flow", + "children": [ + { + "id": 188, + "name": "travel-guide", + "children": [ + { + "id": 189, + "name": "local-recommendations", + "children": [] + }, + { + "id": 190, + "name": "weather-widget", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 1, + "name": "root", + "children": [ + { + "id": 2, + "name": "auth-flow", + "children": [ + { + "id": 3, + "name": "login-screen", + "children": [ + { + "id": 4, + "name": "login-form", + "children": [] + }, + { + "id": 5, + "name": "social-login", + "children": [] + } + ] + } + ] + }, + { + "id": 6, + "name": "main-flow", + "children": [ + { + "id": 7, + "name": "dashboard", + "children": [ + { + "id": 8, + "name": "stats-widget", + "children": [] + }, + { + "id": 9, + "name": "recent-activity", + "children": [] + } + ] + } + ] + }, + { + "id": 10, + "name": "background-tasks", + "children": [ + { + "id": 11, + "name": "sync-service", + "children": [ + { + "id": 12, + "name": "profile-sync", + "children": [] + }, + { + "id": 13, + "name": "preferences-sync", + "children": [] + } + ] + }, + { + "id": 14, + "name": "notification-service", + "children": [ + { + "id": 15, + "name": "push-handler", + "children": [] + } + ] + } + ] + }, + { + "id": 16, + "name": "settings-flow", + "children": [ + { + "id": 17, + "name": "settings-screen", + "children": [ + { + "id": 18, + "name": "profile-settings", + "children": [] + }, + { + "id": 19, + "name": "notification-settings", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 20, + "name": "app-root", + "children": [ + { + "id": 21, + "name": "user-flow", + "children": [ + { + "id": 22, + "name": "signup-screen", + "children": [ + { + "id": 23, + "name": "registration-form", + "children": [] + }, + { + "id": 24, + "name": "oauth-signup", + "children": [] + } + ] + } + ] + }, + { + "id": 25, + "name": "content-flow", + "children": [ + { + "id": 26, + "name": "home-screen", + "children": [ + { + "id": 27, + "name": "featured-content", + "children": [] + }, + { + "id": 28, + "name": "user-feed", + "children": [] + } + ] + } + ] + }, + { + "id": 29, + "name": "system-tasks", + "children": [ + { + "id": 30, + "name": "data-service", + "children": [ + { + "id": 31, + "name": "content-sync", + "children": [] + }, + { + "id": 32, + "name": "cache-manager", + "children": [] + } + ] + }, + { + "id": 33, + "name": "analytics-service", + "children": [ + { + "id": 34, + "name": "event-tracker", + "children": [] + } + ] + } + ] + }, + { + "id": 35, + "name": "preferences-flow", + "children": [ + { + "id": 36, + "name": "preferences-screen", + "children": [ + { + "id": 37, + "name": "account-settings", + "children": [] + }, + { + "id": 38, + "name": "privacy-settings", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 39, + "name": "system-root", + "children": [ + { + "id": 40, + "name": "security-flow", + "children": [ + { + "id": 41, + "name": "auth-screen", + "children": [ + { + "id": 42, + "name": "biometric-auth", + "children": [] + }, + { + "id": 43, + "name": "pin-verification", + "children": [] + } + ] + } + ] + }, + { + "id": 44, + "name": "navigation-flow", + "children": [ + { + "id": 45, + "name": "tab-navigator", + "children": [ + { + "id": 46, + "name": "home-tab", + "children": [] + }, + { + "id": 47, + "name": "search-tab", + "children": [] + } + ] + } + ] + }, + { + "id": 48, + "name": "core-services", + "children": [ + { + "id": 49, + "name": "network-service", + "children": [ + { + "id": 50, + "name": "api-client", + "children": [] + }, + { + "id": 51, + "name": "offline-handler", + "children": [] + } + ] + }, + { + "id": 52, + "name": "storage-service", + "children": [ + { + "id": 53, + "name": "database-manager", + "children": [] + } + ] + } + ] + }, + { + "id": 54, + "name": "admin-flow", + "children": [ + { + "id": 55, + "name": "admin-panel", + "children": [ + { + "id": 56, + "name": "user-management", + "children": [] + }, + { + "id": 57, + "name": "system-monitoring", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 58, + "name": "commerce-root", + "children": [ + { + "id": 59, + "name": "payment-flow", + "children": [ + { + "id": 60, + "name": "checkout-screen", + "children": [ + { + "id": 61, + "name": "payment-form", + "children": [] + }, + { + "id": 62, + "name": "card-scanner", + "children": [] + } + ] + } + ] + }, + { + "id": 63, + "name": "catalog-flow", + "children": [ + { + "id": 64, + "name": "product-list", + "children": [ + { + "id": 65, + "name": "product-card", + "children": [] + }, + { + "id": 66, + "name": "filter-panel", + "children": [] + } + ] + } + ] + }, + { + "id": 67, + "name": "order-tasks", + "children": [ + { + "id": 68, + "name": "fulfillment-service", + "children": [ + { + "id": 69, + "name": "inventory-check", + "children": [] + }, + { + "id": 70, + "name": "shipping-calc", + "children": [] + } + ] + }, + { + "id": 71, + "name": "payment-service", + "children": [ + { + "id": 72, + "name": "transaction-processor", + "children": [] + } + ] + } + ] + }, + { + "id": 73, + "name": "merchant-flow", + "children": [ + { + "id": 74, + "name": "seller-dashboard", + "children": [ + { + "id": 75, + "name": "sales-analytics", + "children": [] + }, + { + "id": 76, + "name": "inventory-manager", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 77, + "name": "social-root", + "children": [ + { + "id": 78, + "name": "messaging-flow", + "children": [ + { + "id": 79, + "name": "chat-screen", + "children": [ + { + "id": 80, + "name": "message-input", + "children": [] + }, + { + "id": 81, + "name": "media-picker", + "children": [] + } + ] + } + ] + }, + { + "id": 82, + "name": "social-flow", + "children": [ + { + "id": 83, + "name": "timeline-screen", + "children": [ + { + "id": 84, + "name": "post-composer", + "children": [] + }, + { + "id": 85, + "name": "story-viewer", + "children": [] + } + ] + } + ] + }, + { + "id": 86, + "name": "connection-tasks", + "children": [ + { + "id": 87, + "name": "friend-service", + "children": [ + { + "id": 88, + "name": "contact-sync", + "children": [] + }, + { + "id": 89, + "name": "suggestion-engine", + "children": [] + } + ] + }, + { + "id": 90, + "name": "activity-service", + "children": [ + { + "id": 91, + "name": "feed-generator", + "children": [] + } + ] + } + ] + }, + { + "id": 92, + "name": "privacy-flow", + "children": [ + { + "id": 93, + "name": "privacy-screen", + "children": [ + { + "id": 94, + "name": "visibility-controls", + "children": [] + }, + { + "id": 95, + "name": "block-manager", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 96, + "name": "media-root", + "children": [ + { + "id": 97, + "name": "streaming-flow", + "children": [ + { + "id": 98, + "name": "player-screen", + "children": [ + { + "id": 99, + "name": "video-player", + "children": [] + }, + { + "id": 100, + "name": "audio-controls", + "children": [] + } + ] + } + ] + }, + { + "id": 101, + "name": "library-flow", + "children": [ + { + "id": 102, + "name": "media-browser", + "children": [ + { + "id": 103, + "name": "playlist-view", + "children": [] + }, + { + "id": 104, + "name": "search-results", + "children": [] + } + ] + } + ] + }, + { + "id": 105, + "name": "processing-tasks", + "children": [ + { + "id": 106, + "name": "encoding-service", + "children": [ + { + "id": 107, + "name": "video-encoder", + "children": [] + }, + { + "id": 108, + "name": "thumbnail-generator", + "children": [] + } + ] + }, + { + "id": 109, + "name": "cdn-service", + "children": [ + { + "id": 110, + "name": "content-distributor", + "children": [] + } + ] + } + ] + }, + { + "id": 111, + "name": "creator-flow", + "children": [ + { + "id": 112, + "name": "upload-screen", + "children": [ + { + "id": 113, + "name": "file-uploader", + "children": [] + }, + { + "id": 114, + "name": "metadata-editor", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 115, + "name": "finance-root", + "children": [ + { + "id": 116, + "name": "banking-flow", + "children": [ + { + "id": 117, + "name": "account-screen", + "children": [ + { + "id": 118, + "name": "balance-widget", + "children": [] + }, + { + "id": 119, + "name": "transaction-list", + "children": [] + } + ] + } + ] + }, + { + "id": 120, + "name": "transfer-flow", + "children": [ + { + "id": 121, + "name": "send-money-screen", + "children": [ + { + "id": 122, + "name": "recipient-selector", + "children": [] + }, + { + "id": 123, + "name": "amount-input", + "children": [] + } + ] + } + ] + }, + { + "id": 124, + "name": "financial-tasks", + "children": [ + { + "id": 125, + "name": "fraud-service", + "children": [ + { + "id": 126, + "name": "risk-analyzer", + "children": [] + }, + { + "id": 127, + "name": "pattern-detector", + "children": [] + } + ] + }, + { + "id": 128, + "name": "compliance-service", + "children": [ + { + "id": 129, + "name": "kyc-validator", + "children": [] + } + ] + } + ] + }, + { + "id": 130, + "name": "investment-flow", + "children": [ + { + "id": 131, + "name": "portfolio-screen", + "children": [ + { + "id": 132, + "name": "asset-overview", + "children": [] + }, + { + "id": 133, + "name": "performance-chart", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 134, + "name": "health-root", + "children": [ + { + "id": 135, + "name": "tracking-flow", + "children": [ + { + "id": 136, + "name": "activity-screen", + "children": [ + { + "id": 137, + "name": "step-counter", + "children": [] + }, + { + "id": 138, + "name": "workout-tracker", + "children": [] + } + ] + } + ] + }, + { + "id": 139, + "name": "wellness-flow", + "children": [ + { + "id": 140, + "name": "health-dashboard", + "children": [ + { + "id": 141, + "name": "vital-signs", + "children": [] + }, + { + "id": 142, + "name": "medication-reminder", + "children": [] + } + ] + } + ] + }, + { + "id": 143, + "name": "monitoring-tasks", + "children": [ + { + "id": 144, + "name": "sensor-service", + "children": [ + { + "id": 145, + "name": "heart-rate-monitor", + "children": [] + }, + { + "id": 146, + "name": "sleep-tracker", + "children": [] + } + ] + }, + { + "id": 147, + "name": "analysis-service", + "children": [ + { + "id": 148, + "name": "trend-analyzer", + "children": [] + } + ] + } + ] + }, + { + "id": 149, + "name": "consultation-flow", + "children": [ + { + "id": 150, + "name": "telemedicine-screen", + "children": [ + { + "id": 151, + "name": "video-call", + "children": [] + }, + { + "id": 152, + "name": "symptom-checker", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 153, + "name": "education-root", + "children": [ + { + "id": 154, + "name": "learning-flow", + "children": [ + { + "id": 155, + "name": "course-screen", + "children": [ + { + "id": 156, + "name": "video-lesson", + "children": [] + }, + { + "id": 157, + "name": "quiz-component", + "children": [] + } + ] + } + ] + }, + { + "id": 158, + "name": "progress-flow", + "children": [ + { + "id": 159, + "name": "achievement-screen", + "children": [ + { + "id": 160, + "name": "badge-display", + "children": [] + }, + { + "id": 161, + "name": "progress-tracker", + "children": [] + } + ] + } + ] + }, + { + "id": 162, + "name": "assessment-tasks", + "children": [ + { + "id": 163, + "name": "grading-service", + "children": [ + { + "id": 164, + "name": "auto-grader", + "children": [] + }, + { + "id": 165, + "name": "feedback-generator", + "children": [] + } + ] + }, + { + "id": 166, + "name": "analytics-service", + "children": [ + { + "id": 167, + "name": "learning-analyzer", + "children": [] + } + ] + } + ] + }, + { + "id": 168, + "name": "collaboration-flow", + "children": [ + { + "id": 169, + "name": "study-group-screen", + "children": [ + { + "id": 170, + "name": "whiteboard-tool", + "children": [] + }, + { + "id": 171, + "name": "discussion-forum", + "children": [] + } + ] + } + ] + } + ] + }, + { + "id": 172, + "name": "travel-root", + "children": [ + { + "id": 173, + "name": "booking-flow", + "children": [ + { + "id": 174, + "name": "search-screen", + "children": [ + { + "id": 175, + "name": "flight-finder", + "children": [] + }, + { + "id": 176, + "name": "hotel-search", + "children": [] + } + ] + } + ] + }, + { + "id": 177, + "name": "itinerary-flow", + "children": [ + { + "id": 178, + "name": "trip-planner", + "children": [ + { + "id": 179, + "name": "schedule-builder", + "children": [] + }, + { + "id": 180, + "name": "map-integration", + "children": [] + } + ] + } + ] + }, + { + "id": 181, + "name": "travel-tasks", + "children": [ + { + "id": 182, + "name": "booking-service", + "children": [ + { + "id": 183, + "name": "reservation-manager", + "children": [] + }, + { + "id": 184, + "name": "price-tracker", + "children": [] + } + ] + }, + { + "id": 185, + "name": "notification-service", + "children": [ + { + "id": 186, + "name": "flight-alerts", + "children": [] + } + ] + } + ] + }, + { + "id": 187, + "name": "companion-flow", + "children": [ + { + "id": 188, + "name": "travel-guide", + "children": [ + { + "id": 189, + "name": "local-recommendations", + "children": [] + }, + { + "id": 190, + "name": "weather-widget", + "children": [] + } + ] + } + ] + } + ] + } +]