Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:popover/popover.dart';

class AdvancedOptionsDropdown extends StatelessWidget {
const AdvancedOptionsDropdown({
required this.tempController,
required this.phController,
super.key,
});

final TextEditingController tempController;
final TextEditingController phController;

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Builder(
builder: (buttonContext) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () async {
await showPopover(
context: buttonContext,
bodyBuilder: _popoverBuilder,
direction: PopoverDirection.bottom,
width: 160,
arrowHeight: 0,
backgroundColor: Theme.of(context).cardColor,
radius: 8,
);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Advanced Options',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(width: 4),
const Icon(Icons.arrow_drop_down),
],
),
),
);
},
),
);
}

Widget _popoverBuilder(context) => Padding(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_decimalTextField(tempController, 'Temp Deviation'),
const SizedBox(height: 8),
_decimalTextField(phController, 'pH Deviation'),
],
),
);

TextField _decimalTextField(
TextEditingController controller,
String label,
) {
return TextField(
controller: controller,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(labelText: label),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')),
],
);
}
}
10 changes: 9 additions & 1 deletion extras/log_file_client/lib/components/tank_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ class TankCard extends StatelessWidget {
required this.log,
required this.onTap,
required this.httpClient,
required this.tempDeviation,
required this.pHDeviation,
super.key,
});

final Log log;
final void Function() onTap;
final HttpClient httpClient;
final double tempDeviation;
final double pHDeviation;

Future<TankSnapshot> getTankSnapshot() async {
final snapshot = await httpClient.getTankSnapshot(log);
Expand Down Expand Up @@ -107,7 +111,11 @@ class TankCard extends StatelessWidget {
top: Radius.circular(20),
),
child: snapshot != null
? TankThumbnail(snapshot: snapshot.data!)
? TankThumbnail(
snapshot: snapshot.data!,
tempDeviation: tempDeviation,
pHDeviation: pHDeviation,
)
: Container(
decoration: BoxDecoration(
image: DecorationImage(
Expand Down
18 changes: 14 additions & 4 deletions extras/log_file_client/lib/components/tank_thumbnail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@ import 'package:log_file_client/utils/http_client.dart';
import 'package:syncfusion_flutter_charts/charts.dart';

class TankThumbnail extends StatelessWidget {
TankThumbnail({required this.snapshot, DateTime? now, super.key})
: now = now ?? DateTime.now();
TankThumbnail({
required this.snapshot,
double? tempDeviation,
double? pHDeviation,
DateTime? now,
super.key,
}) : now = now ?? DateTime.now(),
tempDeviation = tempDeviation ?? 0.5,
pHDeviation = pHDeviation ?? 0.5;

final TankSnapshot snapshot;
final DateTime now;
final double tempDeviation;
final double pHDeviation;

@override
Widget build(BuildContext context) {
Expand All @@ -32,6 +41,7 @@ class TankThumbnail extends StatelessWidget {
Widget _graph(series, String axis) {
final double setpoint =
axis == 'pHAxis' ? snapshot.pHSetpoint! : snapshot.temperatureSetpoint!;
final double deviation = axis == 'pHAxis' ? pHDeviation : tempDeviation;

return Expanded(
child: SfCartesianChart(
Expand All @@ -46,8 +56,8 @@ class TankThumbnail extends StatelessWidget {
),
primaryYAxis: NumericAxis(
name: axis,
minimum: setpoint - 0.5,
maximum: setpoint + 0.5,
minimum: setpoint - deviation,
maximum: setpoint + deviation,
anchorRangeToVisiblePoints: false,
labelStyle: TextStyle(color: Colors.grey.shade700),
),
Expand Down
37 changes: 37 additions & 0 deletions extras/log_file_client/lib/pages/project_page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:log_file_client/components/advanced_options_dropdown.dart';
import 'package:log_file_client/components/page_header.dart';
import 'package:log_file_client/components/tank_card.dart';
import 'package:log_file_client/pages/graph_page.dart';
Expand Down Expand Up @@ -34,6 +35,34 @@ class _ProjectPageState extends State<ProjectPage> {
);
}

final _tempDeviationController = TextEditingController(text: '0.5');
final _pHDeviationController = TextEditingController(text: '0.5');
double _tempDeviation = 0.5;
double _pHDeviation = 0.5;

@override
void initState() {
super.initState();
_tempDeviationController.addListener(_onDeviationChanged);
_pHDeviationController.addListener(_onDeviationChanged);
}

void _onDeviationChanged() {
setState(() {
_tempDeviation = double.tryParse(_tempDeviationController.text) ?? 0.5;
_pHDeviation = double.tryParse(_pHDeviationController.text) ?? 0.5;
});
}

@override
void dispose() {
_tempDeviationController.removeListener(_onDeviationChanged);
_pHDeviationController.removeListener(_onDeviationChanged);
_tempDeviationController.dispose();
_pHDeviationController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
Expand All @@ -48,6 +77,12 @@ class _ProjectPageState extends State<ProjectPage> {
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Tank Monitor'),
actions: [
AdvancedOptionsDropdown(
tempController: _tempDeviationController,
phController: _pHDeviationController,
),
],
),
body: Center(
child: Column(
Expand All @@ -71,6 +106,8 @@ class _ProjectPageState extends State<ProjectPage> {
return TankCard(
log: widget.project.logs[index],
httpClient: widget.httpClient,
tempDeviation: _tempDeviation,
pHDeviation: _pHDeviation,
onTap: () => unawaited(openTankGraph(widget.project.logs[index])),
);
},
Expand Down
64 changes: 64 additions & 0 deletions extras/log_file_client/test/main_test.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:log_file_client/components/advanced_options_dropdown.dart';
import 'package:log_file_client/components/chart_series_selector.dart';
import 'package:log_file_client/components/graph_view.dart';
import 'package:log_file_client/components/project_card.dart';
Expand Down Expand Up @@ -99,6 +100,69 @@ void main() {
expect(find.text('tank-70'), findsOneWidget);
});

testWidgets('Changing deviation controllers updates TankCard widgets',
(WidgetTester tester) async {
tester.view.physicalSize = const Size(1920, 1080);
tester.view.devicePixelRatio = 1.0;
await tester.pumpWidget(
MaterialApp(
home: ProjectPage(
project: Project('ProjectA', [
Log('tank-24', 'ProjectA-tank-24.log'),
]),
httpClient: HttpClientTest(),
),
),
);
await tester.pumpAndSettle();

// Find the chart widgets and verify it built with default deviation values
SfCartesianChart phChart =
tester.widget(find.byType(SfCartesianChart).first);
SfCartesianChart tempChart =
tester.widget(find.byType(SfCartesianChart).last);
NumericAxis phAxis = phChart.primaryYAxis as NumericAxis;
NumericAxis tempAxis = tempChart.primaryYAxis as NumericAxis;
expect(phAxis.minimum, closeTo(6.25 - 0.5, 0.01));
expect(phAxis.maximum, closeTo(6.25 + 0.5, 0.01));
expect(tempAxis.minimum, closeTo(21.45 - 0.5, 0.01));
expect(tempAxis.maximum, closeTo(21.45 + 0.5, 0.01));

// Click on the AdvancedOptionsDropdown to open it
await tester.tap(find.byType(AdvancedOptionsDropdown).first);
await tester.pumpAndSettle();

// Find the AdvancedOptionsDropdown text fields
final tempField = find.byWidgetPredicate(
(w) =>
w is TextField &&
w.decoration!.labelText!.toLowerCase().contains('temp deviation'),
);
final phField = find.byWidgetPredicate(
(w) =>
w is TextField &&
w.decoration!.labelText!.toLowerCase().contains('ph deviation'),
);

expect(tempField, findsOneWidget);
expect(phField, findsOneWidget);

// Enter new values in the controllers
await tester.enterText(phField, '2.5');
await tester.enterText(tempField, '1.2');
await tester.pumpAndSettle();

// Verify the chart rebuilt with new deviation values
phChart = tester.widget(find.byType(SfCartesianChart).first);
tempChart = tester.widget(find.byType(SfCartesianChart).last);
phAxis = phChart.primaryYAxis as NumericAxis;
tempAxis = tempChart.primaryYAxis as NumericAxis;
expect(phAxis.minimum, closeTo(6.25 - 2.5, 0.01));
expect(phAxis.maximum, closeTo(6.25 + 2.5, 0.01));
expect(tempAxis.minimum, closeTo(21.45 - 1.2, 0.01));
expect(tempAxis.maximum, closeTo(21.45 + 1.2, 0.01));
});

testWidgets('TankCards have thumbnail graphs', (WidgetTester tester) async {
// Build the ProjectPage widget
await tester.pumpWidget(
Expand Down