Skip to content

Commit 378f22e

Browse files
committed
feat: replace context menu with multi-select toolbar and checkbox functionality in findings table
1 parent eac126d commit 378f22e

2 files changed

Lines changed: 113 additions & 54 deletions

File tree

-131 KB
Binary file not shown.

internal/modules/gobot/ui/app.js

Lines changed: 113 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3659,9 +3659,12 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId) {
36593659
<table class="dashboard-table" style="margin:0;table-layout:fixed;width:100%">
36603660
<thead style="position:sticky;top:0;z-index:2;background:rgba(2,6,23,.97);backdrop-filter:blur(4px)">
36613661
<tr>
3662-
<th style="width:32%">TARGET</th>
3662+
<th style="width:36px;text-align:center;padding-left:10px">
3663+
<input type="checkbox" id="findings-select-all" title="Select all" style="width:14px;height:14px;accent-color:var(--accent-cyan);cursor:pointer">
3664+
</th>
3665+
<th style="width:31%">TARGET</th>
36633666
<th style="width:8%;text-align:center">SEV</th>
3664-
<th style="width:44%">VULNERABILITY TYPE</th>
3667+
<th style="width:43%">VULNERABILITY TYPE</th>
36653668
<th style="width:16%">MODULE</th>
36663669
</tr>
36673670
</thead>
@@ -3742,7 +3745,10 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId) {
37423745
href = 'https://' + target;
37433746
}
37443747

3745-
return `<tr class="findings-row" data-target="${escAttr(target)}" data-finding="${escAttr(vulnType)}" data-severity="${escAttr(sev)}" data-module="${escAttr(r.module||'')}" data-href="${escAttr(href)}" style="${idx % 2 ? 'background:rgba(255,255,255,.012)' : ''}" title="Right-click for options">
3748+
return `<tr class="findings-row" data-target="${escAttr(target)}" data-finding="${escAttr(vulnType)}" data-severity="${escAttr(sev)}" data-module="${escAttr(r.module||'')}" data-href="${escAttr(href)}" style="cursor:pointer;${idx % 2 ? 'background:rgba(255,255,255,.012)' : ''}">
3749+
<td style="padding:7px 10px;width:36px;text-align:center">
3750+
<input type="checkbox" class="finding-chk" style="width:14px;height:14px;accent-color:var(--accent-cyan);cursor:pointer" onclick="event.stopPropagation()">
3751+
</td>
37463752
<td style="padding:7px 10px;max-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
37473753
<a href="${esc(href)}" target="_blank" rel="noopener"
37483754
onclick="event.stopPropagation()"
@@ -3780,7 +3786,7 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId) {
37803786
<span style="color:${modInfo.color};font-size:11px;font-weight:500">${modInfo.icon} ${esc(modInfo.name)}</span>
37813787
</td>
37823788
</tr>`;
3783-
}).join('') : '<tr><td colspan="4" style="text-align:center;padding:28px;color:var(--text-muted);font-size:13px">No findings match the current filter.</td></tr>';
3789+
}).join('') : '<tr><td colspan="5" style="text-align:center;padding:28px;color:var(--text-muted);font-size:13px">No findings match the current filter.</td></tr>';
37843790
}
37853791
if (cap) {
37863792
cap.style.display = filtered.length > maxRows ? 'block' : 'none';
@@ -3849,70 +3855,123 @@ async function loadReconUnifiedTable(scanId, allFiles, containerId) {
38493855
if (titleInput) titleInput.addEventListener('input', applyFiltersDebounced);
38503856
if (severitySel) severitySel.addEventListener('change', applyFilters);
38513857

3852-
// ── Right-click context menu on findings rows ──────────────────────────────
3853-
let _ctxMenu = document.getElementById('findings-ctx-menu');
3854-
if (!_ctxMenu) {
3855-
_ctxMenu = document.createElement('div');
3856-
_ctxMenu.id = 'findings-ctx-menu';
3857-
_ctxMenu.style.cssText = 'position:fixed;z-index:9999;background:var(--bg-card,#1e293b);border:1px solid var(--border,#334155);border-radius:10px;padding:4px 0;min-width:190px;box-shadow:0 8px 32px rgba(0,0,0,.6);display:none';
3858-
_ctxMenu.innerHTML = `
3859-
<div class="ctx-item" id="ctx-copy-target" style="padding:9px 16px;font-size:12px;cursor:pointer;color:var(--text-primary);display:flex;align-items:center;gap:8px">📋 Copy Target</div>
3860-
<div class="ctx-item" id="ctx-copy-finding" style="padding:9px 16px;font-size:12px;cursor:pointer;color:var(--text-primary);display:flex;align-items:center;gap:8px">📝 Copy Finding</div>
3861-
<div class="ctx-item" id="ctx-open-url" style="padding:9px 16px;font-size:12px;cursor:pointer;color:var(--accent-cyan);display:flex;align-items:center;gap:8px">🌐 Open in Browser</div>
3862-
<div style="border-top:1px solid var(--border);margin:4px 0"></div>
3863-
<div class="ctx-item" id="ctx-validate-ai" style="padding:9px 16px;font-size:12px;cursor:pointer;color:#a78bfa;display:flex;align-items:center;gap:8px">🤖 Validate with AI</div>`;
3864-
document.body.appendChild(_ctxMenu);
3865-
// Hover styles
3866-
_ctxMenu.querySelectorAll('.ctx-item').forEach(el => {
3867-
el.addEventListener('mouseenter', () => el.style.background = 'rgba(255,255,255,.06)');
3868-
el.addEventListener('mouseleave', () => el.style.background = '');
3858+
// ── Selection toolbar ─────────────────────────────────────────────────────
3859+
// Build the floating toolbar (once, outside the root so it stays on DOM)
3860+
let _selToolbar = document.getElementById('findings-sel-toolbar');
3861+
if (!_selToolbar) {
3862+
_selToolbar = document.createElement('div');
3863+
_selToolbar.id = 'findings-sel-toolbar';
3864+
_selToolbar.style.cssText = [
3865+
'position:fixed;bottom:28px;left:50%;transform:translateX(-50%) translateY(80px)',
3866+
'z-index:9000;background:var(--bg-card,#1e293b)',
3867+
'border:1px solid var(--accent-cyan,#22d3ee)44',
3868+
'border-radius:14px;padding:10px 18px',
3869+
'display:flex;align-items:center;gap:12px',
3870+
'box-shadow:0 8px 40px rgba(0,0,0,.7)',
3871+
'transition:transform .25s cubic-bezier(.34,1.56,.64,1),opacity .2s',
3872+
'opacity:0;pointer-events:none;white-space:nowrap',
3873+
].join(';');
3874+
_selToolbar.innerHTML = `
3875+
<span id="sel-count-badge" style="background:var(--accent-cyan,#22d3ee);color:#0f172a;font-size:11px;font-weight:700;padding:3px 9px;border-radius:20px;min-width:22px;text-align:center">0</span>
3876+
<span style="font-size:12px;color:var(--text-secondary)">selected</span>
3877+
<div style="width:1px;height:20px;background:var(--border)"></div>
3878+
<button id="sel-copy-targets" title="Copy all selected targets" style="background:transparent;border:1px solid var(--border);border-radius:8px;padding:5px 12px;font-size:12px;color:var(--text-primary);cursor:pointer">📋 Copy Targets</button>
3879+
<button id="sel-copy-findings" title="Copy all selected finding types" style="background:transparent;border:1px solid var(--border);border-radius:8px;padding:5px 12px;font-size:12px;color:var(--text-primary);cursor:pointer">📝 Copy Findings</button>
3880+
<button id="sel-open-urls" title="Open all selected targets in new tabs" style="background:transparent;border:1px solid var(--border);border-radius:8px;padding:5px 12px;font-size:12px;color:var(--accent-cyan,#22d3ee);cursor:pointer">🌐 Open URLs</button>
3881+
<button id="sel-validate-ai" title="AI-validate the first selected finding" style="background:rgba(167,139,250,.15);border:1px solid #a78bfa44;border-radius:8px;padding:5px 12px;font-size:12px;color:#a78bfa;cursor:pointer">🤖 Validate with AI</button>
3882+
<button id="sel-clear" title="Clear selection" style="background:transparent;border:none;font-size:17px;color:var(--text-muted);cursor:pointer;padding:0 2px;line-height:1">✕</button>`;
3883+
document.body.appendChild(_selToolbar);
3884+
3885+
// Hover effect on toolbar buttons
3886+
_selToolbar.querySelectorAll('button').forEach(b => {
3887+
if (b.id === 'sel-clear') return;
3888+
b.addEventListener('mouseenter', () => b.style.opacity = '0.8');
3889+
b.addEventListener('mouseleave', () => b.style.opacity = '1');
38693890
});
38703891
}
3871-
let _ctxRow = null;
3872-
const closeCtx = () => { _ctxMenu.style.display = 'none'; };
3873-
document.addEventListener('click', e => {
3874-
if (!_ctxMenu.contains(e.target)) {
3875-
closeCtx();
3876-
_ctxRow = null;
3892+
3893+
const _getSelectedRows = () => Array.from(root.querySelectorAll('.finding-chk:checked')).map(cb => cb.closest('.findings-row')).filter(Boolean);
3894+
3895+
const _updateToolbar = () => {
3896+
const rows = _getSelectedRows();
3897+
const n = rows.length;
3898+
const badge = _selToolbar.querySelector('#sel-count-badge');
3899+
if (badge) badge.textContent = String(n);
3900+
if (n > 0) {
3901+
_selToolbar.style.opacity = '1';
3902+
_selToolbar.style.pointerEvents = 'auto';
3903+
_selToolbar.style.transform = 'translateX(-50%) translateY(0)';
3904+
} else {
3905+
_selToolbar.style.opacity = '0';
3906+
_selToolbar.style.pointerEvents = 'none';
3907+
_selToolbar.style.transform = 'translateX(-50%) translateY(80px)';
3908+
}
3909+
};
3910+
3911+
// Delegate checkbox change events from the findings table
3912+
root.addEventListener('change', e => {
3913+
if (e.target.classList.contains('finding-chk') || e.target.id === 'findings-select-all') {
3914+
if (e.target.id === 'findings-select-all') {
3915+
root.querySelectorAll('.finding-chk').forEach(cb => cb.checked = e.target.checked);
3916+
}
3917+
_updateToolbar();
38773918
}
38783919
});
3879-
document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeCtx(); _ctxRow = null; } });
38803920

3881-
root.addEventListener('contextmenu', e => {
3921+
// Clicking a row (not a link/checkbox) toggles its checkbox
3922+
root.addEventListener('click', e => {
38823923
const row = e.target.closest('.findings-row');
3883-
if (!row || !root.contains(row)) return;
3884-
e.preventDefault();
3885-
_ctxRow = row;
3886-
// Position menu
3887-
const x = Math.min(e.clientX, window.innerWidth - 210);
3888-
const y = Math.min(e.clientY, window.innerHeight - 160);
3889-
_ctxMenu.style.left = x + 'px';
3890-
_ctxMenu.style.top = y + 'px';
3891-
_ctxMenu.style.display = 'block';
3924+
if (!row) return;
3925+
if (e.target.tagName === 'A' || e.target.tagName === 'INPUT') return;
3926+
const chk = row.querySelector('.finding-chk');
3927+
if (chk) { chk.checked = !chk.checked; _updateToolbar(); }
3928+
});
3929+
3930+
// Toolbar actions
3931+
_selToolbar.querySelector('#sel-copy-targets').addEventListener('click', async () => {
3932+
const rows = _getSelectedRows();
3933+
if (!rows.length) return;
3934+
const text = rows.map(r => r.dataset.target || '').filter(Boolean).join('\n');
3935+
await copyToClipboard(text).catch(() => {});
3936+
showToast('success', 'Copied', `${rows.length} target(s) copied`);
38923937
});
38933938

3894-
_ctxMenu.querySelector('#ctx-copy-target')?.addEventListener('click', async () => {
3895-
if (!_ctxRow) return; closeCtx();
3896-
await copyToClipboard(_ctxRow.dataset.target || '').catch(() => {});
3897-
showToast('success', 'Copied', _ctxRow.dataset.target || '');
3939+
_selToolbar.querySelector('#sel-copy-findings').addEventListener('click', async () => {
3940+
const rows = _getSelectedRows();
3941+
if (!rows.length) return;
3942+
const text = rows.map(r => r.dataset.finding || '').filter(Boolean).join('\n');
3943+
await copyToClipboard(text).catch(() => {});
3944+
showToast('success', 'Copied', `${rows.length} finding type(s) copied`);
38983945
});
3899-
_ctxMenu.querySelector('#ctx-copy-finding')?.addEventListener('click', async () => {
3900-
if (!_ctxRow) return; closeCtx();
3901-
await copyToClipboard(_ctxRow.dataset.finding || '').catch(() => {});
3902-
showToast('success', 'Copied', _ctxRow.dataset.finding || '');
3946+
3947+
_selToolbar.querySelector('#sel-open-urls').addEventListener('click', () => {
3948+
const rows = _getSelectedRows();
3949+
if (!rows.length) return;
3950+
let opened = 0;
3951+
rows.forEach(r => {
3952+
const href = r.dataset.href;
3953+
if (href && href !== '#') { window.open(href, '_blank', 'noopener'); opened++; }
3954+
});
3955+
if (!opened) showToast('error', 'No URLs', 'None of the selected rows have valid URLs.');
3956+
else showToast('success', 'Opened', `${opened} URL(s) opened in new tabs`);
39033957
});
3904-
_ctxMenu.querySelector('#ctx-open-url')?.addEventListener('click', () => {
3905-
if (!_ctxRow) return; closeCtx();
3906-
const href = _ctxRow.dataset.href;
3907-
if (href && href !== '#') window.open(href, '_blank', 'noopener');
3908-
else showToast('error', 'No URL', 'Target has no valid URL.');
3958+
3959+
_selToolbar.querySelector('#sel-validate-ai').addEventListener('click', () => {
3960+
const rows = _getSelectedRows();
3961+
if (!rows.length) return;
3962+
const r = rows[0]; // validate the first selected row
3963+
openValidateModal(r.dataset.target, r.dataset.finding, r.dataset.severity, r.dataset.module);
39093964
});
3910-
_ctxMenu.querySelector('#ctx-validate-ai')?.addEventListener('click', () => {
3911-
if (!_ctxRow) return; closeCtx();
3912-
openValidateModal(_ctxRow.dataset.target, _ctxRow.dataset.finding, _ctxRow.dataset.severity, _ctxRow.dataset.module);
3965+
3966+
_selToolbar.querySelector('#sel-clear').addEventListener('click', () => {
3967+
root.querySelectorAll('.finding-chk').forEach(cb => cb.checked = false);
3968+
const sa = root.querySelector('#findings-select-all');
3969+
if (sa) sa.checked = false;
3970+
_updateToolbar();
39133971
});
39143972
}
39153973

3974+
39163975
// ── AI Validate Finding Modal ─────────────────────────────────────────────────
39173976
function openValidateModal(target, findingType, severity, module_) {
39183977
let modal = document.getElementById('validate-finding-modal');

0 commit comments

Comments
 (0)