diff --git a/assets/translations/en.json b/assets/translations/en.json index a93e7183..f688ef13 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -61,6 +61,11 @@ "1": "It's a virtual computer, where all your services live.", "2": "General information", "3": "Location" + }, + "chart": { + "month": "Month", + "day": "Day", + "hour": "Hour" } }, "domain": { diff --git a/assets/translations/ru.json b/assets/translations/ru.json index df845f70..e8bc8f2c 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -61,6 +61,11 @@ "1": "Это виртульный компьютер на котором работают все ваши сервисы.", "2": "Общая информация", "3": "Размещение" + }, + "chart": { + "month": "Месяц", + "day": "День", + "hour": "Час" } }, "domain": { diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index 4b2c38a5..47075677 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -164,11 +164,21 @@ class HetznerApi extends ApiMap { return server.copyWith(startTime: DateTime.now()); } - metrics() async { + Future> getMetrics(DateTime start, DateTime end, String type) async { var hetznerServer = getIt().hetznerServer; var client = await getClient(); - await client.post('/servers/${hetznerServer!.id}/metrics'); + + Map queryParameters = { + "start": start.toUtc().toIso8601String(), + "end": end.toUtc().toIso8601String(), + "type": type + }; + var res = await client.get( + '/servers/${hetznerServer!.id}/metrics', + queryParameters: queryParameters, + ); close(client); + return res.data; } Future getInfo() async { diff --git a/lib/logic/common_enum/common_enum.dart b/lib/logic/common_enum/common_enum.dart index 741a1bf4..912c4cb6 100644 --- a/lib/logic/common_enum/common_enum.dart +++ b/lib/logic/common_enum/common_enum.dart @@ -8,3 +8,5 @@ enum InitializingSteps { startServer, checkSystemDnsAndDkimSet, } +enum Period { hour, day, month } + diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart new file mode 100644 index 00000000..0ab6b5eb --- /dev/null +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; + +import 'hetzner_metrics_repository.dart'; + +part 'hetzner_metrics_state.dart'; + +class HetznerMetricsCubit extends Cubit { + HetznerMetricsCubit() : super(HetznerMetricsLoading(Period.day)); + + final repository = HetznerMetricsRepository(); + + Timer? timer; + + close() { + closeTimer(); + return super.close(); + } + + void closeTimer() { + if (timer != null && timer!.isActive) { + timer!.cancel(); + } + } + + void changePeriod(Period period) async { + closeTimer(); + emit(HetznerMetricsLoading(period)); + load(period); + } + + void restart() async { + load(state.period); + } + + void load(Period period) async { + var newState = await repository.getMetrics(state.period); + timer = Timer( + Duration(seconds: newState.stepInSeconds.toInt()), + () => load(newState.period), + ); + + emit(newState); + } +} diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart new file mode 100644 index 00000000..af6d70de --- /dev/null +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart @@ -0,0 +1,56 @@ +import 'package:selfprivacy/logic/api_maps/hetzner.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; + +import 'hetzner_metrics_cubit.dart'; + +class HetznerMetricsRepository { + Future getMetrics(Period period) async { + var end = DateTime.now(); + DateTime start; + + switch (period) { + case Period.hour: + start = end.subtract(Duration(hours: 1)); + break; + case Period.day: + start = end.subtract(Duration(days: 1)); + break; + case Period.month: + start = end.subtract(Duration(days: 15)); + break; + } + + var api = HetznerApi(); + + var results = await Future.wait([ + api.getMetrics(start, end, 'cpu'), + api.getMetrics(start, end, 'network'), + ]); + + var cpuMetricsData = results[0]["metrics"]; + var networkMetricsData = results[1]["metrics"]; + + return HetznerMetricsLoaded( + period: period, + start: start, + end: end, + stepInSeconds: cpuMetricsData["step"], + cpu: timeSeriesSerializer(cpuMetricsData, 'cpu'), + ppsIn: timeSeriesSerializer(networkMetricsData, 'network.0.pps.in'), + ppsOut: timeSeriesSerializer(networkMetricsData, 'network.0.pps.out'), + bandwidthIn: + timeSeriesSerializer(networkMetricsData, 'network.0.bandwidth.in'), + bandwidthOut: timeSeriesSerializer( + networkMetricsData, + 'network.0.bandwidth.out', + ), + ); + } +} + +List timeSeriesSerializer( + Map json, String type) { + List list = json["time_series"][type]["values"]; + return list.map((el) => TimeSeriesData(el[0], double.parse(el[1]))).toList(); +} diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart new file mode 100644 index 00000000..bbd3f7d1 --- /dev/null +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart @@ -0,0 +1,43 @@ +part of 'hetzner_metrics_cubit.dart'; + +abstract class HetznerMetricsState extends Equatable { + const HetznerMetricsState(); + + abstract final Period period; +} + +class HetznerMetricsLoading extends HetznerMetricsState { + HetznerMetricsLoading(this.period); + final Period period; + + @override + List get props => [period]; +} + +class HetznerMetricsLoaded extends HetznerMetricsState { + HetznerMetricsLoaded({ + required this.period, + required this.start, + required this.end, + required this.stepInSeconds, + required this.cpu, + required this.ppsIn, + required this.ppsOut, + required this.bandwidthIn, + required this.bandwidthOut, + }); + + final Period period; + final DateTime start; + final DateTime end; + final num stepInSeconds; + + final List cpu; + final List ppsIn; + final List ppsOut; + final List bandwidthIn; + final List bandwidthOut; + + @override + List get props => [period, start, end]; +} diff --git a/lib/logic/models/hetzner_metrics.dart b/lib/logic/models/hetzner_metrics.dart new file mode 100644 index 00000000..2f41a4b2 --- /dev/null +++ b/lib/logic/models/hetzner_metrics.dart @@ -0,0 +1,11 @@ +class TimeSeriesData { + TimeSeriesData( + this.secondsSinceEpoch, + this.value, + ); + + final int secondsSinceEpoch; + DateTime get time => + DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000); + final double value; +} diff --git a/lib/ui/components/brand_radio/brand_radio.dart b/lib/ui/components/brand_radio/brand_radio.dart new file mode 100644 index 00000000..3ae64bcf --- /dev/null +++ b/lib/ui/components/brand_radio/brand_radio.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/brand_colors.dart'; + +class BrandRadio extends StatelessWidget { + BrandRadio({ + Key? key, + required this.isChecked, + }) : super(key: key); + + final bool isChecked; + + @override + Widget build(BuildContext context) { + return Container( + height: 20, + width: 20, + alignment: Alignment.center, + padding: EdgeInsets.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: _getBorder(), + ), + child: isChecked + ? Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: BrandColors.primary, + ), + ) + : null, + ); + } + + BoxBorder? _getBorder() { + return Border.all( + color: isChecked ? BrandColors.primary : BrandColors.gray1, + width: 2, + ); + } +} diff --git a/lib/ui/components/brand_radio_tile/brand_radio_tile.dart b/lib/ui/components/brand_radio_tile/brand_radio_tile.dart new file mode 100644 index 00000000..4f979a47 --- /dev/null +++ b/lib/ui/components/brand_radio_tile/brand_radio_tile.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/brand_radio/brand_radio.dart'; +import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; + +class BrandRadioTile extends StatelessWidget { + const BrandRadioTile({ + Key? key, + required this.isChecked, + required this.text, + required this.onPress, + }) : super(key: key); + + final bool isChecked; + + final String text; + final VoidCallback onPress; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onPress, + behavior: HitTestBehavior.translucent, + child: Padding( + padding: EdgeInsets.all(2), + child: Row( + children: [ + BrandRadio( + isChecked: isChecked, + ), + SizedBox(width: 9), + BrandText.h5(text) + ], + ), + ), + ); + } +} diff --git a/lib/ui/components/brand_timer/brand_timer.dart b/lib/ui/components/brand_timer/brand_timer.dart index 38eeb356..00f512d3 100644 --- a/lib/ui/components/brand_timer/brand_timer.dart +++ b/lib/ui/components/brand_timer/brand_timer.dart @@ -70,9 +70,9 @@ class _BrandTimerState extends State { _durationToString(DateTime.now().difference(widget.startDateTime)); String _durationToString(Duration duration) { + var timeLeft = widget.duration - duration; String twoDigits(int n) => n.toString().padLeft(2, "0"); - String twoDigitSeconds = - twoDigits(widget.duration.inSeconds - duration.inSeconds.remainder(60)); + String twoDigitSeconds = twoDigits(timeLeft.inSeconds); return "timer.sec".tr(args: [twoDigitSeconds]); } diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index db953281..196ab0ed 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -82,8 +82,6 @@ class _Card extends StatelessWidget { switch (provider.type) { case ProviderType.server: title = 'providers.server.card_title'.tr(); - stableText = 'providers.domain.status'.tr(); - stableText = 'providers.server.status'.tr(); onTap = () => Navigator.of(context).push( SlideBottomRoute( diff --git a/lib/ui/pages/server_details/chart.dart b/lib/ui/pages/server_details/chart.dart new file mode 100644 index 00000000..20c2a5fc --- /dev/null +++ b/lib/ui/pages/server_details/chart.dart @@ -0,0 +1,166 @@ +part of 'server_details.dart'; + +class _Chart extends StatelessWidget { + const _Chart({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + var cubit = context.watch(); + var period = cubit.state.period; + var state = cubit.state; + List charts; + if (state is HetznerMetricsLoading) { + charts = [ + Container( + height: 200, + alignment: Alignment.center, + child: Text('basis.loading'.tr()), + ) + ]; + } else if (state is HetznerMetricsLoaded) { + charts = [ + Legend(color: Colors.red, text: 'CPU %'), + getCpuChart(state), + SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + BrandText.small('Public Network interface packets per sec'), + SizedBox(width: 10), + Legend(color: Colors.red, text: 'IN'), + SizedBox(width: 5), + Legend(color: Colors.green, text: 'OUT'), + ], + ), + getPpsChart(state), + SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + BrandText.small('Public Network interface bytes per sec'), + SizedBox(width: 10), + Legend(color: Colors.red, text: 'IN'), + SizedBox(width: 5), + Legend(color: Colors.green, text: 'OUT'), + ], + ), + getBandwidthChart(state), + ]; + } else { + throw 'wrong state'; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + BrandRadioTile( + isChecked: period == Period.month, + text: 'providers.server.chart.month'.tr(), + onPress: () => cubit.changePeriod(Period.month), + ), + BrandRadioTile( + isChecked: period == Period.day, + text: 'providers.server.chart.day'.tr(), + onPress: () => cubit.changePeriod(Period.day), + ), + BrandRadioTile( + isChecked: period == Period.hour, + text: 'providers.server.chart.hour'.tr(), + onPress: () => cubit.changePeriod(Period.hour), + ), + ], + ), + ), + ...charts, + ], + ), + ); + } + + Widget getCpuChart(HetznerMetricsLoaded state) { + var data = state.cpu; + + return Container( + height: 200, + child: CpuChart(data, state.period, state.start), + ); + } + + Widget getPpsChart(HetznerMetricsLoaded state) { + var ppsIn = state.ppsIn; + var ppsOut = state.ppsOut; + + return Container( + height: 200, + child: NetworkChart( + [ppsIn, ppsOut], + state.period, + state.start, + ), + ); + } + + Widget getBandwidthChart(HetznerMetricsLoaded state) { + var ppsIn = state.bandwidthIn; + var ppsOut = state.bandwidthOut; + + return Container( + height: 200, + child: NetworkChart( + [ppsIn, ppsOut], + state.period, + state.start, + ), + ); + } +} + +class Legend extends StatelessWidget { + const Legend({ + Key? key, + required this.color, + required this.text, + }) : super(key: key); + + final String text; + final Color color; + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _ColoredBox(color: color), + SizedBox(width: 5), + BrandText.small(text), + ], + ); + } +} + +class _ColoredBox extends StatelessWidget { + const _ColoredBox({ + Key? key, + required this.color, + }) : super(key: key); + + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: color.withOpacity(0.3), + border: Border.all( + color: color, + )), + ); + } +} diff --git a/lib/ui/pages/server_details/cpu_chart.dart b/lib/ui/pages/server_details/cpu_chart.dart new file mode 100644 index 00000000..4d667c0d --- /dev/null +++ b/lib/ui/pages/server_details/cpu_chart.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; +import 'package:intl/intl.dart'; + +class CpuChart extends StatelessWidget { + CpuChart(this.data, this.period, this.start); + + final List data; + final Period period; + final DateTime start; + + List getSpots() { + var i = 0; + List res = []; + + for (var d in data) { + res.add(FlSpot(i.toDouble(), d.value)); + i++; + } + + return res; + } + + @override + Widget build(BuildContext context) { + return LineChart( + LineChartData( + lineTouchData: LineTouchData(enabled: false), + lineBarsData: [ + LineChartBarData( + spots: getSpots(), + isCurved: true, + barWidth: 1, + colors: [ + Colors.red, + ], + dotData: FlDotData( + show: false, + ), + ), + ], + minY: 0, + maxY: 100, + minX: data.length - 200, + titlesData: FlTitlesData( + bottomTitles: SideTitles( + interval: 20, + rotateAngle: 90.0, + showTitles: true, + getTextStyles: (value) => const TextStyle( + fontSize: 10, + color: Colors.purple, + fontWeight: FontWeight.bold, + ), + getTitles: (value) { + return bottomTitle(value.toInt()); + }), + leftTitles: SideTitles( + margin: 15, + interval: 25, + showTitles: true, + ), + ), + gridData: FlGridData(show: true), + ), + ); + } + + bool checkToShowTitle( + double minValue, + double maxValue, + SideTitles sideTitles, + double appliedInterval, + double value, + ) { + print(value); + if (value < 0) { + return false; + } else if (value == 0) { + return true; + } + var _value = value - minValue; + var v = _value / 20; + return v - v.floor() == 0; + } + + String bottomTitle(int value) { + final hhmm = DateFormat('HH:mm'); + var day = DateFormat('MMMd'); + String res; + + if (value <= 0) { + return ''; + } + var time = data[value].time; + switch (period) { + case Period.hour: + case Period.day: + res = hhmm.format(time); + break; + case Period.month: + res = day.format(time); + } + + return res; + } +} diff --git a/lib/ui/pages/server_details/header.dart b/lib/ui/pages/server_details/header.dart new file mode 100644 index 00000000..d03d3d08 --- /dev/null +++ b/lib/ui/pages/server_details/header.dart @@ -0,0 +1,61 @@ +part of 'server_details.dart'; + +class _Header extends StatelessWidget { + const _Header({ + Key? key, + required this.providerState, + required this.tabController, + }) : super(key: key); + + final StateType providerState; + final TabController tabController; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + IconStatusMask( + status: providerState, + child: Icon( + BrandIcons.server, + size: 40, + color: Colors.white, + ), + ), + SizedBox(width: 10), + BrandText.h2('providers.server.card_title'.tr()), + Spacer(), + Padding( + padding: EdgeInsets.symmetric( + vertical: 4, + horizontal: 2, + ), + child: PopupMenuButton<_PopupMenuItemType>( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + onSelected: (_PopupMenuItemType result) { + switch (result) { + case _PopupMenuItemType.setting: + tabController.animateTo(1); + break; + } + }, + icon: Icon(Icons.more_vert), + itemBuilder: (BuildContext context) => [ + PopupMenuItem<_PopupMenuItemType>( + value: _PopupMenuItemType.setting, + child: Container( + padding: EdgeInsets.only(left: 5), + child: Text('basis.settings'.tr()), + ), + ), + ], + ), + ), + ], + ); + } +} + +enum _PopupMenuItemType { setting } diff --git a/lib/ui/pages/server_details/network_charts.dart b/lib/ui/pages/server_details/network_charts.dart new file mode 100644 index 00000000..3088c697 --- /dev/null +++ b/lib/ui/pages/server_details/network_charts.dart @@ -0,0 +1,134 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; +import 'package:intl/intl.dart'; + +class NetworkChart extends StatelessWidget { + NetworkChart( + this.listData, + this.period, + this.start, + ); + + final List> listData; + final Period period; + final DateTime start; + + List getSpots(data) { + var i = 0; + List res = []; + + for (var d in data) { + res.add(FlSpot(i.toDouble(), d.value)); + i++; + } + + return res; + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 150, + width: MediaQuery.of(context).size.width * 0.90, + child: LineChart( + LineChartData( + lineTouchData: LineTouchData(enabled: false), + lineBarsData: [ + LineChartBarData( + spots: getSpots(listData[0]), + isCurved: true, + barWidth: 1, + colors: [Colors.red], + dotData: FlDotData( + show: false, + ), + ), + LineChartBarData( + spots: getSpots(listData[1]), + isCurved: true, + barWidth: 1, + colors: [Colors.green], + dotData: FlDotData( + show: false, + ), + ), + ], + minY: 0, + maxY: [ + ...listData[0].map((e) => e.value), + ...listData[1].map((e) => e.value) + ].reduce(max) * + 1.2, + minX: listData[0].length - 200, + titlesData: FlTitlesData( + bottomTitles: SideTitles( + interval: 20, + rotateAngle: 90.0, + showTitles: true, + getTextStyles: (value) => const TextStyle( + fontSize: 10, + color: Colors.purple, + fontWeight: FontWeight.bold, + ), + getTitles: (value) { + return bottomTitle(value.toInt()); + }), + leftTitles: SideTitles( + margin: 15, + interval: [ + ...listData[0].map((e) => e.value), + ...listData[1].map((e) => e.value) + ].reduce(max) * + 1.2 / + 10, + showTitles: true, + ), + ), + gridData: FlGridData(show: true), + ), + ), + ); + } + + bool checkToShowTitle( + double minValue, + double maxValue, + SideTitles sideTitles, + double appliedInterval, + double value, + ) { + if (value < 0) { + return false; + } else if (value == 0) { + return true; + } + var _value = value - minValue; + var v = _value / 20; + return v - v.floor() == 0; + } + + String bottomTitle(int value) { + final hhmm = DateFormat('HH:mm'); + var day = DateFormat('MMMd'); + String res; + + if (value <= 0) { + return ''; + } + var time = listData[0][value].time; + switch (period) { + case Period.hour: + case Period.day: + res = hhmm.format(time); + break; + case Period.month: + res = day.format(time); + } + + return res; + } +} diff --git a/lib/ui/pages/server_details/server_details.dart b/lib/ui/pages/server_details/server_details.dart index 3e578199..7ae33d70 100644 --- a/lib/ui/pages/server_details/server_details.dart +++ b/lib/ui/pages/server_details/server_details.dart @@ -2,18 +2,26 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; +import 'package:selfprivacy/ui/components/brand_radio_tile/brand_radio_tile.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/ui/components/switch_block/switch_bloc.dart'; import 'package:selfprivacy/utils/named_font_weight.dart'; +import 'cpu_chart.dart'; +import 'network_charts.dart'; part 'server_settings.dart'; +part 'text_details.dart'; +part 'chart.dart'; +part 'header.dart'; var navigatorKey = GlobalKey(); @@ -48,240 +56,41 @@ class _ServerDetailsState extends State var isReady = context.watch().state.isFullyInitilized; var providerState = isReady ? StateType.stable : StateType.uninitialized; - late String title = 'providers.server.card_title'.tr(); - return TabBarView( physics: NeverScrollableScrollPhysics(), controller: tabController, children: [ - BlocProvider( - create: (context) => ServerDetailsCubit()..check(), - child: Builder(builder: (context) { - var details = context.watch().state; - if (details is ServerDetailsLoading || - details is ServerDetailsInitial) { - return _TempMessage(message: 'basis.loading'.tr()); - } else if (details is ServerDetailsNotReady) { - return _TempMessage(message: 'basis.no_data'.tr()); - } else if (details is Loaded) { - var data = details.serverInfo; - var checkTime = details.checkTime; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: brandPagePadding2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - IconStatusMask( - status: providerState, - child: Icon( - BrandIcons.server, - size: 40, - color: Colors.white, - ), - ), - SizedBox(width: 10), - BrandText.h2(title), - Spacer(), - Padding( - padding: EdgeInsets.symmetric( - vertical: 4, - horizontal: 2, - ), - child: PopupMenuButton<_PopupMenuItemType>( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - onSelected: (_PopupMenuItemType result) { - switch (result) { - case _PopupMenuItemType.setting: - tabController.animateTo(1); - break; - } - }, - icon: Icon(Icons.more_vert), - itemBuilder: (BuildContext context) => [ - PopupMenuItem<_PopupMenuItemType>( - value: _PopupMenuItemType.setting, - child: Container( - padding: EdgeInsets.only(left: 5), - child: Text('basis.settings'.tr()), - ), - ), - ], - ), - ), - ], - ), - SizedBox(height: 10), - BrandText.body1('providers.server.bottom_sheet.1'.tr()), - SizedBox(height: 30), - Center(child: BrandText.h2('providers.server.2'.tr())), - SizedBox(height: 10), - Table( - columnWidths: { - 0: FractionColumnWidth(.5), - 1: FractionColumnWidth(.5), - }, - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - children: [ - TableRow( - children: [ - getRowTitle('Last check'), - getRowValue(formater.format(checkTime)), - ], - ), - TableRow( - children: [ - getRowTitle('Server Id'), - getRowValue(data.id.toString()), - ], - ), - TableRow( - children: [ - getRowTitle('Status:'), - getRowValue( - '${data.status.toString().split('.')[1].toUpperCase()}', - isBold: true, - ), - ], - ), - TableRow( - children: [ - getRowTitle('CPU'), - getRowValue( - data.serverType.cores.toString(), - ), - ], - ), - TableRow( - children: [ - getRowTitle('Memory'), - getRowValue( - '${data.serverType.memory.toString()} GB', - ), - ], - ), - TableRow( - children: [ - getRowTitle('Disk Local'), - getRowValue( - '${data.serverType.disk.toString()} GB', - ), - ], - ), - TableRow( - children: [ - getRowTitle('Price monthly:'), - getRowValue( - '${data.serverType.prices[1].monthly.toString()}', - ), - ], - ), - TableRow( - children: [ - getRowTitle('Price hourly:'), - getRowValue( - '${data.serverType.prices[1].hourly.toString()}', - ), - ], - ), - ], - ), - SizedBox(height: 30), - Center(child: BrandText.h2('providers.server.3'.tr())), - SizedBox(height: 10), - Table( - columnWidths: { - 0: FractionColumnWidth(.5), - 1: FractionColumnWidth(.5), - }, - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - children: [ - TableRow( - children: [ - getRowTitle('Country'), - getRowValue( - '${data.location.country}', - ), - ], - ), - TableRow( - children: [ - getRowTitle('City'), - getRowValue(data.location.city), - ], - ), - TableRow( - children: [ - getRowTitle('Description'), - getRowValue(data.location.description), - ], - ), - ], - ), - ], + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: brandPagePadding2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Header( + providerState: providerState, + tabController: tabController), + BrandText.body1('providers.server.bottom_sheet.1'.tr()), + SizedBox(height: 10), + BlocProvider( + create: (context) => HetznerMetricsCubit()..restart(), + child: _Chart(), ), - ), - ], - ); - } else { - throw Exception('wrong state'); - } - }), + SizedBox(height: 20), + BlocProvider( + create: (context) => ServerDetailsCubit()..check(), + child: _TextDetails(), + ), + ], + ), + ), + ], + ), ), _ServerSettings(tabController: tabController), ], ); } - - Widget getRowTitle(String title) { - return Padding( - padding: const EdgeInsets.only(right: 10), - child: BrandText.h5( - title, - textAlign: TextAlign.right, - ), - ); - } - - Widget getRowValue(String title, {bool isBold = false}) { - return BrandText.body1( - title, - style: isBold - ? TextStyle( - fontWeight: NamedFontWeight.demiBold, - ) - : null, - ); - } } - -enum _PopupMenuItemType { setting } - -class _TempMessage extends StatelessWidget { - const _TempMessage({ - Key? key, - required this.message, - }) : super(key: key); - - final String message; - @override - Widget build(BuildContext context) { - return SizedBox( - height: MediaQuery.of(context).size.height - 100, - child: Center( - child: BrandText.body2(message), - ), - ); - } -} - -final DateFormat formater = DateFormat('HH:mm:ss'); diff --git a/lib/ui/pages/server_details/text_details.dart b/lib/ui/pages/server_details/text_details.dart new file mode 100644 index 00000000..8ced8ed4 --- /dev/null +++ b/lib/ui/pages/server_details/text_details.dart @@ -0,0 +1,171 @@ +part of 'server_details.dart'; + +class _TextDetails extends StatelessWidget { + const _TextDetails({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + var details = context.watch().state; + + if (details is ServerDetailsLoading || details is ServerDetailsInitial) { + return _TempMessage(message: 'basis.loading'.tr()); + } else if (details is ServerDetailsNotReady) { + return _TempMessage(message: 'basis.no_data'.tr()); + } else if (details is Loaded) { + var data = details.serverInfo; + var checkTime = details.checkTime; + return Column( + children: [ + Center(child: BrandText.h3('providers.server.bottom_sheet.2'.tr())), + SizedBox(height: 10), + Table( + columnWidths: { + 0: FractionColumnWidth(.5), + 1: FractionColumnWidth(.5), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: [ + getRowTitle('Last check'), + getRowValue(formater.format(checkTime)), + ], + ), + TableRow( + children: [ + getRowTitle('Server Id'), + getRowValue(data.id.toString()), + ], + ), + TableRow( + children: [ + getRowTitle('Status:'), + getRowValue( + '${data.status.toString().split('.')[1].toUpperCase()}', + isBold: true, + ), + ], + ), + TableRow( + children: [ + getRowTitle('CPU'), + getRowValue( + data.serverType.cores.toString(), + ), + ], + ), + TableRow( + children: [ + getRowTitle('Memory'), + getRowValue( + '${data.serverType.memory.toString()} GB', + ), + ], + ), + TableRow( + children: [ + getRowTitle('Disk Local'), + getRowValue( + '${data.serverType.disk.toString()} GB', + ), + ], + ), + TableRow( + children: [ + getRowTitle('Price monthly:'), + getRowValue( + '${data.serverType.prices[1].monthly.toString()}', + ), + ], + ), + TableRow( + children: [ + getRowTitle('Price hourly:'), + getRowValue( + '${data.serverType.prices[1].hourly.toString()}', + ), + ], + ), + ], + ), + SizedBox(height: 30), + Center(child: BrandText.h3('providers.server.bottom_sheet.3'.tr())), + SizedBox(height: 10), + Table( + columnWidths: { + 0: FractionColumnWidth(.5), + 1: FractionColumnWidth(.5), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: [ + getRowTitle('Country'), + getRowValue( + '${data.location.country}', + ), + ], + ), + TableRow( + children: [ + getRowTitle('City'), + getRowValue(data.location.city), + ], + ), + TableRow( + children: [ + getRowTitle('Description'), + getRowValue(data.location.description), + ], + ), + ], + ), + SizedBox(height: 20), + ], + ); + } else { + throw Exception('wrong state'); + } + } + + Widget getRowTitle(String title) { + return Padding( + padding: const EdgeInsets.only(right: 10), + child: BrandText.h5( + title, + textAlign: TextAlign.right, + ), + ); + } + + Widget getRowValue(String title, {bool isBold = false}) { + return BrandText.body1( + title, + style: isBold + ? TextStyle( + fontWeight: NamedFontWeight.demiBold, + ) + : null, + ); + } +} + +class _TempMessage extends StatelessWidget { + const _TempMessage({ + Key? key, + required this.message, + }) : super(key: key); + + final String message; + @override + Widget build(BuildContext context) { + return SizedBox( + height: MediaQuery.of(context).size.height - 100, + child: Center( + child: BrandText.body2(message), + ), + ); + } +} + +final DateFormat formater = DateFormat('HH:mm:ss'); diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index c21b0d72..d49fe72c 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -397,7 +397,6 @@ class _ServiceDetails extends StatelessWidget { try { await launch( url, - forceSafariVC: true, enableJavaScript: true, ); } catch (e) { diff --git a/pubspec.lock b/pubspec.lock index 7be23a73..4ac79f5a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -274,6 +274,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + url: "https://pub.dartlang.org" + source: hosted + version: "0.35.0" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index d1775203..79788c9c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: easy_localization: ^3.0.0 either_option: ^2.0.1-dev.1 equatable: ^2.0.0 + fl_chart: ^0.35.0 flutter_bloc: ^7.0.0 flutter_markdown: ^0.6.0 flutter_secure_storage: ^4.1.0