Skip to content

Commit 255dbe6

Browse files
authored
Merge pull request #442 from Sakib25800/connect-table
Add table interactivity
2 parents 8554139 + 1340991 commit 255dbe6

File tree

3 files changed

+185
-104
lines changed

3 files changed

+185
-104
lines changed

src/templates.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
1-
use crate::database::{MergeableState::*, PullRequestModel, QueueStatus::*, TreeState};
1+
use crate::database::{MergeableState::*, PullRequestModel, QueueStatus, TreeState};
22
use askama::Template;
33
use axum::response::{Html, IntoResponse, Response};
44
use http::StatusCode;
55

6+
/// Build status to display on the queue page.
7+
pub fn status_text(pr: &PullRequestModel) -> String {
8+
if let Some(try_build) = &pr.try_build {
9+
try_build.status.to_string()
10+
} else {
11+
match pr.queue_status() {
12+
QueueStatus::Approved(_) => "approved".to_string(),
13+
QueueStatus::ReadyForMerge(_, _) => "ready for merge".to_string(),
14+
QueueStatus::Pending(_, _) => "pending".to_string(),
15+
QueueStatus::Stalled(_, _) => "stalled".to_string(),
16+
QueueStatus::NotApproved => String::new(),
17+
}
18+
}
19+
}
20+
621
pub struct HtmlTemplate<T>(pub T);
722

823
impl<T> IntoResponse for HtmlTemplate<T>

src/utils/sort_queue.rs

Lines changed: 17 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,40 @@
11
use crate::bors::RollupMode;
2-
use crate::database::{BuildStatus, MergeableState, PullRequestModel};
2+
use crate::database::{MergeableState, PullRequestModel, QueueStatus};
33

44
/// Sorts pull requests according to merge queue priority rules.
5-
/// Ordered by pending builds > success builds > approval > mergeability > priority value > rollup > age.
5+
/// Ordered by: ready for merge > pending builds > approved > stalled > not approved > mergeability
6+
/// > priority > rollup > age.
67
pub fn sort_queue_prs(mut prs: Vec<PullRequestModel>) -> Vec<PullRequestModel> {
78
prs.sort_by(|a, b| {
8-
// 1. Pending builds come first (to block merge queue)
9-
get_queue_blocking_priority(a)
10-
.cmp(&get_queue_blocking_priority(b))
11-
// 2. Compare approval status (approved PRs should come first)
12-
.then_with(|| a.is_approved().cmp(&b.is_approved()).reverse())
13-
// 3. Compare build status within approval groups
14-
.then_with(|| get_status_priority(a).cmp(&get_status_priority(b)))
15-
// 4. Compare mergeability state (0 = mergeable, 1 = conflicts/unknown)
9+
// 1. Compare queue status (ready for merge > pending > approved > stalled > not approved)
10+
get_queue_status_priority(&a.queue_status())
11+
.cmp(&get_queue_status_priority(&b.queue_status()))
12+
// 2. Compare mergeability state (0 = mergeable, 1 = conflicts/unknown)
1613
.then_with(|| get_mergeable_priority(a).cmp(&get_mergeable_priority(b)))
17-
// 5. Compare priority numbers (higher priority should come first)
14+
// 3. Compare priority numbers (higher priority should come first)
1815
.then_with(|| {
1916
a.priority
2017
.unwrap_or(0)
2118
.cmp(&b.priority.unwrap_or(0))
2219
.reverse()
2320
})
24-
// 6. Compare rollup mode (-1 = never/iffy, 0 = maybe, 1 = always)
21+
// 4. Compare rollup mode (always > maybe > iffy > never)
2522
.then_with(|| {
2623
get_rollup_priority(a.rollup.as_ref()).cmp(&get_rollup_priority(b.rollup.as_ref()))
2724
})
28-
// 7. Compare PR numbers (older first)
25+
// 5. Compare PR numbers (older first)
2926
.then_with(|| a.number.cmp(&b.number))
3027
});
3128
prs
3229
}
3330

34-
fn get_queue_blocking_priority(pr: &PullRequestModel) -> u32 {
35-
match &pr.auto_build {
36-
Some(build) => match build.status {
37-
// Pending builds must come first to block the merge queue
38-
BuildStatus::Pending => 0,
39-
// All other statuses come after
40-
_ => 1,
41-
},
42-
None => 1, // No build - can potentially start new build
43-
}
44-
}
45-
46-
fn get_status_priority(pr: &PullRequestModel) -> u32 {
47-
match &pr.auto_build {
48-
Some(build) => match build.status {
49-
BuildStatus::Success => 0,
50-
BuildStatus::Pending => 1,
51-
BuildStatus::Failure => 3,
52-
BuildStatus::Cancelled | BuildStatus::Timeouted => 2,
53-
},
54-
None => {
55-
if pr.is_approved() {
56-
1 // Approved but no build - should be prioritized
57-
} else {
58-
2 // No status
59-
}
60-
}
31+
fn get_queue_status_priority(status: &QueueStatus) -> u32 {
32+
match status {
33+
QueueStatus::ReadyForMerge(_, _) => 0,
34+
QueueStatus::Pending(_, _) => 1,
35+
QueueStatus::Approved(_) => 2,
36+
QueueStatus::Stalled(_, _) => 3,
37+
QueueStatus::NotApproved => 4,
6138
}
6239
}
6340

templates/queue.html

Lines changed: 152 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,29 @@
33
{% block title %}Bors queue - {{ repo_name }} {% if tree_state.is_closed() %} [TREECLOSED] {% endif %}{% endblock %}
44

55
{% block head %}
6+
<link rel="stylesheet" href="https://cdn.datatables.net/2.3.4/css/dataTables.dataTables.min.css" />
7+
<link rel="stylesheet" href="https://cdn.datatables.net/rowgroup/1.5.1/css/rowGroup.dataTables.min.css" />
68
<style>
79
main {
810
max-width: 100rem;
911
width: 100%;
1012
margin: 0 auto;
1113
}
14+
15+
.table-wrapper {
16+
overflow-x: auto;
17+
-webkit-overflow-scrolling: touch;
18+
}
19+
20+
table {
21+
min-width: 100%;
22+
white-space: nowrap;
23+
}
24+
25+
table th,
26+
table td {
27+
padding: 0.5rem;
28+
}
1229
</style>
1330
{% endblock %}
1431

@@ -32,72 +49,144 @@ <h1>
3249
{{ stats.failed_count }} failed, {{ stats.rolled_up_count }} rolled up
3350
</p>
3451

35-
<table>
36-
<thead>
37-
<th>#</th>
38-
<th>Status</th>
39-
<th>Mergeable</th>
40-
<th>Title</th>
41-
<th>Author</th>
42-
<th>Assignees</th>
43-
<th>Approved by</th>
44-
<th>Priority</th>
45-
<th>Rollup</th>
46-
</thead>
47-
48-
<tbody>
49-
{% for pr in prs %}
50-
<tr>
51-
<td>
52-
<a href="{{ repo_url }}/pull/{{ pr.number }}">{{ pr.number.0 }}</a>
53-
</td>
54-
<td>
55-
{% if let Some(try_build) = pr.try_build %}
56-
<a href="../results/{{ repo_name }}/{{ pr.number }}">{{ try_build.status }}</a> (try)
57-
{% else %}
58-
{% match pr.queue_status() %}
59-
{% when Approved(_) %}
60-
approved
61-
{% when ReadyForMerge(_, _) %}
62-
ready for merge
63-
{% when Pending(_, _) %}
64-
pending
65-
{% when Stalled(_, _) %}
66-
stalled
67-
{% when NotApproved %}
68-
{% endmatch %}
69-
{% endif %}
70-
</td>
71-
<td>
72-
{% match pr.mergeable_state %}
73-
{% when Mergeable %}
74-
yes
75-
{% when HasConflicts %}
76-
no
77-
{% when Unknown %}
78-
{% endmatch %}
79-
</td>
80-
<td>{{ pr.title }}</td>
81-
<td>{{ pr.author }}</td>
82-
<td>{{ pr.assignees|join(", ") }}</td>
83-
<td>
84-
{% if let Some(approver) = pr.approver() %}
85-
{{ approver }}
86-
{% endif %}
87-
</td>
88-
<td>{{ pr.priority.unwrap_or(0) }}</td>
89-
<td>
90-
{% if let Some(rollup) = pr.rollup %}
91-
{{ rollup }}
92-
{% endif %}
93-
</td>
94-
</tr>
95-
{% endfor %}
96-
</tbody>
97-
</table>
52+
<div style="margin-bottom: 1rem;">
53+
<label for="groupBy">Group by: </label>
54+
<select id="groupBy">
55+
<option value="">None</option>
56+
<option value="1">Status</option>
57+
<option value="2">Mergeable</option>
58+
<option value="4">Author</option>
59+
<option value="7">Priority</option>
60+
</select>
61+
</div>
62+
63+
<div class="table-wrapper">
64+
<table id="table">
65+
<thead>
66+
<tr>
67+
<th>#</th>
68+
<th>Status</th>
69+
<th>Mergeable</th>
70+
<th>Title</th>
71+
<th>Author</th>
72+
<th>Assignees</th>
73+
<th>Approved by</th>
74+
<th>Priority</th>
75+
<th>Rollup</th>
76+
</tr>
77+
</thead>
78+
79+
<tbody>
80+
{% for pr in prs %}
81+
<tr>
82+
<td>
83+
<a href="{{ repo_url }}/pull/{{ pr.number }}">{{ pr.number.0 }}</a>
84+
</td>
85+
<td data-status="{{ crate::templates::status_text(pr) }}">
86+
{% if let Some(try_build) = pr.try_build %}
87+
<a href="../results/{{ repo_name }}/{{ pr.number }}">{{ crate::templates::status_text(pr) }}</a> (try)
88+
{% else %}
89+
{{ crate::templates::status_text(pr) }}
90+
{% endif %}
91+
</td>
92+
<td>
93+
{% match pr.mergeable_state %}
94+
{% when Mergeable %}
95+
yes
96+
{% when HasConflicts %}
97+
no
98+
{% when Unknown %}
99+
{% endmatch %}
100+
</td>
101+
<td>{{ pr.title }}</td>
102+
<td>{{ pr.author }}</td>
103+
<td>{{ pr.assignees|join(", ") }}</td>
104+
<td>
105+
{% if let Some(approver) = pr.approver() %}
106+
{{ approver }}
107+
{% endif %}
108+
</td>
109+
<td>{{ pr.priority.unwrap_or(0) }}</td>
110+
<td>
111+
{% if let Some(rollup) = pr.rollup %}
112+
{{ rollup }}
113+
{% endif %}
114+
</td>
115+
</tr>
116+
{% endfor %}
117+
</tbody>
118+
</table>
119+
</div>
98120

99121
<div style="text-align: center; margin-top: 1em;">
100122
<a href="https://github.com/rust-lang/bors">Contribute on GitHub</a>
101123
</div>
102124
</main>
125+
126+
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
127+
<script src="https://cdn.datatables.net/2.3.4/js/dataTables.min.js"></script>
128+
<script src="https://cdn.datatables.net/rowgroup/1.5.1/js/dataTables.rowGroup.min.js"></script>
129+
130+
<script>
131+
const getDataStatusFromCell = (cell) => cell?.dataset?.status || '';
132+
133+
function initializeTable(colIndex) {
134+
let config = {
135+
paging: false,
136+
info: false,
137+
columnDefs: [
138+
{
139+
targets: 1, // Column 1 (Status column)
140+
render: function(data, type, row, meta) {
141+
if (type === 'display') {
142+
return data;
143+
}
144+
145+
// Use data-status for everything else
146+
if (meta && meta.settings && meta.row !== undefined) {
147+
let rowNode = meta.settings.aoData[meta.row]?.nTr;
148+
if (rowNode) {
149+
return getDataStatusFromCell(rowNode.cells[meta.col]);
150+
}
151+
}
152+
153+
return data;
154+
}
155+
}
156+
],
157+
order: []
158+
};
159+
160+
if (colIndex !== null) {
161+
config.order = [[colIndex, "asc"]];
162+
config.rowGroup = {
163+
dataSrc: colIndex === 1
164+
? ([_, html]) => {
165+
let table = document.getElementById('table');
166+
if (table && table.tBodies[0]) {
167+
let rows = Array.from(table.tBodies[0].rows);
168+
for (let row of rows) {
169+
if (row.cells[1] && row.cells[1].innerHTML === html) {
170+
return getDataStatusFromCell(row.cells[1]);
171+
}
172+
}
173+
}
174+
return html;
175+
}
176+
: colIndex
177+
};
178+
}
179+
180+
return new DataTable("#table", config);
181+
}
182+
183+
let table = initializeTable(null);
184+
185+
// Handle group by dropdown changes
186+
document.getElementById("groupBy").addEventListener("change", function() {
187+
let colIndex = this.value === "" ? null : parseInt(this.value);
188+
table.destroy();
189+
table = initializeTable(colIndex);
190+
});
191+
</script>
103192
{% endblock %}

0 commit comments

Comments
 (0)