Add enhanced terminal history panel UI with animations, keyboard navigation, advanced filtering, search highlighting, and improved storage metrics display with detailed filesystem tables and ZFS/LVM support
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* Tests for the Storage Details UI component.
|
||||
*
|
||||
* Tests cover:
|
||||
* - Rendering of storage details accordion
|
||||
* - Accordion toggle behavior
|
||||
* - Filesystem table rendering with warning thresholds
|
||||
* - ZFS and LVM section rendering
|
||||
* - Inspector mode drawer
|
||||
* - Feature flag badges (ZFS, LVM)
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
// Mock storage_details data fixtures
|
||||
const mockLinuxDebianStorageDetails = {
|
||||
os_type: 'linux-debian',
|
||||
status: 'ok',
|
||||
collected_at: '2025-12-19T10:00:00Z',
|
||||
commands_run: [
|
||||
{ cmd: 'lsblk', status: 'ok' },
|
||||
{ cmd: 'findmnt', status: 'ok' },
|
||||
{ cmd: 'df', status: 'ok' }
|
||||
],
|
||||
partial_failures: [],
|
||||
summary: {
|
||||
total_bytes: 500107862016,
|
||||
used_bytes: 125026965504,
|
||||
free_bytes: 349600112640,
|
||||
used_pct: 25.0
|
||||
},
|
||||
filesystems: [
|
||||
{
|
||||
device: '/dev/sda2',
|
||||
fstype: 'ext4',
|
||||
size_bytes: 499570991104,
|
||||
used_bytes: 125026965504,
|
||||
avail_bytes: 349600112640,
|
||||
use_pct: 26,
|
||||
mountpoint: '/'
|
||||
}
|
||||
],
|
||||
block_devices: [
|
||||
{
|
||||
name: 'sda',
|
||||
type: 'disk',
|
||||
size: 500107862016,
|
||||
model: 'Samsung SSD 860'
|
||||
}
|
||||
],
|
||||
lvm: { pvs: [], vgs: [], lvs: [] },
|
||||
zfs: { pools: [], datasets: [] },
|
||||
feature_flags: {
|
||||
has_lvm: false,
|
||||
has_zfs: false,
|
||||
has_lsblk: true,
|
||||
has_findmnt: true
|
||||
}
|
||||
};
|
||||
|
||||
const mockTrueNASStorageDetails = {
|
||||
os_type: 'truenas-core',
|
||||
status: 'ok',
|
||||
collected_at: '2025-12-19T10:00:00Z',
|
||||
commands_run: [
|
||||
{ cmd: 'df', status: 'ok' },
|
||||
{ cmd: 'zpool list', status: 'ok' },
|
||||
{ cmd: 'zfs list', status: 'ok' }
|
||||
],
|
||||
partial_failures: [],
|
||||
summary: {
|
||||
total_bytes: 4000787030016,
|
||||
used_bytes: 2800550921011,
|
||||
free_bytes: 1200236109005,
|
||||
used_pct: 70.0
|
||||
},
|
||||
filesystems: [],
|
||||
block_devices: [],
|
||||
lvm: { pvs: [], vgs: [], lvs: [] },
|
||||
zfs: {
|
||||
pools: [
|
||||
{
|
||||
name: 'tank',
|
||||
size_bytes: 4000787030016,
|
||||
alloc_bytes: 2800550921011,
|
||||
free_bytes: 1200236109005,
|
||||
cap_pct: 70,
|
||||
health: 'ONLINE'
|
||||
}
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
name: 'tank/apps',
|
||||
used_bytes: 107374182400,
|
||||
avail_bytes: 1200236109005,
|
||||
mountpoint: '/mnt/tank/apps',
|
||||
type: 'filesystem'
|
||||
}
|
||||
]
|
||||
},
|
||||
feature_flags: {
|
||||
has_lvm: false,
|
||||
has_zfs: true,
|
||||
has_lsblk: false,
|
||||
has_findmnt: false
|
||||
}
|
||||
};
|
||||
|
||||
const mockHighUsageStorageDetails = {
|
||||
os_type: 'linux-debian',
|
||||
status: 'ok',
|
||||
collected_at: '2025-12-19T10:00:00Z',
|
||||
commands_run: [{ cmd: 'df', status: 'ok' }],
|
||||
partial_failures: [],
|
||||
summary: {
|
||||
total_bytes: 100000000000,
|
||||
used_bytes: 92000000000,
|
||||
free_bytes: 8000000000,
|
||||
used_pct: 92.0
|
||||
},
|
||||
filesystems: [
|
||||
{
|
||||
device: '/dev/sda1',
|
||||
fstype: 'ext4',
|
||||
size_bytes: 100000000000,
|
||||
used_bytes: 92000000000,
|
||||
avail_bytes: 8000000000,
|
||||
use_pct: 92,
|
||||
mountpoint: '/'
|
||||
}
|
||||
],
|
||||
block_devices: [],
|
||||
lvm: { pvs: [], vgs: [], lvs: [] },
|
||||
zfs: { pools: [], datasets: [] },
|
||||
feature_flags: {
|
||||
has_lvm: false,
|
||||
has_zfs: false,
|
||||
has_lsblk: true,
|
||||
has_findmnt: true
|
||||
}
|
||||
};
|
||||
|
||||
describe('Storage Details UI Component', () => {
|
||||
describe('renderStorageDetailsSection', () => {
|
||||
it('should return empty string when storage_details is null', () => {
|
||||
const metrics = { storage_details: null };
|
||||
const result = renderStorageDetailsSection(metrics, 'host-1');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should return empty string when storage_details is undefined', () => {
|
||||
const metrics = {};
|
||||
const result = renderStorageDetailsSection(metrics, 'host-1');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should render accordion header with correct title', () => {
|
||||
const metrics = { storage_details: mockLinuxDebianStorageDetails };
|
||||
const result = renderStorageDetailsSection(metrics, 'host-1');
|
||||
expect(result).toContain('Stockage détaillé');
|
||||
expect(result).toContain('fa-database');
|
||||
});
|
||||
|
||||
it('should show status badge', () => {
|
||||
const metrics = { storage_details: mockLinuxDebianStorageDetails };
|
||||
const result = renderStorageDetailsSection(metrics, 'host-1');
|
||||
expect(result).toContain('OK');
|
||||
});
|
||||
|
||||
it('should show feature chips for ZFS', () => {
|
||||
const metrics = { storage_details: mockTrueNASStorageDetails };
|
||||
const result = renderStorageDetailsSection(metrics, 'host-1');
|
||||
expect(result).toContain('ZFS');
|
||||
});
|
||||
|
||||
it('should show usage percentage in summary', () => {
|
||||
const metrics = { storage_details: mockLinuxDebianStorageDetails };
|
||||
const result = renderStorageDetailsSection(metrics, 'host-1');
|
||||
expect(result).toContain('25%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filesystem Table', () => {
|
||||
it('should render filesystem table with columns', () => {
|
||||
const html = renderFilesystemsTable(mockLinuxDebianStorageDetails.filesystems);
|
||||
expect(html).toContain('Mount');
|
||||
expect(html).toContain('Device');
|
||||
expect(html).toContain('FS');
|
||||
expect(html).toContain('Taille');
|
||||
});
|
||||
|
||||
it('should show mountpoint in table', () => {
|
||||
const html = renderFilesystemsTable(mockLinuxDebianStorageDetails.filesystems);
|
||||
expect(html).toContain('/');
|
||||
});
|
||||
|
||||
it('should highlight high usage filesystems', () => {
|
||||
const html = renderFilesystemsTable(mockHighUsageStorageDetails.filesystems);
|
||||
expect(html).toContain('text-red-400');
|
||||
expect(html).toContain('92%');
|
||||
});
|
||||
|
||||
it('should filter out virtual filesystems', () => {
|
||||
const filesystems = [
|
||||
{ device: '/dev/sda1', mountpoint: '/', use_pct: 50 },
|
||||
{ device: 'tmpfs', mountpoint: '/run', use_pct: 10 },
|
||||
{ device: 'devtmpfs', mountpoint: '/dev', use_pct: 0 }
|
||||
];
|
||||
const html = renderFilesystemsTable(filesystems);
|
||||
expect(html).not.toContain('/run');
|
||||
expect(html).not.toContain('/dev');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ZFS Section', () => {
|
||||
it('should render ZFS pools', () => {
|
||||
const html = renderZfsSection(mockTrueNASStorageDetails.zfs);
|
||||
expect(html).toContain('tank');
|
||||
expect(html).toContain('fa-water');
|
||||
});
|
||||
|
||||
it('should show pool health status', () => {
|
||||
const html = renderZfsSection(mockTrueNASStorageDetails.zfs);
|
||||
expect(html).toContain('ONLINE');
|
||||
expect(html).toContain('text-green-400');
|
||||
});
|
||||
|
||||
it('should show pool usage percentage', () => {
|
||||
const html = renderZfsSection(mockTrueNASStorageDetails.zfs);
|
||||
expect(html).toContain('70%');
|
||||
});
|
||||
|
||||
it('should render datasets list', () => {
|
||||
const html = renderZfsSection(mockTrueNASStorageDetails.zfs);
|
||||
expect(html).toContain('tank/apps');
|
||||
});
|
||||
|
||||
it('should return empty string when no ZFS', () => {
|
||||
const html = renderZfsSection({ pools: [], datasets: [] });
|
||||
expect(html).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Inspector Mode', () => {
|
||||
it('should show inspector button', () => {
|
||||
const metrics = { storage_details: mockLinuxDebianStorageDetails };
|
||||
const result = renderStorageDetailsSection(metrics, 'host-1');
|
||||
expect(result).toContain('fa-info-circle');
|
||||
expect(result).toContain('toggleStorageInspector');
|
||||
});
|
||||
|
||||
it('should show OS type in inspector', () => {
|
||||
const html = renderInspectorDrawer(mockLinuxDebianStorageDetails, 'host-1', true);
|
||||
expect(html).toContain('linux-debian');
|
||||
expect(html).toContain('OS détecté');
|
||||
});
|
||||
|
||||
it('should show commands run in inspector', () => {
|
||||
const html = renderInspectorDrawer(mockLinuxDebianStorageDetails, 'host-1', true);
|
||||
expect(html).toContain('lsblk');
|
||||
expect(html).toContain('findmnt');
|
||||
expect(html).toContain('df');
|
||||
});
|
||||
|
||||
it('should show collection timestamp', () => {
|
||||
const html = renderInspectorDrawer(mockLinuxDebianStorageDetails, 'host-1', true);
|
||||
expect(html).toContain('Collecté le');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Warning Thresholds', () => {
|
||||
it('should apply warning class for usage >= 75%', () => {
|
||||
const fs = { use_pct: 80, mountpoint: '/var' };
|
||||
const colorClass = getPctColor(fs.use_pct);
|
||||
expect(colorClass).toBe('bg-yellow-500');
|
||||
});
|
||||
|
||||
it('should apply critical class for usage >= 90%', () => {
|
||||
const fs = { use_pct: 95, mountpoint: '/' };
|
||||
const colorClass = getPctColor(fs.use_pct);
|
||||
expect(colorClass).toBe('bg-red-500');
|
||||
});
|
||||
|
||||
it('should apply normal class for usage < 75%', () => {
|
||||
const fs = { use_pct: 50, mountpoint: '/home' };
|
||||
const colorClass = getPctColor(fs.use_pct);
|
||||
expect(colorClass).toBe('bg-green-500');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Byte Formatting', () => {
|
||||
it('should format bytes to GB', () => {
|
||||
const result = formatBytes(107374182400); // 100 GB
|
||||
expect(result).toBe('100 GB');
|
||||
});
|
||||
|
||||
it('should format bytes to TB for large values', () => {
|
||||
const result = formatBytes(1099511627776); // 1 TB
|
||||
expect(result).toBe('1.0 TB');
|
||||
});
|
||||
|
||||
it('should return empty string for zero', () => {
|
||||
const result = formatBytes(0);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle small values', () => {
|
||||
const result = formatBytes(1073741824); // 1 GB
|
||||
expect(result).toBe('1.0 GB');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Helper functions to test (would be extracted from main.js in real implementation)
|
||||
function renderStorageDetailsSection(metrics, hostId) {
|
||||
const storageDetails = metrics?.storage_details;
|
||||
if (!storageDetails) return '';
|
||||
|
||||
const status = storageDetails.status || 'unknown';
|
||||
const flags = storageDetails.feature_flags || {};
|
||||
const summary = storageDetails.summary || {};
|
||||
|
||||
const chips = [];
|
||||
if (flags.has_zfs) chips.push('ZFS');
|
||||
if (flags.has_lvm) chips.push('LVM');
|
||||
|
||||
const usedPct = summary.used_pct ? Number(summary.used_pct).toFixed(0) : null;
|
||||
|
||||
const statusBadge = status === 'ok' ? 'bg-green-500/20 text-green-400' :
|
||||
status === 'partial' ? 'bg-yellow-500/20 text-yellow-400' : 'bg-red-500/20 text-red-400';
|
||||
const statusText = status === 'ok' ? 'OK' : status === 'partial' ? 'Partiel' : 'Erreur';
|
||||
|
||||
return `
|
||||
<div class="mt-3 border-t border-gray-700/50 pt-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fas fa-database text-indigo-400"></i>
|
||||
<span class="text-xs text-gray-400 font-medium">Stockage détaillé</span>
|
||||
<span class="px-1.5 py-0.5 rounded text-[9px] ${statusBadge}">${statusText}</span>
|
||||
${chips.map(c => `<span class="text-[9px]">${c}</span>`).join('')}
|
||||
</div>
|
||||
<span class="text-[10px] text-gray-500">${usedPct}% utilisé</span>
|
||||
<button onclick="dashboard.toggleStorageInspector('${hostId}')" class="text-gray-500">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderFilesystemsTable(filesystems) {
|
||||
if (!filesystems.length) return '';
|
||||
|
||||
const filtered = filesystems.filter(fs => {
|
||||
const mp = (fs.mountpoint || '').toLowerCase();
|
||||
const dev = (fs.device || '').toLowerCase();
|
||||
return !mp.startsWith('/run') && !mp.startsWith('/sys') && !mp.startsWith('/proc') &&
|
||||
!dev.includes('tmpfs') && !dev.includes('devtmpfs');
|
||||
});
|
||||
|
||||
const sanitizeDevice = (device) => {
|
||||
const d = device || '';
|
||||
return (typeof d === 'string' && d.startsWith('/dev/')) ? d.slice('/dev/'.length) : d;
|
||||
};
|
||||
|
||||
const rows = filtered.map(fs => {
|
||||
const pct = fs.use_pct || 0;
|
||||
const device = sanitizeDevice(fs.device || '');
|
||||
const pctClass = pct >= 90 ? 'text-red-400' : pct >= 75 ? 'text-yellow-400' : 'text-green-400';
|
||||
return `<tr><td>${fs.mountpoint}</td><td>${device}</td><td>${fs.fstype || ''}</td><td class="${pctClass}">${pct}%</td></tr>`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<table class="w-full text-[11px]">
|
||||
<thead>
|
||||
<tr><th>Mount</th><th>Device</th><th>FS</th><th>Taille</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderZfsSection(zfs) {
|
||||
if (!zfs.pools.length && !zfs.datasets.length) return '';
|
||||
|
||||
const poolCards = zfs.pools.map(pool => {
|
||||
const healthColor = pool.health === 'ONLINE' ? 'text-green-400' : 'text-red-400';
|
||||
return `
|
||||
<div class="p-2 rounded">
|
||||
<span><i class="fas fa-water"></i>${pool.name}</span>
|
||||
<span class="${healthColor}">${pool.health}</span>
|
||||
<span>${pool.cap_pct}%</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const datasetsList = zfs.datasets.map(ds => `<div>${ds.name}</div>`).join('');
|
||||
|
||||
return `<div>${poolCards}${datasetsList}</div>`;
|
||||
}
|
||||
|
||||
function renderInspectorDrawer(storageDetails, hostId, isOpen) {
|
||||
if (!isOpen) return '';
|
||||
|
||||
const cmdList = storageDetails.commands_run.map(cmd =>
|
||||
`<div>${cmd.cmd} - ${cmd.status}</div>`
|
||||
).join('');
|
||||
|
||||
return `
|
||||
<div class="inspector">
|
||||
<div>OS détecté: ${storageDetails.os_type}</div>
|
||||
<div>Collecté le: ${storageDetails.collected_at}</div>
|
||||
<div>Commandes: ${cmdList}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function getPctColor(pct) {
|
||||
if (pct === null || pct === undefined) return 'bg-gray-600';
|
||||
return pct >= 90 ? 'bg-red-500' : pct >= 75 ? 'bg-yellow-500' : 'bg-green-500';
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes || bytes <= 0) return '';
|
||||
const gb = bytes / (1024 * 1024 * 1024);
|
||||
if (gb >= 1024) return `${(gb / 1024).toFixed(1)} TB`;
|
||||
if (gb >= 100) return `${gb.toFixed(0)} GB`;
|
||||
return `${gb.toFixed(1)} GB`;
|
||||
}
|
||||
Reference in New Issue
Block a user