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 e3e30a7  Merge pull request #16857 from [BEAM-13662] [Playground] 
Support mutlifile examples
e3e30a7 is described below

commit e3e30a74c3254fbc37ba6a4fbbeb872415eaa122
Author: Aydar Farrakhov <stranni...@gmail.com>
AuthorDate: Thu Feb 24 08:25:02 2022 +0300

    Merge pull request #16857 from [BEAM-13662] [Playground] Support mutlifile 
examples
    
    * [BEAM-13771][Playground]
    Add multifile value to the response to the frontend
    
    * [BEAM-13771][Playground]
    Regenerate files
    
    * [BEAM-13771][Playground]
    Regenerate files
    
    * [BEAM-13662] playground - multifile example support
    
    * [BEAM-13662] playground - support multifile
    
    * [BEAM-13662] playground - redesign multifile examples
    
    * [BEAM-13662] playground - support multifile examples
    
    * [BEAM-13662] add licence
    
    * [BEAM-13662] refactor popover list for better readability
    
    * [BEAM-13662] refactor popover list for better readability
    
    Co-authored-by: AydarZaynutdinov <aydar.zaynutdi...@akvelon.com>
    Co-authored-by: Ilya <ilya.kozy...@akvelon.com>
---
 playground/frontend/assets/multifile.svg           |  22 ++++
 playground/frontend/lib/config.g.dart              |  12 +--
 playground/frontend/lib/constants/assets.dart      |   1 +
 playground/frontend/lib/constants/links.dart       |   1 +
 playground/frontend/lib/constants/sizes.dart       |   1 +
 playground/frontend/lib/l10n/app_en.arb            |  18 +++-
 .../lib/modules/editor/components/run_button.dart  |  11 +-
 .../description_popover/description_popover.dart   |  39 +++++--
 .../description_popover_button.dart                |  14 ++-
 .../example_list/example_item_actions.dart         |  66 ++++++++++++
 .../example_list/expansion_panel_item.dart         |   9 +-
 .../multifile_popover.dart}                        |  26 +++--
 .../multifile_popover_button.dart}                 |  36 ++++---
 .../components/outside_click_handler.dart}         |  37 +++++--
 .../lib/modules/examples/example_selector.dart     | 120 +++++++++++++--------
 .../lib/modules/examples/models/example_model.dart |   4 +
 .../examples/models/popover_state.dart}            |  22 ++--
 .../example_client/grpc_example_client.dart        |   2 +
 .../components/editor_textarea_wrapper.dart        |  13 ++-
 19 files changed, 341 insertions(+), 113 deletions(-)

diff --git a/playground/frontend/assets/multifile.svg 
b/playground/frontend/assets/multifile.svg
new file mode 100644
index 0000000..2a27ab2
--- /dev/null
+++ b/playground/frontend/assets/multifile.svg
@@ -0,0 +1,22 @@
+<!--
+    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 fill-rule="evenodd" clip-rule="evenodd" d="M10.75 4.36639C10.75 3.67604 
11.3096 3.11639 12 3.11639H20C20.6904 3.11639 21.25 3.67604 21.25 
4.36639V12.3664C21.25 13.0567 20.6904 13.6164 20 
13.6164H19.5V15.1164H20C21.5188 15.1164 22.75 13.8852 22.75 
12.3664V4.36639C22.75 2.84761 21.5188 1.61639 20 1.61639H12C10.4812 1.61639 
9.25 2.84761 9.25 4.36639H10.75ZM6.75 8.36639C6.75 7.67604 7.30964 7.11639 8 
7.11639H16C16.6904 7.11639 17.25 7.67604 17.25 8.36639V16.3664C17.25 17.0568 
16.6904 1 [...]
+</svg>
diff --git a/playground/frontend/lib/config.g.dart 
b/playground/frontend/lib/config.g.dart
index e189b32..c81baa5 100644
--- a/playground/frontend/lib/config.g.dart
+++ b/playground/frontend/lib/config.g.dart
@@ -16,14 +16,14 @@
  * limitations under the License.
  */
 
-const String kApiClientURL =
-    'https://backend-dot-datatokenization.uc.r.appspot.com';
 const String kAnalyticsUA = 'UA-73650088-1';
+const String kApiClientURL =
+    'https://backend-router-beta-dot-apache-beam-testing.appspot.com';
 const String kApiJavaClientURL =
-    'https://backend-dot-datatokenization.uc.r.appspot.com/java/';
+    'https://backend-java-beta-dot-apache-beam-testing.appspot.com';
 const String kApiGoClientURL =
-    'https://backend-dot-datatokenization.uc.r.appspot.com/go/';
+    'https://backend-go-beta-dot-apache-beam-testing.appspot.com';
 const String kApiPythonClientURL =
-    'https://backend-dot-datatokenization.uc.r.appspot.com/python/';
+    'https://backend-python-beta-dot-apache-beam-testing.appspot.com';
 const String kApiScioClientURL =
-    'https://backend-dot-datatokenization.uc.r.appspot.com/scio/';
+    'https://backend-scio-beta-dot-apache-beam-testing.appspot.com';
diff --git a/playground/frontend/lib/constants/assets.dart 
b/playground/frontend/lib/constants/assets.dart
index deaad15..796a97d 100644
--- a/playground/frontend/lib/constants/assets.dart
+++ b/playground/frontend/lib/constants/assets.dart
@@ -33,6 +33,7 @@ const kCopyIconAsset = 'copy.svg';
 const kLinkIconAsset = 'link.svg';
 const kDragHorizontalIconAsset = 'drag_horizontal.svg';
 const kDragVerticalIconAsset = 'drag_vertical.svg';
+const kMultifileIconAsset = 'multifile.svg';
 
 // notifications icons
 const kErrorNotificationIconAsset = 'error_notification.svg';
diff --git a/playground/frontend/lib/constants/links.dart 
b/playground/frontend/lib/constants/links.dart
index e3b34eb..688a7ad 100644
--- a/playground/frontend/lib/constants/links.dart
+++ b/playground/frontend/lib/constants/links.dart
@@ -24,3 +24,4 @@ const kApacheBeamGithubLink = 
'https://github.com/apache/beam';
 const kBeamWebsiteLink = 'https://beam.apache.org/';
 const kScioGithubLink = 'https://github.com/spotify/scio';
 const kAboutBeamLink = 'https://beam.apache.org/get-started/beam-overview';
+const kAddExampleLink = 
'https://beam.apache.org/get-started/try-beam-playground/#how-to-add-new-examples';
diff --git a/playground/frontend/lib/constants/sizes.dart 
b/playground/frontend/lib/constants/sizes.dart
index 3962a19..74f61ae 100644
--- a/playground/frontend/lib/constants/sizes.dart
+++ b/playground/frontend/lib/constants/sizes.dart
@@ -52,6 +52,7 @@ const double kCursorSize = 1.0;
 // container size
 const double kContainerHeight = 40.0;
 
+const double kCaptionFontSize = 10.0;
 const double kCodeFontSize = 14.0;
 const double kLabelFontSize = 16.0;
 const double kHintFontSize = 16.0;
diff --git a/playground/frontend/lib/l10n/app_en.arb 
b/playground/frontend/lib/l10n/app_en.arb
index 8b85370..2b2b470 100644
--- a/playground/frontend/lib/l10n/app_en.arb
+++ b/playground/frontend/lib/l10n/app_en.arb
@@ -184,7 +184,23 @@
     "description": "Text value label"
   },
   "pipelineOptionsError": "Please check the format (example: --key1 value1 
--key2 value2), only alphanumeric and \",*,/,-,:,;,',. symbols are allowed",
-  "@value": {
+  "@pipelineOptionsError": {
     "description": "Pipeline options parse error"
+  },
+  "viewOnGithub": "View on GitHub",
+  "@viewOnGithub": {
+    "description": "View on Github button"
+  },
+  "addExample": "Add your own example",
+  "@addExample": {
+    "description": "Add example link text"
+  },
+  "multifile": "Multifile",
+  "@multifile": {
+    "description": "Multifile example"
+  },
+  "multifileWarning": "Multifile not supported yet for running in playground. 
Open it on github.",
+  "@multifileWarning": {
+    "description": "Multifile not supported text"
   }
 }
\ No newline at end of file
diff --git a/playground/frontend/lib/modules/editor/components/run_button.dart 
b/playground/frontend/lib/modules/editor/components/run_button.dart
index 6aecc1f..d31e36d 100644
--- a/playground/frontend/lib/modules/editor/components/run_button.dart
+++ b/playground/frontend/lib/modules/editor/components/run_button.dart
@@ -32,12 +32,14 @@ class RunButton extends StatelessWidget {
   final bool isRunning;
   final VoidCallback runCode;
   final VoidCallback cancelRun;
+  final bool disabled;
 
   const RunButton({
     Key? key,
     required this.isRunning,
     required this.runCode,
     required this.cancelRun,
+    this.disabled = false,
   }) : super(key: key);
 
   @override
@@ -71,9 +73,16 @@ class RunButton extends StatelessWidget {
                 }
                 return Text(buttonText);
               }),
-          onPressed: !isRunning ? runCode : cancelRun,
+          onPressed: onPressHandler(),
         ),
       ),
     );
   }
+
+  onPressHandler() {
+    if (disabled) {
+      return null;
+    }
+    return !isRunning ? runCode : cancelRun;
+  }
 }
diff --git 
a/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart
 
b/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart
index 6a37c8f..5de8144 100644
--- 
a/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart
+++ 
b/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart
@@ -17,9 +17,13 @@
  */
 
 import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:playground/constants/assets.dart';
 import 'package:playground/constants/font_weight.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/examples/models/example_model.dart';
+import 'package:url_launcher/url_launcher.dart';
 
 const kDescriptionWidth = 300.0;
 
@@ -30,26 +34,43 @@ class DescriptionPopover extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    final hasLink = example.link?.isNotEmpty ?? false;
     return SizedBox(
       width: kDescriptionWidth,
       child: Card(
         child: Padding(
           padding: const EdgeInsets.all(kLgSpacing),
           child: Wrap(
-            runSpacing: kSmSpacing,
+            runSpacing: kMdSpacing,
             children: [
-              Text(
-                example.name,
-                style: const TextStyle(
-                  fontSize: kTitleFontSize,
-                  fontWeight: kBoldWeight,
-                ),
-              ),
-              Text(example.description),
+              title,
+              description,
+              if (hasLink) getViewOnGithub(context),
             ],
           ),
         ),
       ),
     );
   }
+
+  Widget get title => Text(
+        example.name,
+        style: const TextStyle(
+          fontSize: kTitleFontSize,
+          fontWeight: kBoldWeight,
+        ),
+      );
+
+  Widget get description => Text(example.description);
+
+  Widget getViewOnGithub(BuildContext context) {
+    AppLocalizations appLocale = AppLocalizations.of(context)!;
+    return TextButton.icon(
+      icon: SvgPicture.asset(kGithubIconAsset),
+      onPressed: () {
+        launch(example.link ?? '');
+      },
+      label: Text(appLocale.viewOnGithub),
+    );
+  }
 }
diff --git 
a/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart
 
b/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart
index d0eb6f7..d2e60a9 100644
--- 
a/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart
+++ 
b/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart
@@ -28,6 +28,8 @@ class DescriptionPopoverButton extends StatelessWidget {
   final ExampleModel example;
   final Alignment followerAnchor;
   final Alignment targetAnchor;
+  final void Function()? onOpen;
+  final void Function()? onClose;
 
   const DescriptionPopoverButton({
     Key? key,
@@ -35,6 +37,8 @@ class DescriptionPopoverButton extends StatelessWidget {
     required this.example,
     required this.followerAnchor,
     required this.targetAnchor,
+    this.onOpen,
+    this.onClose,
   }) : super(key: key);
 
   @override
@@ -62,12 +66,15 @@ class DescriptionPopoverButton extends StatelessWidget {
     ExampleModel example,
     Alignment followerAnchor,
     Alignment targetAnchor,
-  ) {
+  ) async {
     // close previous description dialog
     Navigator.of(context, rootNavigator: true).popUntil((route) {
       return route.isFirst;
     });
-    showAlignedDialog(
+    if (onOpen != null) {
+      onOpen!();
+    }
+    await showAlignedDialog(
       context: context,
       builder: (dialogContext) => DescriptionPopover(
         example: example,
@@ -76,5 +83,8 @@ class DescriptionPopoverButton extends StatelessWidget {
       targetAnchor: targetAnchor,
       barrierColor: Colors.transparent,
     );
+    if (onClose != null) {
+      onClose!();
+    }
   }
 }
diff --git 
a/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart
 
b/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart
new file mode 100644
index 0000000..9cae77f
--- /dev/null
+++ 
b/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart
@@ -0,0 +1,66 @@
+/*
+ * 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:playground/modules/examples/components/description_popover/description_popover_button.dart';
+import 'package:playground/modules/examples/models/example_model.dart';
+import 'package:playground/modules/examples/models/popover_state.dart';
+import 'package:provider/provider.dart';
+
+import '../multifile_popover/multifile_popover_button.dart';
+
+class ExampleItemActions extends StatelessWidget {
+  final ExampleModel example;
+  final BuildContext parentContext;
+
+  const ExampleItemActions(
+      {Key? key, required this.parentContext, required this.example})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        if (example.isMultiFile) multifilePopover,
+        descriptionPopover,
+      ],
+    );
+  }
+
+  Widget get multifilePopover => MultifilePopoverButton(
+        parentContext: parentContext,
+        example: example,
+        followerAnchor: Alignment.topLeft,
+        targetAnchor: Alignment.topRight,
+        onOpen: () => _setPopoverOpen(parentContext, true),
+        onClose: () => _setPopoverOpen(parentContext, false),
+      );
+
+  Widget get descriptionPopover => DescriptionPopoverButton(
+        parentContext: parentContext,
+        example: example,
+        followerAnchor: Alignment.topLeft,
+        targetAnchor: Alignment.topRight,
+        onOpen: () => _setPopoverOpen(parentContext, true),
+        onClose: () => _setPopoverOpen(parentContext, false),
+      );
+
+  void _setPopoverOpen(BuildContext context, bool isOpen) {
+    Provider.of<PopoverState>(context, listen: false).setOpen(isOpen);
+  }
+}
diff --git 
a/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart
 
b/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart
index 74a6e23..7f29c44 100644
--- 
a/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart
+++ 
b/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart
@@ -19,7 +19,7 @@
 import 'package:flutter/material.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/analytics/analytics_service.dart';
-import 
'package:playground/modules/examples/components/description_popover/description_popover_button.dart';
+import 
'package:playground/modules/examples/components/example_list/example_item_actions.dart';
 import 'package:playground/modules/examples/models/example_model.dart';
 import 'package:playground/pages/playground/states/examples_state.dart';
 import 'package:playground/pages/playground/states/playground_state.dart';
@@ -72,12 +72,7 @@ class ExpansionPanelItem extends StatelessWidget {
                         ? const TextStyle(fontWeight: FontWeight.bold)
                         : const TextStyle(),
                   ),
-                  DescriptionPopoverButton(
-                    parentContext: context,
-                    example: example,
-                    followerAnchor: Alignment.topLeft,
-                    targetAnchor: Alignment.topRight,
-                  ),
+                  ExampleItemActions(parentContext: context, example: example),
                 ],
               ),
             ),
diff --git 
a/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart
 
b/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover.dart
similarity index 64%
copy from 
playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart
copy to 
playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover.dart
index 6a37c8f..00db2d7 100644
--- 
a/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart
+++ 
b/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover.dart
@@ -17,35 +17,47 @@
  */
 
 import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:playground/constants/assets.dart';
 import 'package:playground/constants/font_weight.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/examples/models/example_model.dart';
+import 'package:url_launcher/url_launcher.dart';
 
-const kDescriptionWidth = 300.0;
+const kMultifileWidth = 300.0;
 
-class DescriptionPopover extends StatelessWidget {
+class MultifilePopover extends StatelessWidget {
   final ExampleModel example;
 
-  const DescriptionPopover({Key? key, required this.example}) : super(key: 
key);
+  const MultifilePopover({Key? key, required this.example}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
+    AppLocalizations appLocale = AppLocalizations.of(context)!;
     return SizedBox(
-      width: kDescriptionWidth,
+      width: kMultifileWidth,
       child: Card(
         child: Padding(
           padding: const EdgeInsets.all(kLgSpacing),
           child: Wrap(
-            runSpacing: kSmSpacing,
+            runSpacing: kMdSpacing,
             children: [
               Text(
-                example.name,
+                appLocale.multifile,
                 style: const TextStyle(
                   fontSize: kTitleFontSize,
                   fontWeight: kBoldWeight,
                 ),
               ),
-              Text(example.description),
+              Text(appLocale.multifileWarning),
+              TextButton.icon(
+                icon: SvgPicture.asset(kGithubIconAsset),
+                onPressed: () {
+                  launch(example.link ?? '');
+                },
+                label: Text(appLocale.viewOnGithub),
+              ),
             ],
           ),
         ),
diff --git 
a/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart
 
b/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover_button.dart
similarity index 73%
copy from 
playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart
copy to 
playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover_button.dart
index d0eb6f7..149194d 100644
--- 
a/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart
+++ 
b/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover_button.dart
@@ -18,23 +18,28 @@
 
 import 'package:aligned_dialog/aligned_dialog.dart';
 import 'package:flutter/material.dart';
-import 'package:playground/config/theme.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:playground/constants/assets.dart';
 import 'package:playground/constants/sizes.dart';
-import 
'package:playground/modules/examples/components/description_popover/description_popover.dart';
+import 
'package:playground/modules/examples/components/multifile_popover/multifile_popover.dart';
 import 'package:playground/modules/examples/models/example_model.dart';
 
-class DescriptionPopoverButton extends StatelessWidget {
+class MultifilePopoverButton extends StatelessWidget {
   final BuildContext? parentContext;
   final ExampleModel example;
   final Alignment followerAnchor;
   final Alignment targetAnchor;
+  final void Function()? onOpen;
+  final void Function()? onClose;
 
-  const DescriptionPopoverButton({
+  const MultifilePopoverButton({
     Key? key,
     this.parentContext,
     required this.example,
     required this.followerAnchor,
     required this.targetAnchor,
+    this.onOpen,
+    this.onClose,
   }) : super(key: key);
 
   @override
@@ -42,12 +47,9 @@ class DescriptionPopoverButton extends StatelessWidget {
     return IconButton(
       iconSize: kIconSizeMd,
       splashRadius: kIconButtonSplashRadius,
-      icon: Icon(
-        Icons.info_outline_rounded,
-        color: ThemeColors.of(context).grey1Color,
-      ),
+      icon: SvgPicture.asset(kMultifileIconAsset),
       onPressed: () {
-        _showDescriptionPopover(
+        _showMultifilePopover(
           parentContext ?? context,
           example,
           followerAnchor,
@@ -57,24 +59,30 @@ class DescriptionPopoverButton extends StatelessWidget {
     );
   }
 
-  void _showDescriptionPopover(
+  void _showMultifilePopover(
     BuildContext context,
     ExampleModel example,
     Alignment followerAnchor,
     Alignment targetAnchor,
-  ) {
-    // close previous description dialog
+  ) async {
+    // close previous dialogs
     Navigator.of(context, rootNavigator: true).popUntil((route) {
       return route.isFirst;
     });
-    showAlignedDialog(
+    if (onOpen != null) {
+      onOpen!();
+    }
+    await showAlignedDialog(
       context: context,
-      builder: (dialogContext) => DescriptionPopover(
+      builder: (dialogContext) => MultifilePopover(
         example: example,
       ),
       followerAnchor: followerAnchor,
       targetAnchor: targetAnchor,
       barrierColor: Colors.transparent,
     );
+    if (onClose != null) {
+      onClose!();
+    }
   }
 }
diff --git a/playground/frontend/lib/config.g.dart 
b/playground/frontend/lib/modules/examples/components/outside_click_handler.dart
similarity index 52%
copy from playground/frontend/lib/config.g.dart
copy to 
playground/frontend/lib/modules/examples/components/outside_click_handler.dart
index e189b32..16db300 100644
--- a/playground/frontend/lib/config.g.dart
+++ 
b/playground/frontend/lib/modules/examples/components/outside_click_handler.dart
@@ -16,14 +16,29 @@
  * limitations under the License.
  */
 
-const String kApiClientURL =
-    'https://backend-dot-datatokenization.uc.r.appspot.com';
-const String kAnalyticsUA = 'UA-73650088-1';
-const String kApiJavaClientURL =
-    'https://backend-dot-datatokenization.uc.r.appspot.com/java/';
-const String kApiGoClientURL =
-    'https://backend-dot-datatokenization.uc.r.appspot.com/go/';
-const String kApiPythonClientURL =
-    'https://backend-dot-datatokenization.uc.r.appspot.com/python/';
-const String kApiScioClientURL =
-    'https://backend-dot-datatokenization.uc.r.appspot.com/scio/';
+import 'package:flutter/material.dart';
+import 'package:playground/modules/examples/models/popover_state.dart';
+import 'package:provider/provider.dart';
+
+class OutsideClickHandler extends StatelessWidget {
+  final void Function() onTap;
+
+  const OutsideClickHandler({Key? key, required this.onTap}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Consumer<PopoverState>(builder: (context, state, child) {
+      if (state.isOpen) {
+        return Container();
+      }
+      return GestureDetector(
+        onTap: onTap,
+        child: Container(
+          color: Colors.transparent,
+          height: double.infinity,
+          width: double.infinity,
+        ),
+      );
+    });
+  }
+}
diff --git a/playground/frontend/lib/modules/examples/example_selector.dart 
b/playground/frontend/lib/modules/examples/example_selector.dart
index 1994a65..8824d6e 100644
--- a/playground/frontend/lib/modules/examples/example_selector.dart
+++ b/playground/frontend/lib/modules/examples/example_selector.dart
@@ -17,21 +17,26 @@
  */
 
 import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 
'package:playground/components/loading_indicator/loading_indicator.dart';
 import 'package:playground/config/theme.dart';
+import 'package:playground/constants/links.dart';
 import 'package:playground/constants/sizes.dart';
 import 
'package:playground/modules/examples/components/examples_components.dart';
+import 
'package:playground/modules/examples/components/outside_click_handler.dart';
+import 'package:playground/modules/examples/models/popover_state.dart';
 import 'package:playground/modules/examples/models/selector_size_model.dart';
 import 
'package:playground/pages/playground/states/example_selector_state.dart';
 import 'package:playground/pages/playground/states/examples_state.dart';
 import 'package:playground/pages/playground/states/playground_state.dart';
 import 'package:provider/provider.dart';
+import 'package:url_launcher/url_launcher.dart';
 
 const int kAnimationDurationInMilliseconds = 80;
 const Offset kAnimationBeginOffset = Offset(0.0, -0.02);
 const Offset kAnimationEndOffset = Offset(0.0, 0.0);
 const double kAdditionalDyAlignment = 50.0;
-const double kLgContainerHeight = 444.0;
+const double kLgContainerHeight = 490.0;
 const double kLgContainerWidth = 400.0;
 
 class ExampleSelector extends StatefulWidget {
@@ -121,60 +126,63 @@ class _ExampleSelectorState extends State<ExampleSelector>
 
     return OverlayEntry(
       builder: (context) {
-        return Consumer2<ExampleState, PlaygroundState>(
-          builder: (context, exampleState, playgroundState, child) => Stack(
-            children: [
-              GestureDetector(
-                onTap: () {
-                  closeDropdown(exampleState);
-                  // handle description dialogs
-                  Navigator.of(context, rootNavigator: true).popUntil((route) {
-                    return route.isFirst;
-                  });
-                },
-                child: Container(
-                  color: Colors.transparent,
-                  height: double.infinity,
-                  width: double.infinity,
-                ),
-              ),
-              ChangeNotifierProvider(
-                create: (context) => ExampleSelectorState(
-                  exampleState,
-                  playgroundState,
-                  exampleState.getCategories(playgroundState.sdk)!,
-                ),
-                builder: (context, _) => Positioned(
-                  left: posModel.xAlignment,
-                  top: posModel.yAlignment + kAdditionalDyAlignment,
-                  child: SlideTransition(
-                    position: offsetAnimation,
-                    child: Material(
-                      elevation: kElevation.toDouble(),
-                      child: Container(
-                        height: kLgContainerHeight,
-                        width: kLgContainerWidth,
-                        decoration: BoxDecoration(
-                          color: Theme.of(context).backgroundColor,
-                          borderRadius: BorderRadius.circular(kMdBorderRadius),
+        return ChangeNotifierProvider<PopoverState>(
+          create: (context) => PopoverState(false),
+          builder: (context, state) {
+            return Consumer2<ExampleState, PlaygroundState>(
+              builder: (context, exampleState, playgroundState, child) => 
Stack(
+                children: [
+                  OutsideClickHandler(
+                    onTap: () {
+                      closeDropdown(exampleState);
+                      // handle description dialogs
+                      Navigator.of(context, rootNavigator: 
true).popUntil((route) {
+                        return route.isFirst;
+                      });
+                    },
+                  ),
+                  ChangeNotifierProvider(
+                    create: (context) => ExampleSelectorState(
+                      exampleState,
+                      playgroundState,
+                      exampleState.getCategories(playgroundState.sdk)!,
+                    ),
+                    builder: (context, _) => Positioned(
+                      left: posModel.xAlignment,
+                      top: posModel.yAlignment + kAdditionalDyAlignment,
+                      child: SlideTransition(
+                        position: offsetAnimation,
+                        child: Material(
+                          elevation: kElevation.toDouble(),
+                          child: Container(
+                            height: kLgContainerHeight,
+                            width: kLgContainerWidth,
+                            decoration: BoxDecoration(
+                              color: Theme.of(context).backgroundColor,
+                              borderRadius: 
BorderRadius.circular(kMdBorderRadius),
+                            ),
+                            child: exampleState.sdkCategories == null ||
+                                    playgroundState.selectedExample == null
+                                ? const LoadingIndicator(size: 
kContainerHeight)
+                                : _buildDropdownContent(context, 
playgroundState),
+                          ),
                         ),
-                        child: exampleState.sdkCategories == null ||
-                                playgroundState.selectedExample == null
-                            ? const LoadingIndicator(size: kContainerHeight)
-                            : _buildDropdownContent(playgroundState),
                       ),
                     ),
                   ),
-                ),
+                ],
               ),
-            ],
-          ),
+            );
+          }
         );
       },
     );
   }
 
-  Widget _buildDropdownContent(PlaygroundState playgroundState) {
+  Widget _buildDropdownContent(
+    BuildContext context,
+    PlaygroundState playgroundState,
+  ) {
     return Column(
       children: [
         SearchField(controller: textController),
@@ -185,6 +193,28 @@ class _ExampleSelectorState extends State<ExampleSelector>
           animationController: animationController,
           dropdown: examplesDropdown,
         ),
+        Divider(
+          height: kDividerHeight,
+          color: ThemeColors.of(context).greyColor,
+          indent: kLgSpacing,
+          endIndent: kLgSpacing,
+        ),
+        SizedBox(
+          width: double.infinity,
+          child: TextButton(
+            child: Padding(
+              padding: const EdgeInsets.all(kXlSpacing),
+              child: Align(
+                alignment: Alignment.centerLeft,
+                child: Text(
+                  AppLocalizations.of(context)!.addExample,
+                  style: TextStyle(color: ThemeColors.of(context).primary),
+                ),
+              ),
+            ),
+            onPressed: () => launch(kAddExampleLink),
+          ),
+        )
       ],
     );
   }
diff --git a/playground/frontend/lib/modules/examples/models/example_model.dart 
b/playground/frontend/lib/modules/examples/models/example_model.dart
index ff95440..e6f850a 100644
--- a/playground/frontend/lib/modules/examples/models/example_model.dart
+++ b/playground/frontend/lib/modules/examples/models/example_model.dart
@@ -43,6 +43,8 @@ class ExampleModel with Comparable<ExampleModel> {
   final String name;
   final String path;
   final String description;
+  bool isMultiFile;
+  String? link;
   String? source;
   String? outputs;
   String? logs;
@@ -54,6 +56,8 @@ class ExampleModel with Comparable<ExampleModel> {
     required this.path,
     required this.description,
     required this.type,
+    this.isMultiFile = false,
+    this.link,
     this.source,
     this.outputs,
     this.logs,
diff --git a/playground/frontend/lib/constants/links.dart 
b/playground/frontend/lib/modules/examples/models/popover_state.dart
similarity index 61%
copy from playground/frontend/lib/constants/links.dart
copy to playground/frontend/lib/modules/examples/models/popover_state.dart
index e3b34eb..af51052 100644
--- a/playground/frontend/lib/constants/links.dart
+++ b/playground/frontend/lib/modules/examples/models/popover_state.dart
@@ -16,11 +16,17 @@
  * limitations under the License.
  */
 
-const kReportIssueLink = 
'https://issues.apache.org/jira/projects/BEAM/issues/';
-const kBeamPrivacyPolicyLink = 'https://beam.apache.org/privacy_policy/';
-const kBeamPlaygroundGithubLink =
-    'https://github.com/apache/beam/tree/master/playground';
-const kApacheBeamGithubLink = 'https://github.com/apache/beam';
-const kBeamWebsiteLink = 'https://beam.apache.org/';
-const kScioGithubLink = 'https://github.com/spotify/scio';
-const kAboutBeamLink = 'https://beam.apache.org/get-started/beam-overview';
+import 'package:flutter/cupertino.dart';
+
+class PopoverState extends ChangeNotifier {
+  bool _isOpen;
+
+  bool get isOpen => _isOpen;
+
+  PopoverState(this._isOpen);
+
+  setOpen(bool open) {
+    _isOpen = open;
+    notifyListeners();
+  }
+}
diff --git 
a/playground/frontend/lib/modules/examples/repositories/example_client/grpc_example_client.dart
 
b/playground/frontend/lib/modules/examples/repositories/example_client/grpc_example_client.dart
index 2b91d91..672b2e5 100644
--- 
a/playground/frontend/lib/modules/examples/repositories/example_client/grpc_example_client.dart
+++ 
b/playground/frontend/lib/modules/examples/repositories/example_client/grpc_example_client.dart
@@ -224,6 +224,8 @@ class GrpcExampleClient implements ExampleClient {
       type: _exampleTypeFromString(example.type),
       path: example.cloudPath,
       pipelineOptions: example.pipelineOptions,
+      isMultiFile: example.multifile,
+      link: example.link,
     );
   }
 }
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 52ab98f..4f88f3d 100644
--- 
a/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart
+++ 
b/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart
@@ -23,6 +23,7 @@ import 
'package:playground/modules/analytics/analytics_service.dart';
 import 'package:playground/modules/editor/components/editor_textarea.dart';
 import 'package:playground/modules/editor/components/run_button.dart';
 import 
'package:playground/modules/examples/components/description_popover/description_popover_button.dart';
+import 
'package:playground/modules/examples/components/multifile_popover/multifile_popover_button.dart';
 import 'package:playground/modules/examples/models/example_model.dart';
 import 'package:playground/modules/notifications/components/notification.dart';
 import 'package:playground/modules/sdk/models/sdk.dart';
@@ -53,7 +54,7 @@ class CodeTextAreaWrapper extends StatelessWidget {
               children: [
                 Positioned.fill(
                   child: EditorTextArea(
-                    enabled: true,
+                    enabled: !(state.selectedExample?.isMultiFile ?? false),
                     example: state.selectedExample,
                     sdk: state.sdk,
                     onSourceChange: state.setSource,
@@ -66,13 +67,21 @@ class CodeTextAreaWrapper extends StatelessWidget {
                   height: kButtonHeight,
                   child: Row(
                     children: [
-                      if (state.selectedExample != null)
+                      if (state.selectedExample != null) ...[
+                        if (state.selectedExample?.isMultiFile ?? false)
+                          MultifilePopoverButton(
+                            example: state.selectedExample!,
+                            followerAnchor: Alignment.topRight,
+                            targetAnchor: Alignment.bottomRight,
+                          ),
                         DescriptionPopoverButton(
                           example: state.selectedExample!,
                           followerAnchor: Alignment.topRight,
                           targetAnchor: Alignment.bottomRight,
                         ),
+                      ],
                       RunButton(
+                        disabled: state.selectedExample?.isMultiFile ?? false,
                         isRunning: state.isCodeRunning,
                         cancelRun: () {
                           state.cancelRun().catchError(

Reply via email to