mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2024-11-19 07:09:14 +00:00
feat(metrics): Implement disk usage metrics
- Refactor metrics_cubit - Implement fallback to legacy when less than 20 dots
This commit is contained in:
parent
48c7d7be2c
commit
68f34dc7b7
|
@ -119,4 +119,60 @@ mixin MonitoringApi on GraphQLApiMap {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<GenericResult<DiskMetrics?>> getDiskMetrics({
|
||||
required final int step,
|
||||
required final DateTime start,
|
||||
required final DateTime end,
|
||||
}) async {
|
||||
QueryResult<Query$GetDiskMetrics> response;
|
||||
|
||||
try {
|
||||
final GraphQLClient client = await getClient();
|
||||
final variables = Variables$Query$GetDiskMetrics(
|
||||
step: step,
|
||||
start: start,
|
||||
end: end,
|
||||
);
|
||||
final query = Options$Query$GetDiskMetrics(variables: variables);
|
||||
response = await client.query$GetDiskMetrics(query);
|
||||
if (response.hasException) {
|
||||
print(response.exception.toString());
|
||||
return GenericResult<DiskMetrics?>(
|
||||
success: false,
|
||||
data: null,
|
||||
);
|
||||
}
|
||||
if (response.parsedData == null) {
|
||||
return GenericResult<DiskMetrics?>(
|
||||
success: false,
|
||||
data: null,
|
||||
);
|
||||
}
|
||||
if (response.parsedData?.monitoring.diskUsage.overallUsage
|
||||
is Fragment$MonitoringQueryError) {
|
||||
return GenericResult<DiskMetrics?>(
|
||||
success: false,
|
||||
data: null,
|
||||
);
|
||||
}
|
||||
final metrics = DiskMetrics.fromGraphQL(
|
||||
data: response.parsedData!.monitoring,
|
||||
stepsInSecond: step,
|
||||
start: start,
|
||||
end: end,
|
||||
);
|
||||
return GenericResult<DiskMetrics?>(
|
||||
success: true,
|
||||
data: metrics,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return GenericResult<DiskMetrics?>(
|
||||
success: false,
|
||||
data: null,
|
||||
message: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,30 +39,21 @@ class MetricsCubit extends Cubit<MetricsState> {
|
|||
|
||||
void load(final Period period) async {
|
||||
try {
|
||||
final MetricsLoaded newState = await repository.getServerMetrics(period);
|
||||
final MetricsStateUpdate newStateUpdate =
|
||||
await repository.getRelevantServerMetrics(period);
|
||||
|
||||
int duration = newStateUpdate.nextCheckInSeconds;
|
||||
if (duration <= 0) {
|
||||
duration = state.period.stepPeriodInSeconds;
|
||||
}
|
||||
timer = Timer(
|
||||
Duration(seconds: newState.metrics.stepsInSecond.toInt()),
|
||||
() => load(newState.period),
|
||||
Duration(seconds: duration),
|
||||
() => load(period),
|
||||
);
|
||||
emit(newState);
|
||||
return;
|
||||
} catch (_) {}
|
||||
try {
|
||||
final MetricsLoaded newState = await repository.getLegacyMetrics(period);
|
||||
timer = Timer(
|
||||
Duration(seconds: newState.metrics.stepsInSecond.toInt()),
|
||||
() => load(newState.period),
|
||||
);
|
||||
emit(newState);
|
||||
|
||||
emit(newStateUpdate.newState);
|
||||
} on StateError {
|
||||
print('Tried to emit metrics when cubit is closed');
|
||||
} on MetricsLoadException {
|
||||
timer = Timer(
|
||||
Duration(seconds: state.period.stepPeriodInSeconds),
|
||||
() => load(state.period),
|
||||
);
|
||||
} on MetricsUnsupportedException {
|
||||
emit(MetricsUnsupported(period));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,50 @@ class MetricsUnsupportedException implements Exception {
|
|||
final String message;
|
||||
}
|
||||
|
||||
class MetricsStateUpdate {
|
||||
MetricsStateUpdate(this.newState, this.nextCheckInSeconds);
|
||||
final MetricsState newState;
|
||||
final int nextCheckInSeconds;
|
||||
}
|
||||
|
||||
class MetricsRepository {
|
||||
static const String metricsSupportedVersion = '>=3.3.0';
|
||||
Future<MetricsLoaded> getServerMetrics(final Period period) async {
|
||||
|
||||
Future<MetricsStateUpdate> getRelevantServerMetrics(
|
||||
final Period period,
|
||||
) async {
|
||||
MetricsLoaded? state;
|
||||
int nextUpdate = 0;
|
||||
|
||||
try {
|
||||
final stateLoaded = await _getServerMetrics(period);
|
||||
nextUpdate = stateLoaded.metrics.stepsInSecond.toInt();
|
||||
state = stateLoaded;
|
||||
} catch (_) {}
|
||||
|
||||
const minAmountForRendering = 20;
|
||||
|
||||
if (state != null &&
|
||||
state.metrics.cpu.length >= minAmountForRendering &&
|
||||
state.metrics.bandwidthIn.length >= minAmountForRendering &&
|
||||
state.metrics.bandwidthOut.length >= minAmountForRendering) {
|
||||
return MetricsStateUpdate(state, nextUpdate);
|
||||
}
|
||||
|
||||
try {
|
||||
final stateLoaded = await _getLegacyMetrics(period);
|
||||
nextUpdate = stateLoaded.metrics.stepsInSecond.toInt();
|
||||
state = stateLoaded;
|
||||
} catch (_) {}
|
||||
|
||||
if (state != null) {
|
||||
return MetricsStateUpdate(state, nextUpdate);
|
||||
}
|
||||
|
||||
return MetricsStateUpdate(MetricsUnsupported(period), nextUpdate);
|
||||
}
|
||||
|
||||
Future<MetricsLoaded> _getServerMetrics(final Period period) async {
|
||||
final String? apiVersion =
|
||||
getIt<ApiConnectionRepository>().apiData.apiVersion.data;
|
||||
if (apiVersion == null) {
|
||||
|
@ -69,15 +110,23 @@ class MetricsRepository {
|
|||
step: end.difference(start).inSeconds ~/ 120,
|
||||
);
|
||||
|
||||
final diskResult =
|
||||
await getIt<ApiConnectionRepository>().api.getDiskMetrics(
|
||||
start: start,
|
||||
end: end,
|
||||
step: end.difference(start).inSeconds ~/ 120,
|
||||
);
|
||||
|
||||
return MetricsLoaded(
|
||||
period: period,
|
||||
metrics: result.data!,
|
||||
source: MetricsDataSource.server,
|
||||
memoryMetrics: memoryResult.data,
|
||||
diskMetrics: diskResult.data,
|
||||
);
|
||||
}
|
||||
|
||||
Future<MetricsLoaded> getLegacyMetrics(final Period period) async {
|
||||
Future<MetricsLoaded> _getLegacyMetrics(final Period period) async {
|
||||
if (!(ProvidersController.currentServerProvider?.isAuthorized ?? false)) {
|
||||
throw MetricsUnsupportedException('Server Provider data is null');
|
||||
}
|
||||
|
|
|
@ -146,8 +146,36 @@ class DiskMetrics {
|
|||
required this.end,
|
||||
});
|
||||
|
||||
DiskMetrics.fromGraphQL({
|
||||
required final Query$GetDiskMetrics$monitoring data,
|
||||
required final int stepsInSecond,
|
||||
required final DateTime start,
|
||||
required final DateTime end,
|
||||
}) : this(
|
||||
stepsInSecond: stepsInSecond,
|
||||
diskMetrics: Map.fromEntries(
|
||||
(data.diskUsage.overallUsage as Fragment$MonitoringMetrics)
|
||||
.metrics
|
||||
.map(
|
||||
(final metric) => MapEntry(
|
||||
metric.metricId,
|
||||
metric.values
|
||||
.map(
|
||||
(final value) => TimeSeriesData(
|
||||
value.timestamp.millisecondsSinceEpoch ~/ 1000,
|
||||
double.parse(value.value),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
start: start,
|
||||
end: end,
|
||||
);
|
||||
|
||||
final num stepsInSecond;
|
||||
final List<TimeSeriesData> diskMetrics;
|
||||
final Map<String, List<TimeSeriesData>> diskMetrics;
|
||||
|
||||
final DateTime start;
|
||||
final DateTime end;
|
||||
|
|
|
@ -145,6 +145,68 @@ class _Chart extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (state is MetricsLoaded && state.diskMetrics != null)
|
||||
FilledCard(
|
||||
clipped: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'resource_chart.network_title'.tr(),
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
fit: FlexFit.loose,
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
alignment: WrapAlignment.end,
|
||||
runAlignment: WrapAlignment.end,
|
||||
children: [
|
||||
Legend(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
text: 'resource_chart.in'.tr(),
|
||||
),
|
||||
Legend(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
text: 'resource_chart.out'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
getDiskChart(state),
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: state is MetricsLoading ? 1 : 0,
|
||||
child: const _GraphLoadingCardContent(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
} else if (state is MetricsUnsupported) {
|
||||
charts = [
|
||||
|
@ -249,6 +311,23 @@ class _Chart extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
Widget getDiskChart(final MetricsLoaded state) {
|
||||
final data = state.diskMetrics;
|
||||
|
||||
if (data == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 200,
|
||||
child: DiskChart(
|
||||
listData: data.diskMetrics.values.toList(),
|
||||
period: state.period,
|
||||
start: state.metrics.start,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _GraphLoadingCardContent extends StatelessWidget {
|
||||
const _GraphLoadingCardContent();
|
||||
|
||||
|
|
230
lib/ui/pages/server_details/charts/disk_charts.dart
Normal file
230
lib/ui/pages/server_details/charts/disk_charts.dart
Normal file
|
@ -0,0 +1,230 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
|
||||
import 'package:selfprivacy/logic/models/disk_size.dart';
|
||||
import 'package:selfprivacy/logic/models/metrics.dart';
|
||||
import 'package:selfprivacy/ui/pages/server_details/charts/bottom_title.dart';
|
||||
|
||||
class DiskChart extends StatelessWidget {
|
||||
const DiskChart({
|
||||
required this.listData,
|
||||
required this.period,
|
||||
required this.start,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<List<TimeSeriesData>> listData;
|
||||
final Period period;
|
||||
final DateTime start;
|
||||
|
||||
List<FlSpot> getSpots(final data) {
|
||||
var i = 0;
|
||||
final List<FlSpot> res = [];
|
||||
|
||||
for (final d in data) {
|
||||
res.add(FlSpot(i.toDouble(), d.value));
|
||||
i++;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => LineChart(
|
||||
LineChartData(
|
||||
lineTouchData: LineTouchData(
|
||||
enabled: true,
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
getTooltipColor: (final LineBarSpot _) =>
|
||||
Theme.of(context).colorScheme.surface,
|
||||
tooltipPadding: const EdgeInsets.all(8),
|
||||
getTooltipItems: (final List<LineBarSpot> touchedBarSpots) {
|
||||
final List<LineTooltipItem> res = [];
|
||||
|
||||
bool timeShown = false;
|
||||
|
||||
for (final spot in touchedBarSpots) {
|
||||
final value = spot.y;
|
||||
final date = listData[0][spot.x.toInt()].time;
|
||||
|
||||
res.add(
|
||||
LineTooltipItem(
|
||||
'${timeShown ? '' : DateFormat('HH:mm dd.MM.yyyy').format(date)} ${spot.barIndex == 0 ? 'resource_chart.in'.tr() : 'resource_chart.out'.tr()} ${DiskSize(byte: value.toInt()).toString()}',
|
||||
TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
timeShown = true;
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
),
|
||||
),
|
||||
lineBarsData: [
|
||||
// IN
|
||||
LineChartBarData(
|
||||
spots: getSpots(listData[0]),
|
||||
isCurved: false,
|
||||
barWidth: 2,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
dotData: const FlDotData(
|
||||
show: false,
|
||||
),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.0),
|
||||
],
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
// OUT
|
||||
LineChartBarData(
|
||||
spots: getSpots(listData[1]),
|
||||
isCurved: false,
|
||||
barWidth: 2,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
dotData: const FlDotData(
|
||||
show: false,
|
||||
),
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.tertiary.withOpacity(0.5),
|
||||
Theme.of(context).colorScheme.tertiary.withOpacity(0.0),
|
||||
],
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
minY: 0,
|
||||
maxY: [
|
||||
...listData[0].map((final e) => e.value),
|
||||
...listData[1].map((final e) => e.value),
|
||||
].reduce(max) *
|
||||
1.2,
|
||||
minX: 0,
|
||||
titlesData: FlTitlesData(
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
interval: 40,
|
||||
reservedSize: 30,
|
||||
getTitlesWidget: (final value, final titleMeta) => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
bottomTitle(
|
||||
value.toInt(),
|
||||
listData[0],
|
||||
period,
|
||||
),
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
showTitles: true,
|
||||
),
|
||||
),
|
||||
leftTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
reservedSize: 50,
|
||||
getTitlesWidget: (final value, final titleMeta) => Padding(
|
||||
padding: const EdgeInsets.only(left: 5),
|
||||
child: Text(
|
||||
DiskSize(byte: value.toInt()).toString(),
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
interval: [
|
||||
...listData[0].map((final e) => e.value),
|
||||
...listData[1].map((final e) => e.value),
|
||||
].reduce(max) *
|
||||
2 /
|
||||
6.5,
|
||||
showTitles: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: true,
|
||||
verticalInterval: 40,
|
||||
horizontalInterval: [
|
||||
...listData[0].map((final e) => e.value),
|
||||
...listData[1].map((final e) => e.value),
|
||||
].reduce(max) *
|
||||
2 /
|
||||
6.5,
|
||||
getDrawingHorizontalLine: (final value) => FlLine(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||||
strokeWidth: 1,
|
||||
),
|
||||
getDrawingVerticalLine: (final value) => FlLine(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||||
strokeWidth: 1,
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: true,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
left: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
right: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
top: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
bool checkToShowTitle(
|
||||
final double minValue,
|
||||
final double maxValue,
|
||||
final SideTitles sideTitles,
|
||||
final double appliedInterval,
|
||||
final double value,
|
||||
) {
|
||||
if (value < 0) {
|
||||
return false;
|
||||
} else if (value == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final diff = value - minValue;
|
||||
final finalValue = diff / 20;
|
||||
return finalValue - finalValue.floor() == 0;
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import 'package:selfprivacy/ui/components/cards/filled_card.dart';
|
|||
import 'package:selfprivacy/ui/components/list_tiles/list_tile_on_surface_variant.dart';
|
||||
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
||||
import 'package:selfprivacy/ui/pages/server_details/charts/cpu_chart.dart';
|
||||
import 'package:selfprivacy/ui/pages/server_details/charts/disk_charts.dart';
|
||||
import 'package:selfprivacy/ui/pages/server_details/charts/memory_chart.dart';
|
||||
import 'package:selfprivacy/ui/pages/server_details/charts/network_charts.dart';
|
||||
import 'package:selfprivacy/ui/pages/server_storage/storage_card.dart';
|
||||
|
|
Loading…
Reference in a new issue