mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2025-01-26 18:56:38 +00:00
add charts
This commit is contained in:
parent
e4d5a4e01f
commit
cd49f9fb45
|
@ -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": {
|
||||
|
|
|
@ -61,6 +61,11 @@
|
|||
"1": "Это виртульный компьютер на котором работают все ваши сервисы.",
|
||||
"2": "Общая информация",
|
||||
"3": "Размещение"
|
||||
},
|
||||
"chart": {
|
||||
"month": "Месяц",
|
||||
"day": "День",
|
||||
"hour": "Час"
|
||||
}
|
||||
},
|
||||
"domain": {
|
||||
|
|
|
@ -164,11 +164,21 @@ class HetznerApi extends ApiMap {
|
|||
return server.copyWith(startTime: DateTime.now());
|
||||
}
|
||||
|
||||
metrics() async {
|
||||
Future<Map<String, dynamic>> getMetrics(DateTime start, DateTime end, String type) async {
|
||||
var hetznerServer = getIt<ApiConfigModel>().hetznerServer;
|
||||
var client = await getClient();
|
||||
await client.post('/servers/${hetznerServer!.id}/metrics');
|
||||
|
||||
Map<String, dynamic> 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<HetznerServerInfo> getInfo() async {
|
||||
|
|
|
@ -8,3 +8,5 @@ enum InitializingSteps {
|
|||
startServer,
|
||||
checkSystemDnsAndDkimSet,
|
||||
}
|
||||
enum Period { hour, day, month }
|
||||
|
||||
|
|
49
lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart
Normal file
49
lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart
Normal file
|
@ -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<HetznerMetricsState> {
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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<HetznerMetricsLoaded> 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<TimeSeriesData> timeSeriesSerializer(
|
||||
Map<String, dynamic> json, String type) {
|
||||
List list = json["time_series"][type]["values"];
|
||||
return list.map((el) => TimeSeriesData(el[0], double.parse(el[1]))).toList();
|
||||
}
|
43
lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart
Normal file
43
lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart
Normal file
|
@ -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<Object?> 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<TimeSeriesData> cpu;
|
||||
final List<TimeSeriesData> ppsIn;
|
||||
final List<TimeSeriesData> ppsOut;
|
||||
final List<TimeSeriesData> bandwidthIn;
|
||||
final List<TimeSeriesData> bandwidthOut;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [period, start, end];
|
||||
}
|
11
lib/logic/models/hetzner_metrics.dart
Normal file
11
lib/logic/models/hetzner_metrics.dart
Normal file
|
@ -0,0 +1,11 @@
|
|||
class TimeSeriesData {
|
||||
TimeSeriesData(
|
||||
this.secondsSinceEpoch,
|
||||
this.value,
|
||||
);
|
||||
|
||||
final int secondsSinceEpoch;
|
||||
DateTime get time =>
|
||||
DateTime.fromMillisecondsSinceEpoch(secondsSinceEpoch * 1000);
|
||||
final double value;
|
||||
}
|
42
lib/ui/components/brand_radio/brand_radio.dart
Normal file
42
lib/ui/components/brand_radio/brand_radio.dart
Normal file
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
37
lib/ui/components/brand_radio_tile/brand_radio_tile.dart
Normal file
37
lib/ui/components/brand_radio_tile/brand_radio_tile.dart
Normal file
|
@ -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)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -70,9 +70,9 @@ class _BrandTimerState extends State<BrandTimer> {
|
|||
_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]);
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
166
lib/ui/pages/server_details/chart.dart
Normal file
166
lib/ui/pages/server_details/chart.dart
Normal file
|
@ -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<HetznerMetricsCubit>();
|
||||
var period = cubit.state.period;
|
||||
var state = cubit.state;
|
||||
List<Widget> 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,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
109
lib/ui/pages/server_details/cpu_chart.dart
Normal file
109
lib/ui/pages/server_details/cpu_chart.dart
Normal file
|
@ -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<TimeSeriesData> data;
|
||||
final Period period;
|
||||
final DateTime start;
|
||||
|
||||
List<FlSpot> getSpots() {
|
||||
var i = 0;
|
||||
List<FlSpot> 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;
|
||||
}
|
||||
}
|
61
lib/ui/pages/server_details/header.dart
Normal file
61
lib/ui/pages/server_details/header.dart
Normal file
|
@ -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 }
|
134
lib/ui/pages/server_details/network_charts.dart
Normal file
134
lib/ui/pages/server_details/network_charts.dart
Normal file
|
@ -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<List<TimeSeriesData>> listData;
|
||||
final Period period;
|
||||
final DateTime start;
|
||||
|
||||
List<FlSpot> getSpots(data) {
|
||||
var i = 0;
|
||||
List<FlSpot> 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;
|
||||
}
|
||||
}
|
|
@ -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<NavigatorState>();
|
||||
|
||||
|
@ -48,240 +56,41 @@ class _ServerDetailsState extends State<ServerDetails>
|
|||
var isReady = context.watch<AppConfigCubit>().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<ServerDetailsCubit>().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');
|
||||
|
|
171
lib/ui/pages/server_details/text_details.dart
Normal file
171
lib/ui/pages/server_details/text_details.dart
Normal file
|
@ -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<ServerDetailsCubit>().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');
|
|
@ -397,7 +397,6 @@ class _ServiceDetails extends StatelessWidget {
|
|||
try {
|
||||
await launch(
|
||||
url,
|
||||
forceSafariVC: true,
|
||||
enableJavaScript: true,
|
||||
);
|
||||
} catch (e) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue