You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@beam.apache.org by pa...@apache.org on 2021/12/07 15:06:06 UTC

[beam] branch master updated: [BEAM-13112]: playground embedded version

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

pabloem 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 8496df0  [BEAM-13112]: playground embedded version
     new 6bd11bc  Merge pull request #15924 from [BEAM-13112] [Playground] embedded version of the playground editor
8496df0 is described below

commit 8496df0f4371d0c78a8075a27418c2618fd961d0
Author: Aydar Farrakhov <st...@gmail.com>
AuthorDate: Thu Dec 2 17:19:50 2021 +0300

    [BEAM-13112]: playground embedded version
---
 playground/frontend/assets/copy.svg                | 27 ++++++++
 playground/frontend/assets/link.svg                | 28 +++++++++
 .../toggle_theme_icon_button.dart}                 | 33 ++++------
 .../lib/{main.dart => configure_nonweb.dart}       |  7 +--
 .../frontend/lib/{main.dart => configure_web.dart} |  7 +--
 playground/frontend/lib/constants/assets.dart      |  2 +
 .../lib/{main.dart => constants/params.dart}       |  7 +--
 playground/frontend/lib/main.dart                  |  3 +
 .../modules/editor/components/editor_textarea.dart | 17 ++++--
 .../modules/editor/components/editor_themes.dart   |  2 +-
 .../components/embedded_actions.dart               | 71 ++++++++++++++++++++++
 .../components/embedded_editor.dart}               | 18 +++++-
 .../embedded_playground_page.dart}                 | 43 +++++++------
 .../components/editor_textarea_wrapper.dart        |  1 +
 .../components/playground_page_providers.dart      | 31 ++++++++--
 .../lib/pages/playground/playground_page.dart      |  1 +
 .../lib/{playground_app.dart => pages/routes.dart} | 44 +++++++-------
 playground/frontend/lib/playground_app.dart        |  4 +-
 playground/frontend/pubspec.lock                   |  2 +-
 playground/frontend/pubspec.yaml                   |  2 +
 playground/frontend/web/index.html                 |  2 +-
 21 files changed, 257 insertions(+), 95 deletions(-)

diff --git a/playground/frontend/assets/copy.svg b/playground/frontend/assets/copy.svg
new file mode 100644
index 0000000..92dbc0a
--- /dev/null
+++ b/playground/frontend/assets/copy.svg
@@ -0,0 +1,27 @@
+<!--
+    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.
+-->
+
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path
+        d="M10 2.25C8.48122 2.25 7.25 3.48122 7.25 5V6H8.75V5C8.75 4.30964 9.30964 3.75 10 3.75H19C19.6904 3.75 20.25 4.30964 20.25 5V14C20.25 14.6904 19.6904 15.25 19 15.25H18V16.75H19C20.5188 16.75 21.75 15.5188 21.75 14V5C21.75 3.48122 20.5188 2.25 19 2.25H10Z"
+        fill="#A0A4AB" />
+    <path fill-rule="evenodd" clip-rule="evenodd"
+        d="M2.25 10C2.25 8.48122 3.48122 7.25 5 7.25H14C15.5188 7.25 16.75 8.48122 16.75 10V19C16.75 20.5188 15.5188 21.75 14 21.75H5C3.48122 21.75 2.25 20.5188 2.25 19V10ZM5 8.75C4.30964 8.75 3.75 9.30964 3.75 10V19C3.75 19.6904 4.30964 20.25 5 20.25H14C14.6904 20.25 15.25 19.6904 15.25 19V10C15.25 9.30964 14.6904 8.75 14 8.75H5Z"
+        fill="#A0A4AB" />
+</svg>
diff --git a/playground/frontend/assets/link.svg b/playground/frontend/assets/link.svg
new file mode 100644
index 0000000..2f2ea58
--- /dev/null
+++ b/playground/frontend/assets/link.svg
@@ -0,0 +1,28 @@
+<!--
+    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.
+-->
+
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path
+        d="M12 11.2692C12 12.2251 11.2251 13 10.2692 13H4.73077C3.77489 13 3 12.2251 3 11.2692V5.73077C3 4.77489 3.77489 4 4.73077 4H6.73591C6.91409 4 7.00332 4.21543 6.87733 4.34142L6.17157 5.04718C5.42143 5.79732 4.95439 6.82123 5.0887 7.87356C5.33206 9.78033 6.11095 10.6198 7.84709 10.8929C8.89508 11.0577 9.92143 10.5786 10.6716 9.82843L11.6586 8.84142C11.7846 8.71543 12 8.80466 12 8.98284V11.2692Z"
+        fill="white" stroke="white" stroke-width="2" stroke-linecap="round" />
+    <path
+        d="M9.57143 1.75C9.19666 1.75 8.89286 2.05381 8.89286 2.42857C8.89286 2.80334 9.19666 3.10714 9.57143 3.10714H11.9332L7.94875 7.09161C7.68375 7.3566 7.68375 7.78625 7.94875 8.05125C8.21375 8.31625 8.6434 8.31625 8.90839 8.05125L12.8929 4.06679V6.37143C12.8929 6.74619 13.1967 7.05 13.5714 7.05C13.9462 7.05 14.25 6.74619 14.25 6.37143V2.42857C14.25 2.05381 13.9462 1.75 13.5714 1.75H9.57143Z"
+        fill="white" stroke="white" stroke-width="0.5" stroke-linecap="round"
+        stroke-linejoin="round" />
+</svg>
diff --git a/playground/frontend/lib/playground_app.dart b/playground/frontend/lib/components/toggle_theme_button/toggle_theme_icon_button.dart
similarity index 55%
copy from playground/frontend/lib/playground_app.dart
copy to playground/frontend/lib/components/toggle_theme_button/toggle_theme_icon_button.dart
index 54955d0..a505e4f 100644
--- a/playground/frontend/lib/playground_app.dart
+++ b/playground/frontend/lib/components/toggle_theme_button/toggle_theme_icon_button.dart
@@ -17,31 +17,24 @@
  */
 
 import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
 import 'package:playground/config/theme.dart';
-import 'package:playground/pages/playground/components/playground_page_providers.dart';
-import 'package:playground/pages/playground/playground_page.dart';
+import 'package:playground/constants/assets.dart';
+import 'package:playground/constants/sizes.dart';
 import 'package:provider/provider.dart';
 
-class PlaygroundApp extends StatelessWidget {
-  const PlaygroundApp({Key? key}) : super(key: key);
+class ToggleThemeIconButton extends StatelessWidget {
+  const ToggleThemeIconButton({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    return ChangeNotifierProvider(
-      create: (context) => ThemeProvider(),
-      builder: (context, _) {
-        final themeProvider = Provider.of<ThemeProvider>(context);
-        return PlaygroundPageProviders(
-          child: MaterialApp(
-            title: 'Apache Beam Playground',
-            themeMode: themeProvider.themeMode,
-            theme: kLightTheme,
-            darkTheme: kDarkTheme,
-            home: const PlaygroundPage(),
-            debugShowCheckedModeBanner: false,
-          ),
-        );
-      },
-    );
+    return Consumer<ThemeProvider>(builder: (context, theme, child) {
+      return IconButton(
+        iconSize: kIconSizeLg,
+        splashRadius: kIconButtonSplashRadius,
+        icon: SvgPicture.asset(kThemeIconAsset),
+        onPressed: theme.toggleTheme,
+      );
+    });
   }
 }
diff --git a/playground/frontend/lib/main.dart b/playground/frontend/lib/configure_nonweb.dart
similarity index 85%
copy from playground/frontend/lib/main.dart
copy to playground/frontend/lib/configure_nonweb.dart
index 3ce8c37..4568ded 100644
--- a/playground/frontend/lib/main.dart
+++ b/playground/frontend/lib/configure_nonweb.dart
@@ -16,9 +16,6 @@
  * limitations under the License.
  */
 
-import 'package:flutter/material.dart';
-import 'package:playground/playground_app.dart';
-
-void main() {
-  runApp(const PlaygroundApp());
+void configureApp() {
+  // see https://flutter.dev/docs/development/ui/navigation/url-strategies
 }
diff --git a/playground/frontend/lib/main.dart b/playground/frontend/lib/configure_web.dart
similarity index 85%
copy from playground/frontend/lib/main.dart
copy to playground/frontend/lib/configure_web.dart
index 3ce8c37..5fa7eb6 100644
--- a/playground/frontend/lib/main.dart
+++ b/playground/frontend/lib/configure_web.dart
@@ -16,9 +16,8 @@
  * limitations under the License.
  */
 
-import 'package:flutter/material.dart';
-import 'package:playground/playground_app.dart';
+import 'package:flutter_web_plugins/flutter_web_plugins.dart';
 
-void main() {
-  runApp(const PlaygroundApp());
+void configureApp() {
+  setUrlStrategy(PathUrlStrategy());
 }
diff --git a/playground/frontend/lib/constants/assets.dart b/playground/frontend/lib/constants/assets.dart
index ab6cf20..bb7e827 100644
--- a/playground/frontend/lib/constants/assets.dart
+++ b/playground/frontend/lib/constants/assets.dart
@@ -27,6 +27,8 @@ const kBeamIconAsset = 'beam.png';
 const kBeamLgIconAsset = 'beam_lg.png';
 const kThumbUpIconAsset = 'thumb_up.svg';
 const kThumbDownIconAsset = 'thumb_down.svg';
+const kCopyIconAsset = 'copy.svg';
+const kLinkIconAsset = 'link.svg';
 
 // notifications icons
 const kErrorNotificationIconAsset = 'error_notification.svg';
diff --git a/playground/frontend/lib/main.dart b/playground/frontend/lib/constants/params.dart
similarity index 85%
copy from playground/frontend/lib/main.dart
copy to playground/frontend/lib/constants/params.dart
index 3ce8c37..ef24e0f 100644
--- a/playground/frontend/lib/main.dart
+++ b/playground/frontend/lib/constants/params.dart
@@ -16,9 +16,4 @@
  * limitations under the License.
  */
 
-import 'package:flutter/material.dart';
-import 'package:playground/playground_app.dart';
-
-void main() {
-  runApp(const PlaygroundApp());
-}
+const kExampleParam = 'example';
diff --git a/playground/frontend/lib/main.dart b/playground/frontend/lib/main.dart
index 3ce8c37..9e7438d 100644
--- a/playground/frontend/lib/main.dart
+++ b/playground/frontend/lib/main.dart
@@ -17,8 +17,11 @@
  */
 
 import 'package:flutter/material.dart';
+import 'package:playground/configure_nonweb.dart'
+    if (dart.library.html) 'package:playground/configure_web.dart';
 import 'package:playground/playground_app.dart';
 
 void main() {
+  configureApp();
   runApp(const PlaygroundApp());
 }
diff --git a/playground/frontend/lib/modules/editor/components/editor_textarea.dart b/playground/frontend/lib/modules/editor/components/editor_textarea.dart
index 7cdc7df..5e2df77 100644
--- a/playground/frontend/lib/modules/editor/components/editor_textarea.dart
+++ b/playground/frontend/lib/modules/editor/components/editor_textarea.dart
@@ -35,13 +35,15 @@ const kCodeAreaSemantics = 'Code textarea';
 class EditorTextArea extends StatefulWidget {
   final SDK sdk;
   final ExampleModel? example;
-  final void Function(String) onSourceChange;
+  final bool enabled;
+  final void Function(String)? onSourceChange;
 
   const EditorTextArea({
     Key? key,
     required this.sdk,
     this.example,
-    required this.onSourceChange,
+    this.onSourceChange,
+    required this.enabled,
   }) : super(key: key);
 
   @override
@@ -63,7 +65,11 @@ class _EditorTextAreaState extends State<EditorTextArea> {
       text: _codeController?.text ?? widget.example?.source ?? '',
       language: _getLanguageFromSdk(),
       theme: themeProvider.isDarkMode ? kDarkCodeTheme : kLightCodeTheme,
-      onChange: (newSource) => widget.onSourceChange(newSource),
+      onChange: (newSource) {
+        if (widget.onSourceChange != null) {
+          widget.onSourceChange!(newSource);
+        }
+      },
       webSpaceFix: false,
     );
     super.didChangeDependencies();
@@ -81,10 +87,11 @@ class _EditorTextAreaState extends State<EditorTextArea> {
       container: true,
       textField: true,
       multiline: true,
-      enabled: true,
-      readOnly: false,
+      enabled: widget.enabled,
+      readOnly: widget.enabled,
       label: kCodeAreaSemantics,
       child: CodeField(
+        enabled: widget.enabled,
         controller: _codeController!,
         textStyle: getCodeFontStyle(
           textStyle: const TextStyle(fontSize: kCodeFontSize),
diff --git a/playground/frontend/lib/modules/editor/components/editor_themes.dart b/playground/frontend/lib/modules/editor/components/editor_themes.dart
index 8df5acf..c26df06 100644
--- a/playground/frontend/lib/modules/editor/components/editor_themes.dart
+++ b/playground/frontend/lib/modules/editor/components/editor_themes.dart
@@ -19,7 +19,7 @@
 import 'package:flutter/material.dart';
 import 'package:playground/config/theme.dart';
 
-Map<String, TextStyle>? createTheme(ThemeColors colors) {
+Map<String, TextStyle> createTheme(ThemeColors colors) {
   return {
     'root': TextStyle(
       backgroundColor: colors.primaryBackground,
diff --git a/playground/frontend/lib/pages/embedded_playground/components/embedded_actions.dart b/playground/frontend/lib/pages/embedded_playground/components/embedded_actions.dart
new file mode 100644
index 0000000..19313d1
--- /dev/null
+++ b/playground/frontend/lib/pages/embedded_playground/components/embedded_actions.dart
@@ -0,0 +1,71 @@
+/*
+ * 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 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:playground/components/toggle_theme_button/toggle_theme_icon_button.dart';
+import 'package:playground/constants/assets.dart';
+import 'package:playground/constants/params.dart';
+import 'package:playground/constants/sizes.dart';
+import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:provider/provider.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+const kPlaygroundText = 'Try in Playground';
+const kTryPlaygroundButtonWidth = 200.0;
+const kTryPlaygroundButtonHeight = 40.0;
+
+class EmbeddedActions extends StatelessWidget {
+  const EmbeddedActions({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Wrap(
+      runSpacing: kMdSpacing,
+      crossAxisAlignment: WrapCrossAlignment.center,
+      alignment: WrapAlignment.end,
+      children: [
+        const ToggleThemeIconButton(),
+        IconButton(
+          iconSize: kIconSizeLg,
+          splashRadius: kIconButtonSplashRadius,
+          icon: SvgPicture.asset(kCopyIconAsset),
+          onPressed: () {
+            final source =
+                Provider.of<PlaygroundState>(context, listen: false).source;
+            Clipboard.setData(ClipboardData(text: source));
+          },
+        ),
+        ElevatedButton.icon(
+          style: ButtonStyle(
+            fixedSize: MaterialStateProperty.all(
+              const Size(kTryPlaygroundButtonWidth, kTryPlaygroundButtonHeight),
+            ),
+          ),
+          icon: SvgPicture.asset(kLinkIconAsset),
+          label: const Text(kPlaygroundText),
+          onPressed: () {
+            final exampleId = Uri.base.queryParameters[kExampleParam];
+            launch('/?$kExampleParam=$exampleId');
+          },
+        ),
+      ],
+    );
+  }
+}
diff --git a/playground/frontend/lib/main.dart b/playground/frontend/lib/pages/embedded_playground/components/embedded_editor.dart
similarity index 61%
copy from playground/frontend/lib/main.dart
copy to playground/frontend/lib/pages/embedded_playground/components/embedded_editor.dart
index 3ce8c37..624e300 100644
--- a/playground/frontend/lib/main.dart
+++ b/playground/frontend/lib/pages/embedded_playground/components/embedded_editor.dart
@@ -17,8 +17,20 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/playground_app.dart';
+import 'package:playground/modules/editor/components/editor_textarea.dart';
+import 'package:playground/pages/playground/states/playground_state.dart';
+import 'package:provider/provider.dart';
 
-void main() {
-  runApp(const PlaygroundApp());
+class EmbeddedEditor extends StatelessWidget {
+  const EmbeddedEditor({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final state = Provider.of<PlaygroundState>(context);
+    return EditorTextArea(
+      enabled: true,
+      sdk: state.sdk,
+      example: state.selectedExample,
+    );
+  }
 }
diff --git a/playground/frontend/lib/playground_app.dart b/playground/frontend/lib/pages/embedded_playground/embedded_playground_page.dart
similarity index 51%
copy from playground/frontend/lib/playground_app.dart
copy to playground/frontend/lib/pages/embedded_playground/embedded_playground_page.dart
index 54955d0..4459bcd 100644
--- a/playground/frontend/lib/playground_app.dart
+++ b/playground/frontend/lib/pages/embedded_playground/embedded_playground_page.dart
@@ -17,31 +17,34 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/config/theme.dart';
-import 'package:playground/pages/playground/components/playground_page_providers.dart';
-import 'package:playground/pages/playground/playground_page.dart';
-import 'package:provider/provider.dart';
+import 'package:playground/constants/sizes.dart';
+import 'package:playground/pages/embedded_playground/components/embedded_actions.dart';
+import 'package:playground/pages/embedded_playground/components/embedded_editor.dart';
 
-class PlaygroundApp extends StatelessWidget {
-  const PlaygroundApp({Key? key}) : super(key: key);
+const kPlaygroundText = 'Try in Playground';
+const kActionsWidth = 300.0;
+const kActionsHeight = 40.0;
+
+class EmbeddedPlaygroundPage extends StatelessWidget {
+  const EmbeddedPlaygroundPage({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    return ChangeNotifierProvider(
-      create: (context) => ThemeProvider(),
-      builder: (context, _) {
-        final themeProvider = Provider.of<ThemeProvider>(context);
-        return PlaygroundPageProviders(
-          child: MaterialApp(
-            title: 'Apache Beam Playground',
-            themeMode: themeProvider.themeMode,
-            theme: kLightTheme,
-            darkTheme: kDarkTheme,
-            home: const PlaygroundPage(),
-            debugShowCheckedModeBanner: false,
+    return Scaffold(
+      body: Stack(
+        children: const [
+          Positioned.fill(
+            child: EmbeddedEditor(),
+          ),
+          Positioned(
+            right: kXlSpacing,
+            top: kXlSpacing,
+            width: kActionsWidth,
+            height: kActionsHeight,
+            child: EmbeddedActions(),
           ),
-        );
-      },
+        ],
+      ),
     );
   }
 }
diff --git a/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart b/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart
index e0d8b00..69f483e 100644
--- a/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart
+++ b/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart
@@ -43,6 +43,7 @@ class CodeTextAreaWrapper extends StatelessWidget {
         children: [
           Positioned.fill(
             child: EditorTextArea(
+              enabled: true,
               key: ValueKey(EditorKeyObject(
                 state.sdk,
                 state.selectedExample,
diff --git a/playground/frontend/lib/pages/playground/components/playground_page_providers.dart b/playground/frontend/lib/pages/playground/components/playground_page_providers.dart
index 464c9ef..66ae46d 100644
--- a/playground/frontend/lib/pages/playground/components/playground_page_providers.dart
+++ b/playground/frontend/lib/pages/playground/components/playground_page_providers.dart
@@ -17,8 +17,10 @@
  */
 
 import 'package:flutter/material.dart';
+import 'package:playground/constants/params.dart';
 import 'package:playground/modules/editor/repository/code_repository/code_client/grpc_code_client.dart';
 import 'package:playground/modules/editor/repository/code_repository/code_repository.dart';
+import 'package:playground/modules/examples/models/example_model.dart';
 import 'package:playground/modules/examples/repositories/example_client/grpc_example_client.dart';
 import 'package:playground/modules/examples/repositories/example_repository.dart';
 import 'package:playground/modules/output/models/output_placement_state.dart';
@@ -54,13 +56,18 @@ class PlaygroundPageProviders extends StatelessWidget {
 
             if (exampleState.sdkCategories != null &&
                 playground.selectedExample == null) {
-              final defaultExample =
-                  exampleState.defaultExamplesMap![playground.sdk]!;
-              return PlaygroundState(
+              final example = _getExample(exampleState, playground);
+              final newPlayground = PlaygroundState(
                 codeRepository: kCodeRepository,
                 sdk: playground.sdk,
-                selectedExample: defaultExample,
+                selectedExample: null,
               );
+              if (example != null) {
+                exampleState.loadExampleInfo(example, playground.sdk,).then(
+                    (exampleWithInfo) =>
+                        newPlayground.setExample(exampleWithInfo));
+              }
+              return newPlayground;
             }
             return playground;
           },
@@ -72,4 +79,20 @@ class PlaygroundPageProviders extends StatelessWidget {
       child: child,
     );
   }
+
+  ExampleModel? _getExample(
+    ExampleState exampleState,
+    PlaygroundState playground,
+  ) {
+    final examplePath = Uri.base.queryParameters[kExampleParam];
+    final allExamples = exampleState.sdkCategories?.values
+        .expand((sdkCategory) => sdkCategory.map((e) => e.examples))
+        .expand((element) => element)
+        .toList();
+    final defaultExample = exampleState.defaultExamplesMap![playground.sdk]!;
+    return allExamples?.firstWhere(
+      (example) => example.path == examplePath,
+      orElse: () => defaultExample,
+    );
+  }
 }
diff --git a/playground/frontend/lib/pages/playground/playground_page.dart b/playground/frontend/lib/pages/playground/playground_page.dart
index 03730d6..bffb2ed 100644
--- a/playground/frontend/lib/pages/playground/playground_page.dart
+++ b/playground/frontend/lib/pages/playground/playground_page.dart
@@ -42,6 +42,7 @@ class PlaygroundPage extends StatelessWidget {
       shortcuts: globalShortcuts,
       child: Scaffold(
         appBar: AppBar(
+          automaticallyImplyLeading: false,
           title: Consumer<PlaygroundState>(
             builder: (context, state, child) {
               return Wrap(
diff --git a/playground/frontend/lib/playground_app.dart b/playground/frontend/lib/pages/routes.dart
similarity index 51%
copy from playground/frontend/lib/playground_app.dart
copy to playground/frontend/lib/pages/routes.dart
index 54955d0..5c7bc77 100644
--- a/playground/frontend/lib/playground_app.dart
+++ b/playground/frontend/lib/pages/routes.dart
@@ -17,31 +17,29 @@
  */
 
 import 'package:flutter/material.dart';
-import 'package:playground/config/theme.dart';
-import 'package:playground/pages/playground/components/playground_page_providers.dart';
+import 'package:playground/pages/embedded_playground/embedded_playground_page.dart';
 import 'package:playground/pages/playground/playground_page.dart';
-import 'package:provider/provider.dart';
 
-class PlaygroundApp extends StatelessWidget {
-  const PlaygroundApp({Key? key}) : super(key: key);
+class Routes {
+  static const String playground = '/';
+  static const String embedded = '/embedded';
 
-  @override
-  Widget build(BuildContext context) {
-    return ChangeNotifierProvider(
-      create: (context) => ThemeProvider(),
-      builder: (context, _) {
-        final themeProvider = Provider.of<ThemeProvider>(context);
-        return PlaygroundPageProviders(
-          child: MaterialApp(
-            title: 'Apache Beam Playground',
-            themeMode: themeProvider.themeMode,
-            theme: kLightTheme,
-            darkTheme: kDarkTheme,
-            home: const PlaygroundPage(),
-            debugShowCheckedModeBanner: false,
-          ),
-        );
-      },
-    );
+  static Route<dynamic> generateRoute(RouteSettings settings) {
+    final name = settings.name ?? '';
+    final queryIndex = name.indexOf('?');
+    final routePath =
+        name.substring(0, queryIndex < 0 ? name.length : queryIndex);
+    switch (routePath) {
+      case Routes.playground:
+        return Routes.renderRoute(const PlaygroundPage());
+      case Routes.embedded:
+        return Routes.renderRoute(const EmbeddedPlaygroundPage());
+      default:
+        return Routes.renderRoute(const PlaygroundPage());
+    }
+  }
+
+  static renderRoute(Widget widget) {
+    return MaterialPageRoute(builder: (context) => widget);
   }
 }
diff --git a/playground/frontend/lib/playground_app.dart b/playground/frontend/lib/playground_app.dart
index 54955d0..24fa28d 100644
--- a/playground/frontend/lib/playground_app.dart
+++ b/playground/frontend/lib/playground_app.dart
@@ -19,7 +19,7 @@
 import 'package:flutter/material.dart';
 import 'package:playground/config/theme.dart';
 import 'package:playground/pages/playground/components/playground_page_providers.dart';
-import 'package:playground/pages/playground/playground_page.dart';
+import 'package:playground/pages/routes.dart';
 import 'package:provider/provider.dart';
 
 class PlaygroundApp extends StatelessWidget {
@@ -37,7 +37,7 @@ class PlaygroundApp extends StatelessWidget {
             themeMode: themeProvider.themeMode,
             theme: kLightTheme,
             darkTheme: kDarkTheme,
-            home: const PlaygroundPage(),
+            onGenerateRoute: Routes.generateRoute,
             debugShowCheckedModeBanner: false,
           ),
         );
diff --git a/playground/frontend/pubspec.lock b/playground/frontend/pubspec.lock
index 937e936..c883ed9 100644
--- a/playground/frontend/pubspec.lock
+++ b/playground/frontend/pubspec.lock
@@ -243,7 +243,7 @@ packages:
     source: sdk
     version: "0.0.0"
   flutter_web_plugins:
-    dependency: transitive
+    dependency: "direct main"
     description: flutter
     source: sdk
     version: "0.0.0"
diff --git a/playground/frontend/pubspec.yaml b/playground/frontend/pubspec.yaml
index 32173c97..5dcc5a2 100644
--- a/playground/frontend/pubspec.yaml
+++ b/playground/frontend/pubspec.yaml
@@ -52,6 +52,8 @@ dependencies:
   url_launcher: ^6.0.12
   expansion_widget: ^0.0.2
   grpc: ^3.0.0
+  flutter_web_plugins:
+    sdk: flutter
 
 dev_dependencies:
   flutter_test:
diff --git a/playground/frontend/web/index.html b/playground/frontend/web/index.html
index 6651974..765f92e 100644
--- a/playground/frontend/web/index.html
+++ b/playground/frontend/web/index.html
@@ -33,7 +33,7 @@
     This is a placeholder for base href that will be replaced by the value of
     the `--base-href` argument provided to `flutter build`.
   -->
-  <base href="$FLUTTER_BASE_HREF">
+  <base href="/">
 
   <meta charset="UTF-8">
   <meta content="IE=Edge" http-equiv="X-UA-Compatible">