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 a4fcd93  Merge pull request #16505 from [BEAM-13527] [Playground] 
Pipeline options dialog
a4fcd93 is described below

commit a4fcd939dcfa35ad2517e5c7f05eb5bb1333f66f
Author: Aydar Farrakhov <stranni...@gmail.com>
AuthorDate: Fri Jan 28 21:34:33 2022 +0300

    Merge pull request #16505 from [BEAM-13527] [Playground] Pipeline options 
dialog
    
    * [BEAM-13527] pipeline options dropdown
    
    * [BEAM-13527] playground - parse pipeline error message
    
    * [BEAM-13527] playground - fix parse options
    
    * [BEAM-13527] playground - move pipelines options lines count to const
    
    * [BEAM-13527] playground fix tests
    
    * fix merge
    
    * [BEAM-13527] pipeline options fix review comments
    
    * [BEAM-13527] pipeline options fix review comments
---
 playground/frontend/lib/config/theme.dart          |  16 ++
 playground/frontend/lib/constants/sizes.dart       |   3 +-
 playground/frontend/lib/l10n/app_en.arb            |  36 ++++
 .../pipeline_option_label.dart                     |  35 ++++
 .../pipeline_option_model.dart                     |  29 +++
 .../pipeline_options_dropdown.dart                 |  51 +++++
 .../pipeline_options_dropdown_body.dart            | 229 +++++++++++++++++++++
 .../pipeline_options_dropdown_input.dart           |  48 +++++
 .../pipeline_options_dropdown_separator.dart       |  35 ++++
 .../pipeline_options_form.dart                     |  86 ++++++++
 .../pipeline_options_text_field.dart               |  66 ++++++
 .../components/pipeline_options_text_field.dart    |  81 --------
 .../lib/modules/editor/components/run_button.dart  |   2 +-
 .../components/editor_textarea_wrapper.dart        |   7 +-
 .../lib/pages/playground/playground_page.dart      |   7 +-
 .../pages/playground/states/playground_state.dart  |   1 +
 playground/frontend/pubspec.lock                   |   2 +-
 playground/frontend/pubspec.yaml                   |   1 +
 18 files changed, 644 insertions(+), 91 deletions(-)

diff --git a/playground/frontend/lib/config/theme.dart 
b/playground/frontend/lib/config/theme.dart
index fed037a..d9fd937 100644
--- a/playground/frontend/lib/config/theme.dart
+++ b/playground/frontend/lib/config/theme.dart
@@ -70,6 +70,17 @@ TextButtonThemeData createTextButtonTheme(Color textColor) {
   );
 }
 
+OutlinedButtonThemeData createOutlineButtonTheme(Color textColor) {
+  return OutlinedButtonThemeData(
+    style: OutlinedButton.styleFrom(
+      primary: textColor,
+      shape: const RoundedRectangleBorder(
+        borderRadius: BorderRadius.all(Radius.circular(kSmBorderRadius)),
+      ),
+    ),
+  );
+}
+
 ElevatedButtonThemeData createElevatedButtonTheme(Color primaryColor) {
   return ElevatedButtonThemeData(
     style: ElevatedButton.styleFrom(primary: primaryColor),
@@ -95,9 +106,12 @@ AppBarTheme createAppBarTheme(Color backgroundColor) {
 }
 
 TabBarTheme createTabBarTheme(Color textColor, Color indicatorColor) {
+  const labelStyle = TextStyle(fontWeight: kMediumWeight);
   return TabBarTheme(
     unselectedLabelColor: textColor,
     labelColor: textColor,
+    labelStyle: labelStyle,
+    unselectedLabelStyle: labelStyle,
     indicator: UnderlineTabIndicator(
       borderSide: BorderSide(width: 2.0, color: indicatorColor),
     ),
@@ -122,6 +136,7 @@ final kLightTheme = ThemeData(
   textTheme: createTextTheme(kLightText),
   popupMenuTheme: createPopupMenuTheme(),
   textButtonTheme: createTextButtonTheme(kLightText),
+  outlinedButtonTheme: createOutlineButtonTheme(kLightText),
   elevatedButtonTheme: createElevatedButtonTheme(kLightPrimary),
   tabBarTheme: createTabBarTheme(kLightText, kLightPrimary),
   dialogTheme: createDialogTheme(kLightText),
@@ -135,6 +150,7 @@ final kDarkTheme = ThemeData(
   textTheme: createTextTheme(kDarkText),
   popupMenuTheme: createPopupMenuTheme(),
   textButtonTheme: createTextButtonTheme(kDarkText),
+  outlinedButtonTheme: createOutlineButtonTheme(kDarkText),
   elevatedButtonTheme: createElevatedButtonTheme(kDarkPrimary),
   tabBarTheme: createTabBarTheme(kDarkText, kDarkPrimary),
   dialogTheme: createDialogTheme(kDarkText),
diff --git a/playground/frontend/lib/constants/sizes.dart 
b/playground/frontend/lib/constants/sizes.dart
index 24728b0..3962a19 100644
--- a/playground/frontend/lib/constants/sizes.dart
+++ b/playground/frontend/lib/constants/sizes.dart
@@ -27,7 +27,7 @@ const double kXxlSpacing = 36.0;
 // sizes
 const kHeaderButtonHeight = 46.0;
 const kRunButtonWidth = 150.0;
-const kRunButtonHeight = 40.0;
+const kButtonHeight = 40.0;
 const kIconButtonSplashRadius = 24.0;
 const kFooterHeight = 32.0;
 
@@ -59,3 +59,4 @@ const double kTitleFontSize = 18.0;
 
 //divider size
 const double kDividerHeight = 1.0;
+const double kLgDividerHeight = 2.0;
diff --git a/playground/frontend/lib/l10n/app_en.arb 
b/playground/frontend/lib/l10n/app_en.arb
index fdb6553..df18234 100644
--- a/playground/frontend/lib/l10n/app_en.arb
+++ b/playground/frontend/lib/l10n/app_en.arb
@@ -154,5 +154,41 @@
   "clearOutput": "Clear Output",
   "@clearOutput": {
     "description": "Title for the Clear Output shortcut row"
+  },
+  "pipelineOptions": "Pipeline Options",
+  "@pipelineOptions": {
+    "description": "Title for the Pipeline Options"
+  },
+  "rawPipelineOptions": "Raw",
+  "@rawPipelineOptions": {
+    "description": "Title for the Raw Pipeline Options Tab"
+  },
+  "optionsPipelineOptions": "Options",
+  "@optionsPipelineOptions": {
+    "description": "Title for the Options Pipeline Options Tab"
+  },
+  "saveAndClose": "Save & close",
+  "@saveAndClose": {
+    "description": "Text for save and close button on pipeline dropdown"
+  },
+  "addPipelineOptionParameter": "Add parameter",
+  "@addPipelineOptionParameter": {
+    "description": "Text for add parameter button on pipeline dropdown"
+  },
+  "input": "Input",
+  "@input": {
+    "description": "Text input label"
+  },
+  "name": "Name",
+  "@name": {
+    "description": "Text name label"
+  },
+  "value": "Value",
+  "@value": {
+    "description": "Text value label"
+  },
+  "pipelineOptionsError": "Please check the format (example: --key1 value1 
--key2 value2), only alphanumeric and \",*,/,-,:,;,',. symbols are allowed",
+  "@value": {
+    "description": "Pipeline options parse error"
   }
 }
\ No newline at end of file
diff --git 
a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_option_label.dart
 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_option_label.dart
new file mode 100644
index 0000000..733c433
--- /dev/null
+++ 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_option_label.dart
@@ -0,0 +1,35 @@
+/*
+ * 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/constants/font_weight.dart';
+import 'package:playground/constants/sizes.dart';
+
+class PipelineOptionLabel extends StatelessWidget {
+  final String text;
+
+  const PipelineOptionLabel({Key? key, required this.text}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Text(
+      text,
+      style: const TextStyle(fontWeight: kMediumWeight, fontSize: 
kLabelFontSize),
+    );
+  }
+}
diff --git 
a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_option_model.dart
 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_option_model.dart
new file mode 100644
index 0000000..8c8a059
--- /dev/null
+++ 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_option_model.dart
@@ -0,0 +1,29 @@
+/*
+ * 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/cupertino.dart';
+
+class PipelineOptionController {
+  final TextEditingController name = TextEditingController();
+  final TextEditingController value = TextEditingController();
+
+  PipelineOptionController({String name = '', String value = ''}) {
+    this.name.text = name;
+    this.value.text = value;
+  }
+}
diff --git 
a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown.dart
 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown.dart
new file mode 100644
index 0000000..acca8708
--- /dev/null
+++ 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown.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 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:playground/components/dropdown_button/dropdown_button.dart';
+import 
'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart';
+
+const kDropdownWidth = 400.0;
+const kDropdownHeight = 375.0;
+
+class PipelineOptionsDropdown extends StatelessWidget {
+  final String pipelineOptions;
+  final Function(String) setPipelineOptions;
+
+  const PipelineOptionsDropdown({
+    Key? key,
+    required this.pipelineOptions,
+    required this.setPipelineOptions,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    AppLocalizations appLocale = AppLocalizations.of(context)!;
+    return AppDropdownButton(
+      buttonText: Text(appLocale.pipelineOptions),
+      height: kDropdownHeight,
+      width: kDropdownWidth,
+      createDropdown: (close) => PipelineOptionsDropdownBody(
+        pipelineOptions: pipelineOptions,
+        setPipelineOptions: setPipelineOptions,
+        close: close,
+      ),
+    );
+  }
+}
diff --git 
a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart
 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart
new file mode 100644
index 0000000..f2ae442
--- /dev/null
+++ 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart
@@ -0,0 +1,229 @@
+/*
+ * 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_gen/gen_l10n/app_localizations.dart';
+import 'package:playground/config/theme.dart';
+import 'package:playground/constants/colors.dart';
+import 'package:playground/constants/sizes.dart';
+import 
'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_option_model.dart';
+import 
'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_input.dart';
+import 
'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart';
+import 
'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart';
+import 'package:playground/modules/editor/parsers/run_options_parser.dart';
+import 'package:playground/modules/notifications/components/notification.dart';
+
+const kOptionsTabIndex = 0;
+const kRawTabIndex = 1;
+
+final kDefaultOption = [PipelineOptionController()];
+
+class PipelineOptionsDropdownBody extends StatefulWidget {
+  final String pipelineOptions;
+  final Function(String) setPipelineOptions;
+  final Function close;
+
+  const PipelineOptionsDropdownBody({
+    Key? key,
+    required this.pipelineOptions,
+    required this.setPipelineOptions,
+    required this.close,
+  }) : super(key: key);
+
+  @override
+  State<PipelineOptionsDropdownBody> createState() =>
+      _PipelineOptionsDropdownBodyState();
+}
+
+class _PipelineOptionsDropdownBodyState
+    extends State<PipelineOptionsDropdownBody>
+    with SingleTickerProviderStateMixin {
+  late final TabController tabController;
+  final TextEditingController pipelineOptionsController =
+      TextEditingController();
+  List<PipelineOptionController> pipelineOptionsList = kDefaultOption;
+  int selectedTab = kOptionsTabIndex;
+  bool showError = false;
+
+  @override
+  void initState() {
+    tabController = TabController(vsync: this, length: 2);
+    tabController.addListener(onTabChange);
+    pipelineOptionsController.text = widget.pipelineOptions;
+    pipelineOptionsList = _pipelineOptionsMapToList(widget.pipelineOptions);
+    if (pipelineOptionsList.isEmpty) {
+      pipelineOptionsList = kDefaultOption;
+    }
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    tabController.removeListener(onTabChange);
+    tabController.dispose();
+    super.dispose();
+  }
+
+  onTabChange() {
+    setState(() {
+      selectedTab = tabController.index;
+    });
+    if (tabController.index == kRawTabIndex) {
+      _updateRawValue();
+    } else {
+      _updateFormValue();
+    }
+  }
+
+  onDelete(int index) {
+    setState(() {
+      pipelineOptionsList.removeAt(index);
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    AppLocalizations appLocale = AppLocalizations.of(context)!;
+    return Column(
+      children: [
+        TabBar(
+          controller: tabController,
+          tabs: <Widget>[
+            Tab(text: appLocale.optionsPipelineOptions),
+            Tab(text: appLocale.rawPipelineOptions),
+          ],
+        ),
+        const PipelineOptionsDropdownSeparator(),
+        Expanded(
+          child: Padding(
+            padding: const EdgeInsets.all(kXlSpacing),
+            child: TabBarView(
+              controller: tabController,
+              physics: const NeverScrollableScrollPhysics(),
+              children: <Widget>[
+                PipelineOptionsForm(
+                  options: pipelineOptionsList,
+                  onDelete: onDelete,
+                ),
+                PipelineOptionsDropdownInput(
+                  controller: pipelineOptionsController,
+                ),
+              ],
+            ),
+          ),
+        ),
+        const PipelineOptionsDropdownSeparator(),
+        Padding(
+          padding: const EdgeInsets.all(kXlSpacing),
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              SizedBox(
+                height: kButtonHeight,
+                child: ElevatedButton(
+                  child: Text(appLocale.saveAndClose),
+                  onPressed: () => _save(context),
+                ),
+              ),
+              const SizedBox(width: kLgSpacing),
+              if (selectedTab == kOptionsTabIndex)
+                SizedBox(
+                  height: kButtonHeight,
+                  child: OutlinedButton(
+                    child: Text(appLocale.addPipelineOptionParameter),
+                    onPressed: () => setState(() {
+                      pipelineOptionsList.add(PipelineOptionController());
+                    }),
+                  ),
+                ),
+              if (showError && selectedTab == kRawTabIndex)
+                Flexible(
+                  child: Text(
+                    appLocale.pipelineOptionsError,
+                    style: Theme.of(context)
+                        .textTheme
+                        .caption
+                        !.copyWith(color: kErrorNotificationColor),
+                    softWrap: true,
+                  ),
+                ),
+            ],
+          ),
+        )
+      ],
+    );
+  }
+
+  Map<String, String> get pipelineOptionsListValue {
+    final notEmptyOptions = pipelineOptionsList
+        .where((option) =>
+            option.name.text.isNotEmpty && option.value.text.isNotEmpty)
+        .toList();
+    return {for (var item in notEmptyOptions) item.name.text: item.value.text};
+  }
+
+  String get pipelineOptionsValue {
+    if (selectedTab == kRawTabIndex) {
+      return pipelineOptionsController.text;
+    }
+    return pipelineOptionsToString(pipelineOptionsListValue);
+  }
+
+  _save(BuildContext context) {
+    if (selectedTab == kRawTabIndex && !_isPipelineOptionsTextValid()) {
+      setState(() {
+        showError = true;
+      });
+      return;
+    }
+    widget.setPipelineOptions(pipelineOptionsValue);
+    widget.close();
+  }
+
+  bool _isPipelineOptionsTextValid() {
+    final options = pipelineOptionsController.text;
+    final parsedOptions = parsePipelineOptions(options);
+    return options.isEmpty || (parsedOptions != null);
+  }
+
+  _updateRawValue() {
+    if (pipelineOptionsListValue.isNotEmpty) {
+      pipelineOptionsController.text =
+          pipelineOptionsToString(pipelineOptionsListValue);
+    }
+  }
+
+  _updateFormValue() {
+    final parsedOptions =
+        _pipelineOptionsMapToList(pipelineOptionsController.text);
+    if (parsedOptions.isNotEmpty) {
+      setState(() {
+        pipelineOptionsList = parsedOptions;
+      });
+    }
+  }
+
+  List<PipelineOptionController> _pipelineOptionsMapToList(
+      String pipelineOptions) {
+    return parsePipelineOptions(pipelineOptions)
+            ?.entries
+            .map((e) => PipelineOptionController(name: e.key, value: e.value))
+            .toList() ??
+        [];
+  }
+}
diff --git 
a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_input.dart
 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_input.dart
new file mode 100644
index 0000000..c9cd125
--- /dev/null
+++ 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_input.dart
@@ -0,0 +1,48 @@
+/*
+ * 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_gen/gen_l10n/app_localizations.dart';
+import 
'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_option_label.dart';
+import 
'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart';
+
+const kPipelineOptionsInputLines = 8;
+
+class PipelineOptionsDropdownInput extends StatelessWidget {
+  final TextEditingController controller;
+
+  const PipelineOptionsDropdownInput({
+    Key? key,
+    required this.controller,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    AppLocalizations appLocale = AppLocalizations.of(context)!;
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        PipelineOptionLabel(text: appLocale.input),
+        PipelineOptionsTextField(
+          lines: kPipelineOptionsInputLines,
+          controller: controller,
+        ),
+      ],
+    );
+  }
+}
diff --git 
a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart
 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart
new file mode 100644
index 0000000..17070dd
--- /dev/null
+++ 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart
@@ -0,0 +1,35 @@
+/*
+ * 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/config/theme.dart';
+import 'package:playground/constants/sizes.dart';
+
+class PipelineOptionsDropdownSeparator extends StatelessWidget {
+  const PipelineOptionsDropdownSeparator({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      height: kDividerHeight,
+      decoration: BoxDecoration(
+        color: ThemeColors.of(context).lightGreyColor,
+      ),
+    );
+  }
+}
diff --git 
a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart
 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart
new file mode 100644
index 0000000..1d2179f
--- /dev/null
+++ 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart
@@ -0,0 +1,86 @@
+/*
+ * 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_gen/gen_l10n/app_localizations.dart';
+import 'package:collection/collection.dart';
+import 'package:playground/config/theme.dart';
+import 'package:playground/constants/sizes.dart';
+import 
'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_option_label.dart';
+import 
'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_option_model.dart';
+import 
'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart';
+
+const kSpace = SizedBox(width: kMdSpacing);
+const kTextFieldHeight = 50.0;
+
+class PipelineOptionsForm extends StatelessWidget {
+  final List<PipelineOptionController> options;
+  final Function(int) onDelete;
+
+  const PipelineOptionsForm({
+    Key? key,
+    required this.options,
+    required this.onDelete,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    AppLocalizations appLocale = AppLocalizations.of(context)!;
+    return Column(
+      children: [
+        Row(
+          children: [
+            Expanded(child: PipelineOptionLabel(text: appLocale.name)),
+            kSpace,
+            Expanded(child: PipelineOptionLabel(text: appLocale.value)),
+            const SizedBox(width: kIconSizeLg),
+          ],
+        ),
+        ...options.mapIndexed(
+          (index, option) => Row(
+            children: [
+              Expanded(
+                child: SizedBox(
+                  height: kTextFieldHeight,
+                  child: PipelineOptionsTextField(controller: option.name),
+                ),
+              ),
+              kSpace,
+              Expanded(
+                child: SizedBox(
+                  height: kTextFieldHeight,
+                  child: PipelineOptionsTextField(controller: option.value),
+                ),
+              ),
+              SizedBox(
+                width: kIconSizeLg,
+                child: IconButton(
+                  iconSize: kIconSizeMd,
+                  splashRadius: kIconButtonSplashRadius,
+                  icon: const Icon(Icons.delete_outlined),
+                  color: ThemeColors.of(context).grey1Color,
+                  onPressed: () => onDelete(index),
+                ),
+              ),
+            ],
+          ),
+        )
+      ],
+    );
+  }
+}
diff --git 
a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart
 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart
new file mode 100644
index 0000000..2c11873
--- /dev/null
+++ 
b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.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/config/theme.dart';
+import 'package:playground/constants/sizes.dart';
+
+class PipelineOptionsTextField extends StatelessWidget {
+  final TextEditingController controller;
+  final int lines;
+
+  const PipelineOptionsTextField({
+    Key? key,
+    required this.controller,
+    this.lines = 1,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      margin: const EdgeInsets.only(
+        top: kMdSpacing,
+      ),
+      decoration: BoxDecoration(
+        color: Theme.of(context).backgroundColor,
+        borderRadius: BorderRadius.circular(kMdBorderRadius),
+      ),
+      child: ClipRRect(
+        borderRadius: BorderRadius.circular(kMdBorderRadius),
+        child: TextFormField(
+          minLines: lines,
+          maxLines: lines,
+          controller: controller,
+          decoration: InputDecoration(
+            contentPadding: const EdgeInsets.all(kMdSpacing),
+            border: _getInputBorder(ThemeColors.of(context).lightGreyColor),
+            focusedBorder: _getInputBorder(ThemeColors.of(context).primary),
+          ),
+          cursorColor: ThemeColors.of(context).textColor,
+        ),
+      ),
+    );
+  }
+
+  _getInputBorder(Color color) {
+    return OutlineInputBorder(
+      borderSide: BorderSide(color: color),
+      borderRadius: BorderRadius.circular(kMdBorderRadius),
+    );
+  }
+}
diff --git 
a/playground/frontend/lib/modules/editor/components/pipeline_options_text_field.dart
 
b/playground/frontend/lib/modules/editor/components/pipeline_options_text_field.dart
deleted file mode 100644
index f727721..0000000
--- 
a/playground/frontend/lib/modules/editor/components/pipeline_options_text_field.dart
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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_gen/gen_l10n/app_localizations.dart';
-import 'package:playground/config/theme.dart';
-import 'package:playground/constants/sizes.dart';
-
-class PipelineOptionsTextField extends StatefulWidget {
-  final String pipelineOptions;
-  final Function(String value) onChange;
-
-  const PipelineOptionsTextField(
-      {Key? key, required this.pipelineOptions, required this.onChange})
-      : super(key: key);
-
-  @override
-  State<PipelineOptionsTextField> createState() =>
-      _PipelineOptionsTextFieldState();
-}
-
-class _PipelineOptionsTextFieldState extends State<PipelineOptionsTextField> {
-  TextEditingController? _controller;
-
-  @override
-  void didChangeDependencies() {
-    _controller = TextEditingController(text: widget.pipelineOptions);
-    _controller?.addListener(() => widget.onChange(_controller?.text ?? ''));
-    super.didChangeDependencies();
-  }
-
-  @override
-  void dispose() {
-    super.dispose();
-    _controller?.dispose();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    AppLocalizations appLocale = AppLocalizations.of(context)!;
-
-    return TextField(
-      decoration: InputDecoration(
-        // it should be prefix props, but text inside prefix disappears 
without focus
-        icon: Padding(
-          padding:
-              const EdgeInsets.fromLTRB(kLgSpacing, kLgSpacing, 0, kLgSpacing),
-          child: Text(
-            appLocale.pipelineOptions,
-            style: TextStyle(
-              fontSize: kLabelFontSize,
-              color: ThemeColors.of(context).textColor,
-            ),
-          ),
-        ),
-        border: InputBorder.none,
-        hintText: appLocale.pipelineOptionsPlaceholder,
-        hintStyle: TextStyle(
-          fontSize: kHintFontSize,
-          color: ThemeColors.of(context).grey1Color,
-        ),
-      ),
-      controller: _controller,
-    );
-  }
-}
diff --git a/playground/frontend/lib/modules/editor/components/run_button.dart 
b/playground/frontend/lib/modules/editor/components/run_button.dart
index f29b014..6aecc1f 100644
--- a/playground/frontend/lib/modules/editor/components/run_button.dart
+++ b/playground/frontend/lib/modules/editor/components/run_button.dart
@@ -44,7 +44,7 @@ class RunButton extends StatelessWidget {
   Widget build(BuildContext context) {
     return SizedBox(
       width: kRunButtonWidth,
-      height: kRunButtonHeight,
+      height: kButtonHeight,
       child: ShortcutTooltip(
         shortcut: kRunShortcut,
         child: ElevatedButton.icon(
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 ca0d1e7..d854ad9 100644
--- 
a/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart
+++ 
b/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart
@@ -21,7 +21,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/analytics/analytics_service.dart';
 import 'package:playground/modules/editor/components/editor_textarea.dart';
-import 
'package:playground/modules/editor/components/pipeline_options_text_field.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/models/example_model.dart';
@@ -63,7 +62,7 @@ class CodeTextAreaWrapper extends StatelessWidget {
                 Positioned(
                   right: kXlSpacing,
                   top: kXlSpacing,
-                  height: kRunButtonHeight,
+                  height: kButtonHeight,
                   child: Row(
                     children: [
                       if (state.selectedExample != null)
@@ -104,10 +103,6 @@ class CodeTextAreaWrapper extends StatelessWidget {
               ],
             ),
           ),
-          PipelineOptionsTextField(
-            pipelineOptions: state.pipelineOptions,
-            onChange: state.setPipelineOptions,
-          ),
         ],
       );
     });
diff --git a/playground/frontend/lib/pages/playground/playground_page.dart 
b/playground/frontend/lib/pages/playground/playground_page.dart
index 09b4522..8af0437 100644
--- a/playground/frontend/lib/pages/playground/playground_page.dart
+++ b/playground/frontend/lib/pages/playground/playground_page.dart
@@ -23,6 +23,7 @@ import 'package:playground/constants/sizes.dart';
 import 'package:playground/modules/actions/components/new_example_action.dart';
 import 'package:playground/modules/actions/components/reset_action.dart';
 import 'package:playground/modules/analytics/analytics_service.dart';
+import 
'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown.dart';
 import 'package:playground/modules/examples/example_selector.dart';
 import 'package:playground/modules/sdk/components/sdk_selector.dart';
 import 
'package:playground/modules/shortcuts/components/shortcuts_manager.dart';
@@ -72,6 +73,10 @@ class PlaygroundPage extends StatelessWidget {
                       },
                       setExample: state.setExample,
                     ),
+                    PipelineOptionsDropdown(
+                      pipelineOptions: state.pipelineOptions,
+                      setPipelineOptions: state.setPipelineOptions,
+                    ),
                     const NewExampleAction(),
                     ResetAction(reset: state.reset),
                   ],
@@ -87,7 +92,7 @@ class PlaygroundPage extends StatelessWidget {
             ],
           ),
         ),
-      ),
+      )
     );
   }
 }
diff --git 
a/playground/frontend/lib/pages/playground/states/playground_state.dart 
b/playground/frontend/lib/pages/playground/states/playground_state.dart
index e4df474..d9e612e 100644
--- a/playground/frontend/lib/pages/playground/states/playground_state.dart
+++ b/playground/frontend/lib/pages/playground/states/playground_state.dart
@@ -120,6 +120,7 @@ class PlaygroundState with ChangeNotifier {
 
   setPipelineOptions(String options) {
     _pipelineOptions = options;
+    notifyListeners();
   }
 
   void runCode({Function? onFinish}) {
diff --git a/playground/frontend/pubspec.lock b/playground/frontend/pubspec.lock
index cb91023..dd9b4bd 100644
--- a/playground/frontend/pubspec.lock
+++ b/playground/frontend/pubspec.lock
@@ -156,7 +156,7 @@ packages:
     source: hosted
     version: "1.0.2"
   collection:
-    dependency: transitive
+    dependency: "direct dev"
     description:
       name: collection
       url: "https://pub.dartlang.org";
diff --git a/playground/frontend/pubspec.yaml b/playground/frontend/pubspec.yaml
index a518a2b..0d70124 100644
--- a/playground/frontend/pubspec.yaml
+++ b/playground/frontend/pubspec.yaml
@@ -66,6 +66,7 @@ dev_dependencies:
   build_runner: ^2.1.4
   google_fonts: ^2.1.0
   usage: ^4.0.2
+  collection: ^1.15.0
 
   # The "flutter_lints" package below contains a set of recommended lints to
   # encourage good coding practices. The lint set provided by the package is

Reply via email to