Skip to content

Commit 7bea70a

Browse files
authored
Added custom deviation controls on project page (#502)
* Deviation control UI * Deviation control functionality * Tests * Updated test
1 parent fa7576d commit 7bea70a

5 files changed

Lines changed: 202 additions & 5 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter/services.dart';
3+
import 'package:popover/popover.dart';
4+
5+
class AdvancedOptionsDropdown extends StatelessWidget {
6+
const AdvancedOptionsDropdown({
7+
required this.tempController,
8+
required this.phController,
9+
super.key,
10+
});
11+
12+
final TextEditingController tempController;
13+
final TextEditingController phController;
14+
15+
@override
16+
Widget build(BuildContext context) {
17+
return Padding(
18+
padding: const EdgeInsets.only(right: 16.0),
19+
child: Builder(
20+
builder: (buttonContext) {
21+
return MouseRegion(
22+
cursor: SystemMouseCursors.click,
23+
child: GestureDetector(
24+
onTap: () async {
25+
await showPopover(
26+
context: buttonContext,
27+
bodyBuilder: _popoverBuilder,
28+
direction: PopoverDirection.bottom,
29+
width: 160,
30+
arrowHeight: 0,
31+
backgroundColor: Theme.of(context).cardColor,
32+
radius: 8,
33+
);
34+
},
35+
child: Row(
36+
mainAxisSize: MainAxisSize.min,
37+
children: [
38+
Text(
39+
'Advanced Options',
40+
style: Theme.of(context).textTheme.titleMedium,
41+
),
42+
const SizedBox(width: 4),
43+
const Icon(Icons.arrow_drop_down),
44+
],
45+
),
46+
),
47+
);
48+
},
49+
),
50+
);
51+
}
52+
53+
Widget _popoverBuilder(context) => Padding(
54+
padding: const EdgeInsets.all(12),
55+
child: Column(
56+
mainAxisSize: MainAxisSize.min,
57+
children: [
58+
_decimalTextField(tempController, 'Temp Deviation'),
59+
const SizedBox(height: 8),
60+
_decimalTextField(phController, 'pH Deviation'),
61+
],
62+
),
63+
);
64+
65+
TextField _decimalTextField(
66+
TextEditingController controller,
67+
String label,
68+
) {
69+
return TextField(
70+
controller: controller,
71+
keyboardType: const TextInputType.numberWithOptions(decimal: true),
72+
decoration: InputDecoration(labelText: label),
73+
inputFormatters: [
74+
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')),
75+
],
76+
);
77+
}
78+
}

extras/log_file_client/lib/components/tank_card.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ class TankCard extends StatelessWidget {
1313
required this.log,
1414
required this.onTap,
1515
required this.httpClient,
16+
required this.tempDeviation,
17+
required this.pHDeviation,
1618
super.key,
1719
});
1820

1921
final Log log;
2022
final void Function() onTap;
2123
final HttpClient httpClient;
24+
final double tempDeviation;
25+
final double pHDeviation;
2226

2327
Future<TankSnapshot> getTankSnapshot() async {
2428
final snapshot = await httpClient.getTankSnapshot(log);
@@ -107,7 +111,11 @@ class TankCard extends StatelessWidget {
107111
top: Radius.circular(20),
108112
),
109113
child: snapshot != null
110-
? TankThumbnail(snapshot: snapshot.data!)
114+
? TankThumbnail(
115+
snapshot: snapshot.data!,
116+
tempDeviation: tempDeviation,
117+
pHDeviation: pHDeviation,
118+
)
111119
: Container(
112120
decoration: BoxDecoration(
113121
image: DecorationImage(

extras/log_file_client/lib/components/tank_thumbnail.dart

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,20 @@ import 'package:log_file_client/utils/http_client.dart';
33
import 'package:syncfusion_flutter_charts/charts.dart';
44

55
class TankThumbnail extends StatelessWidget {
6-
TankThumbnail({required this.snapshot, DateTime? now, super.key})
7-
: now = now ?? DateTime.now();
6+
TankThumbnail({
7+
required this.snapshot,
8+
double? tempDeviation,
9+
double? pHDeviation,
10+
DateTime? now,
11+
super.key,
12+
}) : now = now ?? DateTime.now(),
13+
tempDeviation = tempDeviation ?? 0.5,
14+
pHDeviation = pHDeviation ?? 0.5;
815

916
final TankSnapshot snapshot;
1017
final DateTime now;
18+
final double tempDeviation;
19+
final double pHDeviation;
1120

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

3646
return Expanded(
3747
child: SfCartesianChart(
@@ -46,8 +56,8 @@ class TankThumbnail extends StatelessWidget {
4656
),
4757
primaryYAxis: NumericAxis(
4858
name: axis,
49-
minimum: setpoint - 0.5,
50-
maximum: setpoint + 0.5,
59+
minimum: setpoint - deviation,
60+
maximum: setpoint + deviation,
5161
anchorRangeToVisiblePoints: false,
5262
labelStyle: TextStyle(color: Colors.grey.shade700),
5363
),

extras/log_file_client/lib/pages/project_page.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22

33
import 'package:flutter/material.dart';
4+
import 'package:log_file_client/components/advanced_options_dropdown.dart';
45
import 'package:log_file_client/components/page_header.dart';
56
import 'package:log_file_client/components/tank_card.dart';
67
import 'package:log_file_client/pages/graph_page.dart';
@@ -34,6 +35,34 @@ class _ProjectPageState extends State<ProjectPage> {
3435
);
3536
}
3637

38+
final _tempDeviationController = TextEditingController(text: '0.5');
39+
final _pHDeviationController = TextEditingController(text: '0.5');
40+
double _tempDeviation = 0.5;
41+
double _pHDeviation = 0.5;
42+
43+
@override
44+
void initState() {
45+
super.initState();
46+
_tempDeviationController.addListener(_onDeviationChanged);
47+
_pHDeviationController.addListener(_onDeviationChanged);
48+
}
49+
50+
void _onDeviationChanged() {
51+
setState(() {
52+
_tempDeviation = double.tryParse(_tempDeviationController.text) ?? 0.5;
53+
_pHDeviation = double.tryParse(_pHDeviationController.text) ?? 0.5;
54+
});
55+
}
56+
57+
@override
58+
void dispose() {
59+
_tempDeviationController.removeListener(_onDeviationChanged);
60+
_pHDeviationController.removeListener(_onDeviationChanged);
61+
_tempDeviationController.dispose();
62+
_pHDeviationController.dispose();
63+
super.dispose();
64+
}
65+
3766
@override
3867
Widget build(BuildContext context) {
3968
final screenWidth = MediaQuery.of(context).size.width;
@@ -48,6 +77,12 @@ class _ProjectPageState extends State<ProjectPage> {
4877
appBar: AppBar(
4978
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
5079
title: const Text('Tank Monitor'),
80+
actions: [
81+
AdvancedOptionsDropdown(
82+
tempController: _tempDeviationController,
83+
phController: _pHDeviationController,
84+
),
85+
],
5186
),
5287
body: Center(
5388
child: Column(
@@ -71,6 +106,8 @@ class _ProjectPageState extends State<ProjectPage> {
71106
return TankCard(
72107
log: widget.project.logs[index],
73108
httpClient: widget.httpClient,
109+
tempDeviation: _tempDeviation,
110+
pHDeviation: _pHDeviation,
74111
onTap: () => unawaited(openTankGraph(widget.project.logs[index])),
75112
);
76113
},

extras/log_file_client/test/main_test.dart

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:log_file_client/components/advanced_options_dropdown.dart';
34
import 'package:log_file_client/components/chart_series_selector.dart';
45
import 'package:log_file_client/components/graph_view.dart';
56
import 'package:log_file_client/components/project_card.dart';
@@ -99,6 +100,69 @@ void main() {
99100
expect(find.text('tank-70'), findsOneWidget);
100101
});
101102

103+
testWidgets('Changing deviation controllers updates TankCard widgets',
104+
(WidgetTester tester) async {
105+
tester.view.physicalSize = const Size(1920, 1080);
106+
tester.view.devicePixelRatio = 1.0;
107+
await tester.pumpWidget(
108+
MaterialApp(
109+
home: ProjectPage(
110+
project: Project('ProjectA', [
111+
Log('tank-24', 'ProjectA-tank-24.log'),
112+
]),
113+
httpClient: HttpClientTest(),
114+
),
115+
),
116+
);
117+
await tester.pumpAndSettle();
118+
119+
// Find the chart widgets and verify it built with default deviation values
120+
SfCartesianChart phChart =
121+
tester.widget(find.byType(SfCartesianChart).first);
122+
SfCartesianChart tempChart =
123+
tester.widget(find.byType(SfCartesianChart).last);
124+
NumericAxis phAxis = phChart.primaryYAxis as NumericAxis;
125+
NumericAxis tempAxis = tempChart.primaryYAxis as NumericAxis;
126+
expect(phAxis.minimum, closeTo(6.25 - 0.5, 0.01));
127+
expect(phAxis.maximum, closeTo(6.25 + 0.5, 0.01));
128+
expect(tempAxis.minimum, closeTo(21.45 - 0.5, 0.01));
129+
expect(tempAxis.maximum, closeTo(21.45 + 0.5, 0.01));
130+
131+
// Click on the AdvancedOptionsDropdown to open it
132+
await tester.tap(find.byType(AdvancedOptionsDropdown).first);
133+
await tester.pumpAndSettle();
134+
135+
// Find the AdvancedOptionsDropdown text fields
136+
final tempField = find.byWidgetPredicate(
137+
(w) =>
138+
w is TextField &&
139+
w.decoration!.labelText!.toLowerCase().contains('temp deviation'),
140+
);
141+
final phField = find.byWidgetPredicate(
142+
(w) =>
143+
w is TextField &&
144+
w.decoration!.labelText!.toLowerCase().contains('ph deviation'),
145+
);
146+
147+
expect(tempField, findsOneWidget);
148+
expect(phField, findsOneWidget);
149+
150+
// Enter new values in the controllers
151+
await tester.enterText(phField, '2.5');
152+
await tester.enterText(tempField, '1.2');
153+
await tester.pumpAndSettle();
154+
155+
// Verify the chart rebuilt with new deviation values
156+
phChart = tester.widget(find.byType(SfCartesianChart).first);
157+
tempChart = tester.widget(find.byType(SfCartesianChart).last);
158+
phAxis = phChart.primaryYAxis as NumericAxis;
159+
tempAxis = tempChart.primaryYAxis as NumericAxis;
160+
expect(phAxis.minimum, closeTo(6.25 - 2.5, 0.01));
161+
expect(phAxis.maximum, closeTo(6.25 + 2.5, 0.01));
162+
expect(tempAxis.minimum, closeTo(21.45 - 1.2, 0.01));
163+
expect(tempAxis.maximum, closeTo(21.45 + 1.2, 0.01));
164+
});
165+
102166
testWidgets('TankCards have thumbnail graphs', (WidgetTester tester) async {
103167
// Build the ProjectPage widget
104168
await tester.pumpWidget(

0 commit comments

Comments
 (0)