You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@beam.apache.org by da...@apache.org on 2022/10/06 20:51:44 UTC

[beam] branch master updated: [Tour of Beam][Frontend] Content Tree and SDK models (#23316) (#23417)

This is an automated email from the ASF dual-hosted git repository.

damccorm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 41ddc4a4bb2 [Tour of Beam][Frontend] Content Tree and SDK models (#23316) (#23417)
41ddc4a4bb2 is described below

commit 41ddc4a4bb2bdeb14cd4065df4b781b061f5fd18
Author: Darkhan Nausharipov <31...@users.noreply.github.com>
AuthorDate: Fri Oct 7 02:51:36 2022 +0600

    [Tour of Beam][Frontend] Content Tree and SDK models (#23316) (#23417)
    
    * Content Tree and SDK models (#23316)
    
    models from JSON (#23316)
    
    removed generated files (#23316)
    
    const model constructors (#23316)
    
    fvm config (#23316)
    
    ignore *g.dart files (#23316)
    
    server node, group, unit models
    
    abstract node model (#23316)
    
    node type in enums (#23316)
    
    gitignore sort (#23316)
    
    sdk model (#23316)
    
    api calls (#23316)
    
    getSdks in SdkDropdown (#23316)
    
    requests in functions (#23316)
    
    k in functions (#23316)
    
    server folder in models (#23316)
    
    review comments (#23316)
    
    Move response model files (#23316)
    
    Add CloudFunctionsTobClient (#23316)
    
    Move a file (#23316)
    
    Rename a file (#23316)
    
    ContentTreeCache (#23316)
    
    ContentTreeCache (#23316)
    
    * Update README with building manual (#23316)
    
    * Move hardcoded URLs to a config file (#23316)
    
    Co-authored-by: darkhan.nausharipov <da...@kzn.akvelon.com>
    Co-authored-by: Alexey Inkin <le...@inkin.ru>
---
 .gitignore                                         |   5 +-
 learning/tour-of-beam/frontend/README.md           |  22 ++++-
 .../lib/{locator.dart => cache/content_tree.dart}  |  36 +++++++-
 .../lib/{locator.dart => cache/sdk_cache.dart}     |  37 +++++++-
 .../builders/content_tree.dart}                    |  29 +++++-
 .../builders/sdks_builder.dart}                    |  25 ++++-
 .../frontend/lib/components/sdk_dropdown.dart      |  48 ++++++----
 learning/tour-of-beam/frontend/lib/config.dart     |  39 ++++++++
 learning/tour-of-beam/frontend/lib/locator.dart    |  12 ++-
 .../frontend/lib/models/abstract_node.dart         |  50 ++++++++++
 .../lib/{locator.dart => models/content_tree.dart} |  21 ++++-
 .../lib/{locator.dart => models/group.dart}        |  17 +++-
 .../lib/{locator.dart => models/module.dart}       |  27 +++++-
 .../frontend/lib/{locator.dart => models/sdk.dart} |  16 +++-
 .../lib/{locator.dart => models/unit.dart}         |  12 ++-
 .../frontend/lib/pages/tour/playground_demo.dart   |  13 +--
 .../frontend/lib/pages/tour/screen.dart            | 101 +++++++++++++--------
 .../frontend/lib/pages/welcome/screen.dart         |  76 ++++++++++------
 .../frontend/lib/repositories/client/client.dart   |  11 ++-
 .../client/cloud_functions_client.dart             |  51 +++++++++++
 .../models/get_sdks_response.dart}                 |  17 +++-
 .../models/group.dart}                             |  21 ++++-
 .../models/node.dart}                              |  25 ++++-
 .../lib/repositories/models/node_type_enum.dart    |   7 +-
 .../models/unit.dart}                              |  19 +++-
 learning/tour-of-beam/frontend/pubspec.lock        |  31 ++++++-
 learning/tour-of-beam/frontend/pubspec.yaml        |   8 +-
 .../lib/src/enums/complexity.dart                  |   5 +
 28 files changed, 605 insertions(+), 176 deletions(-)

diff --git a/.gitignore b/.gitignore
index fede9851091..62f5a20d45e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -120,12 +120,13 @@ website/www/yarn-error.log
 **/node_modules
 
 # Dart/Flutter
-**/.dart_tool
-**/.packages
 **/.flutter-plugins
 **/.flutter-plugins-dependencies
+**/.dart_tool
 **/generated_plugin_registrant.dart
+**/*.g.dart
 **/*.mocks.dart
+**/.packages
 
 # Ignore Beam Playground Terraform
 **/.terraform
diff --git a/learning/tour-of-beam/frontend/README.md b/learning/tour-of-beam/frontend/README.md
index afd892b3f8e..2e27a05ad16 100644
--- a/learning/tour-of-beam/frontend/README.md
+++ b/learning/tour-of-beam/frontend/README.md
@@ -22,9 +22,25 @@
 
  # About
 
- # Getting started
- Flutter installation guide: https://docs.flutter.dev/get-started/install
- Run the app: `flutter run --web-renderer html`
+## Getting started
+Running, debugging, and testing all require this first step that fetches
+dependencies and generates code:
+
+```bash
+cd ../../../playground/frontend/playground_components
+flutter pub get
+flutter pub run build_runner build
+cd ../../../learning/tour-of-beam/frontend
+flutter pub get
+flutter pub run build_runner build
+```
+
+### Run
+
+The following command is used to build and serve the frontend app locally:
+
+`$ flutter run -d chrome`
+
 
  # Deployment
 
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/cache/content_tree.dart
similarity index 53%
copy from learning/tour-of-beam/frontend/lib/locator.dart
copy to learning/tour-of-beam/frontend/lib/cache/content_tree.dart
index 8ab88a830d0..b8a27c4eb7e 100644
--- a/learning/tour-of-beam/frontend/lib/locator.dart
+++ b/learning/tour-of-beam/frontend/lib/cache/content_tree.dart
@@ -16,9 +16,37 @@
  * limitations under the License.
  */
 
-//import 'package:get_it/get_it.dart';
+import 'dart:async';
 
-Future<void> initializeServiceLocator() async {
-  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
-  // as an example.
+import 'package:flutter/widgets.dart';
+
+import '../models/content_tree.dart';
+import '../repositories/client/client.dart';
+
+class ContentTreeCache extends ChangeNotifier {
+  final TobClient client;
+
+  ContentTreeModel? _contentTree;
+  Future<ContentTreeModel>? _future;
+
+  ContentTreeCache({
+    required this.client,
+  });
+
+  ContentTreeModel? getContentTree() {
+    if (_future == null) {
+      unawaited(_loadContentTree());
+    }
+
+    return _contentTree;
+  }
+
+  Future<ContentTreeModel?> _loadContentTree() async {
+    _future = client.getContentTree();
+    final result = await _future!;
+    _contentTree = result;
+
+    notifyListeners();
+    return _contentTree;
+  }
 }
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/cache/sdk_cache.dart
similarity index 54%
copy from learning/tour-of-beam/frontend/lib/locator.dart
copy to learning/tour-of-beam/frontend/lib/cache/sdk_cache.dart
index 8ab88a830d0..22045556805 100644
--- a/learning/tour-of-beam/frontend/lib/locator.dart
+++ b/learning/tour-of-beam/frontend/lib/cache/sdk_cache.dart
@@ -16,9 +16,38 @@
  * limitations under the License.
  */
 
-//import 'package:get_it/get_it.dart';
+import 'dart:async';
 
-Future<void> initializeServiceLocator() async {
-  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
-  // as an example.
+import 'package:flutter/widgets.dart';
+
+import '../models/sdk.dart';
+import '../repositories/client/client.dart';
+import '../repositories/models/get_sdks_response.dart';
+
+class SdkCache extends ChangeNotifier {
+  final TobClient client;
+
+  final _sdks = <SdkModel>[];
+  Future<GetSdksResponse>? _future;
+
+  SdkCache({
+    required this.client,
+  });
+
+  List<SdkModel> getSdks() {
+    if (_future == null) {
+      unawaited(_loadSdks());
+    }
+
+    return _sdks;
+  }
+
+  Future<List<SdkModel>> _loadSdks() async {
+    _future = client.getSdks();
+    final result = await _future!;
+
+    _sdks.addAll(result.sdks);
+    notifyListeners();
+    return _sdks;
+  }
 }
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/components/builders/content_tree.dart
similarity index 56%
copy from learning/tour-of-beam/frontend/lib/locator.dart
copy to learning/tour-of-beam/frontend/lib/components/builders/content_tree.dart
index 8ab88a830d0..4aec6ba6ef0 100644
--- a/learning/tour-of-beam/frontend/lib/locator.dart
+++ b/learning/tour-of-beam/frontend/lib/components/builders/content_tree.dart
@@ -16,9 +16,30 @@
  * limitations under the License.
  */
 
-//import 'package:get_it/get_it.dart';
+import 'package:flutter/widgets.dart';
+import 'package:get_it/get_it.dart';
 
-Future<void> initializeServiceLocator() async {
-  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
-  // as an example.
+import '../../cache/content_tree.dart';
+import '../../models/content_tree.dart';
+
+class ContentTreeBuilder extends StatelessWidget {
+  final ValueWidgetBuilder<ContentTreeModel?> builder;
+
+  const ContentTreeBuilder({
+    required this.builder,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final cache = GetIt.instance.get<ContentTreeCache>();
+
+    return AnimatedBuilder(
+      animation: cache,
+      builder: (context, child) => builder(
+        context,
+        cache.getContentTree(),
+        child,
+      ),
+    );
+  }
 }
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/components/builders/sdks_builder.dart
similarity index 59%
copy from learning/tour-of-beam/frontend/lib/locator.dart
copy to learning/tour-of-beam/frontend/lib/components/builders/sdks_builder.dart
index 8ab88a830d0..2a21185c084 100644
--- a/learning/tour-of-beam/frontend/lib/locator.dart
+++ b/learning/tour-of-beam/frontend/lib/components/builders/sdks_builder.dart
@@ -16,9 +16,26 @@
  * limitations under the License.
  */
 
-//import 'package:get_it/get_it.dart';
+import 'package:flutter/widgets.dart';
+import 'package:get_it/get_it.dart';
 
-Future<void> initializeServiceLocator() async {
-  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
-  // as an example.
+import '../../cache/sdk_cache.dart';
+import '../../models/sdk.dart';
+
+class SdksBuilder extends StatelessWidget {
+  final ValueWidgetBuilder<List<SdkModel>> builder;
+
+  const SdksBuilder({
+    required this.builder,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final cache = GetIt.instance.get<SdkCache>();
+
+    return AnimatedBuilder(
+      animation: cache,
+      builder: (context, child) => builder(context, cache.getSdks(), child),
+    );
+  }
 }
diff --git a/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart b/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart
index 47f1a728b8e..7c4fc9b3006 100644
--- a/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart
+++ b/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart
@@ -19,30 +19,40 @@
 import 'package:flutter/material.dart';
 import 'package:playground_components/playground_components.dart';
 
+import 'builders/sdks_builder.dart';
+
 class SdkDropdown extends StatelessWidget {
   const SdkDropdown();
 
   @override
   Widget build(BuildContext context) {
-    return _DropdownWrapper(
-      child: DropdownButton(
-        value: 'Java',
-        onChanged: (sdk) {
-          // TODO(nausharipov): change SDK
-        },
-        items: const ['Java', 'Python', 'Go']
-            .map(
-              (sdk) => DropdownMenuItem(
-                value: sdk,
-                child: Text(sdk),
-              ),
-            )
-            .toList(growable: false),
-        isDense: true,
-        alignment: Alignment.center,
-        focusColor: BeamColors.transparent,
-        borderRadius: BorderRadius.circular(BeamSizes.size6),
-      ),
+    return SdksBuilder(
+      builder: (context, sdks, _) {
+        if (sdks.isEmpty) {
+          return Container();
+        }
+
+        return _DropdownWrapper(
+          child: DropdownButton(
+            value: sdks.first.id,
+            onChanged: (sdk) {
+              // TODO(nausharipov): change SDK
+            },
+            items: sdks
+                .map(
+                  (sdk) => DropdownMenuItem(
+                    value: sdk.id,
+                    child: Text(sdk.title),
+                  ),
+                )
+                .toList(growable: false),
+            isDense: true,
+            alignment: Alignment.center,
+            focusColor: BeamColors.transparent,
+            borderRadius: BorderRadius.circular(BeamSizes.size6),
+          ),
+        );
+      },
     );
   }
 }
diff --git a/learning/tour-of-beam/frontend/lib/config.dart b/learning/tour-of-beam/frontend/lib/config.dart
new file mode 100644
index 00000000000..b7ed542e1b0
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/config.dart
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// TODO(alexeyinkin): Generate this file on deployment.
+
+const _cloudFunctionsProjectRegion = 'us-central1';
+const _cloudFunctionsProjectId = 'tour-of-beam-2';
+const cloudFunctionsBaseUrl = 'https://'
+    '$_cloudFunctionsProjectRegion-$_cloudFunctionsProjectId'
+    '.cloudfunctions.net';
+
+// Copied from Playground's config.g.dart
+
+const String kAnalyticsUA = 'UA-73650088-2';
+const String kApiClientURL =
+    'https://backend-router-beta-dot-apache-beam-testing.appspot.com';
+const String kApiJavaClientURL =
+    'https://backend-java-beta-dot-apache-beam-testing.appspot.com';
+const String kApiGoClientURL =
+    'https://backend-go-beta-dot-apache-beam-testing.appspot.com';
+const String kApiPythonClientURL =
+    'https://backend-python-beta-dot-apache-beam-testing.appspot.com';
+const String kApiScioClientURL =
+    'https://backend-scio-beta-dot-apache-beam-testing.appspot.com';
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/locator.dart
index 8ab88a830d0..4ac8c4c4d35 100644
--- a/learning/tour-of-beam/frontend/lib/locator.dart
+++ b/learning/tour-of-beam/frontend/lib/locator.dart
@@ -16,9 +16,15 @@
  * limitations under the License.
  */
 
-//import 'package:get_it/get_it.dart';
+import 'package:get_it/get_it.dart';
+
+import 'cache/content_tree.dart';
+import 'cache/sdk_cache.dart';
+import 'repositories/client/cloud_functions_client.dart';
 
 Future<void> initializeServiceLocator() async {
-  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
-  // as an example.
+  final client = CloudFunctionsTobClient();
+
+  GetIt.instance.registerSingleton(ContentTreeCache(client: client));
+  GetIt.instance.registerSingleton(SdkCache(client: client));
 }
diff --git a/learning/tour-of-beam/frontend/lib/models/abstract_node.dart b/learning/tour-of-beam/frontend/lib/models/abstract_node.dart
new file mode 100644
index 00000000000..00769490e28
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/models/abstract_node.dart
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../repositories/models/node.dart';
+import '../repositories/models/node_type_enum.dart';
+import 'group.dart';
+import 'unit.dart';
+
+abstract class NodeModel {
+  final String title;
+  const NodeModel({required this.title});
+
+  /// Constructs nodes from the response data.
+  ///
+  /// Models from the response are inconvenient for a direct use in the app
+  /// because they come from a golang backend which does not
+  /// support inheritance, and so they use an extra layer of composition
+  /// which is inconvenient in Flutter.
+  static List<NodeModel> fromMaps(List json) {
+    return json
+        .cast<Map<String, dynamic>>()
+        .map<NodeResponseModel>(NodeResponseModel.fromJson)
+        .map(fromResponse)
+        .toList();
+  }
+
+  static NodeModel fromResponse(NodeResponseModel node) {
+    switch (node.type) {
+      case NodeType.group:
+        return GroupModel.fromResponse(node.group!);
+      case NodeType.unit:
+        return UnitModel.fromResponse(node.unit!);
+    }
+  }
+}
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/models/content_tree.dart
similarity index 65%
copy from learning/tour-of-beam/frontend/lib/locator.dart
copy to learning/tour-of-beam/frontend/lib/models/content_tree.dart
index 8ab88a830d0..7e3aa74f8c2 100644
--- a/learning/tour-of-beam/frontend/lib/locator.dart
+++ b/learning/tour-of-beam/frontend/lib/models/content_tree.dart
@@ -16,9 +16,22 @@
  * limitations under the License.
  */
 
-//import 'package:get_it/get_it.dart';
+import 'package:json_annotation/json_annotation.dart';
 
-Future<void> initializeServiceLocator() async {
-  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
-  // as an example.
+import 'module.dart';
+
+part 'content_tree.g.dart';
+
+@JsonSerializable(createToJson: false)
+class ContentTreeModel {
+  final String sdkId;
+  final List<ModuleModel> modules;
+
+  const ContentTreeModel({
+    required this.sdkId,
+    required this.modules,
+  });
+
+  factory ContentTreeModel.fromJson(Map<String, dynamic> json) =>
+      _$ContentTreeModelFromJson(json);
 }
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/models/group.dart
similarity index 67%
copy from learning/tour-of-beam/frontend/lib/locator.dart
copy to learning/tour-of-beam/frontend/lib/models/group.dart
index 8ab88a830d0..64a3d79c200 100644
--- a/learning/tour-of-beam/frontend/lib/locator.dart
+++ b/learning/tour-of-beam/frontend/lib/models/group.dart
@@ -16,9 +16,18 @@
  * limitations under the License.
  */
 
-//import 'package:get_it/get_it.dart';
+import '../repositories/models/group.dart';
+import 'abstract_node.dart';
 
-Future<void> initializeServiceLocator() async {
-  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
-  // as an example.
+class GroupModel extends NodeModel {
+  final List<NodeModel> nodes;
+
+  const GroupModel({
+    required super.title,
+    required this.nodes,
+  });
+
+  GroupModel.fromResponse(GroupResponseModel group)
+      : nodes = group.nodes.map(NodeModel.fromResponse).toList(growable: false),
+        super(title: group.title);
 }
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/models/module.dart
similarity index 57%
copy from learning/tour-of-beam/frontend/lib/locator.dart
copy to learning/tour-of-beam/frontend/lib/models/module.dart
index 8ab88a830d0..fc057fd9397 100644
--- a/learning/tour-of-beam/frontend/lib/locator.dart
+++ b/learning/tour-of-beam/frontend/lib/models/module.dart
@@ -16,9 +16,28 @@
  * limitations under the License.
  */
 
-//import 'package:get_it/get_it.dart';
+import 'package:json_annotation/json_annotation.dart';
+import 'package:playground_components/playground_components.dart';
 
-Future<void> initializeServiceLocator() async {
-  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
-  // as an example.
+import 'abstract_node.dart';
+
+part 'module.g.dart';
+
+@JsonSerializable(createToJson: false)
+class ModuleModel {
+  final String id;
+  final String title;
+  final Complexity complexity;
+  @JsonKey(fromJson: NodeModel.fromMaps)
+  final List<NodeModel> nodes;
+
+  const ModuleModel({
+    required this.id,
+    required this.title,
+    required this.complexity,
+    required this.nodes,
+  });
+
+  factory ModuleModel.fromJson(Map<String, dynamic> json) =>
+      _$ModuleModelFromJson(json);
 }
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/models/sdk.dart
similarity index 71%
copy from learning/tour-of-beam/frontend/lib/locator.dart
copy to learning/tour-of-beam/frontend/lib/models/sdk.dart
index 8ab88a830d0..64ef531a501 100644
--- a/learning/tour-of-beam/frontend/lib/locator.dart
+++ b/learning/tour-of-beam/frontend/lib/models/sdk.dart
@@ -16,9 +16,17 @@
  * limitations under the License.
  */
 
-//import 'package:get_it/get_it.dart';
+import 'package:json_annotation/json_annotation.dart';
 
-Future<void> initializeServiceLocator() async {
-  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
-  // as an example.
+part 'sdk.g.dart';
+
+@JsonSerializable(createToJson: false)
+class SdkModel {
+  final String id;
+  final String title;
+
+  const SdkModel({required this.id, required this.title});
+
+  factory SdkModel.fromJson(Map<String, dynamic> json) =>
+      _$SdkModelFromJson(json);
 }
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/models/unit.dart
similarity index 77%
copy from learning/tour-of-beam/frontend/lib/locator.dart
copy to learning/tour-of-beam/frontend/lib/models/unit.dart
index 8ab88a830d0..9d37cbab058 100644
--- a/learning/tour-of-beam/frontend/lib/locator.dart
+++ b/learning/tour-of-beam/frontend/lib/models/unit.dart
@@ -16,9 +16,13 @@
  * limitations under the License.
  */
 
-//import 'package:get_it/get_it.dart';
+import '../repositories/models/unit.dart';
+import 'abstract_node.dart';
 
-Future<void> initializeServiceLocator() async {
-  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
-  // as an example.
+class UnitModel extends NodeModel {
+  final String id;
+
+  UnitModel.fromResponse(UnitResponseModel unit)
+      : id = unit.id,
+        super(title: unit.title);
 }
diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart b/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart
index 384d46b0fc2..6f44223f189 100644
--- a/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart
+++ b/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart
@@ -20,18 +20,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter/widgets.dart';
 import 'package:playground_components/playground_components.dart';
 
-// This is for demo only. Need a thought-through import in production.
-
-const String kApiClientURL =
-    'https://backend-router-beta-dot-apache-beam-testing.appspot.com';
-const String kApiJavaClientURL =
-    'https://backend-java-beta-dot-apache-beam-testing.appspot.com';
-const String kApiGoClientURL =
-    'https://backend-go-beta-dot-apache-beam-testing.appspot.com';
-const String kApiPythonClientURL =
-    'https://backend-python-beta-dot-apache-beam-testing.appspot.com';
-const String kApiScioClientURL =
-    'https://backend-scio-beta-dot-apache-beam-testing.appspot.com';
+import '../../config.dart';
 
 class PlaygroundDemoWidget extends StatefulWidget {
   const PlaygroundDemoWidget({super.key});
diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart
index 7d708ff2508..3a20b5a6786 100644
--- a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart
+++ b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart
@@ -21,11 +21,16 @@ import 'package:flutter/material.dart';
 import 'package:flutter_svg/svg.dart';
 import 'package:playground_components/playground_components.dart';
 
+import '../../components/builders/content_tree.dart';
 import '../../components/expansion_tile_wrapper.dart';
 import '../../components/filler_text.dart';
 import '../../components/scaffold.dart';
 import '../../constants/sizes.dart';
 import '../../generated/assets.gen.dart';
+import '../../models/abstract_node.dart';
+import '../../models/group.dart';
+import '../../models/module.dart';
+import '../../models/unit.dart';
 import 'playground_demo.dart';
 
 class TourScreen extends StatelessWidget {
@@ -99,33 +104,40 @@ class _ContentTree extends StatelessWidget {
     return Container(
       width: 250,
       padding: const EdgeInsets.symmetric(horizontal: BeamSizes.size12),
-      child: SingleChildScrollView(
-        child: Column(
-          children: [
-            const _ContentTreeTitle(),
-            ...[
-              'Core Transforms',
-              'Common Transforms',
-            ].map((e) => _Module(module: e)).toList(growable: false),
-            const SizedBox(height: BeamSizes.size12),
-          ],
-        ),
+      child: ContentTreeBuilder(
+        builder: (context, contentTree, child) {
+          if (contentTree == null) {
+            return Container();
+          }
+
+          return SingleChildScrollView(
+            child: Column(
+              children: [
+                const _ContentTreeTitle(),
+                ...contentTree.modules
+                    .map((module) => _Module(module: module))
+                    .toList(growable: false),
+                const SizedBox(height: BeamSizes.size12),
+              ],
+            ),
+          );
+        },
       ),
     );
   }
 }
 
 class _Module extends StatelessWidget {
-  final String module;
+  final ModuleModel module;
   const _Module({required this.module});
 
   @override
   Widget build(BuildContext context) {
     return Column(
       children: [
-        _ModuleTitle(title: module),
-        ...['Map', 'Combine']
-            .map((group) => _Group(group: group))
+        _ModuleTitle(module: module),
+        ...module.nodes
+            .map((node) => _Node(node: node))
             .toList(growable: false),
         const BeamDivider(
           margin: EdgeInsets.symmetric(vertical: BeamSizes.size10),
@@ -156,8 +168,8 @@ class _ContentTreeTitle extends StatelessWidget {
 }
 
 class _ModuleTitle extends StatelessWidget {
-  final String title;
-  const _ModuleTitle({required this.title});
+  final ModuleModel module;
+  const _ModuleTitle({required this.module});
 
   @override
   Widget build(BuildContext context) {
@@ -167,12 +179,12 @@ class _ModuleTitle extends StatelessWidget {
         mainAxisAlignment: MainAxisAlignment.spaceBetween,
         children: [
           Text(
-            title,
+            module.title,
             style: Theme.of(context).textTheme.headlineMedium,
           ),
-          const Padding(
-            padding: EdgeInsets.only(right: BeamSizes.size4),
-            child: ComplexityWidget(complexity: Complexity.basic),
+          Padding(
+            padding: const EdgeInsets.only(right: BeamSizes.size4),
+            child: ComplexityWidget(complexity: module.complexity),
           ),
         ],
       ),
@@ -180,8 +192,23 @@ class _ModuleTitle extends StatelessWidget {
   }
 }
 
+class _Node extends StatelessWidget {
+  final NodeModel node;
+  const _Node({required this.node});
+
+  @override
+  Widget build(BuildContext context) {
+    if (node is GroupModel) {
+      return _Group(group: node as GroupModel);
+    } else if (node is UnitModel) {
+      return _Unit(unit: node as UnitModel);
+    }
+    throw Exception('A node with an unknown type');
+  }
+}
+
 class _Group extends StatelessWidget {
-  final String group;
+  final GroupModel group;
   const _Group({required this.group});
 
   @override
@@ -189,44 +216,40 @@ class _Group extends StatelessWidget {
     return ExpansionTileWrapper(
       ExpansionTile(
         tilePadding: EdgeInsets.zero,
-        title: _GroupTitle(title: group),
+        title: _GroupTitle(title: group.title),
         childrenPadding: const EdgeInsets.only(
           left: BeamSizes.size24,
-          top: BeamSizes.size10,
         ),
-        children: const [_Units()],
+        children: [_GroupNodes(nodes: group.nodes)],
       ),
     );
   }
 }
 
-class _Units extends StatelessWidget {
-  const _Units();
+class _GroupNodes extends StatelessWidget {
+  final List<NodeModel> nodes;
+  const _GroupNodes({required this.nodes});
 
   @override
   Widget build(BuildContext context) {
     return Column(
-      children: ['ParDo one-to-one', 'ParDo one-to-many']
-          .map((e) => _Unit(title: e))
-          .toList(growable: false),
+      children: nodes.map((node) => _Node(node: node)).toList(growable: false),
     );
   }
 }
 
 class _Unit extends StatelessWidget {
-  final String title;
-  const _Unit({required this.title});
+  final UnitModel unit;
+  const _Unit({required this.unit});
 
   @override
   Widget build(BuildContext context) {
     return Padding(
-      padding: const EdgeInsets.only(bottom: BeamSizes.size18),
+      padding: const EdgeInsets.symmetric(vertical: BeamSizes.size10),
       child: Row(
         children: [
-          _ProgressIndicator(
-            assetPath: Assets.svg.unitProgress100,
-          ),
-          Text(title),
+          _ProgressIndicator(assetPath: Assets.svg.unitProgress0),
+          Expanded(child: Text(unit.title)),
         ],
       ),
     );
@@ -241,9 +264,7 @@ class _GroupTitle extends StatelessWidget {
   Widget build(BuildContext context) {
     return Row(
       children: [
-        _ProgressIndicator(
-          assetPath: Assets.svg.unitProgress100,
-        ),
+        _ProgressIndicator(assetPath: Assets.svg.unitProgress0),
         Text(
           title,
           style: Theme.of(context).textTheme.headlineMedium,
diff --git a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart
index ae799b7e77c..25e2fbb65cb 100644
--- a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart
+++ b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart
@@ -22,10 +22,14 @@ import 'package:flutter/material.dart';
 import 'package:flutter_svg/svg.dart';
 import 'package:playground_components/playground_components.dart';
 
+import '../../components/builders/content_tree.dart';
+import '../../components/builders/sdks_builder.dart';
 import '../../components/filler_text.dart';
 import '../../components/scaffold.dart';
 import '../../constants/sizes.dart';
 import '../../generated/assets.gen.dart';
+import '../../models/module.dart';
+import '../../models/sdk.dart';
 
 class WelcomeScreen extends StatelessWidget {
   const WelcomeScreen();
@@ -106,10 +110,18 @@ class _SdkSelection extends StatelessWidget {
             padding: const EdgeInsets.fromLTRB(50, 60, 50, 20),
             child: Column(
               crossAxisAlignment: CrossAxisAlignment.start,
-              children: const [
-                _IntroText(),
-                SizedBox(height: BeamSizes.size32),
-                _Buttons(),
+              children: [
+                const _IntroText(),
+                const SizedBox(height: BeamSizes.size32),
+                SdksBuilder(
+                  builder: (context, sdks, child) {
+                    if (sdks.isEmpty) {
+                      return Container();
+                    }
+
+                    return _Buttons(sdks: sdks);
+                  },
+                ),
               ],
             ),
           ),
@@ -129,26 +141,26 @@ class _TourSummary extends StatelessWidget {
         vertical: BeamSizes.size20,
         horizontal: 27,
       ),
-      child: Column(
-        children: _modules
-            .map(
-              (module) => _Module(
-                title: module,
-                isLast: module == _modules.last,
-              ),
-            )
-            .toList(growable: false),
+      child: ContentTreeBuilder(
+        builder: (context, contentTree, child) {
+          if (contentTree == null) {
+            return Container();
+          }
+
+          return Column(
+            children: contentTree.modules
+                .map(
+                  (module) => _Module(
+                    module: module,
+                    isLast: module == contentTree.modules.last,
+                  ),
+                )
+                .toList(growable: false),
+          );
+        },
       ),
     );
   }
-
-  static const List<String> _modules = [
-    'Core Transforms',
-    'Common Transforms',
-    'IO',
-    'Windowing',
-    'Triggers',
-  ];
 }
 
 class _IntroText extends StatelessWidget {
@@ -199,7 +211,8 @@ class _IntroText extends StatelessWidget {
 }
 
 class _Buttons extends StatelessWidget {
-  const _Buttons();
+  final List<SdkModel> sdks;
+  const _Buttons({required this.sdks});
 
   void _onSdkChanged(String value) {
     // TODO(nausharipov): change sdk
@@ -210,10 +223,11 @@ class _Buttons extends StatelessWidget {
     return Wrap(
       children: [
         Wrap(
-          children: ['Java', 'Python', 'Go']
+          children: sdks
               .map(
-                (e) => _SdkButton(
-                  value: e,
+                (sdk) => _SdkButton(
+                  title: sdk.title,
+                  value: sdk.id,
                   groupValue: _sdk,
                   onChanged: _onSdkChanged,
                 ),
@@ -230,15 +244,17 @@ class _Buttons extends StatelessWidget {
     );
   }
 
-  static const String _sdk = 'Java';
+  static const String _sdk = 'java';
 }
 
 class _SdkButton extends StatelessWidget {
+  final String title;
   final String value;
   final String groupValue;
   final ValueChanged<String> onChanged;
 
   const _SdkButton({
+    required this.title,
     required this.value,
     required this.groupValue,
     required this.onChanged,
@@ -258,18 +274,18 @@ class _SdkButton extends StatelessWidget {
         onPressed: () {
           onChanged(value);
         },
-        child: Text(value),
+        child: Text(title),
       ),
     );
   }
 }
 
 class _Module extends StatelessWidget {
-  final String title;
+  final ModuleModel module;
   final bool isLast;
 
   const _Module({
-    required this.title,
+    required this.module,
     required this.isLast,
   });
 
@@ -277,7 +293,7 @@ class _Module extends StatelessWidget {
   Widget build(BuildContext context) {
     return Column(
       children: [
-        _ModuleHeader(title: title),
+        _ModuleHeader(title: module.title),
         if (isLast) const _LastModuleBody() else const _ModuleBody(),
       ],
     );
diff --git a/playground/frontend/playground_components/lib/src/enums/complexity.dart b/learning/tour-of-beam/frontend/lib/repositories/client/client.dart
similarity index 80%
copy from playground/frontend/playground_components/lib/src/enums/complexity.dart
copy to learning/tour-of-beam/frontend/lib/repositories/client/client.dart
index 79767efa9b9..455488abbc2 100644
--- a/playground/frontend/playground_components/lib/src/enums/complexity.dart
+++ b/learning/tour-of-beam/frontend/lib/repositories/client/client.dart
@@ -16,8 +16,11 @@
  * limitations under the License.
  */
 
-enum Complexity {
-  basic,
-  medium,
-  advanced,
+import '../../models/content_tree.dart';
+import '../models/get_sdks_response.dart';
+
+abstract class TobClient {
+  Future<ContentTreeModel> getContentTree();
+
+  Future<GetSdksResponse> getSdks();
 }
diff --git a/learning/tour-of-beam/frontend/lib/repositories/client/cloud_functions_client.dart b/learning/tour-of-beam/frontend/lib/repositories/client/cloud_functions_client.dart
new file mode 100644
index 00000000000..d062dfd97e2
--- /dev/null
+++ b/learning/tour-of-beam/frontend/lib/repositories/client/cloud_functions_client.dart
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'dart:convert';
+
+import 'package:http/http.dart' as http;
+
+import '../../config.dart';
+import '../../models/content_tree.dart';
+import '../models/get_sdks_response.dart';
+import 'client.dart';
+
+class CloudFunctionsTobClient extends TobClient {
+  @override
+  Future<GetSdksResponse> getSdks() async {
+    final json = await http.get(
+      Uri.parse(
+        '$cloudFunctionsBaseUrl/getSdkList',
+      ),
+    );
+
+    final map = jsonDecode(utf8.decode(json.bodyBytes)) as Map<String, dynamic>;
+    return GetSdksResponse.fromJson(map);
+  }
+
+  @override
+  Future<ContentTreeModel> getContentTree() async {
+    final json = await http.get(
+      Uri.parse(
+        '$cloudFunctionsBaseUrl/getContentTree?sdk=Python',
+      ),
+    );
+    final map = jsonDecode(utf8.decode(json.bodyBytes)) as Map<String, dynamic>;
+    return ContentTreeModel.fromJson(map);
+  }
+}
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/repositories/models/get_sdks_response.dart
similarity index 68%
copy from learning/tour-of-beam/frontend/lib/locator.dart
copy to learning/tour-of-beam/frontend/lib/repositories/models/get_sdks_response.dart
index 8ab88a830d0..c7b5af64657 100644
--- a/learning/tour-of-beam/frontend/lib/locator.dart
+++ b/learning/tour-of-beam/frontend/lib/repositories/models/get_sdks_response.dart
@@ -16,9 +16,18 @@
  * limitations under the License.
  */
 
-//import 'package:get_it/get_it.dart';
+import 'package:json_annotation/json_annotation.dart';
 
-Future<void> initializeServiceLocator() async {
-  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
-  // as an example.
+import '../../models/sdk.dart';
+
+part 'get_sdks_response.g.dart';
+
+@JsonSerializable(createToJson: false)
+class GetSdksResponse {
+  final List<SdkModel> sdks;
+
+  const GetSdksResponse({required this.sdks});
+
+  factory GetSdksResponse.fromJson(Map<String, dynamic> json) =>
+      _$GetSdksResponseFromJson(json);
 }
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/repositories/models/group.dart
similarity index 65%
copy from learning/tour-of-beam/frontend/lib/locator.dart
copy to learning/tour-of-beam/frontend/lib/repositories/models/group.dart
index 8ab88a830d0..58705112a8b 100644
--- a/learning/tour-of-beam/frontend/lib/locator.dart
+++ b/learning/tour-of-beam/frontend/lib/repositories/models/group.dart
@@ -16,9 +16,22 @@
  * limitations under the License.
  */
 
-//import 'package:get_it/get_it.dart';
+import 'package:json_annotation/json_annotation.dart';
 
-Future<void> initializeServiceLocator() async {
-  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
-  // as an example.
+import 'node.dart';
+
+part 'group.g.dart';
+
+@JsonSerializable(createToJson: false)
+class GroupResponseModel {
+  final String title;
+  final List<NodeResponseModel> nodes;
+
+  const GroupResponseModel({
+    required this.title,
+    required this.nodes,
+  });
+
+  factory GroupResponseModel.fromJson(Map<String, dynamic> json) =>
+      _$GroupResponseModelFromJson(json);
 }
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/repositories/models/node.dart
similarity index 60%
copy from learning/tour-of-beam/frontend/lib/locator.dart
copy to learning/tour-of-beam/frontend/lib/repositories/models/node.dart
index 8ab88a830d0..38f23f819b4 100644
--- a/learning/tour-of-beam/frontend/lib/locator.dart
+++ b/learning/tour-of-beam/frontend/lib/repositories/models/node.dart
@@ -16,9 +16,26 @@
  * limitations under the License.
  */
 
-//import 'package:get_it/get_it.dart';
+import 'package:json_annotation/json_annotation.dart';
 
-Future<void> initializeServiceLocator() async {
-  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
-  // as an example.
+import 'group.dart';
+import 'node_type_enum.dart';
+import 'unit.dart';
+
+part 'node.g.dart';
+
+@JsonSerializable(createToJson: false)
+class NodeResponseModel {
+  final NodeType type;
+  final UnitResponseModel? unit;
+  final GroupResponseModel? group;
+
+  const NodeResponseModel({
+    required this.type,
+    required this.unit,
+    required this.group,
+  });
+
+  factory NodeResponseModel.fromJson(Map<String, dynamic> json) =>
+      _$NodeResponseModelFromJson(json);
 }
diff --git a/playground/frontend/playground_components/lib/src/enums/complexity.dart b/learning/tour-of-beam/frontend/lib/repositories/models/node_type_enum.dart
similarity index 94%
copy from playground/frontend/playground_components/lib/src/enums/complexity.dart
copy to learning/tour-of-beam/frontend/lib/repositories/models/node_type_enum.dart
index 79767efa9b9..602635d560b 100644
--- a/playground/frontend/playground_components/lib/src/enums/complexity.dart
+++ b/learning/tour-of-beam/frontend/lib/repositories/models/node_type_enum.dart
@@ -16,8 +16,7 @@
  * limitations under the License.
  */
 
-enum Complexity {
-  basic,
-  medium,
-  advanced,
+enum NodeType {
+  group,
+  unit,
 }
diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/repositories/models/unit.dart
similarity index 68%
copy from learning/tour-of-beam/frontend/lib/locator.dart
copy to learning/tour-of-beam/frontend/lib/repositories/models/unit.dart
index 8ab88a830d0..eeb3c1ffce0 100644
--- a/learning/tour-of-beam/frontend/lib/locator.dart
+++ b/learning/tour-of-beam/frontend/lib/repositories/models/unit.dart
@@ -16,9 +16,20 @@
  * limitations under the License.
  */
 
-//import 'package:get_it/get_it.dart';
+import 'package:json_annotation/json_annotation.dart';
 
-Future<void> initializeServiceLocator() async {
-  // See https://github.com/alexeyinkin/mefolio-standalone/blob/main/flutter/lib/locator.dart
-  // as an example.
+part 'unit.g.dart';
+
+@JsonSerializable(createToJson: false)
+class UnitResponseModel {
+  final String id;
+  final String title;
+
+  const UnitResponseModel({
+    required this.id,
+    required this.title,
+  });
+
+  factory UnitResponseModel.fromJson(Map<String, dynamic> json) =>
+      _$UnitResponseModelFromJson(json);
 }
diff --git a/learning/tour-of-beam/frontend/pubspec.lock b/learning/tour-of-beam/frontend/pubspec.lock
index c59bb0affd6..b132dbc7564 100644
--- a/learning/tour-of-beam/frontend/pubspec.lock
+++ b/learning/tour-of-beam/frontend/pubspec.lock
@@ -221,7 +221,7 @@ packages:
     source: hosted
     version: "0.0.2"
   equatable:
-    dependency: transitive
+    dependency: "direct dev"
     description:
       name: equatable
       url: "https://pub.dartlang.org"
@@ -370,12 +370,12 @@ packages:
     source: hosted
     version: "0.7.0"
   http:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: http
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.13.4"
+    version: "0.13.5"
   http2:
     dependency: transitive
     description:
@@ -424,12 +424,19 @@ packages:
     source: hosted
     version: "0.6.4"
   json_annotation:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: json_annotation
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "4.6.0"
+    version: "4.7.0"
+  json_serializable:
+    dependency: "direct dev"
+    description:
+      name: json_serializable
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.4.0"
   linked_scroll_controller:
     dependency: transitive
     description:
@@ -701,6 +708,20 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.99"
+  source_gen:
+    dependency: transitive
+    description:
+      name: source_gen
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.5"
+  source_helper:
+    dependency: transitive
+    description:
+      name: source_helper
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.3.3"
   source_span:
     dependency: transitive
     description:
diff --git a/learning/tour-of-beam/frontend/pubspec.yaml b/learning/tour-of-beam/frontend/pubspec.yaml
index fb9ee46307d..ea86db0e0c0 100644
--- a/learning/tour-of-beam/frontend/pubspec.yaml
+++ b/learning/tour-of-beam/frontend/pubspec.yaml
@@ -23,8 +23,8 @@ publish_to: 'none'
 version: 0.1.0
 
 environment:
-  sdk: ">=2.18.1 <3.0.0"
-  flutter: ">=3.3.2"
+  sdk: '>=2.18.1 <3.0.0'
+  flutter: '>=3.3.2'
 
 dependencies:
   code_text_field:
@@ -38,6 +38,8 @@ dependencies:
   flutter_svg: ^1.0.3
   get_it: ^7.2.0
   google_fonts: ^3.0.1
+  http: ^0.13.5
+  json_annotation: ^4.7.0
   playground_components: { path: ../../../playground/frontend/playground_components }
   provider: ^6.0.3
   shared_preferences: ^2.0.15
@@ -46,9 +48,11 @@ dependencies:
 
 dev_dependencies:
   build_runner: ^2.2.0
+  equatable: ^2.0.5
   flutter_gen_runner: ^4.3.0
   flutter_test: { sdk: flutter }
   integration_test: { sdk: flutter }
+  json_serializable: ^6.4.0
   total_lints: ^2.17.0
 
 flutter:
diff --git a/playground/frontend/playground_components/lib/src/enums/complexity.dart b/playground/frontend/playground_components/lib/src/enums/complexity.dart
index 79767efa9b9..f43ffae19fe 100644
--- a/playground/frontend/playground_components/lib/src/enums/complexity.dart
+++ b/playground/frontend/playground_components/lib/src/enums/complexity.dart
@@ -16,8 +16,13 @@
  * limitations under the License.
  */
 
+import 'package:json_annotation/json_annotation.dart';
+
 enum Complexity {
+  @JsonValue('BASIC')
   basic,
+  @JsonValue('MEDIUM')
   medium,
+  @JsonValue('ADVANCED')
   advanced,
 }