Skip to content
Open
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
2 changes: 1 addition & 1 deletion dashboard/lib/views/presubmit_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class _PreSubmitViewState extends State<PreSubmitView>
try {
await showDialog<void>(
context: context,
builder: (context) => const FilterDialog(autofocusRegex: true),
builder: (context) => const FilterDialog(),
);
} finally {
_isFilterDialogOpen = false;
Expand Down
158 changes: 87 additions & 71 deletions dashboard/lib/widgets/filter_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@

import 'package:cocoon_common/task_status.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';

import '../state/presubmit.dart';
import 'task_box.dart';

/// A dialog that allows users to filter jobs in the Presubmit Dashboard.
class FilterDialog extends StatefulWidget {
const FilterDialog({super.key, this.autofocusRegex = false});

final bool autofocusRegex;
const FilterDialog({super.key});

@override
State<FilterDialog> createState() => _FilterDialogState();
Expand Down Expand Up @@ -45,6 +44,8 @@ class _FilterDialogState extends State<FilterDialog> {
super.dispose();
}

bool _isClosing = false;

void _onRegexFocusChange() {
if (!_regexFocusNode.hasFocus) {
_applyFilters();
Expand All @@ -60,6 +61,13 @@ class _FilterDialogState extends State<FilterDialog> {
);
}

void _closeDialog() {
if (_isClosing || !mounted) return;
_isClosing = true;
_applyFilters();
Navigator.of(context).pop();
}

void _onRegexChanged(String value) {
setState(() {});
_applyFilters();
Expand Down Expand Up @@ -118,79 +126,87 @@ class _FilterDialogState extends State<FilterDialog> {
) ??
0;

return AlertDialog(
title: const Text('Filter jobs'),
content: SizedBox(
width: 500,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Status', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: TaskStatus.values.map((status) {
final isSelected = _selectedStatuses.contains(status);
return FilterChip(
label: Text(status.value),
selected: isSelected,
onSelected: (_) => _toggleStatus(status),
avatar: _getStatusIcon(status),
);
}).toList(),
),
const SizedBox(height: 16),
Text('Platform', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: availablePlatforms.map((platform) {
return FilterChip(
label: Text(platform),
selected: _selectedPlatforms.contains(platform),
onSelected: (_) => _togglePlatform(platform),
);
}).toList(),
),
const SizedBox(height: 16),
Text('Job Name (Regex)', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
TextField(
controller: _regexController,
focusNode: _regexFocusNode,
autofocus: widget.autofocusRegex,
decoration: const InputDecoration(
hintText: 'e.g. .*test.*',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
return CallbackShortcuts(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(LogicalKeyboardKey.enter): _closeDialog,
const SingleActivator(LogicalKeyboardKey.numpadEnter): _closeDialog,
const SingleActivator(LogicalKeyboardKey.escape): _closeDialog,
},
child: FocusScope(
autofocus: true,
child: AlertDialog(
title: const Text('Filter jobs'),
content: SizedBox(
width: 500,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Status', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: TaskStatus.values.map((status) {
final isSelected = _selectedStatuses.contains(status);
return FilterChip(
label: Text(status.value),
selected: isSelected,
onSelected: (_) => _toggleStatus(status),
avatar: _getStatusIcon(status),
);
}).toList(),
),
const SizedBox(height: 16),
Text('Platform', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: availablePlatforms.map((platform) {
return FilterChip(
label: Text(platform),
selected: _selectedPlatforms.contains(platform),
onSelected: (_) => _togglePlatform(platform),
);
}).toList(),
),
const SizedBox(height: 16),
Text('Job Name (Regex)', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
TextField(
controller: _regexController,
focusNode: _regexFocusNode,
autofocus: true,
decoration: const InputDecoration(
hintText: 'e.g. .*test.*',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
onChanged: _onRegexChanged,
onEditingComplete: _applyFilters,
onSubmitted: (_) => _closeDialog(),
),
),
onChanged: _onRegexChanged,
onEditingComplete: _applyFilters,
],
),
],
),
),
actions: [
TextButton(
onPressed: _clearAll,
child: const Text('Clear all filters'),
),
ElevatedButton(
onPressed: _closeDialog,
child: Text('Show $filteredCount jobs'),
),
],
),
),
actions: [
TextButton(
onPressed: _clearAll,
child: const Text('Clear all filters'),
),
ElevatedButton(
onPressed: () {
_applyFilters();
Navigator.of(context).pop();
},
child: Text('Show $filteredCount jobs'),
),
],
);
}

Expand Down
47 changes: 47 additions & 0 deletions dashboard/test/widgets/filter_dialog_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:cocoon_common/guard_status.dart';
import 'package:cocoon_common/rpc_model.dart';
import 'package:cocoon_common/task_status.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dashboard/service/cocoon.dart';
import 'package:flutter_dashboard/state/presubmit.dart';
import 'package:flutter_dashboard/widgets/filter_dialog.dart';
Expand Down Expand Up @@ -69,6 +70,7 @@ void main() {
),
),
);
await tester.pumpAndSettle();
}

testWidgets('FilterDialog shows all statuses and platforms', (
Expand Down Expand Up @@ -189,4 +191,49 @@ void main() {
isEmpty,
);
});

testWidgets('Pressing Escape closes FilterDialog', (
WidgetTester tester,
) async {
await pumpDialog(tester);
expect(find.byType(FilterDialog), findsOneWidget);

await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();

expect(find.byType(FilterDialog), findsNothing);
});

testWidgets('Pressing Enter closes FilterDialog and applies filters', (
WidgetTester tester,
) async {
await pumpDialog(tester);
expect(find.byType(FilterDialog), findsOneWidget);

await tester.enterText(find.byType(TextField), 'linux');
await tester.pump();

await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();

expect(find.byType(FilterDialog), findsNothing);
expect(presubmitState.jobNameFilter, 'linux');
});

testWidgets(
'Pressing Enter closes FilterDialog when focus is not on TextField',
(WidgetTester tester) async {
await pumpDialog(tester);
expect(find.byType(FilterDialog), findsOneWidget);

await tester.tap(find.text('linux'));
await tester.pump();

await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();

expect(find.byType(FilterDialog), findsNothing);
expect(presubmitState.selectedPlatforms, {'mac'});
},
);
}
Loading