add delete profile

This commit is contained in:
larssand
2026-07-06 22:54:19 +02:00
parent b9ca0e1760
commit d027645af7
2 changed files with 49 additions and 3 deletions

View File

@@ -669,7 +669,7 @@ async function refresh() {
{label:'Common denominators', render:r => esc((r.common_fields || []).slice(0,5).map(item => `${item.field} ${(item.coverage*100).toFixed(0)}%`).join(', ') || '-')},
{label:'Shared fields', render:r => esc((r.shared_fields || []).slice(0,4).map(sharedFieldLabel).join(', ') || '-')},
{label:'Discovery', render:r => profileDiscoveryDetails(r)},
{label:'Action', render:r => `<button type="button" class="apply-suggested-profile" data-stream-id="${esc(r.stream_id)}">${r.profile_exists ? 'Update profile' : 'Apply profile'}</button>`}
{label:'Action', render:r => `<button type="button" class="apply-suggested-profile" data-stream-id="${esc(r.stream_id)}">${r.profile_exists ? 'Update profile' : 'Apply profile'}</button>${r.profile_exists ? ` <button type="button" class="delete-stream-profile" data-stream-id="${esc(r.stream_id)}" data-stream-name="${esc(r.stream_name || r.stream_id)}">Delete profile</button>` : ''}`}
], 'profile-suggestions') : '<p class="muted">No missing stream profiles. Enable "show existing profiles" to inspect already configured profiles.</p>';
const llmText = llm.text ? esc(llm.text).replace(/\\n/g, '<br>') : esc(llm.error || 'LLM assessment disabled or waiting for first run.');
document.getElementById('llmAssessment').innerHTML = `<div class="ai-box"><details><summary>Status: ${esc(llm.status || (llmEnabled ? 'starting' : 'disabled'))}</summary><p>${llmText}</p></details></div>`;
@@ -843,7 +843,6 @@ async function refresh() {
'<h3>Local-in Failures</h3>' + table(d.local_in_failures || [], [{label:'Source', key:'src_ip', render:r => entityCell(r, 'src_ip')}, {label:'Service', key:'service'}, {label:'Policy', key:'policy'}, {label:'Count', key:'count'}]);
document.querySelectorAll('details[data-detail-id]').forEach(item => { if (openDetails.has(item.dataset.detailId)) item.open = true; });
document.querySelectorAll('[data-sort-table]').forEach(button => button.addEventListener('click', () => { const current=tableSort[button.dataset.sortTable]; tableSort[button.dataset.sortTable]={key:button.dataset.sortKey,direction:current && current.key===button.dataset.sortKey ? -current.direction : 1}; refresh(); }));
document.querySelectorAll('.apply-suggested-profile').forEach(button => button.addEventListener('click', () => applySuggestedProfile(button.dataset.streamId)));
} catch (error) {
showRefreshError(error);
}
@@ -945,7 +944,8 @@ function renderStreamPicker(errorText='') {
<div class="stream-counts">${esc(enabledCount)} enabled, ${esc(streams.length)} visible, ${esc(window.availableStreams.length)} total</div>
<div class="stream-list">${streams.map(stream => {
const selected = window.streamSelection[stream.id] || {id:stream.id, title:stream.title, enabled:false};
return `<div class="stream-row" data-stream-row="${esc(stream.id)}"><label title="${esc(stream.title || stream.id)}"><input type="checkbox" class="graylog-stream" data-id="${esc(stream.id)}" data-title="${esc(stream.title)}" ${selected.enabled ? 'checked' : ''}> <span class="stream-title">${esc(stream.title || stream.id)}</span></label><button type="button" class="edit-profile" data-id="${esc(stream.id)}" data-title="${esc(stream.title)}">Edit profile</button></div>`;
const hasProfile = (window.streamProfiles || []).some(profile => String(profile.stream_id) === String(stream.id));
return `<div class="stream-row" data-stream-row="${esc(stream.id)}"><label title="${esc(stream.title || stream.id)}"><input type="checkbox" class="graylog-stream" data-id="${esc(stream.id)}" data-title="${esc(stream.title)}" ${selected.enabled ? 'checked' : ''}> <span class="stream-title">${esc(stream.title || stream.id)}</span></label><button type="button" class="edit-profile" data-id="${esc(stream.id)}" data-title="${esc(stream.title)}">Edit profile</button>${hasProfile ? `<button type="button" class="delete-stream-profile" data-stream-id="${esc(stream.id)}" data-stream-name="${esc(stream.title || stream.id)}">Delete profile</button>` : ''}</div>`;
}).join('')}</div>`;
document.getElementById('streamFilter').addEventListener('input', () => renderStreamPicker());
document.getElementById('streamEnabledOnly').addEventListener('change', () => renderStreamPicker());
@@ -1043,6 +1043,32 @@ async function applySuggestedProfile(streamId) {
if (button) button.disabled = false;
}
}
async function deleteStreamProfile(streamId, streamName='') {
const notice = document.getElementById('profileApplyResult') || document.getElementById('settingsResult');
const title = streamName || streamId;
if (!confirm(`Delete profile for ${title}? Baseline data is not deleted, but this stream will stop using the profile until you apply or create a new one.`)) return;
try {
if (notice) notice.textContent = `Deleting profile for ${title}...`;
const configResponse = await fetch('/api/config', {cache: 'no-store'});
const config = await configResponse.json();
const profiles = (config.graylog_stream_profiles || []).filter(item => String(item.stream_id) !== String(streamId));
const response = await fetch('/api/config', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({graylog_stream_profiles: profiles})});
let result = {};
try { result = await response.json(); } catch {}
if (!response.ok) throw new Error(result.error || response.statusText || response.status);
window.streamProfiles = result.graylog_stream_profiles || profiles;
if (window.activeProfileStream === streamId) {
window.activeProfileStream = '';
window.activeProfileTitle = '';
document.getElementById('profileEditorStatus').textContent = 'Profile deleted. Select a stream to edit or apply a recommended profile.';
document.getElementById('fieldPicker').textContent = 'No profile selected for editing.';
}
if (notice) notice.textContent = `Deleted profile for ${title}. Monitor will stop using it on the next cycle.`;
loadSettings();
} catch (error) {
if (notice) notice.textContent = `Could not delete profile: ${error}`;
}
}
async function editStreamProfile(streamId, title) {
document.getElementById('profileEditorStatus').innerHTML = `Editing profile for <code>${esc(title || streamId)}</code>`;
document.getElementById('fieldPicker').textContent = 'Loading stream fields...';
@@ -1059,10 +1085,25 @@ async function editStreamProfile(streamId, title) {
window.activeProfileTitle = title || streamId;
}
document.getElementById('streamPicker').addEventListener('click', event => {
const deleteButton = event.target.closest('.delete-stream-profile');
if (deleteButton) {
deleteStreamProfile(deleteButton.dataset.streamId, deleteButton.dataset.streamName);
return;
}
const button = event.target.closest('.edit-profile');
if (!button) return;
editStreamProfile(button.dataset.id, button.dataset.title);
});
document.getElementById('profileSuggestions').addEventListener('click', event => {
const deleteButton = event.target.closest('.delete-stream-profile');
if (deleteButton) {
deleteStreamProfile(deleteButton.dataset.streamId, deleteButton.dataset.streamName);
return;
}
const applyButton = event.target.closest('.apply-suggested-profile');
if (!applyButton) return;
applySuggestedProfile(applyButton.dataset.streamId);
});
document.getElementById('streamPicker').addEventListener('change', event => {
const checkbox = event.target.closest('.graylog-stream');
if (!checkbox || !window.streamSelection[checkbox.dataset.id]) return;

View File

@@ -34,6 +34,11 @@ class DashboardTests(unittest.TestCase):
self.assertIn("Update profile", HTML)
self.assertIn("function mergeProfile", HTML)
def test_dashboard_can_delete_stream_profiles(self):
self.assertIn("Delete profile", HTML)
self.assertIn("function deleteStreamProfile", HTML)
self.assertIn("delete-stream-profile", HTML)
def test_dashboard_has_direct_stream_selection_save(self):
self.assertIn("Save streams", HTML)
self.assertIn("function saveStreamSelection", HTML)