diff --git a/playground/frontend/lib/constants/sizes.dart b/playground/frontend/lib/constants/sizes.dart index c73097f4cc21..b5fcc2392395 100644 --- a/playground/frontend/lib/constants/sizes.dart +++ b/playground/frontend/lib/constants/sizes.dart @@ -41,6 +41,7 @@ const double kXlBorderRadius = 28.0; const double kElevation = 2; // icon sizes +const double kIconSizeXs = 8.0; const double kIconSizeSm = 16.0; const double kIconSizeMd = 24.0; const double kIconSizeLg = 32.0; diff --git a/playground/frontend/lib/modules/editor/repository/code_repository/code_repository.dart b/playground/frontend/lib/modules/editor/repository/code_repository/code_repository.dart index 9e86f0103e63..108bf0892dc5 100644 --- a/playground/frontend/lib/modules/editor/repository/code_repository/code_repository.dart +++ b/playground/frontend/lib/modules/editor/repository/code_repository/code_repository.dart @@ -30,7 +30,7 @@ const kTimeoutErrorText = 'to try examples without timeout limitation.'; const kUnknownErrorText = 'Something went wrong. Please try again later or create a jira ticket'; -const kProcessingStartedText = 'The processing has started'; +const kProcessingStartedText = 'The processing has started\n'; class CodeRepository { late final CodeClient _client; @@ -126,6 +126,7 @@ class CodeRepository { status: status, errorMessage: kTimeoutErrorText, output: kTimeoutErrorText, + log: prevLog, ); case RunCodeStatus.runError: final output = await _client.getRunErrorOutput(pipelineUuid, request); @@ -166,6 +167,7 @@ class CodeRepository { final log = responses[1]; final error = responses[2]; return RunCodeResult( + pipelineUuid: pipelineUuid, status: status, output: prevOutput + output.output + error.output, log: prevLog + log.output, @@ -173,6 +175,7 @@ class CodeRepository { default: return RunCodeResult( pipelineUuid: pipelineUuid, + log: prevLog, status: status, ); } diff --git a/playground/frontend/lib/modules/output/components/output.dart b/playground/frontend/lib/modules/output/components/output.dart index 526f3f15cbc1..fba1e2c96f11 100644 --- a/playground/frontend/lib/modules/output/components/output.dart +++ b/playground/frontend/lib/modules/output/components/output.dart @@ -20,16 +20,44 @@ import 'package:flutter/material.dart'; import 'package:playground/modules/output/components/output_area.dart'; import 'package:playground/modules/output/components/output_header/output_header.dart'; -class Output extends StatelessWidget { +class Output extends StatefulWidget { const Output({Key? key}) : super(key: key); + @override + State createState() => _OutputState(); +} + +class _OutputState extends State with SingleTickerProviderStateMixin { + late final TabController tabController; + int selectedTab = 0; + + @override + void initState() { + tabController = TabController(vsync: this, length: 3); + tabController.addListener(onTabChange); + super.initState(); + } + + @override + void dispose() { + tabController.removeListener(onTabChange); + tabController.dispose(); + super.dispose(); + } + + onTabChange() { + setState(() { + selectedTab = tabController.index; + }); + } + @override Widget build(BuildContext context) { - return DefaultTabController( - length: 3, - child: Column( - children: const [OutputHeader(), Expanded(child: OutputArea())], - ), + return Column( + children: [ + OutputHeader(tabController: tabController), + Expanded(child: OutputArea(tabController: tabController)), + ], ); } } diff --git a/playground/frontend/lib/modules/output/components/output_area.dart b/playground/frontend/lib/modules/output/components/output_area.dart index 2257b07e5e72..36dc7551ffdf 100644 --- a/playground/frontend/lib/modules/output/components/output_area.dart +++ b/playground/frontend/lib/modules/output/components/output_area.dart @@ -23,7 +23,9 @@ import 'package:playground/pages/playground/states/playground_state.dart'; import 'package:provider/provider.dart'; class OutputArea extends StatelessWidget { - const OutputArea({Key? key}) : super(key: key); + final TabController tabController; + + const OutputArea({Key? key, required this.tabController}) : super(key: key); @override Widget build(BuildContext context) { @@ -32,10 +34,17 @@ class OutputArea extends StatelessWidget { child: Consumer( builder: (context, state, child) { return TabBarView( + controller: tabController, physics: const NeverScrollableScrollPhysics(), children: [ - OutputResult(text: state.result?.output ?? ''), - OutputResult(text: state.result?.log ?? ''), + OutputResult( + text: state.result?.output ?? '', + isSelected: tabController.index == 0, + ), + OutputResult( + text: state.result?.log ?? '', + isSelected: tabController.index == 1, + ), Center(child: Text(AppLocalizations.of(context)!.graph)), ], ); diff --git a/playground/frontend/lib/modules/output/components/output_header/output_header.dart b/playground/frontend/lib/modules/output/components/output_header/output_header.dart index b2313e677da4..a69082f3a742 100644 --- a/playground/frontend/lib/modules/output/components/output_header/output_header.dart +++ b/playground/frontend/lib/modules/output/components/output_header/output_header.dart @@ -23,7 +23,12 @@ import 'package:playground/modules/output/components/output_header/output_placem import 'output_tabs.dart'; class OutputHeader extends StatelessWidget { - const OutputHeader({Key? key}) : super(key: key); + final TabController tabController; + + const OutputHeader({ + Key? key, + required this.tabController, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -36,9 +41,9 @@ class OutputHeader extends StatelessWidget { ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ - OutputTabs(), - OutputPlacements(), + children: [ + OutputTabs(tabController: tabController), + const OutputPlacements(), ], ), ), diff --git a/playground/frontend/lib/modules/output/components/output_header/output_tab.dart b/playground/frontend/lib/modules/output/components/output_header/output_tab.dart new file mode 100644 index 000000000000..1a188e998cef --- /dev/null +++ b/playground/frontend/lib/modules/output/components/output_header/output_tab.dart @@ -0,0 +1,80 @@ +/* + * 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 OutputTab extends StatefulWidget { + final String name; + final bool isSelected; + final String value; + + const OutputTab({ + Key? key, + required this.name, + required this.isSelected, + required this.value, + }) : super(key: key); + + @override + State createState() => _OutputTabState(); +} + +class _OutputTabState extends State { + bool hasNewContent = false; + + @override + void didUpdateWidget(OutputTab oldWidget) { + if (widget.isSelected && hasNewContent) { + setState(() { + hasNewContent = false; + }); + } else if (!widget.isSelected && + widget.value.isNotEmpty && + oldWidget.value != widget.value) { + setState(() { + hasNewContent = true; + }); + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return Tab( + child: Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 8.0, + children: [ + Text(widget.name), + if (hasNewContent) + Container( + width: kIconSizeXs, + height: kIconSizeXs, + decoration: BoxDecoration( + color: ThemeColors.of(context).primary, + shape: BoxShape.circle, + ), + ), + ], + ), + ); + } +} diff --git a/playground/frontend/lib/modules/output/components/output_header/output_tabs.dart b/playground/frontend/lib/modules/output/components/output_header/output_tabs.dart index 14455899817b..b64e436cd11c 100644 --- a/playground/frontend/lib/modules/output/components/output_header/output_tabs.dart +++ b/playground/frontend/lib/modules/output/components/output_header/output_tabs.dart @@ -18,23 +18,42 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:playground/modules/output/components/output_header/output_tab.dart'; +import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:provider/provider.dart'; class OutputTabs extends StatelessWidget { - const OutputTabs({Key? key}) : super(key: key); + final TabController tabController; + + const OutputTabs({Key? key, required this.tabController}) : super(key: key); @override Widget build(BuildContext context) { AppLocalizations appLocale = AppLocalizations.of(context)!; - - return SizedBox( - width: 300, - child: TabBar( - tabs: [ - Tab(text: appLocale.output), - Tab(text: appLocale.log), - Tab(text: appLocale.graph), - ], - ), - ); + return Consumer(builder: (context, state, child) { + return SizedBox( + width: 300, + child: TabBar( + controller: tabController, + tabs: [ + OutputTab( + name: appLocale.output, + isSelected: tabController.index == 0, + value: state.result?.output ?? '', + ), + OutputTab( + name: appLocale.log, + isSelected: tabController.index == 1, + value: state.result?.log ?? '', + ), + OutputTab( + name: appLocale.graph, + isSelected: tabController.index == 2, + value: '', + ), + ], + ), + ); + }); } } diff --git a/playground/frontend/lib/modules/output/components/output_result.dart b/playground/frontend/lib/modules/output/components/output_result.dart index e43e8607144b..5497209f437c 100644 --- a/playground/frontend/lib/modules/output/components/output_result.dart +++ b/playground/frontend/lib/modules/output/components/output_result.dart @@ -20,15 +20,34 @@ import 'package:flutter/material.dart'; import 'package:playground/constants/fonts.dart'; import 'package:playground/constants/sizes.dart'; -class OutputResult extends StatelessWidget { +class OutputResult extends StatefulWidget { final String text; + final bool isSelected; - const OutputResult({Key? key, required this.text}) : super(key: key); + const OutputResult({Key? key, required this.text, required this.isSelected}) + : super(key: key); @override - Widget build(BuildContext context) { - final ScrollController _scrollController = ScrollController(); + State createState() => _OutputResultState(); +} + +class _OutputResultState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void didUpdateWidget(OutputResult oldWidget) { + WidgetsBinding.instance?.addPostFrameCallback((_) { + if (_scrollController.hasClients && + !widget.isSelected && + oldWidget.text != widget.text) { + _scrollController.jumpTo(_scrollController.position.maxScrollExtent); + } + }); + super.didUpdateWidget(oldWidget); + } + @override + Widget build(BuildContext context) { return SingleChildScrollView( controller: _scrollController, child: Scrollbar( @@ -37,7 +56,7 @@ class OutputResult extends StatelessWidget { controller: _scrollController, child: Padding( padding: const EdgeInsets.all(kXlSpacing), - child: SelectableText(text, style: getCodeFontStyle()), + child: SelectableText(widget.text, style: getCodeFontStyle()), ), ), );