Skip to content

Commit 49d3355

Browse files
authored
183863 sort jobs (#4989)
- For presubmit dashboard implemented checks sorting by task status first and by name within same status. - Added the functionality to auto-select the first check if none is currently selected. - Small fixes: - lowered padding - increased retry count while decreasing retry delay Fix: flutter/flutter#183863
1 parent abb4b58 commit 49d3355

10 files changed

Lines changed: 380 additions & 7 deletions

File tree

app_dart/lib/src/service/scheduler.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1468,7 +1468,10 @@ $stacktrace
14681468
// We're doing a transactional update, which could fail if multiple tasks
14691469
// are running at the same time so retry a sane amount of times before
14701470
// giving up.
1471-
const r = RetryOptions(maxAttempts: 5, delayFactor: Duration(seconds: 5));
1471+
const r = RetryOptions(
1472+
delayFactor: Duration(seconds: 2),
1473+
maxDelay: Duration(minutes: 2),
1474+
);
14721475

14731476
try {
14741477
return await r.retry(() {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Track presubmit_dashboard_sorting_20260318 Context
2+
3+
- [Specification](./spec.md)
4+
- [Implementation Plan](./plan.md)
5+
- [Metadata](./metadata.json)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"track_id": "presubmit_dashboard_sorting_20260318",
3+
"type": "feature",
4+
"status": "new",
5+
"created_at": "2026-03-18T10:00:00Z",
6+
"updated_at": "2026-03-18T10:00:00Z",
7+
"description": "For presubmit dashboard implement checks sorting by task status first and by name within same status. The order of statuses is the following: Failed, Infra Failure, In Progress, New, Cancelled, Skipped, Succeeded."
8+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Implementation Plan - Presubmit Dashboard Sorting [checkpoint: 6de1e57]
2+
3+
Implement sorting of checks in the Presubmit Guard Details view by status first (priority: Failed, Infra Failure, In Progress, New, Cancelled, Skipped, Succeeded) and then alphabetically by name.
4+
5+
## User Review Required
6+
7+
> [!IMPORTANT]
8+
> This change only affects the `_ChecksSidebar` in `PreSubmitView`. The sorting is fixed and applied to every stage within the view.
9+
10+
- **Status Priority Order:**
11+
1. Failed
12+
2. Infra Failure
13+
3. In Progress
14+
4. New (waitingForBackfill)
15+
5. Cancelled
16+
6. Skipped
17+
7. Succeeded
18+
19+
## Proposed Changes
20+
21+
### Dashboard Logic
22+
23+
#### [x] Task: Create task sorting utility
24+
- Create `dashboard/lib/logic/task_sorting.dart`.
25+
- Implement `compareTasks(String nameA, TaskStatus statusA, String nameB, TaskStatus statusB)` function.
26+
- Implement `_statusPriority(TaskStatus status)` helper.
27+
28+
### Dashboard Views
29+
30+
#### [x] Task: Apply sorting to `_ChecksSidebar`
31+
- Modify `dashboard/lib/views/presubmit_view.dart`.
32+
- Import `../logic/task_sorting.dart`.
33+
- In `_ChecksSidebar.build`, sort `stage.builds.entries` before mapping them to `_CheckItem`.
34+
35+
## Verification Plan
36+
37+
### Automated Tests
38+
- Create `dashboard/test/logic/task_sorting_test.dart` to verify the sorting logic with various status and name combinations.
39+
- Add a test case to `dashboard/test/views/presubmit_view_test.dart` to ensure that checks are rendered in the expected sorted order.
40+
41+
### Manual Verification
42+
1. Open the Cocoon dashboard.
43+
2. Navigate to a PR's presubmit details page.
44+
3. Verify that the checks in the sidebar are sorted correctly by status first, then by name.
45+
4. Ensure Failed and Infra Failure tasks appear at the top of their respective stages.
46+
47+
- [x] Task: Conductor - User Manual Verification 'Presubmit Dashboard Sorting' (Protocol in workflow.md)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Track Specification - Presubmit Dashboard Sorting
2+
3+
## Overview
4+
This track focuses on improving the usability of the Presubmit Dashboard by implementing a consistent sorting logic for checks in the "Presubmit Guard Details View". Users currently find it difficult to locate failed or in-progress tasks when many checks are present. This new sorting logic will prioritize tasks requiring attention (Failed, Infra Failure, In Progress) and organize them alphabetically within each status.
5+
6+
## Functional Requirements
7+
- Implement sorting for checks in the `Presubmit Guard Details View` of the Flutter dashboard.
8+
- The primary sort criteria is the **Task Status** following this priority order (highest to lowest):
9+
1. Failed
10+
2. Infra Failure
11+
3. In Progress
12+
4. New (waitingForBackfill)
13+
5. Cancelled
14+
6. Skipped
15+
7. Succeeded
16+
- The secondary sort criteria is the **Check Name** (alphabetical, ascending) within each status.
17+
- This sorting logic must be the **Fixed Default** behavior for the Details View.
18+
- The sorting should be handled entirely within the **Frontend (Dashboard)** using data already provided by the backend APIs.
19+
20+
## Non-Functional Requirements
21+
- **Performance:** Sorting should be efficient and not cause noticeable latency in the UI, even for checks with many tasks.
22+
- **Maintainability:** Sorting logic should be encapsulated in a reusable utility or within the relevant Flutter component to ensure it's easy to test and update.
23+
- **Consistency:** Ensure the UI correctly updates and reflects the sorted order whenever task data is refreshed.
24+
25+
## Acceptance Criteria
26+
- [ ] In the `Presubmit Guard Details View`, checks are sorted first by status according to the specified priority.
27+
- [ ] Within the same status, checks are sorted alphabetically by their name.
28+
- [ ] The sorting is applied automatically when the view is loaded or data is updated.
29+
- [ ] The implementation includes unit tests covering various combinations of statuses and names to verify correct sorting.
30+
- [ ] >95% code coverage for the new sorting logic and its integration.
31+
32+
## Out of Scope
33+
- Backend API modifications for pre-sorted data.
34+
- User-selectable sorting options or toggles.
35+
- Sorting in the "Presubmit Guard Summary View" (unless explicitly requested later).
36+
- Sorting of other dashboards or views in Cocoon.

conductor/product.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Cocoon is the CI coordination and orchestration system for the Flutter project.
1919
* **Tree Status Dashboard:** A Flutter-based web application that provides a visual overview of build health across various commits and branches.
2020
* **Presubmit Check Details:** Backend APIs to retrieve detailed attempt history and status for specific presubmit checks, aiding in debugging and visibility.
2121
* **Presubmit Guard Summaries:** Backend APIs to retrieve summaries of all presubmit checks (Presubmit Guards) of the provided pull request to the dashboard.
22-
* **Presubmit Guard Details:** Displays detailed information and CI check statuses for a specific presubmit check (Presubmit Guard), and allows authenticated users to trigger re-runs for individual jobs or all failed jobs directly from the dashboard.
22+
* **Presubmit Guard Details:** Displays detailed information and CI check statuses for a specific presubmit check (Presubmit Guard), sorted by status (prioritizing failures) and name for better visibility. Authenticated users can trigger re-runs for individual jobs or all failed jobs directly from the dashboard.
2323
* **Merge Queue Visibility:** APIs for querying and inspecting recent GitHub Merge Queue webhook events to diagnose integration issues.
2424
* **Auto-submit Bot:** Handles automated pull request management, including label-based merges, reverts, and validation checks.
2525
* **GitHub Integration:** Robust handling of GitHub webhooks to sync commits, manage check runs, and report build statuses back to PRs.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2019 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:cocoon_common/task_status.dart';
6+
7+
/// Compares two tasks by status first, then by name.
8+
///
9+
/// Status priority (highest to lowest):
10+
/// 1. Failed
11+
/// 2. Infra Failure
12+
/// 3. In Progress
13+
/// 4. New (waitingForBackfill)
14+
/// 5. Cancelled
15+
/// 6. Skipped
16+
/// 7. Succeeded
17+
int compareTasks(
18+
String nameA,
19+
TaskStatus statusA,
20+
String nameB,
21+
TaskStatus statusB,
22+
) {
23+
final priorityA = _statusPriority(statusA);
24+
final priorityB = _statusPriority(statusB);
25+
26+
if (priorityA != priorityB) {
27+
return priorityA.compareTo(priorityB);
28+
}
29+
30+
return nameA.toLowerCase().compareTo(nameB.toLowerCase());
31+
}
32+
33+
int _statusPriority(TaskStatus status) {
34+
switch (status) {
35+
case TaskStatus.failed:
36+
return 1;
37+
case TaskStatus.infraFailure:
38+
return 2;
39+
case TaskStatus.inProgress:
40+
return 3;
41+
case TaskStatus.waitingForBackfill:
42+
return 4;
43+
case TaskStatus.cancelled:
44+
return 5;
45+
case TaskStatus.skipped:
46+
return 6;
47+
case TaskStatus.succeeded:
48+
return 7;
49+
}
50+
}

dashboard/lib/views/presubmit_view.dart

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'package:provider/provider.dart';
1414
import 'package:url_launcher/url_launcher.dart';
1515

1616
import '../dashboard_navigation_drawer.dart';
17+
import '../logic/task_sorting.dart';
1718
import '../state/presubmit.dart';
1819
import '../widgets/app_bar.dart';
1920
import '../widgets/guard_status.dart' as pw;
@@ -520,6 +521,47 @@ class _ChecksSidebar extends StatefulWidget {
520521

521522
class _ChecksSidebarState extends State<_ChecksSidebar> {
522523
final ScrollController _scrollController = ScrollController();
524+
late List<List<MapEntry<String, TaskStatus>>> _sortedBuildsPerStage;
525+
526+
@override
527+
void initState() {
528+
super.initState();
529+
_updateSortedBuilds();
530+
_selectFirstCheck();
531+
}
532+
533+
@override
534+
void didUpdateWidget(_ChecksSidebar oldWidget) {
535+
super.didUpdateWidget(oldWidget);
536+
if (widget.guardResponse != oldWidget.guardResponse) {
537+
_updateSortedBuilds();
538+
}
539+
if (widget.selectedCheck == null) {
540+
_selectFirstCheck();
541+
}
542+
}
543+
544+
void _selectFirstCheck() {
545+
if (widget.selectedCheck != null) return;
546+
for (final stage in _sortedBuildsPerStage) {
547+
if (stage.isNotEmpty) {
548+
final firstCheck = stage.first.key;
549+
WidgetsBinding.instance.addPostFrameCallback((_) {
550+
if (mounted && widget.selectedCheck == null) {
551+
widget.onCheckSelected(firstCheck);
552+
}
553+
});
554+
break;
555+
}
556+
}
557+
}
558+
559+
void _updateSortedBuilds() {
560+
_sortedBuildsPerStage = widget.guardResponse.stages.map((stage) {
561+
return stage.builds.entries.toList()
562+
..sort((a, b) => compareTasks(a.key, a.value, b.key, b.value));
563+
}).toList();
564+
}
523565

524566
@override
525567
void dispose() {
@@ -545,6 +587,7 @@ class _ChecksSidebarState extends State<_ChecksSidebar> {
545587
itemCount: widget.guardResponse.stages.length,
546588
itemBuilder: (context, stageIndex) {
547589
final stage = widget.guardResponse.stages[stageIndex];
590+
final sortedBuilds = _sortedBuildsPerStage[stageIndex];
548591
return Column(
549592
crossAxisAlignment: CrossAxisAlignment.start,
550593
children: [
@@ -567,7 +610,7 @@ class _ChecksSidebarState extends State<_ChecksSidebar> {
567610
),
568611
),
569612
),
570-
...stage.builds.entries.map((entry) {
613+
...sortedBuilds.map((entry) {
571614
final isSelected = widget.selectedCheck == entry.key;
572615
return _CheckItem(
573616
name: entry.key,
@@ -616,7 +659,7 @@ class _CheckItem extends StatelessWidget {
616659
return InkWell(
617660
onTap: onTap,
618661
child: Container(
619-
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
662+
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
620663
decoration: BoxDecoration(
621664
color: isSelected
622665
? (isDark
@@ -633,13 +676,13 @@ class _CheckItem extends StatelessWidget {
633676
child: Row(
634677
children: [
635678
_getStatusIcon(status),
636-
const SizedBox(width: 12),
679+
const SizedBox(width: 4),
637680
Expanded(
638681
child: Text(
639682
name,
640683
style: TextStyle(
641684
fontSize: 14,
642-
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
685+
fontWeight: FontWeight.normal,
643686
color: isSelected && !isDark ? const Color(0xFF1F2937) : null,
644687
),
645688
overflow: TextOverflow.ellipsis,
@@ -660,6 +703,7 @@ class _CheckItem extends StatelessWidget {
660703
icon: const Icon(Icons.refresh, size: 18),
661704
label: const Text('Re-run'),
662705
style: TextButton.styleFrom(
706+
minimumSize: const Size(64, 18),
663707
foregroundColor: isDark
664708
? const Color(0xFF58A6FF)
665709
: const Color(0xFF0969DA),
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2019 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:cocoon_common/task_status.dart';
6+
import 'package:flutter_dashboard/logic/task_sorting.dart';
7+
import 'package:flutter_test/flutter_test.dart';
8+
9+
void main() {
10+
group('compareTasks', () {
11+
test('sorts by status priority', () {
12+
final tasks = [
13+
('a', TaskStatus.succeeded),
14+
('b', TaskStatus.failed),
15+
('c', TaskStatus.infraFailure),
16+
('d', TaskStatus.inProgress),
17+
('e', TaskStatus.waitingForBackfill),
18+
('f', TaskStatus.cancelled),
19+
('g', TaskStatus.skipped),
20+
];
21+
22+
tasks.sort((a, b) => compareTasks(a.$1, a.$2, b.$1, b.$2));
23+
24+
expect(tasks, [
25+
('b', TaskStatus.failed),
26+
('c', TaskStatus.infraFailure),
27+
('d', TaskStatus.inProgress),
28+
('e', TaskStatus.waitingForBackfill),
29+
('f', TaskStatus.cancelled),
30+
('g', TaskStatus.skipped),
31+
('a', TaskStatus.succeeded),
32+
]);
33+
});
34+
35+
test('sorts by name when status is the same', () {
36+
final tasks = [
37+
('beta', TaskStatus.failed),
38+
('alpha', TaskStatus.failed),
39+
('gamma', TaskStatus.succeeded),
40+
('delta', TaskStatus.succeeded),
41+
];
42+
43+
tasks.sort((a, b) => compareTasks(a.$1, a.$2, b.$1, b.$2));
44+
45+
expect(tasks, [
46+
('alpha', TaskStatus.failed),
47+
('beta', TaskStatus.failed),
48+
('delta', TaskStatus.succeeded),
49+
('gamma', TaskStatus.succeeded),
50+
]);
51+
});
52+
53+
test('complex sorting', () {
54+
final tasks = [
55+
('z', TaskStatus.succeeded),
56+
('y', TaskStatus.failed),
57+
('x', TaskStatus.infraFailure),
58+
('w', TaskStatus.inProgress),
59+
('v', TaskStatus.failed),
60+
('u', TaskStatus.succeeded),
61+
];
62+
63+
tasks.sort((a, b) => compareTasks(a.$1, a.$2, b.$1, b.$2));
64+
65+
expect(tasks, [
66+
('v', TaskStatus.failed),
67+
('y', TaskStatus.failed),
68+
('x', TaskStatus.infraFailure),
69+
('w', TaskStatus.inProgress),
70+
('u', TaskStatus.succeeded),
71+
('z', TaskStatus.succeeded),
72+
]);
73+
});
74+
});
75+
}

0 commit comments

Comments
 (0)