update
This commit is contained in:
@@ -0,0 +1,721 @@
|
||||
/**
|
||||
* BIOS Baseline 비교 JavaScript
|
||||
* backend/static/js/bios_baseline.js
|
||||
*/
|
||||
|
||||
// 전역 변수
|
||||
let baselines = [];
|
||||
let comparisonResults = [];
|
||||
let showDiffOnly = true;
|
||||
|
||||
// ========================================
|
||||
// 초기화
|
||||
// ========================================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadBaselines();
|
||||
|
||||
// IP 입력 카운트 업데이트
|
||||
document.getElementById('ip-addresses').addEventListener('input', updateIPCount);
|
||||
|
||||
// Baseline 생성 폼 제출
|
||||
document.getElementById('create-baseline-form').addEventListener('submit', handleCreateBaseline);
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Baseline 관리
|
||||
// ========================================
|
||||
|
||||
async function loadBaselines() {
|
||||
try {
|
||||
const res = await fetch('/bios-baseline/api/baselines');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
baselines = data.baselines;
|
||||
renderBaselineSelect();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Baseline 로드 실패:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderBaselineSelect() {
|
||||
const select = document.getElementById('baseline-select');
|
||||
select.innerHTML = '<option value="">Baseline을 선택하세요</option>';
|
||||
|
||||
baselines.forEach(baseline => {
|
||||
const option = document.createElement('option');
|
||||
option.value = baseline.id;
|
||||
option.textContent = `${baseline.name} (${baseline.server_model})`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function showBaselineManageModal() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('baseline-manage-modal'));
|
||||
modal.show();
|
||||
loadBaselineList();
|
||||
}
|
||||
|
||||
async function loadBaselineList() {
|
||||
const container = document.getElementById('baseline-list');
|
||||
container.innerHTML = '<div class="text-center text-muted py-4"><i class="bi bi-hourglass-split"></i> 로딩 중...</div>';
|
||||
|
||||
try {
|
||||
const res = await fetch('/bios-baseline/api/baselines');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success && data.baselines.length > 0) {
|
||||
let html = '<div class="list-group">';
|
||||
data.baselines.forEach(baseline => {
|
||||
html += `
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<h6 class="mb-1">${baseline.name}</h6>
|
||||
<p class="mb-1 text-muted small">
|
||||
<i class="bi bi-server me-1"></i>${baseline.server_model}
|
||||
${baseline.server_type ? `<span class="badge bg-secondary ms-2">${baseline.server_type}</span>` : ''}
|
||||
</p>
|
||||
<p class="mb-0 text-muted small">
|
||||
${baseline.description || '설명 없음'}
|
||||
</p>
|
||||
<p class="mb-0 text-muted small">
|
||||
<i class="bi bi-gear me-1"></i>${baseline.total_settings}개 설정
|
||||
<i class="bi bi-calendar ms-2 me-1"></i>${new Date(baseline.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<div class="btn-group-vertical btn-group-sm">
|
||||
<button class="btn btn-outline-secondary" onclick="editBaseline(${baseline.id})">
|
||||
<i class="bi bi-pencil"></i> 편집
|
||||
</button>
|
||||
<button class="btn btn-outline-primary" onclick="duplicateBaseline(${baseline.id})">
|
||||
<i class="bi bi-files"></i> 복사
|
||||
</button>
|
||||
<button class="btn btn-outline-danger" onclick="deleteBaseline(${baseline.id})">
|
||||
<i class="bi bi-trash"></i> 삭제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
} else {
|
||||
container.innerHTML = '<div class="text-center text-muted py-4">등록된 Baseline이 없습니다</div>';
|
||||
}
|
||||
} catch (e) {
|
||||
container.innerHTML = '<div class="text-center text-danger py-4">로드 실패</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateBaseline(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
ip_address: document.getElementById('create-ip').value,
|
||||
username: document.getElementById('create-username').value,
|
||||
password: document.getElementById('create-password').value,
|
||||
name: document.getElementById('create-name').value,
|
||||
server_type: document.getElementById('create-type').value,
|
||||
description: document.getElementById('create-description').value,
|
||||
created_by: 'admin'
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/bios-baseline/api/baselines/create-from-server', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('✓ Baseline 생성 완료!');
|
||||
document.getElementById('create-baseline-form').reset();
|
||||
loadBaselines();
|
||||
loadBaselineList();
|
||||
} else {
|
||||
alert('✗ 생성 실패: ' + result.message);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('✗ 오류: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function duplicateBaseline(baselineId) {
|
||||
const newName = prompt('새 Baseline 이름을 입력하세요:');
|
||||
if (!newName) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/bios-baseline/api/baselines/${baselineId}/duplicate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ new_name: newName })
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('✓ Baseline 복사 완료!');
|
||||
loadBaselines();
|
||||
loadBaselineList();
|
||||
} else {
|
||||
alert('✗ 복사 실패: ' + result.message);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('✗ 오류: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Baseline 삭제
|
||||
async function deleteBaseline(baselineId) {
|
||||
if (!confirm('정말 이 Baseline을 삭제하시겠습니까?')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/bios-baseline/api/baselines/${baselineId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('✓ Baseline 삭제 완료!');
|
||||
loadBaselines();
|
||||
loadBaselineList();
|
||||
} else {
|
||||
alert('✗ 삭제 실패: ' + result.message);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('✗ 오류: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Baseline 상세 편집
|
||||
// ========================================
|
||||
|
||||
async function editBaseline(baselineId) {
|
||||
try {
|
||||
const res = await fetch(`/bios-baseline/api/baselines/${baselineId}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
const baseline = data.baseline;
|
||||
currentBaselineId = baselineId; // 전역 변수 설정
|
||||
|
||||
// 메타데이터 채우기
|
||||
document.getElementById('edit-id').value = baseline.id;
|
||||
document.getElementById('edit-name').value = baseline.name;
|
||||
document.getElementById('edit-model').value = baseline.server_model;
|
||||
document.getElementById('edit-type').value = baseline.server_type || '';
|
||||
document.getElementById('edit-description').value = baseline.description || '';
|
||||
document.getElementById('edit-created-at').textContent = baseline.created_at || '-';
|
||||
document.getElementById('edit-source-ip').textContent = baseline.source_ip || '-';
|
||||
|
||||
// BIOS, iDRAC, Firmware Table 렌더링
|
||||
renderEditTable('bios', baseline.bios_data || {});
|
||||
renderEditTable('idrac', baseline.idrac_data || {});
|
||||
renderEditTable('firmware', baseline.firmware_data || {});
|
||||
|
||||
// RAID, NIC, Boot는 JSON Textarea에 표시
|
||||
document.getElementById('json-edit-raid').value = JSON.stringify(baseline.raid_data || {}, null, 4);
|
||||
document.getElementById('json-edit-nic').value = JSON.stringify(baseline.nic_data || {}, null, 4);
|
||||
document.getElementById('json-edit-boot').value = JSON.stringify(baseline.boot_data || {}, null, 4);
|
||||
|
||||
// 모달 표시
|
||||
const modal = new bootstrap.Modal(document.getElementById('edit-baseline-modal'));
|
||||
modal.show();
|
||||
|
||||
// 첫 번째 탭(BIOS) 활성화
|
||||
const firstTab = new bootstrap.Tab(document.querySelector('#tab-edit-bios'));
|
||||
firstTab.show();
|
||||
} else {
|
||||
alert('로드 실패: ' + data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('오류: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function renderEditTable(type, settings) {
|
||||
const tbody = document.querySelector(`#table-edit-${type} tbody`);
|
||||
tbody.innerHTML = '';
|
||||
|
||||
const entries = Object.entries(settings).sort((a, b) => a[0].localeCompare(b[0]));
|
||||
document.getElementById(`edit-${type}-count`).textContent = entries.length;
|
||||
|
||||
entries.forEach(([key, value]) => {
|
||||
addSettingRow(type, key, value);
|
||||
});
|
||||
}
|
||||
|
||||
function addSettingRow(type, key = '', value = '') {
|
||||
const tbody = document.querySelector(`#table-edit-${type} tbody`);
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
tr.innerHTML = `
|
||||
<td><input type="text" class="form-control form-control-sm setting-key" value="${key}" placeholder="Key"></td>
|
||||
<td><input type="text" class="form-control form-control-sm setting-value" value="${value}" placeholder="Value"></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="removeSettingRow(this, '${type}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
// 새 행은 맨 위에 추가 (비어있을 경우) 혹은 맨 아래
|
||||
if (!key) {
|
||||
tbody.prepend(tr);
|
||||
tr.querySelector('.setting-key').focus();
|
||||
} else {
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
function removeSettingRow(btn, type) {
|
||||
btn.closest('tr').remove();
|
||||
updateSettingsCount(type);
|
||||
}
|
||||
|
||||
function updateSettingsCount(type) {
|
||||
const count = document.querySelectorAll(`#table-edit-${type} tbody tr`).length;
|
||||
document.getElementById(`edit-${type}-count`).textContent = count;
|
||||
}
|
||||
|
||||
async function saveBaselineChanges() {
|
||||
const id = document.getElementById('edit-id').value;
|
||||
const name = document.getElementById('edit-name').value;
|
||||
|
||||
if (!name) {
|
||||
alert('Baseline 이름을 입력하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper function to collect table data
|
||||
const collectTableData = (type) => {
|
||||
const data = {};
|
||||
const rows = document.querySelectorAll(`#table-edit-${type} tbody tr`);
|
||||
rows.forEach(row => {
|
||||
const key = row.querySelector('.setting-key').value.trim();
|
||||
const value = row.querySelector('.setting-value').value.trim();
|
||||
if (key) data[key] = value;
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
const biosData = collectTableData('bios');
|
||||
const idracData = collectTableData('idrac');
|
||||
const firmwareData = collectTableData('firmware');
|
||||
|
||||
// JSON Data Parsing
|
||||
let raidData = {};
|
||||
let nicData = {};
|
||||
let bootData = {};
|
||||
|
||||
try {
|
||||
raidData = JSON.parse(document.getElementById('json-edit-raid').value || '{}');
|
||||
} catch (e) {
|
||||
alert('RAID 구성 JSON 형식이 올바르지 않습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
nicData = JSON.parse(document.getElementById('json-edit-nic').value || '{}');
|
||||
} catch (e) {
|
||||
alert('NIC 설정 JSON 형식이 올바르지 않습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
bootData = JSON.parse(document.getElementById('json-edit-boot').value || '{}');
|
||||
} catch (e) {
|
||||
alert('부팅 설정 JSON 형식이 올바르지 않습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name: name,
|
||||
server_type: document.getElementById('edit-type').value,
|
||||
description: document.getElementById('edit-description').value,
|
||||
bios_data: biosData,
|
||||
idrac_data: idracData,
|
||||
raid_data: raidData,
|
||||
nic_data: nicData,
|
||||
boot_data: bootData,
|
||||
firmware_data: firmwareData
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`/bios-baseline/api/baselines/${currentBaselineId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('✓ 변경사항이 저장되었습니다!');
|
||||
const modalEl = document.getElementById('edit-baseline-modal');
|
||||
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||
modal.hide();
|
||||
loadBaselines();
|
||||
loadBaselineList();
|
||||
} else {
|
||||
alert('✗ 저장 실패: ' + result.message);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('✗ 오류: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 비교 기능
|
||||
// ========================================
|
||||
|
||||
async function startComparison() {
|
||||
const baselineId = document.getElementById('baseline-select').value;
|
||||
const ipText = document.getElementById('ip-addresses').value;
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
if (!baselineId) {
|
||||
alert('Baseline을 선택하세요');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ipText.trim()) {
|
||||
alert('서버 IP를 입력하세요');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
alert('비밀번호를 입력하세요');
|
||||
return;
|
||||
}
|
||||
|
||||
// IP 파싱
|
||||
const ipAddresses = ipText.split(/[\n,\s]+/).filter(ip => ip.trim());
|
||||
|
||||
if (ipAddresses.length === 0) {
|
||||
alert('유효한 IP 주소가 없습니다');
|
||||
return;
|
||||
}
|
||||
|
||||
// 진행률 표시
|
||||
document.getElementById('progress-section').style.display = 'block';
|
||||
document.getElementById('results-section').style.display = 'none';
|
||||
updateProgress(0, ipAddresses.length);
|
||||
|
||||
try {
|
||||
const res = await fetch('/bios-baseline/api/compare', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
ip_addresses: ipAddresses,
|
||||
baseline_id: parseInt(baselineId),
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
comparisonResults = data.results;
|
||||
|
||||
// Toggle 버튼 초기 상태 설정
|
||||
const toggleBtn = document.getElementById('diff-only-btn');
|
||||
if (showDiffOnly) {
|
||||
toggleBtn.classList.add('active');
|
||||
} else {
|
||||
toggleBtn.classList.remove('active');
|
||||
}
|
||||
|
||||
renderSummary(data.baseline, data.results);
|
||||
renderResults(data.results);
|
||||
document.getElementById('results-section').style.display = 'block';
|
||||
} else {
|
||||
alert('비교 실패: ' + data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('오류: ' + e.message);
|
||||
} finally {
|
||||
document.getElementById('progress-section').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgress(current, total) {
|
||||
const percent = Math.round((current / total) * 100);
|
||||
const bar = document.getElementById('progress-bar');
|
||||
bar.style.width = percent + '%';
|
||||
bar.querySelector('span').textContent = percent + '%';
|
||||
}
|
||||
|
||||
function renderSummary(baseline, results) {
|
||||
const successResults = results.filter(r => r.status === 'success');
|
||||
const totalServers = results.length;
|
||||
const successCount = successResults.length;
|
||||
const failedCount = totalServers - successCount;
|
||||
|
||||
let totalMatched = 0;
|
||||
let totalMismatched = 0;
|
||||
let perfectMatchCount = 0;
|
||||
|
||||
successResults.forEach(r => {
|
||||
totalMatched += r.matched_items;
|
||||
totalMismatched += r.mismatched_items;
|
||||
if (r.mismatched_items === 0) perfectMatchCount++;
|
||||
});
|
||||
|
||||
const avgMatchRate = successCount > 0
|
||||
? Math.round((totalMatched / (totalMatched + totalMismatched)) * 100)
|
||||
: 0;
|
||||
|
||||
const html = `
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-primary">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-server fs-1 text-primary"></i>
|
||||
<h3 class="mt-2 mb-0">${totalServers}</h3>
|
||||
<p class="text-muted small mb-0">총 서버</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-success">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-check-circle fs-1 text-success"></i>
|
||||
<h3 class="mt-2 mb-0">${avgMatchRate}%</h3>
|
||||
<p class="text-muted small mb-0">평균 일치율</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-warning">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-exclamation-triangle fs-1 text-warning"></i>
|
||||
<h3 class="mt-2 mb-0">${totalMismatched}</h3>
|
||||
<p class="text-muted small mb-0">불일치 항목</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-info">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-patch-check fs-1 text-info"></i>
|
||||
<h3 class="mt-2 mb-0">${perfectMatchCount}</h3>
|
||||
<p class="text-muted small mb-0">완전 일치</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 p-3 bg-light rounded">
|
||||
<p class="mb-0 small">
|
||||
<strong>Baseline:</strong> ${baseline.name} (${baseline.server_model})
|
||||
<span class="ms-3"><strong>총 설정:</strong> ${baseline.total_settings}개</span>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('summary-section').innerHTML = html;
|
||||
}
|
||||
|
||||
function renderResults(results) {
|
||||
const container = document.getElementById('results-container');
|
||||
|
||||
if (results.length === 0) {
|
||||
container.innerHTML = '<p class="text-muted text-center">결과가 없습니다</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="accordion" id="results-accordion">';
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const statusClass = result.status === 'success'
|
||||
? (result.mismatched_items === 0 ? 'success' : 'warning')
|
||||
: 'danger';
|
||||
const statusIcon = result.status === 'success'
|
||||
? (result.mismatched_items === 0 ? 'check-circle-fill' : 'exclamation-triangle-fill')
|
||||
: 'x-circle-fill';
|
||||
|
||||
html += `
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button ${index > 0 ? 'collapsed' : ''}" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#collapse-${index}">
|
||||
<i class="bi bi-${statusIcon} text-${statusClass} me-2"></i>
|
||||
<strong>${result.ip}</strong>
|
||||
${result.service_tag ? `<span class="badge bg-secondary ms-2">${result.service_tag}</span>` : ''}
|
||||
${result.status === 'success' ? `
|
||||
<span class="ms-auto me-3">
|
||||
<span class="badge bg-${statusClass}">${result.match_rate}% 일치</span>
|
||||
${result.mismatched_items > 0 ? `<span class="badge bg-danger ms-1">${result.mismatched_items}개 불일치</span>` : ''}
|
||||
</span>
|
||||
` : ''}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-${index}" class="accordion-collapse collapse ${index === 0 ? 'show' : ''}"
|
||||
data-bs-parent="#results-accordion">
|
||||
<div class="accordion-body">
|
||||
${renderServerDetail(result)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderServerDetail(result) {
|
||||
if (result.status !== 'success') {
|
||||
return `<div class="alert alert-danger mb-0">${result.message}</div>`;
|
||||
}
|
||||
|
||||
// 통계바
|
||||
let html = `
|
||||
<div class="mb-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4"><strong>모델:</strong> ${result.server_model}</div>
|
||||
<div class="col-md-4"><strong>Service Tag:</strong> ${result.service_tag}</div>
|
||||
<div class="col-md-4"><strong>BIOS:</strong> ${result.bios_version}</div>
|
||||
</div>
|
||||
<div class="progress mt-2" style="height: 20px;">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: ${result.match_rate}%">
|
||||
${result.match_rate}% 일치
|
||||
</div>
|
||||
<div class="progress-bar bg-danger" role="progressbar" style="width: ${100 - result.match_rate}%">
|
||||
${result.mismatched_items} 불일치
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 탭 헤더 생성 (Nav Pills)
|
||||
const uid = Math.random().toString(36).substr(2, 9); // 유니크 ID
|
||||
html += `
|
||||
<div class="nav-pills-container">
|
||||
<ul class="nav nav-pills mb-0" id="result-tabs-${uid}" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" data-bs-toggle="pill" data-bs-target="#tab-bios-${uid}" type="button">BIOS</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-idrac-${uid}" type="button">iDRAC</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-raid-${uid}" type="button">RAID</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-nic-${uid}" type="button">NIC</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-boot-${uid}" type="button">Boot</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-firmware-${uid}" type="button">Firmware</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
`;
|
||||
|
||||
// 섹션별 Diff 필터링
|
||||
const diffs = result.differences || [];
|
||||
|
||||
// 탭 콘텐츠 생성 Helper
|
||||
const renderTabPane = (section, id, isActive = false) => {
|
||||
const sectionDiffs = diffs.filter(d => d.section === section);
|
||||
// 섹션 이름이 영어로 저장되어 있다고 가정 (BIOS, iDRAC, RAID, NIC, Boot, Firmware)
|
||||
return `
|
||||
<div class="tab-pane fade ${isActive ? 'show active' : ''}" id="tab-${id}-${uid}" role="tabpanel">
|
||||
${renderDiffTable(sectionDiffs, `${section} 설정이 기준과 일치합니다.`)}
|
||||
</div>`;
|
||||
};
|
||||
|
||||
html += renderTabPane('BIOS', 'bios', true);
|
||||
html += renderTabPane('iDRAC', 'idrac');
|
||||
html += renderTabPane('RAID', 'raid');
|
||||
html += renderTabPane('NIC', 'nic');
|
||||
html += renderTabPane('Boot', 'boot');
|
||||
html += renderTabPane('Firmware', 'firmware');
|
||||
|
||||
html += `</div>`; // tab-content end
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderDiffTable(diffs, emptyMsg) {
|
||||
if (diffs.length === 0) {
|
||||
return `<div class="alert alert-success mb-0"><i class="bi bi-check-circle me-1"></i> ${emptyMsg}</div>`;
|
||||
}
|
||||
|
||||
if (showDiffOnly && diffs.every(d => d.status !== 'mismatch')) {
|
||||
return `<div class="alert alert-success mb-0"><i class="bi bi-check-circle me-1"></i> ${emptyMsg} (필터링됨)</div>`;
|
||||
}
|
||||
|
||||
let html = `
|
||||
<table class="table table-sm table-bordered table-hover" style="table-layout: fixed;">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th width="40%">항목</th>
|
||||
<th width="30%">기준값 (Baseline)</th>
|
||||
<th width="30%">실제값 (Server)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
diffs.forEach(diff => {
|
||||
const isMismatch = diff.status === 'mismatch';
|
||||
const rowClass = isMismatch ? 'table-danger' : '';
|
||||
|
||||
if (!showDiffOnly || isMismatch) {
|
||||
html += `
|
||||
<tr class="${rowClass}">
|
||||
<td style="word-wrap: break-word;">
|
||||
${diff.display_name ? `<strong>${diff.display_name}</strong><br><small class="text-muted" style="word-break: break-all;">${diff.setting_name}</small>` : `<code style="word-break: break-all;">${diff.setting_name}</code>`}
|
||||
</td>
|
||||
<td style="word-break: break-all;">${diff.expected}</td>
|
||||
<td style="word-break: break-all;"><span class="${isMismatch ? '' : 'text-success'}">${diff.actual}</span></td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function toggleDiffOnly() {
|
||||
showDiffOnly = !showDiffOnly;
|
||||
const btn = document.getElementById('diff-only-btn');
|
||||
btn.classList.toggle('active');
|
||||
renderResults(comparisonResults);
|
||||
}
|
||||
|
||||
function downloadResults() {
|
||||
alert('Excel 다운로드 기능은 곧 추가될 예정입니다.');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 유틸리티
|
||||
// ========================================
|
||||
|
||||
function updateIPCount() {
|
||||
const ipText = document.getElementById('ip-addresses').value;
|
||||
const ips = ipText.split(/[\n,\s]+/).filter(ip => ip.trim());
|
||||
document.getElementById('ip-count').textContent = ips.length;
|
||||
}
|
||||
|
||||
function clearIPs() {
|
||||
document.getElementById('ip-addresses').value = '';
|
||||
updateIPCount();
|
||||
}
|
||||
Reference in New Issue
Block a user