feat: replace YAML editor with form UI for threshold configurations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -383,6 +383,29 @@
|
||||
.backup-row { display: flex; align-items: center; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #f0f0f0; font-size: .87em; }
|
||||
.backup-row:last-child { border-bottom: none; }
|
||||
|
||||
/* ---- Threshold config cards ---- */
|
||||
.thresh-cfg-card {
|
||||
margin-bottom: 14px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.thresh-cfg-header {
|
||||
background: #f5f5f5;
|
||||
padding: 8px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
.thresh-cfg-name-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
color: #1a237e;
|
||||
}
|
||||
.thresh-metric-table { width: 100%; }
|
||||
.thresh-metric-table th { white-space: nowrap; }
|
||||
|
||||
/* ---- Multi-picker ---- */
|
||||
.mpick-wrapper { display: block; }
|
||||
.mpick-display {
|
||||
@@ -689,6 +712,84 @@
|
||||
<button class="btn btn-primary" onclick="openChannelModal()">+ Add channel</button>
|
||||
</div>
|
||||
|
||||
{# ---- Threshold configurations (form-based) ---- #}
|
||||
{% elif section.section_mode == 'thresholds' %}
|
||||
{% for f in section.fields %}
|
||||
<div class="field-row" style="border-bottom:1px solid #f0f0f0">
|
||||
<div class="field-label">{{ f.label }}</div>
|
||||
<div class="field-body">
|
||||
<input type="text" class="field-input thresh-default-config"
|
||||
value="{{ f.raw if f.raw is not none else '' }}"
|
||||
placeholder="default"
|
||||
list="thresh-cfg-names-{{ section.id }}">
|
||||
<datalist id="thresh-cfg-names-{{ section.id }}">
|
||||
{% for tc in section.threshold_configs %}<option value="{{ tc.name | e }}">{% endfor %}
|
||||
</datalist>
|
||||
{% if f.description %}<p class="field-desc">{{ f.description }}</p>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div id="thresh-cfgs-{{ section.id }}" style="padding:8px 20px 0">
|
||||
{% for tc in section.threshold_configs %}
|
||||
<div class="thresh-cfg-card" data-config-name="{{ tc.name | e }}">
|
||||
<div class="thresh-cfg-header">
|
||||
<span class="thresh-cfg-name-label">{{ tc.name | e }}</span>
|
||||
{% if tc.name != 'default' %}
|
||||
<button class="btn-danger" style="margin-left:auto" onclick="deleteThresholdConfigCard(this)">✕ Delete</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="overflow-x:auto">
|
||||
<table class="crud-table thresh-metric-table">
|
||||
<thead><tr>
|
||||
<th>Metric path</th><th>Op</th>
|
||||
<th>Warning</th><th>Critical</th>
|
||||
<th>Hysteresis</th><th>Count</th>
|
||||
<th style="max-width:160px">Display</th>
|
||||
<th>En</th><th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for m in tc.metrics %}
|
||||
<tr data-metric-row="true" data-metric-path="{{ m.metric | e }}">
|
||||
<td style="font-family:monospace;font-size:.85em;white-space:nowrap">{{ m.metric | e }}</td>
|
||||
<td>
|
||||
<select class="field-input thresh-op" style="width:80px" onchange="onThreshOpChange(this)">
|
||||
{% for op in ['>', '>=', '<', '<=', '==', '!=', 'nagios'] %}
|
||||
<option value="{{ op }}" {% if m.operator == op %}selected{% endif %}>{{ op }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="number" class="field-input thresh-warn" step="any" style="width:80px"
|
||||
value="{{ m.warning if m.warning is not none else '' }}"
|
||||
{% if m.operator == 'nagios' %}disabled{% endif %}></td>
|
||||
<td><input type="number" class="field-input thresh-crit" step="any" style="width:80px"
|
||||
value="{{ m.critical if m.critical is not none else '' }}"
|
||||
{% if m.operator == 'nagios' %}disabled{% endif %}></td>
|
||||
<td><input type="number" class="field-input thresh-hyst" step="any" style="width:72px"
|
||||
value="{{ m.hysteresis if m.hysteresis is not none else 0.02 }}"></td>
|
||||
<td><input type="number" class="field-input thresh-count" step="1" min="1" style="width:52px"
|
||||
value="{{ m.count if m.count is not none else 1 }}"></td>
|
||||
<td><input type="text" class="field-input thresh-display" style="width:150px"
|
||||
value="{{ m.display | e }}" placeholder="(default)"></td>
|
||||
<td style="text-align:center"><input type="checkbox" class="thresh-enabled"
|
||||
{% if m.enabled %}checked{% endif %}></td>
|
||||
<td><button class="btn-danger" onclick="this.closest('tr').remove()">✕</button></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="padding:6px 14px 8px;border-top:1px solid #f0f0f0">
|
||||
<button class="btn btn-secondary" style="font-size:.8em;padding:3px 10px"
|
||||
onclick="addThresholdMetricRow(this.closest('.thresh-cfg-card').querySelector('tbody'))">+ Add metric</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="section-footer" style="justify-content:space-between">
|
||||
<button class="btn btn-secondary" onclick="addThresholdConfigCard('thresh-cfgs-{{ section.id }}')">+ Add config</button>
|
||||
<button class="btn btn-primary" onclick="stageThresholdsSection('{{ section.id }}')">Stage changes</button>
|
||||
</div>
|
||||
|
||||
{# ---- YAML editor section ---- #}
|
||||
{% elif section.section_mode == 'yaml' %}
|
||||
<div style="padding: 12px 20px">
|
||||
@@ -1351,6 +1452,122 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Threshold configurations form ----
|
||||
function stageThresholdsSection(sectionId) {
|
||||
const section = document.getElementById(sectionId);
|
||||
const configs = {};
|
||||
|
||||
function readMetrics(card) {
|
||||
const metrics = {};
|
||||
card.querySelectorAll('tbody tr').forEach(row => {
|
||||
if (row.dataset.deleted === 'true') return;
|
||||
const metric = row.dataset.metricPath
|
||||
|| (row.querySelector('.new-metric-path')?.value || '').trim();
|
||||
if (!metric) return;
|
||||
const op = row.querySelector('.thresh-op')?.value || '>';
|
||||
const warn = row.querySelector('.thresh-warn')?.value;
|
||||
const crit = row.querySelector('.thresh-crit')?.value;
|
||||
const hyst = row.querySelector('.thresh-hyst')?.value;
|
||||
const count = row.querySelector('.thresh-count')?.value;
|
||||
const display = row.querySelector('.thresh-display')?.value || '';
|
||||
const enabled = row.querySelector('.thresh-enabled')?.checked ?? true;
|
||||
const entry = { operator: op, enabled: enabled };
|
||||
if (warn !== '' && warn !== undefined) entry.warning = parseFloat(warn);
|
||||
if (crit !== '' && crit !== undefined) entry.critical = parseFloat(crit);
|
||||
if (hyst !== '' && hyst !== undefined) entry.hysteresis = parseFloat(hyst);
|
||||
if (count !== '' && count !== undefined) entry.count = parseInt(count, 10);
|
||||
if (display) entry.display = display;
|
||||
metrics[metric] = entry;
|
||||
});
|
||||
return metrics;
|
||||
}
|
||||
|
||||
const cfgsContainer = document.getElementById('thresh-cfgs-' + sectionId);
|
||||
cfgsContainer.querySelectorAll('.thresh-cfg-card').forEach(card => {
|
||||
const configName = card.dataset.configName
|
||||
|| (card.querySelector('.new-config-name')?.value || '').trim();
|
||||
if (!configName) return;
|
||||
configs[configName] = readMetrics(card);
|
||||
});
|
||||
|
||||
_staged['thresholds'] = configs;
|
||||
|
||||
const defInput = section.querySelector('.thresh-default-config');
|
||||
if (defInput) {
|
||||
if (!_staged['server']) _staged['server'] = {};
|
||||
_staged['server']['default_threshold_config'] = defInput.value || 'default';
|
||||
}
|
||||
|
||||
updatePendingBanner();
|
||||
flashStaged(sectionId);
|
||||
}
|
||||
|
||||
function onThreshOpChange(select) {
|
||||
const row = select.closest('tr');
|
||||
const isNagios = select.value === 'nagios';
|
||||
const w = row.querySelector('.thresh-warn');
|
||||
const c = row.querySelector('.thresh-crit');
|
||||
if (w) w.disabled = isNagios;
|
||||
if (c) c.disabled = isNagios;
|
||||
}
|
||||
|
||||
function _threshOpSelect(selected) {
|
||||
const ops = ['>', '>=', '<', '<=', '==', '!=', 'nagios'];
|
||||
return '<select class="field-input thresh-op" style="width:80px" onchange="onThreshOpChange(this)">' +
|
||||
ops.map(op => `<option value="${escHtml(op)}"${op === selected ? ' selected' : ''}>${escHtml(op)}</option>`).join('') +
|
||||
'</select>';
|
||||
}
|
||||
|
||||
function addThresholdMetricRow(tbody) {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td><input type="text" class="field-input new-metric-path" placeholder="plugin.metric" style="min-width:160px;font-family:monospace;font-size:.85em" required></td>
|
||||
<td>${_threshOpSelect('>')}</td>
|
||||
<td><input type="number" class="field-input thresh-warn" step="any" style="width:80px"></td>
|
||||
<td><input type="number" class="field-input thresh-crit" step="any" style="width:80px"></td>
|
||||
<td><input type="number" class="field-input thresh-hyst" step="any" style="width:72px" value="0.02"></td>
|
||||
<td><input type="number" class="field-input thresh-count" step="1" min="1" style="width:52px" value="1"></td>
|
||||
<td><input type="text" class="field-input thresh-display" style="width:150px" placeholder="(default)"></td>
|
||||
<td style="text-align:center"><input type="checkbox" class="thresh-enabled" checked></td>
|
||||
<td><button class="btn-danger" onclick="this.closest('tr').remove()">✕</button></td>`;
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
|
||||
function addThresholdConfigCard(containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
const card = document.createElement('div');
|
||||
card.className = 'thresh-cfg-card';
|
||||
card.innerHTML = `
|
||||
<div class="thresh-cfg-header">
|
||||
<input type="text" class="field-input new-config-name" placeholder="Config name (e.g. servers)" style="max-width:220px">
|
||||
<button class="btn-danger" style="margin-left:auto" onclick="this.closest('.thresh-cfg-card').remove()">✕ Delete</button>
|
||||
</div>
|
||||
<div style="overflow-x:auto">
|
||||
<table class="crud-table thresh-metric-table">
|
||||
<thead><tr>
|
||||
<th>Metric path</th><th>Op</th>
|
||||
<th>Warning</th><th>Critical</th>
|
||||
<th>Hysteresis</th><th>Count</th>
|
||||
<th style="max-width:160px">Display</th>
|
||||
<th>En</th><th></th>
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="padding:6px 14px 8px;border-top:1px solid #f0f0f0">
|
||||
<button class="btn btn-secondary" style="font-size:.8em;padding:3px 10px"
|
||||
onclick="addThresholdMetricRow(this.closest('.thresh-cfg-card').querySelector('tbody'))">+ Add metric</button>
|
||||
</div>`;
|
||||
container.appendChild(card);
|
||||
}
|
||||
|
||||
function deleteThresholdConfigCard(btn) {
|
||||
const card = btn.closest('.thresh-cfg-card');
|
||||
const name = card.dataset.configName || 'this config';
|
||||
if (!confirm(`Delete config "${name}"?`)) return;
|
||||
card.remove();
|
||||
}
|
||||
|
||||
function closeSidebar() {
|
||||
var sidebarNav = document.getElementById('sidebar-nav');
|
||||
var sidebarToggle = document.getElementById('sidebar-toggle');
|
||||
|
||||
Reference in New Issue
Block a user