mirror of
https://github.com/Sea-Haven-Industries/pr-reviewer.git
synced 2026-09-30 04:33:14 +00:00
Add sidebar quick buttons and Dependabot review rendering
Add Expand all / Collapse all controls and a Dependabot-only filter to the top of the queue sidebar. Render the dependency-risk assessment in the detail view (update-type and risk badges, packages, reasons, title and description notes) reusing the post, revise, and auto-merge controls, and show a risk chip on Dependabot rows in the queue. Reviews without a "_kind" fall back to the code-review layout.
This commit is contained in:
parent
667afd73f9
commit
138d300048
1 changed files with 82 additions and 19 deletions
|
|
@ -44,7 +44,11 @@
|
||||||
button.primary { background: var(--accent); color: #0b0e14; border-color: var(--accent); font-weight: 600; }
|
button.primary { background: var(--accent); color: #0b0e14; border-color: var(--accent); font-weight: 600; }
|
||||||
button.danger { border-color: var(--block); }
|
button.danger { border-color: var(--block); }
|
||||||
.layout { display: grid; grid-template-columns: 340px 1fr; height: calc(100vh - 51px); }
|
.layout { display: grid; grid-template-columns: 340px 1fr; height: calc(100vh - 51px); }
|
||||||
.queue { border-right: 1px solid var(--line); overflow-y: auto; background: var(--panel); }
|
.queue-col { display: flex; flex-direction: column; border-right: 1px solid var(--line); overflow: hidden; background: var(--panel); }
|
||||||
|
.queue-tools { display: flex; gap: 6px; padding: 8px 10px; border-bottom: 1px solid var(--line); background: var(--panel-2); }
|
||||||
|
.queue-tools button { font-size: 12px; padding: 4px 9px; }
|
||||||
|
.queue-tools button.on { background: var(--accent); color: #0b0e14; border-color: var(--accent); font-weight: 600; }
|
||||||
|
.queue { overflow-y: auto; background: var(--panel); flex: 1; }
|
||||||
.queue-item {
|
.queue-item {
|
||||||
padding: 12px 16px; border-bottom: 1px solid var(--line); cursor: pointer;
|
padding: 12px 16px; border-bottom: 1px solid var(--line); cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
@ -84,6 +88,12 @@
|
||||||
.rec.APPROVE { color: var(--nit); border-color: var(--nit); }
|
.rec.APPROVE { color: var(--nit); border-color: var(--nit); }
|
||||||
.rec.REQUEST_CHANGES { color: var(--block); border-color: var(--block); }
|
.rec.REQUEST_CHANGES { color: var(--block); border-color: var(--block); }
|
||||||
.rec.COMMENT { color: var(--fix); border-color: var(--fix); }
|
.rec.COMMENT { color: var(--fix); border-color: var(--fix); }
|
||||||
|
.rec.dep-safe, .rec.dep-low_risk { color: var(--nit); border-color: var(--nit); }
|
||||||
|
.rec.dep-risky { color: var(--fix); border-color: var(--fix); }
|
||||||
|
.rec.dep-breaking { color: var(--block); border-color: var(--block); }
|
||||||
|
.badge.dep-safe, .badge.dep-low_risk { color: var(--nit); border-color: var(--nit); }
|
||||||
|
.badge.dep-risky { color: var(--fix); border-color: var(--fix); }
|
||||||
|
.badge.dep-breaking { color: var(--block); border-color: var(--block); }
|
||||||
.summary { background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--accent);
|
.summary { background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--accent);
|
||||||
border-radius: 8px; padding: 14px 16px; margin: 8px 0 16px; }
|
border-radius: 8px; padding: 14px 16px; margin: 8px 0 16px; }
|
||||||
.cat { margin: 14px 0; }
|
.cat { margin: 14px 0; }
|
||||||
|
|
@ -127,14 +137,21 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
|
<div class="queue-col">
|
||||||
|
<div class="queue-tools">
|
||||||
|
<button id="expandAllBtn">Expand all</button>
|
||||||
|
<button id="collapseAllBtn">Collapse all</button>
|
||||||
|
<button id="depFilterBtn">Dependabot only</button>
|
||||||
|
</div>
|
||||||
<div class="queue" id="queue"><div class="empty spin">Loading queue...</div></div>
|
<div class="queue" id="queue"><div class="empty spin">Loading queue...</div></div>
|
||||||
|
</div>
|
||||||
<div class="detail" id="detail"><div class="empty">Select a PR. Reviews are prepared automatically in the background.</div></div>
|
<div class="detail" id="detail"><div class="empty">Select a PR. Reviews are prepared automatically in the background.</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="toastHost"></div>
|
<div id="toastHost"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const state = { items: [], byKey: {}, me: "", active: null, mentions: [], shownKey: null, pollMs: 8000 };
|
const state = { items: [], byKey: {}, me: "", active: null, mentions: [], shownKey: null, pollMs: 8000, depFilter: false };
|
||||||
|
|
||||||
function key(pr){ return `${pr.owner}/${pr.repo}#${pr.number}`; }
|
function key(pr){ return `${pr.owner}/${pr.repo}#${pr.number}`; }
|
||||||
|
|
||||||
|
|
@ -233,18 +250,35 @@ function saveCollapsed(set, repoKeys){
|
||||||
// PR age for oldest->newest ordering; fall back to updated_at if created_at absent.
|
// PR age for oldest->newest ordering; fall back to updated_at if created_at absent.
|
||||||
function prTime(it){ return Date.parse(it.created_at || it.updated_at || "") || 0; }
|
function prTime(it){ return Date.parse(it.created_at || it.updated_at || "") || 0; }
|
||||||
|
|
||||||
|
function isDependabot(it){ return (it.author || "").toLowerCase().startsWith("dependabot"); }
|
||||||
|
function visibleItems(){ return state.depFilter ? state.items.filter(isDependabot) : state.items; }
|
||||||
|
function currentRepoKeys(){ return new Set(visibleItems().map(it => `${it.owner}/${it.repo}`)); }
|
||||||
|
|
||||||
|
function expandAll(){ localStorage.setItem(COLLAPSE_KEY, "[]"); renderQueue(); }
|
||||||
|
function collapseAll(){ localStorage.setItem(COLLAPSE_KEY, JSON.stringify([...currentRepoKeys()])); renderQueue(); }
|
||||||
|
function toggleDepFilter(){
|
||||||
|
state.depFilter = !state.depFilter;
|
||||||
|
document.getElementById("depFilterBtn").classList.toggle("on", state.depFilter);
|
||||||
|
renderQueue();
|
||||||
|
}
|
||||||
|
|
||||||
function queueItem(it){
|
function queueItem(it){
|
||||||
const k = key(it);
|
const k = key(it);
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "queue-item" + (state.active===k ? " active":"") + (it.status==="closed" ? " closed":"");
|
div.className = "queue-item" + (state.active===k ? " active":"") + (it.status==="closed" ? " closed":"");
|
||||||
const mention = (state.mentions||[]).includes((it.author||"").toLowerCase());
|
const mention = (state.mentions||[]).includes((it.author||"").toLowerCase());
|
||||||
const statusBadge = `<span class="badge ${it.status}">${STATUS_LABEL[it.status]||it.status}</span>`;
|
const statusBadge = `<span class="badge ${it.status}">${STATUS_LABEL[it.status]||it.status}</span>`;
|
||||||
|
const rv = it.review;
|
||||||
|
const depBadge = (rv && rv._kind === "dependabot" && rv.assessment)
|
||||||
|
? `<span class="badge dep-${rv.assessment}">${escapeHtml(String(rv.assessment).replace("_"," "))}</span>`
|
||||||
|
: "";
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<div class="repo">#${it.number}</div>
|
<div class="repo">#${it.number}</div>
|
||||||
<div class="title">${escapeHtml(it.title||"(no title)")}</div>
|
<div class="title">${escapeHtml(it.title||"(no title)")}</div>
|
||||||
<div class="sub">
|
<div class="sub">
|
||||||
<span>@${escapeHtml(it.author||"")}</span>
|
<span>@${escapeHtml(it.author||"")}</span>
|
||||||
${mention ? '<span class="badge mention">will @mention</span>':''}
|
${mention ? '<span class="badge mention">will @mention</span>':''}
|
||||||
|
${depBadge}
|
||||||
${statusBadge}
|
${statusBadge}
|
||||||
</div>`;
|
</div>`;
|
||||||
div.onclick = ()=>selectPR(k);
|
div.onclick = ()=>selectPR(k);
|
||||||
|
|
@ -253,11 +287,17 @@ function queueItem(it){
|
||||||
|
|
||||||
function renderQueue(){
|
function renderQueue(){
|
||||||
const q = document.getElementById("queue");
|
const q = document.getElementById("queue");
|
||||||
if(!state.items.length){ q.innerHTML = '<div class="empty">No PRs in the queue yet. The worker reviews new PRs automatically.</div>'; return; }
|
const items = visibleItems();
|
||||||
|
if(!items.length){
|
||||||
|
q.innerHTML = `<div class="empty">${state.depFilter
|
||||||
|
? "No Dependabot PRs in the queue."
|
||||||
|
: "No PRs in the queue yet. The worker reviews new PRs automatically."}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Group by owner/repo.
|
// Group by owner/repo.
|
||||||
const groups = {};
|
const groups = {};
|
||||||
state.items.forEach(it => {
|
items.forEach(it => {
|
||||||
const g = `${it.owner}/${it.repo}`;
|
const g = `${it.owner}/${it.repo}`;
|
||||||
(groups[g] = groups[g] || []).push(it);
|
(groups[g] = groups[g] || []).push(it);
|
||||||
});
|
});
|
||||||
|
|
@ -341,6 +381,26 @@ async function runReview(pr){
|
||||||
function renderReview(pr, rv){
|
function renderReview(pr, rv){
|
||||||
const d = document.getElementById("detail");
|
const d = document.getElementById("detail");
|
||||||
state.shownKey = key(pr);
|
state.shownKey = key(pr);
|
||||||
|
const rec = rv.recommended_event || "COMMENT";
|
||||||
|
const isDep = rv._kind === "dependabot";
|
||||||
|
let headline, middle;
|
||||||
|
if(isDep){
|
||||||
|
const asmt = String(rv.assessment || "unknown");
|
||||||
|
const ut = String(rv.update_type || "unknown");
|
||||||
|
headline = `<span class="rec dep-${asmt}">${escapeHtml(asmt.replace("_"," "))}</span>` +
|
||||||
|
`<span class="badge">update: ${escapeHtml(ut)}</span>` +
|
||||||
|
`<button id="rerunBtn">Re-run</button>`;
|
||||||
|
const pkgs = (rv.packages||[]).filter(p=>p && typeof p==="object");
|
||||||
|
const pkgHtml = pkgs.length ? `<div class="cat"><h3>PACKAGES</h3><ul>${
|
||||||
|
pkgs.map(p=>`<li><code>${escapeHtml(p.name||"?")}</code> ${escapeHtml(String(p.from||"?"))} → ${escapeHtml(String(p.to||"?"))}</li>`).join("")}</ul></div>` : "";
|
||||||
|
const reasons = (rv.reasons||[]).filter(Boolean);
|
||||||
|
const reasonHtml = reasons.length ? `<div class="cat"><h3>ASSESSMENT</h3><ul>${
|
||||||
|
reasons.map(r=>`<li>${mdInline(r)}</li>`).join("")}</ul></div>` : "";
|
||||||
|
const notes = (rv.title_desc_notes||[]).filter(Boolean);
|
||||||
|
const noteHtml = notes.length ? `<div class="cat question"><h3>TITLE / DESCRIPTION</h3><ul>${
|
||||||
|
notes.map(n=>`<li>${mdInline(n)}</li>`).join("")}</ul></div>` : "";
|
||||||
|
middle = pkgHtml + reasonHtml + noteHtml;
|
||||||
|
} else {
|
||||||
const cats = [["block","BLOCK"],["fix","FIX"],["nit","NIT"],["question","QUESTION"]];
|
const cats = [["block","BLOCK"],["fix","FIX"],["nit","NIT"],["question","QUESTION"]];
|
||||||
let catHtml = "";
|
let catHtml = "";
|
||||||
cats.forEach(([k,label])=>{
|
cats.forEach(([k,label])=>{
|
||||||
|
|
@ -349,17 +409,17 @@ function renderReview(pr, rv){
|
||||||
catHtml += `<div class="cat ${k}"><h3>${label}</h3><ul>${
|
catHtml += `<div class="cat ${k}"><h3>${label}</h3><ul>${
|
||||||
items.map(i=>`<li>${mdInline(i)}</li>`).join("")}</ul></div>`;
|
items.map(i=>`<li>${mdInline(i)}</li>`).join("")}</ul></div>`;
|
||||||
});
|
});
|
||||||
const rec = rv.recommended_event || "COMMENT";
|
headline = `<span class="rec ${rec}">recommends ${rec}</span>` +
|
||||||
|
`<button id="rerunBtn">Re-run</button>`;
|
||||||
|
middle = catHtml +
|
||||||
|
(rv.overall ? `<div class="cat"><h3 style="color:var(--ink-dim)">OVERALL</h3><div>${mdInline(rv.overall)}</div></div>` : "");
|
||||||
|
}
|
||||||
d.innerHTML = `
|
d.innerHTML = `
|
||||||
<h2>${escapeHtml(pr.title)}</h2>
|
<h2>${escapeHtml(pr.title)}</h2>
|
||||||
<a class="prlink" href="${pr.url}" target="_blank">${pr.owner}/${pr.repo} #${pr.number}</a>
|
<a class="prlink" href="${pr.url}" target="_blank">${pr.owner}/${pr.repo} #${pr.number}</a>
|
||||||
<div class="toolbar">
|
<div class="toolbar">${headline}</div>
|
||||||
<span class="rec ${rec}">recommends ${rec}</span>
|
|
||||||
<button id="rerunBtn">Re-run</button>
|
|
||||||
</div>
|
|
||||||
<div class="summary">${mdInline(rv.summary||"")}</div>
|
<div class="summary">${mdInline(rv.summary||"")}</div>
|
||||||
${catHtml}
|
${middle}
|
||||||
${rv.overall ? `<div class="cat"><h3 style="color:var(--ink-dim)">OVERALL</h3><div>${mdInline(rv.overall)}</div></div>`:""}
|
|
||||||
|
|
||||||
<label class="small">Request a revision (tell the model what to change about this review)</label>
|
<label class="small">Request a revision (tell the model what to change about this review)</label>
|
||||||
<textarea id="notes" placeholder="e.g. The auth check on line 40 is fine, drop that BLOCK. Be harsher on missing tests."></textarea>
|
<textarea id="notes" placeholder="e.g. The auth check on line 40 is fine, drop that BLOCK. Be harsher on missing tests."></textarea>
|
||||||
|
|
@ -443,6 +503,9 @@ function escapeHtml(s){ return (s||"").replace(/[&<>"']/g, c=>({"&":"&","<":
|
||||||
function mdInline(s){ return escapeHtml(s).replace(/`([^`]+)`/g, '<code>$1</code>'); }
|
function mdInline(s){ return escapeHtml(s).replace(/`([^`]+)`/g, '<code>$1</code>'); }
|
||||||
|
|
||||||
document.getElementById("refreshBtn").onclick = refreshNow;
|
document.getElementById("refreshBtn").onclick = refreshNow;
|
||||||
|
document.getElementById("expandAllBtn").onclick = expandAll;
|
||||||
|
document.getElementById("collapseAllBtn").onclick = collapseAll;
|
||||||
|
document.getElementById("depFilterBtn").onclick = toggleDepFilter;
|
||||||
loadConfig();
|
loadConfig();
|
||||||
loadMe();
|
loadMe();
|
||||||
poll();
|
poll();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue