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
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
136 changes: 76 additions & 60 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 @@ -120,61 +128,72 @@ class _FilterDialogState extends State<FilterDialog> {

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,
content: CallbackShortcuts(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(LogicalKeyboardKey.enter): _closeDialog,
const SingleActivator(LogicalKeyboardKey.numpadEnter): _closeDialog,
const SingleActivator(LogicalKeyboardKey.escape): _closeDialog,
},
child: FocusScope(
autofocus: true,
child: 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,
],
),
],
),
),
),
),
Expand All @@ -184,10 +203,7 @@ class _FilterDialogState extends State<FilterDialog> {
child: const Text('Clear all filters'),
),
ElevatedButton(
onPressed: () {
_applyFilters();
Navigator.of(context).pop();
},
onPressed: _closeDialog,
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