fix dashboard profile edit

This commit is contained in:
larssand
2026-06-30 15:15:30 +02:00
parent 4a441bf443
commit 70191d93fb

View File

@@ -75,7 +75,12 @@ HTML = """<!doctype html>
[data-view] { display: none; } [data-view].active { display: block; }
.split { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(320px, 0.7fr); gap: 12px; }
.table-wrap { overflow-x: auto; }
#settingsForm label:has(#fieldPicker) { grid-column: 1 / -1; }
#settingsForm label:has(#streamPicker), #settingsForm label:has(#fieldPicker) { grid-column: 1 / -1; }
#streamPicker { margin-top: 8px; border: 1px solid #163b59; background: #061a2e; border-radius: 6px; padding: 10px; }
.stream-tools { display: grid; grid-template-columns: minmax(240px, 1fr) repeat(3, auto); gap: 8px; align-items: center; margin-bottom: 10px; }
.stream-tools input[type="search"] { width: 100%; }
.stream-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(310px, 1fr)); gap: 6px; max-height: 430px; overflow: auto; padding-right: 4px; }
.stream-counts { color: #91abc4; font-size: 12px; margin-bottom: 8px; }
#fieldPicker { margin-top: 8px; max-height: 460px; overflow: auto; border: 1px solid #d9e0e7; background: #fbfcfd; }
.field-header, .field-row { display: grid; grid-template-columns: minmax(190px, 1.2fr) 100px minmax(180px, 1fr) minmax(320px, 1.4fr); gap: 10px; align-items: center; padding: 8px 10px; }
.field-header { position: sticky; top: 0; background: #edf3f7; color: #536170; font-size: 12px; font-weight: 700; z-index: 1; }
@@ -83,8 +88,10 @@ HTML = """<!doctype html>
.field-row code { width: fit-content; }
.field-controls { display: flex; flex-wrap: wrap; gap: 10px; }
.field-controls label { white-space: nowrap; }
.stream-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 4px 0; }
.stream-row button { border: 1px solid #39709a; background: #0b2944; color: #d9e8f7; padding: 4px 8px; cursor: pointer; }
.stream-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; padding: 7px 8px; border: 1px solid #163b59; background: #071d33; border-radius: 4px; }
.stream-row label { min-width: 0; display: flex; align-items: center; gap: 7px; }
.stream-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stream-row button, .stream-tools button { border: 1px solid #39709a; background: #0b2944; color: #d9e8f7; padding: 5px 8px; cursor: pointer; white-space: nowrap; }
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 8px 0 12px; color: #91abc4; }
.toolbar label { display: inline-flex; align-items: center; gap: 6px; }
.summary-card { border: 1px solid #163b59; background: #061a2e; padding: 10px; margin-bottom: 8px; border-radius: 6px; }
@@ -128,6 +135,8 @@ function metric(label, value) {
function bytes(value) { const n=Number(value||0); if (!n) return '0 B'; const units=['B','KB','MB','GB','TB']; const i=Math.min(units.length-1, Math.floor(Math.log(n)/Math.log(1024))); return `${(n/Math.pow(1024,i)).toFixed(i?1:0)} ${units[i]}`; }
const tableSort = {};
const uiCache = {correlations: [], fieldRows: []};
window.availableStreams = [];
window.streamSelection = {};
function table(rows, columns, id = '') {
if (!rows || rows.length === 0) return '<p class="muted">No data.</p>';
const sort = tableSort[id];
@@ -468,9 +477,40 @@ async function loadStreams() {
const payload = await response.json();
const selected = new Set((payload.selected || []).filter(item => item.enabled).map(item => item.id));
window.availableStreams = payload.streams || [];
document.getElementById('streamPicker').innerHTML = (payload.streams || []).map(stream => `<div class="stream-row"><label><input type="checkbox" class="graylog-stream" data-id="${esc(stream.id)}" data-title="${esc(stream.title)}" ${selected.has(stream.id) ? 'checked' : ''}> ${esc(stream.title)}</label> <button type="button" class="edit-profile" data-id="${esc(stream.id)}" data-title="${esc(stream.title)}">Edit profile</button></div>`).join('') || esc(payload.error || 'No streams found.');
window.streamSelection = Object.fromEntries(window.availableStreams.map(stream => [stream.id, {id:stream.id, title:stream.title, enabled:selected.has(stream.id)}]));
renderStreamPicker(payload.error || '');
}
document.getElementById('loadStreams').addEventListener('click', loadStreams);
function renderStreamPicker(errorText='') {
const target = document.getElementById('streamPicker');
if (!window.availableStreams.length) {
target.textContent = errorText || 'No streams found.';
return;
}
const previousFilter = document.getElementById('streamFilter')?.value || '';
const previousEnabledOnly = document.getElementById('streamEnabledOnly')?.checked || false;
const filter = previousFilter.toLowerCase().trim();
const streams = window.availableStreams
.filter(stream => !filter || `${stream.title || ''} ${stream.id || ''}`.toLowerCase().includes(filter))
.filter(stream => !previousEnabledOnly || window.streamSelection[stream.id]?.enabled);
const enabledCount = Object.values(window.streamSelection).filter(item => item.enabled).length;
target.innerHTML = `
<div class="stream-tools">
<input id="streamFilter" type="search" placeholder="Search streams by name or id" value="${esc(previousFilter)}">
<label><input id="streamEnabledOnly" type="checkbox" ${previousEnabledOnly ? 'checked' : ''}> enabled only</label>
<button type="button" id="enableVisibleStreams">Enable visible</button>
<button type="button" id="disableVisibleStreams">Disable visible</button>
</div>
<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>`;
}).join('')}</div>`;
document.getElementById('streamFilter').addEventListener('input', () => renderStreamPicker());
document.getElementById('streamEnabledOnly').addEventListener('change', () => renderStreamPicker());
document.getElementById('enableVisibleStreams').addEventListener('click', () => { streams.forEach(stream => { window.streamSelection[stream.id].enabled = true; }); renderStreamPicker(); });
document.getElementById('disableVisibleStreams').addEventListener('click', () => { streams.forEach(stream => { window.streamSelection[stream.id].enabled = false; }); renderStreamPicker(); });
}
async function applySuggestedProfile(streamId) {
const notice = document.getElementById('profileApplyResult');
const button = document.querySelector(`.apply-suggested-profile[data-stream-id="${CSS.escape(streamId)}"]`);
@@ -549,11 +589,17 @@ document.getElementById('streamPicker').addEventListener('click', event => {
if (!button) return;
editStreamProfile(button.dataset.id, button.dataset.title);
});
document.getElementById('streamPicker').addEventListener('change', event => {
const checkbox = event.target.closest('.graylog-stream');
if (!checkbox || !window.streamSelection[checkbox.dataset.id]) return;
window.streamSelection[checkbox.dataset.id].enabled = checkbox.checked;
document.querySelector('.stream-counts').textContent = `${Object.values(window.streamSelection).filter(item => item.enabled).length} enabled, ${document.querySelectorAll('.stream-row').length} visible, ${window.availableStreams.length} total`;
});
document.getElementById('loadFields').addEventListener('click', async () => {
if (window.activeProfileStream) { editStreamProfile(window.activeProfileStream, window.activeProfileTitle); return; }
const checked = [...document.querySelectorAll('.graylog-stream:checked')];
if (checked.length !== 1) { document.getElementById('fieldPicker').textContent = 'Click Edit profile on the stream you want to edit, or check exactly one stream first.'; return; }
editStreamProfile(checked[0].dataset.id, checked[0].dataset.title);
const checked = Object.values(window.streamSelection || {}).filter(item => item.enabled);
if (checked.length !== 1) { document.getElementById('fieldPicker').textContent = 'Click Edit profile on the stream you want to edit, or enable exactly one stream first.'; return; }
editStreamProfile(checked[0].id, checked[0].title);
});
document.getElementById('settingsForm').addEventListener('submit', async event => {
event.preventDefault();
@@ -563,7 +609,7 @@ document.getElementById('settingsForm').addEventListener('submit', async event =
values.profile_advisor_enabled = form.elements.profile_advisor_enabled.checked;
values.threat_intel_enabled = form.elements.threat_intel_enabled.checked;
values.graylog_tls_verify = form.elements.graylog_tls_verify.checked;
values.graylog_streams = [...document.querySelectorAll('.graylog-stream')].map(item => ({id:item.dataset.id, title:item.dataset.title, enabled:item.checked}));
values.graylog_streams = Object.values(window.streamSelection || {}).length ? Object.values(window.streamSelection) : [...document.querySelectorAll('.graylog-stream')].map(item => ({id:item.dataset.id, title:item.dataset.title, enabled:item.checked}));
if (window.activeProfileStream) { let detectors={},fieldWeights={}; try { detectors=form.elements.profile_detectors.value.trim() ? JSON.parse(form.elements.profile_detectors.value) : {}; } catch { document.getElementById('settingsResult').textContent='Detector thresholds must be valid JSON.'; return; } try { fieldWeights=form.elements.profile_field_weights.value.trim() ? JSON.parse(form.elements.profile_field_weights.value) : {}; } catch { document.getElementById('settingsResult').textContent='Field weights must be valid JSON.'; return; } const entityFields=[...form.querySelectorAll('.profile-entity:checked')].map(item=>item.value); if (!entityFields.length) { document.getElementById('settingsResult').textContent='Select at least one Entity field for the active profile.'; return; } const profileTitle=window.activeProfileTitle || window.activeProfileStream; const profile={stream_id:window.activeProfileStream,name:form.elements.profile_name.value.trim() || `${profileTitle} profile`,entity_field:entityFields[0]||'',entity_fields:entityFields,timestamp_field:form.querySelector('[name="profile_timestamp"]:checked')?.value||'timestamp',categorical_fields:[...form.querySelectorAll('.profile-categorical:checked')].map(item=>item.value),numeric_fields:[...form.querySelectorAll('.profile-numeric:checked')].map(item=>item.value),detectors,field_weights:fieldWeights}; values.graylog_stream_profiles=[...(window.streamProfiles||[]).filter(item=>item.stream_id!==profile.stream_id),profile]; }
const savedProfile = window.activeProfileStream ? ` Profile saved for ${window.activeProfileTitle || window.activeProfileStream}.` : ' No profile editor active, so profiles were not changed.';
const response = await fetch('/api/config', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(values)});