mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-11-14 00:07:10 +01:00
refactor(vulnerabilities): use compact rows with drawer actions
This commit is contained in:
1 parent
cab10bfcc4
commit
648002b09b
8 files changed
+181
-153
No files matched your search
@@ -0,0 +1,139 @@
|
||||
.vulnerability-record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vulnerability-record-row {
|
||||
display: grid;
|
||||
grid-template-columns: 18px 52px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
min-height: 72px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.vulnerability-record-row:hover {
|
||||
border-color: color-mix(in srgb, var(--accent-color) 35%, var(--border-color));
|
||||
}
|
||||
|
||||
.vulnerability-record-row:has(.vulnerability-select-record:checked) {
|
||||
background: color-mix(in srgb, var(--accent-color) 5%, var(--bg-primary));
|
||||
}
|
||||
|
||||
.vulnerability-record-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.vulnerability-record-select .vulnerability-select-record {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.vulnerability-record-row > .severity-badge {
|
||||
justify-self: start;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vulnerability-record-main {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
font: inherit;
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vulnerability-record-title-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vulnerability-record-title,
|
||||
.vulnerability-record-summary {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.vulnerability-record-title {
|
||||
min-width: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vulnerability-record-summary {
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.vulnerability-record-title-line .vulnerability-case-badge {
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.vulnerability-record-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.vulnerability-record-view {
|
||||
padding: 6px 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vulnerability-record-view:hover,
|
||||
.vulnerability-record-main:hover .vulnerability-record-title {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.vulnerability-record-main:focus-visible,
|
||||
.vulnerability-record-view:focus-visible {
|
||||
outline: 2px solid var(--accent-color);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
.vulnerability-inspector-actions .btn-delete {
|
||||
margin-inline-start: auto;
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.vulnerability-record-row {
|
||||
grid-template-columns: 18px 46px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
.vulnerability-record-main {
|
||||
padding: 0 !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
.vulnerability-record-actions {
|
||||
grid-column: 2 / -1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
@@ -2720,6 +2720,7 @@
|
||||
"errorGeneric": "حدث خطأ ما. يُرجى المحاولة مرة أخرى."
|
||||
},
|
||||
"vulnerabilityPage": {
|
||||
"viewFinding": "عرض التفاصيل",
|
||||
"taskSearch": "البحث في المهام أو قوائم الانتظار أو الوسوم…",
|
||||
"allTasks": "جميع المهام",
|
||||
"clearTaskSelection": "مسح مرشح المهام",
|
||||
|
||||
@@ -2862,6 +2862,7 @@
|
||||
"errorGeneric": "Something went wrong. Please try again."
|
||||
},
|
||||
"vulnerabilityPage": {
|
||||
"viewFinding": "View details",
|
||||
"taskSearch": "Search tasks, queues or tags…",
|
||||
"allTasks": "All tasks",
|
||||
"clearTaskSelection": "Clear task filter",
|
||||
|
||||
@@ -2875,6 +2875,7 @@
|
||||
"errorGeneric": "Что-то пошло не так. Повторите попытку."
|
||||
},
|
||||
"vulnerabilityPage": {
|
||||
"viewFinding": "Подробнее",
|
||||
"taskSearch": "Поиск задач, очередей или тегов…",
|
||||
"allTasks": "Все задачи",
|
||||
"clearTaskSelection": "Сбросить фильтр задач",
|
||||
|
||||
@@ -2850,6 +2850,7 @@
|
||||
"errorGeneric": "操作失败,请稍后重试。"
|
||||
},
|
||||
"vulnerabilityPage": {
|
||||
"viewFinding": "查看详情",
|
||||
"taskSearch": "搜索任务、队列或标签…",
|
||||
"allTasks": "全部任务",
|
||||
"clearTaskSelection": "清除任务筛选",
|
||||
|
||||
@@ -152,6 +152,7 @@ async function openVulnerabilityInspector(id) {
|
||||
<button class="btn-primary" data-require-permission="vulnerability:write" onclick="openVulnerabilityRetest(${jsID})" ${v.conversation_id ? '' : 'disabled'}>${escapeHtml(vulnT('vulnerabilityRetest.button'))}</button>
|
||||
<button class="btn-secondary" data-require-permission="vulnerability:write" onclick="editVulnerability(${jsID})">${escapeHtml(vulnT('common.edit'))}</button>
|
||||
<button class="btn-secondary" onclick="openVulnerabilityDownload(${jsID},event)">${escapeHtml(vulnT('vulnerabilityPage.tableDownload'))}</button>
|
||||
<button class="btn-secondary btn-delete" data-require-permission="vulnerability:delete" onclick="deleteVulnerability(${jsID})">${escapeHtml(vulnT('common.delete'))}</button>
|
||||
</div>
|
||||
<div class="vulnerability-inspector-tabs" role="group">
|
||||
${['overview','evidence','history'].map(tab => `<button type="button" data-inspector-tab="${tab}" aria-pressed="${tab==='overview'}" onclick="switchVulnerabilityInspectorTab('${tab}')">${escapeHtml(vulnT('vulnerabilityPage.inspector_' + tab))}</button>`).join('')}
|
||||
|
||||
+34
-151
@@ -47,33 +47,6 @@ function getVulnerabilityScrollContainer() {
|
||||
return page ? page.querySelector('.page-content') : null;
|
||||
}
|
||||
|
||||
function getExpandedVulnerabilityIds() {
|
||||
const ids = [];
|
||||
document.querySelectorAll('#vulnerabilities-list .vulnerability-content').forEach(function (el) {
|
||||
if (el.style.display !== 'none') {
|
||||
const id = (el.id || '').replace(/^content-/, '');
|
||||
if (id) ids.push(id);
|
||||
}
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
function restoreExpandedVulnerabilityDetails(expandedIds) {
|
||||
if (!expandedIds || !expandedIds.length) return;
|
||||
expandedIds.forEach(function (id) {
|
||||
const content = document.getElementById('content-' + id);
|
||||
const icon = document.getElementById('expand-icon-' + id);
|
||||
if (!content || content.style.display !== 'none') return;
|
||||
content.style.display = 'block';
|
||||
const row = content.closest('.vulnerability-table-detail');
|
||||
if (row) row.hidden = false;
|
||||
const trigger = document.getElementById('vulnerability-row-toggle-' + id);
|
||||
if (trigger) trigger.setAttribute('aria-expanded', 'true');
|
||||
if (icon) icon.style.transform = 'rotate(90deg)';
|
||||
loadVulnerabilityRelatedFacts(id).catch(function (e) { console.warn(e); });
|
||||
});
|
||||
}
|
||||
|
||||
function buildVulnerabilityStatusPicker(vuln) {
|
||||
const current = vuln.status || 'open';
|
||||
const id = escapeHtml(vuln.id);
|
||||
@@ -1304,13 +1277,9 @@ async function loadVulnerabilities(page = null, options = {}) {
|
||||
const opts = options && typeof options === 'object' ? options : {};
|
||||
const preserveScroll = !!opts.preserveScroll;
|
||||
const silent = !!opts.silent;
|
||||
let expandedIds = opts.expandedIds;
|
||||
|
||||
const scrollEl = preserveScroll ? getVulnerabilityScrollContainer() : null;
|
||||
const scrollTop = scrollEl ? scrollEl.scrollTop : 0;
|
||||
if (expandedIds === undefined && preserveScroll) {
|
||||
expandedIds = getExpandedVulnerabilityIds();
|
||||
}
|
||||
|
||||
const listContainer = document.getElementById('vulnerabilities-list');
|
||||
const hostOpenStates = preserveScroll
|
||||
@@ -1375,7 +1344,7 @@ async function loadVulnerabilities(page = null, options = {}) {
|
||||
}
|
||||
|
||||
renderVulnerabilities(vulnerabilities, {
|
||||
view, expandedIds: expandedIds || [], hostOpenStates,
|
||||
view, hostOpenStates,
|
||||
preserveHostSelection: preserveScroll
|
||||
});
|
||||
renderVulnerabilityPagination();
|
||||
@@ -1419,18 +1388,15 @@ function renderVulnerabilities(vulnerabilities, renderOptions) {
|
||||
const exactMatch = vulnerabilities.length === 1 && vulnerabilityFilters.id === vulnerabilities[0].id
|
||||
? vulnerabilities[0].id
|
||||
: null;
|
||||
const expandedIds = exactMatch ? [exactMatch] : opts.expandedIds;
|
||||
listContainer.innerHTML = renderVulnerabilityTable(vulnerabilities);
|
||||
listContainer.innerHTML = renderVulnerabilityList(vulnerabilities);
|
||||
initVulnerabilityStatusPickers(listContainer);
|
||||
initVulnerabilityProjectBindSelects(listContainer);
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(listContainer);
|
||||
}
|
||||
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(listContainer);
|
||||
if (exactMatch && vulnerabilities[0].summary_only && typeof openVulnerabilityInspector === 'function') {
|
||||
if (exactMatch && typeof openVulnerabilityInspector === 'function') {
|
||||
openVulnerabilityInspector(exactMatch);
|
||||
} else {
|
||||
restoreExpandedVulnerabilityDetails(expandedIds);
|
||||
}
|
||||
updateVulnerabilityListSummary();
|
||||
}
|
||||
@@ -1448,7 +1414,7 @@ function updateVulnerabilityListSummary() {
|
||||
if (!list || !toolbar) return;
|
||||
const count = ['host','task'].includes(vulnerabilityView) && list.querySelector('.vulnerability-host-browser')
|
||||
? new Set(vulnerabilityBrowserRows.map(vuln => vuln.id)).size
|
||||
: list.querySelectorAll('.vulnerability-card, .vulnerability-table-item').length;
|
||||
: list.querySelectorAll('.vulnerability-record-row').length;
|
||||
document.getElementById('vulnerability-page-summary').textContent = count
|
||||
? vulnT('vulnerabilityPage.pageFindings', { count }) : '';
|
||||
}
|
||||
@@ -1547,7 +1513,7 @@ function renderVulnerabilityBrowser(vulnerabilities, opts) {
|
||||
!Array.from(list.querySelectorAll('[data-host-key]')).some(el => el.dataset.hostKey === vulnerabilityBrowserSelection)) {
|
||||
vulnerabilityBrowserSelection = '';
|
||||
}
|
||||
renderVulnerabilityBrowserTable(opts.expandedIds);
|
||||
renderVulnerabilityBrowserTable();
|
||||
if (activeSearch) {
|
||||
search.focus({ preventScroll: true });
|
||||
search.setSelectionRange(caret, caret);
|
||||
@@ -1586,7 +1552,7 @@ function renderVulnerabilityHostRiskDot(items) {
|
||||
return highest ? `<span class="vulnerability-host-risk-dot severity-${highest}" title="${escapeAttr(vulnSeverityLabel(highest))}"></span>` : '';
|
||||
}
|
||||
|
||||
function renderVulnerabilityBrowserTable(expandedIds) {
|
||||
function renderVulnerabilityBrowserTable() {
|
||||
const selection = vulnerabilityBrowserSelection;
|
||||
const items = getVisibleVulnerabilityBrowserItems();
|
||||
document.querySelectorAll('#vulnerabilities-list [data-host-key]').forEach(option => {
|
||||
@@ -1613,100 +1579,41 @@ function renderVulnerabilityBrowserTable(expandedIds) {
|
||||
).join('') + `<span class="vulnerability-group-pending">${escapeHtml(vulnT('vulnerabilityPage.groupPending', { count: pending }))}</span>`;
|
||||
document.getElementById('vulnerability-host-export').disabled = items.length === 0;
|
||||
const container = document.getElementById('vulnerability-host-table');
|
||||
container.innerHTML = renderVulnerabilityTable(items);
|
||||
container.innerHTML = renderVulnerabilityList(items);
|
||||
initVulnerabilityStatusPickers(container);
|
||||
initVulnerabilityProjectBindSelects(container);
|
||||
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(container);
|
||||
const exact = items.find(vuln => vulnerabilityFilters.id === vuln.id);
|
||||
if (exact?.summary_only && typeof openVulnerabilityInspector === 'function') openVulnerabilityInspector(exact.id);
|
||||
else restoreExpandedVulnerabilityDetails(exact ? [exact.id] : expandedIds);
|
||||
if (exact && typeof openVulnerabilityInspector === 'function') openVulnerabilityInspector(exact.id);
|
||||
updateVulnerabilityListSummary();
|
||||
}
|
||||
|
||||
function renderVulnerabilityTable(items) {
|
||||
return items.length ? `<div class="vulnerability-table-scroll">
|
||||
<table class="vulnerability-host-table">
|
||||
<colgroup>
|
||||
<col class="vulnerability-col-select"><col class="vulnerability-col-severity"><col>
|
||||
<col class="vulnerability-col-target"><col class="vulnerability-col-status">
|
||||
<col class="vulnerability-col-task"><col class="vulnerability-col-date"><col class="vulnerability-col-actions">
|
||||
</colgroup>
|
||||
<thead><tr>
|
||||
<th scope="col" class="vulnerability-select-column">${escapeHtml(vulnT('vulnerabilityPage.selectColumn'))}</th>
|
||||
${['tableSeverity', 'tableTitle', 'detailTarget', 'tableStatus', 'tableTask', 'tableDate', 'tableActions']
|
||||
.map(key => '<th scope="col">' + escapeHtml(vulnT('vulnerabilityPage.' + key)) + '</th>').join('')}
|
||||
</tr></thead>
|
||||
${items.map(renderVulnerabilityTableItem).join('')}
|
||||
</table></div>` : `<div class="empty-state">${escapeHtml(vulnT('vulnerabilityPage.noRecords'))}</div>`;
|
||||
function renderVulnerabilityList(items) {
|
||||
if (!items.length) {
|
||||
return `<div class="empty-state">${escapeHtml(vulnT('vulnerabilityPage.noRecords'))}</div>`;
|
||||
}
|
||||
return `<ul class="vulnerability-record-list">${items.map(renderVulnerabilityListItem).join('')}</ul>`;
|
||||
}
|
||||
|
||||
function renderVulnerabilityTableItem(vuln) {
|
||||
function renderVulnerabilityListItem(vuln) {
|
||||
const id = escapeAttr(vuln.id);
|
||||
const jsID = escapeJsStringAttr(vuln.id);
|
||||
const task = vuln.task_tag || vuln.task_id || vulnT('vulnerabilityPage.viewNoTask');
|
||||
const targetHosts = Array.from(new Map((vuln.targets || []).map(target => [target.host,target])).values());
|
||||
const created = new Date(vuln.created_at);
|
||||
return `<tbody class="vulnerability-table-item" id="vulnerability-card-${id}" data-vuln-id="${id}">
|
||||
<tr>
|
||||
<td class="vulnerability-select-column">${typeof vulnerabilitySelectionInput === 'function' ? vulnerabilitySelectionInput(vuln) : ''}</td>
|
||||
<td><span class="severity-badge severity-${escapeAttr(vuln.severity)}">${escapeHtml(vulnSeverityLabel(vuln.severity))}</span></td>
|
||||
<td class="vulnerability-table-title">
|
||||
<button type="button" id="vulnerability-row-toggle-${id}" class="vulnerability-row-toggle"
|
||||
aria-expanded="false" aria-controls="content-${id}" onclick="${vuln.summary_only ? 'openVulnerabilityInspector' : 'toggleVulnerabilityDetails'}(${jsID})">
|
||||
<span id="expand-icon-${id}" class="vulnerability-row-caret" aria-hidden="true">›</span>
|
||||
<span class="vulnerability-table-name" title="${escapeAttr(vuln.title)}">${escapeHtml(vuln.title)}</span>
|
||||
</button>
|
||||
<p>${escapeHtml((vuln.description || '').slice(0, 160))}</p>
|
||||
const summary = (vuln.description || vuln.target || '').replace(/\s+/g, ' ').trim();
|
||||
return `<li class="vulnerability-record-row" id="vulnerability-card-${id}" data-vuln-id="${id}">
|
||||
<span class="vulnerability-record-select">${typeof vulnerabilitySelectionInput === 'function' ? vulnerabilitySelectionInput(vuln) : ''}</span>
|
||||
<span class="severity-badge severity-${escapeAttr(vuln.severity)}">${escapeHtml(vulnSeverityLabel(vuln.severity))}</span>
|
||||
<button type="button" class="vulnerability-record-main" aria-haspopup="dialog" onclick="openVulnerabilityInspector(${jsID})">
|
||||
<span class="vulnerability-record-title-line">
|
||||
<span class="vulnerability-record-title" title="${escapeAttr(vuln.title)}">${escapeHtml(vuln.title)}</span>
|
||||
${vuln.case_id ? `<span class="vulnerability-case-badge">${escapeHtml(vulnT('vulnerabilityPage.mergedRecord'))}</span>` : ''}
|
||||
</td>
|
||||
<td>${targetHosts.length ? targetHosts.slice(0,2).map(target =>
|
||||
`<span class="vulnerability-table-target" title="${escapeAttr(target.raw)}">${escapeHtml(target.host)}</span>`).join('')
|
||||
: `<span class="vulnerability-table-target" title="${escapeAttr(vuln.target || '')}">${escapeHtml(vuln.target_host || vuln.target || '—')}</span>`}
|
||||
${(vuln.targets || []).length > 1 ? `<small>${escapeHtml(vulnT('vulnerabilityPage.targetCount', {count:vuln.targets.length}))}</small>` : ''}
|
||||
</td>
|
||||
<td>${buildVulnerabilityStatusPicker(vuln)}</td>
|
||||
<td><span class="vulnerability-table-task" title="${escapeAttr(vuln.task_id || task)}">${escapeHtml(task)}</span></td>
|
||||
<td class="vulnerability-table-date" title="${escapeAttr(created.toLocaleString(vulnDateLocale()))}">${escapeHtml(created.toLocaleString(vulnDateLocale(), {
|
||||
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
|
||||
}))}</td>
|
||||
<td><div class="vulnerability-table-actions">
|
||||
<button type="button" class="btn-ghost" data-require-permission="vulnerability:write" onclick="openVulnerabilityRetest(${jsID})" ${vuln.conversation_id ? '' : 'disabled'}>${escapeHtml(vulnT('vulnerabilityRetest.button'))}</button>
|
||||
<button type="button" class="btn-ghost" onclick="openVulnerabilityDownload(${jsID}, event)">${escapeHtml(vulnT('vulnerabilityPage.tableDownload'))}</button>
|
||||
<button type="button" class="btn-ghost" data-require-permission="vulnerability:write" onclick="editVulnerability(${jsID})">${escapeHtml(vulnT('common.edit'))}</button>
|
||||
<button type="button" class="btn-ghost" data-require-permission="vulnerability:delete" onclick="deleteVulnerability(${jsID})">${escapeHtml(vulnT('common.delete'))}</button>
|
||||
</div></td>
|
||||
</tr>
|
||||
${vuln.summary_only ? '' : `<tr class="vulnerability-table-detail" hidden><td colspan="8">${renderVulnerabilityDetailsContent(vuln)}</td></tr>`}
|
||||
</tbody>`;
|
||||
}
|
||||
|
||||
function renderVulnerabilityDetailsContent(vuln) {
|
||||
return `
|
||||
<div class="vulnerability-content" id="content-${vuln.id}" style="display: none;">
|
||||
${vuln.description ? `<div class="vulnerability-description">${escapeHtml(vuln.description)}</div>` : ''}
|
||||
<div class="vulnerability-details">
|
||||
${vulnDetailField(vulnT('vulnerabilityPage.detailVulnId'), vuln.id, true)}
|
||||
${vulnDetailProjectField(vuln)}
|
||||
${vuln.type ? vulnDetailField(vulnT('vulnerabilityPage.detailType'), vuln.type, false) : ''}
|
||||
${vuln.target ? vulnDetailField(vulnT('vulnerabilityPage.detailTarget'), vuln.target, false) : ''}
|
||||
${vulnDetailConversationField(vuln.conversation_id)}
|
||||
${vuln.task_id ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskId'), vuln.task_id, true) : ''}
|
||||
${vuln.task_queue_id ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskQueueId'), vuln.task_queue_id, true) : ''}
|
||||
${vuln.conversation_tag ? vulnDetailField(vulnT('vulnerabilityPage.detailConversationTag'), vuln.conversation_tag, false) : ''}
|
||||
${vuln.task_tag ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskTag'), vuln.task_tag, false) : ''}
|
||||
</div>
|
||||
<div class="vulnerability-repro">
|
||||
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailPreconditions'), vuln.preconditions)}
|
||||
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailReproductionSteps'), vuln.reproduction_steps)}
|
||||
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailEvidence'), vuln.evidence, { code: true })}
|
||||
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailImpact'), vuln.impact)}
|
||||
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRecommendation'), vuln.recommendation)}
|
||||
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRetestNotes'), vuln.retest_notes)}
|
||||
${vulnNarrativeSection(vulnT('vulnerabilityPage.detailRetestLog'), vuln.retest_log, { code: true })}
|
||||
</div>
|
||||
<div class="vulnerability-related-facts" id="vuln-related-facts-${escapeAttr(vuln.id)}" data-project-id="${escapeAttr(vuln.project_id || '')}" data-vuln-id="${escapeAttr(vuln.id)}" hidden></div>
|
||||
</div>
|
||||
`;
|
||||
</span>
|
||||
${summary ? `<span class="vulnerability-record-summary" title="${escapeAttr(summary)}">${escapeHtml(summary)}</span>` : ''}
|
||||
</button>
|
||||
<div class="vulnerability-record-actions">
|
||||
${buildVulnerabilityStatusPicker(vuln)}
|
||||
<button type="button" class="vulnerability-record-view" aria-haspopup="dialog" onclick="openVulnerabilityInspector(${jsID})">${escapeHtml(vulnT('vulnerabilityPage.viewFinding'))}</button>
|
||||
</div>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
// 渲染分页控件
|
||||
@@ -1977,7 +1884,6 @@ async function saveVulnerability() {
|
||||
if (typeof refreshVulnerabilityInspector === 'function') refreshVulnerabilityInspector(currentVulnerabilityId);
|
||||
const editedId = currentVulnerabilityId;
|
||||
const isEdit = !!editedId;
|
||||
const expandedIds = isEdit ? getExpandedVulnerabilityIds() : [];
|
||||
|
||||
closeVulnerabilityModal();
|
||||
loadVulnerabilityStats();
|
||||
@@ -1994,7 +1900,7 @@ async function saveVulnerability() {
|
||||
return;
|
||||
}
|
||||
|
||||
await loadVulnerabilities(null, { preserveScroll: true, silent: true, expandedIds: expandedIds });
|
||||
await loadVulnerabilities(null, { preserveScroll: true, silent: true });
|
||||
} catch (error) {
|
||||
console.error('保存漏洞失败:', error);
|
||||
alert(vulnT('vulnerabilityPage.saveFailed') + ': ' + error.message);
|
||||
@@ -2014,6 +1920,9 @@ async function deleteVulnerability(id) {
|
||||
|
||||
if (!response.ok) throw new Error(vulnT('vulnerabilityPage.deleteFailed'));
|
||||
|
||||
if (typeof vulnerabilityInspectorID !== 'undefined' && vulnerabilityInspectorID === id) {
|
||||
closeAppModal('vulnerability-inspector');
|
||||
}
|
||||
loadVulnerabilityStats();
|
||||
await removeVulnerabilityCard(id);
|
||||
} catch (error) {
|
||||
@@ -2151,28 +2060,6 @@ async function saveVulnerabilityAlertSubscription() {
|
||||
}
|
||||
}
|
||||
|
||||
// 切换漏洞详情展开/折叠
|
||||
function toggleVulnerabilityDetails(id) {
|
||||
const content = document.getElementById(`content-${id}`);
|
||||
const icon = document.getElementById(`expand-icon-${id}`);
|
||||
|
||||
if (!content || !icon) return;
|
||||
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
icon.style.transform = 'rotate(90deg)';
|
||||
loadVulnerabilityRelatedFacts(id).catch((e) => console.warn(e));
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
icon.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
const expanded = content.style.display !== 'none';
|
||||
const row = content.closest('.vulnerability-table-detail');
|
||||
if (row) row.hidden = !expanded;
|
||||
const trigger = document.getElementById('vulnerability-row-toggle-' + id);
|
||||
if (trigger) trigger.setAttribute('aria-expanded', String(expanded));
|
||||
}
|
||||
|
||||
/** 展开漏洞详情时加载关联项目事实 */
|
||||
async function loadVulnerabilityRelatedFacts(vulnId) {
|
||||
const el = document.getElementById(`vuln-related-facts-${vulnId}`);
|
||||
@@ -2658,11 +2545,7 @@ async function bindVulnerabilityProject(vulnId, projectId, silent) {
|
||||
alert(vulnT('vulnerabilityPage.projectBindOk'));
|
||||
}
|
||||
loadVulnerabilityStats();
|
||||
const expandedIds = getExpandedVulnerabilityIds();
|
||||
if (!expandedIds.includes(vulnId)) {
|
||||
expandedIds.push(vulnId);
|
||||
}
|
||||
await loadVulnerabilities(null, { preserveScroll: true, silent: true, expandedIds: expandedIds });
|
||||
await loadVulnerabilities(null, { preserveScroll: true, silent: true });
|
||||
} catch (error) {
|
||||
console.error('绑定项目失败:', error);
|
||||
alert(vulnT('vulnerabilityPage.projectBindFailed') + ': ' + error.message);
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
<!-- 移动端样式层:全部规则限定在 767.98px 以下,桌面端零影响 -->
|
||||
<link rel="stylesheet" href="/static/css/mobile.css?v=20261015-project-table-3">
|
||||
<link rel="stylesheet" href="/static/css/vulnerability-workspace.css?v=20261113-tree-panel-height">
|
||||
<link rel="stylesheet" href="/static/css/vulnerability-list.css?v=20261113-compact-rows">
|
||||
<script src="/static/js/router.js?v=20261011-token-settings"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -7096,8 +7097,8 @@
|
||||
<script src="/static/js/skills.js?v=20261027-pagination-custom-select"></script>
|
||||
<script src="/static/js/fact-graph.js"></script>
|
||||
<script src="/static/js/projects.js?v=20260819-1-pagination-20261027"></script>
|
||||
<script src="/static/js/vulnerability.js?v=20261113-unified-list"></script>
|
||||
<script src="/static/js/vulnerability-workspace.js?v=20261113-drawer-dismiss"></script>
|
||||
<script src="/static/js/vulnerability.js?v=20261113-compact-rows"></script>
|
||||
<script src="/static/js/vulnerability-workspace.js?v=20261113-compact-rows"></script>
|
||||
<script src="/static/js/vulnerability-download.js?v=20261030-2"></script>
|
||||
<script src="/static/js/webshell.js?v=20261011-token-usage"></script>
|
||||
<script src="/static/js/chat-files.js?v=20261027-pagination-custom-select"></script>
|
||||
|
||||
Reference in new issue
Block a user