Skip to content

Commit a428598

Browse files
committed
ui: sync AIR UI with the service
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
1 parent 5c1a6e1 commit a428598

2 files changed

Lines changed: 750 additions & 69 deletions

File tree

ui/ai-review.html

Lines changed: 255 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,16 @@
177177
color: var(--status-error-text);
178178
}
179179

180+
.chip-warning {
181+
display: inline-block;
182+
padding: 4px 12px;
183+
border-radius: 4px;
184+
font-size: 0.9em;
185+
font-weight: bold;
186+
background-color: var(--status-queued-bg);
187+
color: var(--status-queued-text);
188+
}
189+
180190
.error-message {
181191
background-color: var(--error-bg);
182192
color: var(--error-text);
@@ -302,6 +312,11 @@
302312
border: 1px solid var(--border-color);
303313
}
304314

315+
.patch-nav-item.patch-error {
316+
background-color: var(--status-error-bg);
317+
color: var(--status-error-text);
318+
}
319+
305320
.patch-nav-item:hover {
306321
opacity: 0.8;
307322
transform: translateY(-2px);
@@ -324,6 +339,87 @@
324339
font-weight: normal;
325340
font-style: italic;
326341
}
342+
343+
.patch-error-content {
344+
background-color: var(--error-bg);
345+
color: var(--error-text);
346+
border: 1px solid var(--error-border);
347+
padding: 15px;
348+
border-radius: 4px;
349+
font-style: italic;
350+
}
351+
352+
.collapsible-box {
353+
background-color: var(--bg-secondary);
354+
border: 1px solid var(--border-color);
355+
border-radius: 5px;
356+
margin-bottom: 20px;
357+
}
358+
359+
.collapsible-header {
360+
padding: 15px 20px;
361+
cursor: pointer;
362+
display: flex;
363+
justify-content: space-between;
364+
align-items: center;
365+
user-select: none;
366+
}
367+
368+
.collapsible-header:hover {
369+
opacity: 0.8;
370+
}
371+
372+
.collapsible-header .title {
373+
font-weight: bold;
374+
color: var(--heading-color);
375+
}
376+
377+
.collapsible-header .toggle-icon {
378+
color: var(--text-muted);
379+
transition: transform 0.2s ease;
380+
}
381+
382+
.collapsible-header.expanded .toggle-icon {
383+
transform: rotate(90deg);
384+
}
385+
386+
.collapsible-content {
387+
display: none;
388+
padding: 0 20px 20px 20px;
389+
}
390+
391+
.collapsible-content.expanded {
392+
display: block;
393+
}
394+
395+
.metadata-table {
396+
width: 100%;
397+
border-collapse: collapse;
398+
}
399+
400+
.metadata-table th,
401+
.metadata-table td {
402+
padding: 8px 12px;
403+
text-align: left;
404+
border-bottom: 1px solid var(--border-color);
405+
}
406+
407+
.metadata-table th {
408+
font-weight: bold;
409+
color: var(--text-secondary);
410+
width: 30%;
411+
}
412+
413+
.metadata-table td {
414+
font-family: 'Courier New', monospace;
415+
color: var(--text-primary);
416+
word-break: break-word;
417+
}
418+
419+
.metadata-table tr:last-child th,
420+
.metadata-table tr:last-child td {
421+
border-bottom: none;
422+
}
327423
</style>
328424
</head>
329425
<body>
@@ -392,13 +488,19 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
392488
<label>Feedback:</label>
393489
<span id="review-feedback"></span>
394490
</div>
491+
<div class="review-info-item" id="public-in-info" style="display: none;">
492+
<label>Public in:</label>
493+
<span id="review-public-in"></span>
494+
</div>
395495
</div>
396496
<div id="progress-info" style="margin-top: 15px; display: none;">
397497
<label style="font-weight: bold; color: var(--text-secondary);">Progress:</label>
398498
<span id="review-progress" style="color: var(--text-primary);"></span>
399499
</div>
400500
</div>
401501

502+
<div id="metadata-container" style="display: none;"></div>
503+
402504
<div id="message-container" style="display: none;">
403505
<div class="error-message" id="review-message"></div>
404506
</div>
@@ -413,6 +515,7 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
413515
<option value="emailed">Emailed</option>
414516
<option value="false-positive">False Positive</option>
415517
<option value="false-negative">False Negative</option>
518+
<option value="nitpick">Ignored (nitpick)</option>
416519
</select>
417520
</div>
418521
<div class="format-selector">
@@ -470,6 +573,7 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
470573

471574
let currentFormat = 'inline'; // Default to inline/Review format
472575
let inlineReviews = []; // Store inline reviews for navigation coloring
576+
let failedPatchNums = []; // Store failed patch numbers
473577
let refreshInterval = null; // Interval for auto-refreshing in-progress reviews
474578

475579
if (!reviewId) {
@@ -540,6 +644,9 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
540644
const completedCount = data.completed_patches || 0;
541645
document.getElementById('review-patches').textContent = patchCount;
542646

647+
// Store failed patch numbers for navigation display
648+
failedPatchNums = data.failed_patch_nums || [];
649+
543650
// Progress info for in-progress reviews
544651
if (data.status === 'in-progress' && patchCount > 0) {
545652
document.getElementById('progress-info').style.display = 'block';
@@ -561,7 +668,7 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
561668
// Patchwork series ID
562669
if (data.patchwork_series_id) {
563670
document.getElementById('patchwork-info').style.display = 'block';
564-
const pwUrl = `https://patchwork.kernel.org/project/netdevbpf/list/?series=${data.patchwork_series_id}`;
671+
const pwUrl = `https://patchwork.kernel.org/project/${data.pw_project || 'netdevbpf'}/list/?series=${data.patchwork_series_id}`;
565672
document.getElementById('review-pw-series').innerHTML = `<a href="${pwUrl}" target="_blank" rel="noopener noreferrer">${data.patchwork_series_id}</a>`;
566673
}
567674

@@ -590,7 +697,8 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
590697
const feedbackLabels = {
591698
'emailed': '📧 Emailed',
592699
'false-positive': '❌ False Positive',
593-
'false-negative': '⚠️ False Negative'
700+
'false-negative': '⚠️ False Negative',
701+
'nitpick': '💬 Ignored (nitpick)'
594702
};
595703

596704
// Always show feedback in info section if it exists
@@ -599,6 +707,19 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
599707
document.getElementById('review-feedback').textContent = feedbackLabels[data.feedback] || data.feedback;
600708
}
601709

710+
// Show time until public access (for owners viewing public reviews)
711+
if (data.public_in_hours !== undefined && data.public_in_hours > 0) {
712+
document.getElementById('public-in-info').style.display = 'block';
713+
const hours = data.public_in_hours;
714+
let timeText;
715+
if (hours < 1) {
716+
timeText = `${Math.round(hours * 60)} minutes`;
717+
} else {
718+
timeText = `${hours.toFixed(1)} hours`;
719+
}
720+
document.getElementById('review-public-in').innerHTML = `<span class="chip-warning">${timeText}</span>`;
721+
}
722+
602723
// Message (error or warning)
603724
if (data.message) {
604725
document.getElementById('message-container').style.display = 'block';
@@ -640,6 +761,8 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
640761
// Then load the current format for display
641762
loadReview(currentFormat);
642763
});
764+
// Load metadata (only with token)
765+
loadMetadata();
643766
} else if (data.status === 'done' || (data.status === 'error' && patchCount > 0)) {
644767
// No token - just load reviews without controls bar
645768
loadInlineForNavigation().then(() => {
@@ -672,6 +795,100 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
672795
});
673796
}
674797

798+
function loadMetadata() {
799+
if (!token) {
800+
return;
801+
}
802+
803+
let url = `/api/review?id=${encodeURIComponent(reviewId)}`;
804+
url += `&token=${encodeURIComponent(token)}`;
805+
url += '&format=metadata';
806+
807+
fetch(url)
808+
.then(response => {
809+
if (!response.ok) {
810+
throw new Error(`HTTP ${response.status}`);
811+
}
812+
return response.json();
813+
})
814+
.then(data => {
815+
if (data.review && data.review.length > 0) {
816+
displayMetadata(data.review);
817+
}
818+
})
819+
.catch(error => {
820+
console.error('Error loading metadata:', error);
821+
});
822+
}
823+
824+
function displayMetadata(metadataArray) {
825+
const container = document.getElementById('metadata-container');
826+
827+
// Collect all metadata from all patches
828+
const allMetadata = {};
829+
metadataArray.forEach((metadata, index) => {
830+
if (metadata && metadata.trim()) {
831+
try {
832+
const parsed = JSON.parse(metadata);
833+
if (typeof parsed === 'object' && parsed !== null) {
834+
// Prefix keys with patch number if multiple patches
835+
if (metadataArray.length > 1) {
836+
Object.entries(parsed).forEach(([key, value]) => {
837+
allMetadata[`Patch ${index + 1}: ${key}`] = value;
838+
});
839+
} else {
840+
Object.assign(allMetadata, parsed);
841+
}
842+
}
843+
} catch (e) {
844+
console.error(`Error parsing metadata for patch ${index + 1}:`, e);
845+
}
846+
}
847+
});
848+
849+
if (Object.keys(allMetadata).length === 0) {
850+
return;
851+
}
852+
853+
// Create collapsible box
854+
const box = document.createElement('div');
855+
box.className = 'collapsible-box';
856+
857+
const header = document.createElement('div');
858+
header.className = 'collapsible-header';
859+
header.innerHTML = '<span class="title">Metadata</span><span class="toggle-icon">▶</span>';
860+
header.onclick = function() {
861+
header.classList.toggle('expanded');
862+
content.classList.toggle('expanded');
863+
};
864+
865+
const content = document.createElement('div');
866+
content.className = 'collapsible-content';
867+
868+
// Create table
869+
const table = document.createElement('table');
870+
table.className = 'metadata-table';
871+
872+
Object.entries(allMetadata).forEach(([key, value]) => {
873+
const row = document.createElement('tr');
874+
const th = document.createElement('th');
875+
th.textContent = key;
876+
const td = document.createElement('td');
877+
td.textContent = typeof value === 'object' ? JSON.stringify(value) : String(value);
878+
row.appendChild(th);
879+
row.appendChild(td);
880+
table.appendChild(row);
881+
});
882+
883+
content.appendChild(table);
884+
box.appendChild(header);
885+
box.appendChild(content);
886+
887+
container.innerHTML = '';
888+
container.appendChild(box);
889+
container.style.display = 'block';
890+
}
891+
675892
function displayReviews(reviews) {
676893
const container = document.getElementById('reviews-container');
677894
const navContainer = document.getElementById('patch-navigation');
@@ -688,6 +905,9 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
688905
navContainer.style.display = 'flex';
689906

690907
reviews.forEach((review, index) => {
908+
const patchNum = index + 1;
909+
const isFailed = failedPatchNums.includes(patchNum);
910+
691911
// Use inline reviews to determine if patch has comments (for navigation coloring)
692912
const inlineReview = inlineReviews[index];
693913
const hasCommentsInline = inlineReview !== null && inlineReview !== '';
@@ -696,12 +916,20 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
696916
const hasComments = review !== null && review !== '';
697917
const patchId = `patch-${index}`;
698918

699-
// Create navigation item - color based on inline format
919+
// Create navigation item - color based on error status or inline format
700920
const navItem = document.createElement('a');
701-
navItem.className = `patch-nav-item ${hasCommentsInline ? 'has-comments' : 'no-comments'}`;
702-
navItem.textContent = `${index + 1}`;
921+
if (isFailed) {
922+
navItem.className = 'patch-nav-item patch-error';
923+
navItem.title = `Patch ${patchNum}: Review failed (timeout or error)`;
924+
} else if (hasCommentsInline) {
925+
navItem.className = 'patch-nav-item has-comments';
926+
navItem.title = `Patch ${patchNum}: Has review comments`;
927+
} else {
928+
navItem.className = 'patch-nav-item no-comments';
929+
navItem.title = `Patch ${patchNum}: No comments`;
930+
}
931+
navItem.textContent = `${patchNum}`;
703932
navItem.href = `#${patchId}`;
704-
navItem.title = hasCommentsInline ? `Patch ${index + 1}: Has review comments` : `Patch ${index + 1}: No comments`;
705933
navContainer.appendChild(navItem);
706934

707935
// Create patch review section
@@ -713,11 +941,17 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
713941
headerDiv.className = 'patch-header';
714942

715943
const headerTitle = document.createElement('span');
716-
headerTitle.textContent = `Patch ${index + 1}`;
944+
headerTitle.textContent = `Patch ${patchNum}`;
717945
headerDiv.appendChild(headerTitle);
718946

719-
// Show "No review comments" based on inline format
720-
if (!hasCommentsInline) {
947+
// Show status in header based on error or inline format
948+
if (isFailed) {
949+
const errorSpan = document.createElement('span');
950+
errorSpan.className = 'no-comments-inline';
951+
errorSpan.style.color = 'var(--status-error-text)';
952+
errorSpan.textContent = 'Review failed';
953+
headerDiv.appendChild(errorSpan);
954+
} else if (!hasCommentsInline) {
721955
const noCommentsSpan = document.createElement('span');
722956
noCommentsSpan.className = 'no-comments-inline';
723957
noCommentsSpan.textContent = 'No review comments';
@@ -726,8 +960,18 @@ <h2>Review Information <span style="font-family: monospace; font-size: 0.75em; f
726960

727961
patchDiv.appendChild(headerDiv);
728962

729-
// Only add content div if there are comments in current format
730-
if (hasComments) {
963+
// Add content div - error message for failed patches, review content otherwise
964+
if (isFailed) {
965+
const contentDiv = document.createElement('div');
966+
contentDiv.className = 'patch-content';
967+
968+
const errorDiv = document.createElement('div');
969+
errorDiv.className = 'patch-error-content';
970+
errorDiv.textContent = 'This patch could not be reviewed due to a timeout or error. Please try resubmitting the review.';
971+
contentDiv.appendChild(errorDiv);
972+
973+
patchDiv.appendChild(contentDiv);
974+
} else if (hasComments) {
731975
const contentDiv = document.createElement('div');
732976
contentDiv.className = 'patch-content';
733977

0 commit comments

Comments
 (0)