Some checks failed
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
815 lines
32 KiB
JavaScript
815 lines
32 KiB
JavaScript
/**
|
|
* Docker Section Management
|
|
* Handles Docker hosts, containers, images, volumes, and alerts
|
|
*/
|
|
|
|
const dockerSection = {
|
|
hosts: [],
|
|
currentHostId: null,
|
|
_initialized: false,
|
|
stats: {
|
|
total_hosts: 0,
|
|
enabled_hosts: 0,
|
|
total_containers: 0,
|
|
running_containers: 0,
|
|
total_images: 0,
|
|
open_alerts: 0
|
|
},
|
|
|
|
async init() {
|
|
await this.loadDockerHosts();
|
|
await this.loadStats();
|
|
this.setupEventListeners();
|
|
this.setupWebSocket();
|
|
},
|
|
|
|
async ensureInit() {
|
|
if (this._initialized) return;
|
|
this._initialized = true;
|
|
await this.init();
|
|
},
|
|
|
|
setupEventListeners() {
|
|
// Search filter
|
|
const searchInput = document.getElementById('docker-search');
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', (e) => {
|
|
this.filterHosts(e.target.value);
|
|
});
|
|
}
|
|
|
|
// Tab switching
|
|
document.querySelectorAll('.docker-tab').forEach(tab => {
|
|
tab.addEventListener('click', (e) => {
|
|
const tabName = e.currentTarget.dataset.tab;
|
|
this.switchTab(tabName);
|
|
});
|
|
});
|
|
},
|
|
|
|
setupWebSocket() {
|
|
if (window.dashboard && window.dashboard.ws) {
|
|
const originalOnMessage = window.dashboard.ws.onmessage;
|
|
window.dashboard.ws.onmessage = (event) => {
|
|
if (originalOnMessage) originalOnMessage(event);
|
|
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
this.handleWebSocketMessage(data);
|
|
} catch (e) {
|
|
console.error('Docker WS parse error:', e);
|
|
}
|
|
};
|
|
}
|
|
},
|
|
|
|
handleWebSocketMessage(data) {
|
|
switch (data.type) {
|
|
case 'docker_host_updated':
|
|
this.updateHostCard(data.host || data.data);
|
|
break;
|
|
case 'docker_alert_opened':
|
|
this.showAlertNotification(data.data);
|
|
this.updateAlertsBadge();
|
|
break;
|
|
case 'docker_alert_closed':
|
|
case 'docker_alert_acknowledged':
|
|
this.updateAlertsBadge();
|
|
break;
|
|
}
|
|
},
|
|
|
|
async fetchAPI(endpoint, options = {}) {
|
|
const token = localStorage.getItem('accessToken');
|
|
const apiKey = localStorage.getItem('apiKey');
|
|
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
...options.headers
|
|
};
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
} else if (apiKey) {
|
|
headers['X-API-Key'] = apiKey;
|
|
}
|
|
|
|
const response = await fetch(`${window.location.origin}${endpoint}`, {
|
|
...options,
|
|
headers
|
|
});
|
|
|
|
if (!response.ok) {
|
|
// Handle 401 Unauthorized - redirect to login
|
|
if (response.status === 401) {
|
|
this.showToast('Session expirée, reconnexion requise', 'error');
|
|
if (window.dashboard && typeof window.dashboard.logout === 'function') {
|
|
window.dashboard.logout();
|
|
}
|
|
return null;
|
|
}
|
|
throw new Error(`API Error: ${response.status}`);
|
|
}
|
|
|
|
return response.json();
|
|
},
|
|
|
|
async loadDockerHosts() {
|
|
try {
|
|
const response = await this.fetchAPI('/api/docker/hosts');
|
|
this.hosts = response.hosts || [];
|
|
this.renderHostsGrid();
|
|
} catch (error) {
|
|
console.error('Error loading Docker hosts:', error);
|
|
this.renderError('Erreur lors du chargement des hosts Docker');
|
|
}
|
|
},
|
|
|
|
async loadStats() {
|
|
try {
|
|
const stats = await this.fetchAPI('/api/docker/stats');
|
|
this.stats = stats;
|
|
this.updateStatsDisplay();
|
|
} catch (error) {
|
|
console.error('Error loading Docker stats:', error);
|
|
}
|
|
},
|
|
|
|
updateStatsDisplay() {
|
|
const el = (id, value) => {
|
|
const elem = document.getElementById(id);
|
|
if (elem) elem.textContent = value;
|
|
};
|
|
|
|
el('docker-stat-hosts', this.stats.enabled_hosts || 0);
|
|
el('docker-stat-containers', this.stats.running_containers || 0);
|
|
el('docker-stat-images', this.stats.total_images || 0);
|
|
el('docker-stat-alerts', this.stats.open_alerts || 0);
|
|
|
|
// Update alerts badge
|
|
const badge = document.getElementById('docker-alerts-badge');
|
|
if (badge) {
|
|
if (this.stats.open_alerts > 0) {
|
|
badge.textContent = this.stats.open_alerts;
|
|
badge.classList.remove('hidden');
|
|
} else {
|
|
badge.classList.add('hidden');
|
|
}
|
|
}
|
|
},
|
|
|
|
renderHostsGrid() {
|
|
const grid = document.getElementById('docker-hosts-grid');
|
|
if (!grid) return;
|
|
|
|
if (this.hosts.length === 0) {
|
|
grid.innerHTML = `
|
|
<div class="glass-card p-6 text-center col-span-full">
|
|
<i class="fab fa-docker text-4xl text-gray-600 mb-4"></i>
|
|
<p class="text-gray-400">Aucun host Docker configuré</p>
|
|
<p class="text-gray-500 text-sm mt-2">Activez Docker sur un host dans la section Hosts</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = this.hosts.map(host => this.renderHostCard(host)).join('');
|
|
},
|
|
|
|
renderHostCard(host) {
|
|
const statusColor = host.docker_status === 'online' ? 'green' :
|
|
host.docker_status === 'offline' ? 'red' : 'yellow';
|
|
const statusText = host.docker_status || 'unknown';
|
|
|
|
const lastCollect = host.docker_last_collect_at
|
|
? this.formatRelativeTime(host.docker_last_collect_at)
|
|
: 'Jamais';
|
|
|
|
return `
|
|
<div class="glass-card p-4 cursor-pointer hover:border-purple-500/50 transition-all"
|
|
data-host-id="${host.host_id}" onclick="dockerSection.viewDetails('${host.host_id}')">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h3 class="font-semibold text-lg truncate">${host.host_name}</h3>
|
|
<span class="px-2 py-1 rounded-full text-xs font-medium bg-${statusColor}-500/20 text-${statusColor}-400">
|
|
${statusText}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="text-sm text-gray-400 mb-3">${host.host_ip}</div>
|
|
|
|
<div class="grid grid-cols-2 gap-2 mb-3 text-sm">
|
|
<div class="flex items-center gap-2">
|
|
<i class="fas fa-box text-blue-400"></i>
|
|
<span>${host.containers_running}/${host.containers_total} containers</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<i class="fas fa-layer-group text-purple-400"></i>
|
|
<span>${host.images_total} images</span>
|
|
</div>
|
|
</div>
|
|
|
|
${host.open_alerts > 0 ? `
|
|
<div class="flex items-center gap-2 text-sm text-red-400 mb-3">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
<span>${host.open_alerts} alerte(s)</span>
|
|
</div>
|
|
` : ''}
|
|
|
|
<div class="flex items-center justify-between text-xs text-gray-500">
|
|
<span><i class="fas fa-clock mr-1"></i>${lastCollect}</span>
|
|
${host.docker_version ? `<span>v${host.docker_version}</span>` : ''}
|
|
</div>
|
|
|
|
<div class="flex gap-2 mt-3 pt-3 border-t border-gray-700">
|
|
<button onclick="event.stopPropagation(); dockerSection.collectNow('${host.host_id}')"
|
|
class="flex-1 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-sm transition-colors">
|
|
<i class="fas fa-sync mr-1"></i>Collecter
|
|
</button>
|
|
<button onclick="event.stopPropagation(); dockerSection.toggleDockerEnabled('${host.host_id}', ${!host.docker_enabled})"
|
|
class="px-3 py-1.5 ${host.docker_enabled ? 'bg-red-600/20 text-red-400 hover:bg-red-600/30' : 'bg-green-600/20 text-green-400 hover:bg-green-600/30'} rounded text-sm transition-colors">
|
|
<i class="fas fa-${host.docker_enabled ? 'pause' : 'play'}"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
filterHosts(query) {
|
|
const cards = document.querySelectorAll('#docker-hosts-grid > div[data-host-id]');
|
|
const lowerQuery = query.toLowerCase();
|
|
|
|
cards.forEach(card => {
|
|
const hostId = card.dataset.hostId;
|
|
const host = this.hosts.find(h => h.host_id === hostId);
|
|
if (host) {
|
|
const matches = host.host_name.toLowerCase().includes(lowerQuery) ||
|
|
host.host_ip.toLowerCase().includes(lowerQuery);
|
|
card.style.display = matches ? '' : 'none';
|
|
}
|
|
});
|
|
},
|
|
|
|
async viewDetails(hostId) {
|
|
this.currentHostId = hostId;
|
|
const host = this.hosts.find(h => h.host_id === hostId);
|
|
|
|
if (!host) return;
|
|
|
|
document.getElementById('docker-host-name').innerHTML =
|
|
`<i class="fab fa-docker mr-2 text-blue-400"></i>${host.host_name}`;
|
|
|
|
// Show modal
|
|
document.getElementById('docker-detail-modal').classList.remove('hidden');
|
|
|
|
// Load containers by default
|
|
this.switchTab('containers');
|
|
await this.loadContainers(hostId);
|
|
},
|
|
|
|
closeModal() {
|
|
document.getElementById('docker-detail-modal').classList.add('hidden');
|
|
this.currentHostId = null;
|
|
},
|
|
|
|
switchTab(tabName) {
|
|
// Update tab buttons
|
|
document.querySelectorAll('.docker-tab').forEach(tab => {
|
|
tab.classList.toggle('active', tab.dataset.tab === tabName);
|
|
tab.classList.toggle('text-gray-400', tab.dataset.tab !== tabName);
|
|
});
|
|
|
|
// Update tab content
|
|
document.querySelectorAll('.docker-tab-content').forEach(content => {
|
|
content.classList.toggle('hidden', !content.id.endsWith(tabName));
|
|
});
|
|
|
|
// Load content for the tab
|
|
if (this.currentHostId) {
|
|
switch (tabName) {
|
|
case 'containers':
|
|
this.loadContainers(this.currentHostId);
|
|
break;
|
|
case 'images':
|
|
this.loadImages(this.currentHostId);
|
|
break;
|
|
case 'volumes':
|
|
this.loadVolumes(this.currentHostId);
|
|
break;
|
|
case 'host-alerts':
|
|
this.loadHostAlerts(this.currentHostId);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
async loadContainers(hostId) {
|
|
const container = document.getElementById('docker-containers-list');
|
|
container.innerHTML = '<div class="loading-spinner mx-auto"></div>';
|
|
|
|
try {
|
|
const response = await this.fetchAPI(`/api/docker/hosts/${hostId}/containers`);
|
|
const containers = response.containers || [];
|
|
|
|
if (containers.length === 0) {
|
|
container.innerHTML = '<p class="text-gray-400 text-center py-4">Aucun container</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = containers.map(c => this.renderContainerRow(c, hostId)).join('');
|
|
} catch (error) {
|
|
container.innerHTML = `<p class="text-red-400 text-center py-4">Erreur: ${error.message}</p>`;
|
|
}
|
|
},
|
|
|
|
renderContainerRow(c, hostId) {
|
|
const stateColors = {
|
|
running: 'green',
|
|
exited: 'red',
|
|
paused: 'yellow',
|
|
created: 'blue',
|
|
dead: 'red'
|
|
};
|
|
const stateColor = stateColors[c.state] || 'gray';
|
|
|
|
const healthBadge = c.health ? `
|
|
<span class="px-2 py-0.5 rounded text-xs bg-${c.health === 'healthy' ? 'green' : 'red'}-500/20 text-${c.health === 'healthy' ? 'green' : 'red'}-400">
|
|
${c.health}
|
|
</span>
|
|
` : '';
|
|
|
|
// Parse ports and create clickable links
|
|
const portLinks = this.parsePortLinks(c.ports, hostId);
|
|
|
|
return `
|
|
<div class="bg-gray-800/50 rounded-lg p-3 flex items-center justify-between gap-4">
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<span class="w-2 h-2 rounded-full bg-${stateColor}-500"></span>
|
|
<span class="font-medium truncate">${c.name}</span>
|
|
${c.compose_project ? `<span class="px-2 py-0.5 rounded text-xs bg-purple-500/20 text-purple-400">${c.compose_project}</span>` : ''}
|
|
${healthBadge}
|
|
${portLinks}
|
|
</div>
|
|
<div class="text-sm text-gray-400 truncate mt-1">${c.image}</div>
|
|
<div class="text-xs text-gray-500 mt-1">${c.status || c.state}</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-1">
|
|
${c.state !== 'running' ? `
|
|
<button onclick="dockerSection.startContainer('${hostId}', '${c.container_id}')"
|
|
class="p-2 hover:bg-gray-700 rounded transition-colors text-green-400" title="Démarrer">
|
|
<i class="fas fa-play"></i>
|
|
</button>
|
|
` : `
|
|
<button onclick="dockerSection.stopContainer('${hostId}', '${c.container_id}')"
|
|
class="p-2 hover:bg-gray-700 rounded transition-colors text-red-400" title="Arrêter">
|
|
<i class="fas fa-stop"></i>
|
|
</button>
|
|
`}
|
|
<button onclick="dockerSection.restartContainer('${hostId}', '${c.container_id}')"
|
|
class="p-2 hover:bg-gray-700 rounded transition-colors text-yellow-400" title="Redémarrer">
|
|
<i class="fas fa-redo"></i>
|
|
</button>
|
|
<button onclick="dockerSection.showLogs('${hostId}', '${c.container_id}', '${c.name}')"
|
|
class="p-2 hover:bg-gray-700 rounded transition-colors text-blue-400" title="Logs">
|
|
<i class="fas fa-file-alt"></i>
|
|
</button>
|
|
<button onclick="dockerSection.showInspect('${hostId}', '${c.container_id}', '${c.name}')"
|
|
class="p-2 hover:bg-gray-700 rounded transition-colors text-cyan-400" title="Inspect">
|
|
<i class="fas fa-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
parsePortLinks(ports, hostId) {
|
|
if (!ports) return '';
|
|
|
|
// Get host IP for building URLs
|
|
const host = this.hosts.find(h => h.host_id === hostId);
|
|
const hostIp = host ? host.host_ip : 'localhost';
|
|
|
|
// Parse ports from the raw string or object
|
|
const portStr = ports.raw || (typeof ports === 'string' ? ports : '');
|
|
if (!portStr) return '';
|
|
|
|
// Extract port mappings like "0.0.0.0:8080->80/tcp" or "8080/tcp"
|
|
const portRegex = /(?:([\d.]+):)?(\d+)->\d+\/tcp/g;
|
|
const links = [];
|
|
let match;
|
|
|
|
while ((match = portRegex.exec(portStr)) !== null) {
|
|
const bindIp = match[1] || '0.0.0.0';
|
|
const hostPort = match[2];
|
|
|
|
// Skip if bound to 127.0.0.1 only (not accessible externally)
|
|
if (bindIp === '127.0.0.1') continue;
|
|
|
|
// Determine protocol (https for common secure ports)
|
|
const protocol = ['443', '8443', '9443'].includes(hostPort) ? 'https' : 'http';
|
|
const url = `${protocol}://${hostIp}:${hostPort}`;
|
|
|
|
links.push(`
|
|
<a href="${url}" target="_blank" rel="noopener noreferrer"
|
|
onclick="event.stopPropagation()"
|
|
class="px-2 py-0.5 rounded text-xs bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 transition-colors"
|
|
title="Ouvrir ${url}">
|
|
<i class="fas fa-external-link-alt mr-1"></i>${hostPort}
|
|
</a>
|
|
`);
|
|
}
|
|
|
|
return links.join('');
|
|
},
|
|
|
|
async loadImages(hostId) {
|
|
const container = document.getElementById('docker-images-list');
|
|
container.innerHTML = '<div class="loading-spinner mx-auto"></div>';
|
|
|
|
try {
|
|
const response = await this.fetchAPI(`/api/docker/hosts/${hostId}/images`);
|
|
if (!response) return; // 401 handled
|
|
|
|
const images = response.images || [];
|
|
|
|
if (images.length === 0) {
|
|
container.innerHTML = '<p class="text-gray-400 text-center py-4">Aucune image</p>';
|
|
return;
|
|
}
|
|
|
|
// Show unused count if any
|
|
const unusedInfo = response.unused_count > 0
|
|
? `<div class="text-sm text-yellow-400 mb-3"><i class="fas fa-info-circle mr-1"></i>${response.unused_count} image(s) non utilisée(s)</div>`
|
|
: '';
|
|
|
|
container.innerHTML = unusedInfo + images.map(img => {
|
|
const unusedBadge = !img.in_use
|
|
? `<span class="px-2 py-0.5 rounded text-xs bg-yellow-500/20 text-yellow-400 cursor-help" title="Cette image n'est utilisée par aucun container">Unused</span>`
|
|
: '';
|
|
|
|
return `
|
|
<div class="bg-gray-800/50 rounded-lg p-3 flex items-center justify-between gap-4">
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<span class="font-medium">${img.repo_tags?.[0] || img.image_id.substring(0, 12)}</span>
|
|
${unusedBadge}
|
|
</div>
|
|
<div class="text-sm text-gray-400 mt-1">
|
|
<span class="mr-4"><i class="fas fa-hdd mr-1"></i>${this.formatBytes(img.size)}</span>
|
|
<span><i class="fas fa-clock mr-1"></i>${this.formatRelativeTime(img.created)}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
<button onclick="dockerSection.removeImage('${hostId}', '${img.image_id}', ${!img.in_use})"
|
|
class="p-2 hover:bg-gray-700 rounded transition-colors ${img.in_use ? 'text-gray-500' : 'text-red-400'}"
|
|
title="${img.in_use ? 'Supprimer (forcé car en cours d\'utilisation)' : 'Supprimer l\'image'}">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
} catch (error) {
|
|
container.innerHTML = `<p class="text-red-400 text-center py-4">Erreur: ${error.message}</p>`;
|
|
}
|
|
},
|
|
|
|
async loadVolumes(hostId) {
|
|
const container = document.getElementById('docker-volumes-list');
|
|
container.innerHTML = '<div class="loading-spinner mx-auto"></div>';
|
|
|
|
try {
|
|
const response = await this.fetchAPI(`/api/docker/hosts/${hostId}/volumes`);
|
|
const volumes = response.volumes || [];
|
|
|
|
if (volumes.length === 0) {
|
|
container.innerHTML = '<p class="text-gray-400 text-center py-4">Aucun volume</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = volumes.map(vol => `
|
|
<div class="bg-gray-800/50 rounded-lg p-3">
|
|
<div class="font-medium">${vol.name}</div>
|
|
<div class="text-sm text-gray-400 mt-1">
|
|
<span class="mr-4"><i class="fas fa-cog mr-1"></i>${vol.driver}</span>
|
|
<span class="truncate"><i class="fas fa-folder mr-1"></i>${vol.mountpoint}</span>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (error) {
|
|
container.innerHTML = `<p class="text-red-400 text-center py-4">Erreur: ${error.message}</p>`;
|
|
}
|
|
},
|
|
|
|
async loadHostAlerts(hostId) {
|
|
const container = document.getElementById('docker-host-alerts-list');
|
|
container.innerHTML = '<div class="loading-spinner mx-auto"></div>';
|
|
|
|
try {
|
|
const response = await this.fetchAPI(`/api/docker/alerts?host_id=${hostId}`);
|
|
const alerts = response.alerts || [];
|
|
|
|
if (alerts.length === 0) {
|
|
container.innerHTML = '<p class="text-gray-400 text-center py-4">Aucune alerte</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = alerts.map(alert => this.renderAlertRow(alert)).join('');
|
|
} catch (error) {
|
|
container.innerHTML = `<p class="text-red-400 text-center py-4">Erreur: ${error.message}</p>`;
|
|
}
|
|
},
|
|
|
|
renderAlertRow(alert) {
|
|
const severityColors = {
|
|
warning: 'yellow',
|
|
error: 'red',
|
|
critical: 'red'
|
|
};
|
|
const color = severityColors[alert.severity] || 'gray';
|
|
|
|
return `
|
|
<div class="bg-gray-800/50 rounded-lg p-3 border-l-4 border-${color}-500">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<i class="fas fa-exclamation-triangle text-${color}-400"></i>
|
|
<span class="font-medium">${alert.container_name}</span>
|
|
<span class="px-2 py-0.5 rounded text-xs bg-${color}-500/20 text-${color}-400">${alert.severity}</span>
|
|
</div>
|
|
${alert.state === 'open' ? `
|
|
<button onclick="dockerSection.acknowledgeAlert(${alert.id})"
|
|
class="px-2 py-1 text-xs bg-gray-700 hover:bg-gray-600 rounded transition-colors">
|
|
Acquitter
|
|
</button>
|
|
` : `
|
|
<span class="text-xs text-gray-500">${alert.state}</span>
|
|
`}
|
|
</div>
|
|
<div class="text-sm text-gray-400 mt-2">${alert.message}</div>
|
|
<div class="text-xs text-gray-500 mt-1">${this.formatRelativeTime(alert.opened_at)}</div>
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
// Container Actions
|
|
async startContainer(hostId, containerId) {
|
|
await this.executeAction(hostId, containerId, 'start');
|
|
},
|
|
|
|
async stopContainer(hostId, containerId) {
|
|
await this.executeAction(hostId, containerId, 'stop');
|
|
},
|
|
|
|
async restartContainer(hostId, containerId) {
|
|
await this.executeAction(hostId, containerId, 'restart');
|
|
},
|
|
|
|
async executeAction(hostId, containerId, action) {
|
|
try {
|
|
const response = await this.fetchAPI(`/api/docker/containers/${hostId}/${containerId}/${action}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.success) {
|
|
this.showToast(`Container ${action} successful`, 'success');
|
|
// Refresh containers list
|
|
setTimeout(() => this.loadContainers(hostId), 1000);
|
|
} else {
|
|
this.showToast(`Failed: ${response.error || response.message}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
this.showToast(`Error: ${error.message}`, 'error');
|
|
}
|
|
},
|
|
|
|
async showLogs(hostId, containerId, containerName) {
|
|
document.getElementById('docker-logs-title').innerHTML =
|
|
`<i class="fas fa-file-alt mr-2 text-green-400"></i>${containerName} - Logs`;
|
|
document.getElementById('docker-logs-content').textContent = 'Chargement des logs...';
|
|
document.getElementById('docker-logs-modal').classList.remove('hidden');
|
|
|
|
try {
|
|
const response = await this.fetchAPI(`/api/docker/containers/${hostId}/${containerId}/logs?tail=500`);
|
|
document.getElementById('docker-logs-content').textContent = response.logs || 'Aucun log disponible';
|
|
} catch (error) {
|
|
document.getElementById('docker-logs-content').textContent = `Erreur: ${error.message}`;
|
|
}
|
|
},
|
|
|
|
closeLogsModal() {
|
|
document.getElementById('docker-logs-modal').classList.add('hidden');
|
|
},
|
|
|
|
async showInspect(hostId, containerId, containerName) {
|
|
document.getElementById('docker-logs-title').innerHTML =
|
|
`<i class="fas fa-search mr-2 text-cyan-400"></i>${containerName} - Inspect`;
|
|
document.getElementById('docker-logs-content').textContent = 'Chargement des informations...';
|
|
document.getElementById('docker-logs-modal').classList.remove('hidden');
|
|
|
|
try {
|
|
const response = await this.fetchAPI(`/api/docker/containers/${hostId}/${containerId}/inspect`);
|
|
if (!response) return; // 401 handled
|
|
|
|
// Format JSON nicely
|
|
const inspectData = response.inspect_data || {};
|
|
document.getElementById('docker-logs-content').textContent =
|
|
JSON.stringify(inspectData, null, 2);
|
|
} catch (error) {
|
|
document.getElementById('docker-logs-content').textContent = `Erreur: ${error.message}`;
|
|
}
|
|
},
|
|
|
|
async removeImage(hostId, imageId, canDeleteDirectly = false) {
|
|
const force = !canDeleteDirectly;
|
|
const confirmMsg = force
|
|
? 'Cette image est utilisée par un ou plusieurs containers. Voulez-vous forcer la suppression ?'
|
|
: 'Voulez-vous supprimer cette image ?';
|
|
|
|
if (!confirm(confirmMsg)) return;
|
|
|
|
try {
|
|
const response = await this.fetchAPI(`/api/docker/images/${hostId}/${imageId}?force=${force}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (!response) return; // 401 handled
|
|
|
|
if (response.success) {
|
|
this.showToast('Image supprimée avec succès', 'success');
|
|
// Refresh images list
|
|
await this.loadImages(hostId);
|
|
} else {
|
|
this.showToast(`Erreur: ${response.error || response.message}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
this.showToast(`Erreur: ${error.message}`, 'error');
|
|
}
|
|
},
|
|
|
|
async collectNow(hostId) {
|
|
try {
|
|
this.showToast('Collection en cours...', 'info');
|
|
const response = await this.fetchAPI(`/api/docker/hosts/${hostId}/collect`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.success) {
|
|
this.showToast(`Collection terminée: ${response.containers_count} containers`, 'success');
|
|
await this.loadDockerHosts();
|
|
await this.loadStats();
|
|
} else {
|
|
this.showToast(`Erreur: ${response.error}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
this.showToast(`Erreur: ${error.message}`, 'error');
|
|
}
|
|
},
|
|
|
|
async collectAll() {
|
|
try {
|
|
this.showToast('Collection de tous les hosts...', 'info');
|
|
const response = await this.fetchAPI('/api/docker/collect-all', {
|
|
method: 'POST'
|
|
});
|
|
|
|
this.showToast(`Collecte terminée: ${response.successful}/${response.total_hosts} hosts`,
|
|
response.failed > 0 ? 'warning' : 'success');
|
|
await this.loadDockerHosts();
|
|
await this.loadStats();
|
|
} catch (error) {
|
|
this.showToast(`Erreur: ${error.message}`, 'error');
|
|
}
|
|
},
|
|
|
|
async toggleDockerEnabled(hostId, enabled) {
|
|
try {
|
|
await this.fetchAPI(`/api/docker/hosts/${hostId}/enable`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ enabled })
|
|
});
|
|
|
|
this.showToast(`Docker monitoring ${enabled ? 'activé' : 'désactivé'}`, 'success');
|
|
await this.loadDockerHosts();
|
|
await this.loadStats();
|
|
} catch (error) {
|
|
this.showToast(`Erreur: ${error.message}`, 'error');
|
|
}
|
|
},
|
|
|
|
async acknowledgeAlert(alertId) {
|
|
try {
|
|
await this.fetchAPI(`/api/docker/alerts/${alertId}/acknowledge`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
this.showToast('Alerte acquittée', 'success');
|
|
if (this.currentHostId) {
|
|
await this.loadHostAlerts(this.currentHostId);
|
|
}
|
|
await this.loadStats();
|
|
} catch (error) {
|
|
this.showToast(`Erreur: ${error.message}`, 'error');
|
|
}
|
|
},
|
|
|
|
showAlerts() {
|
|
// Navigate to alerts tab in modal or show alerts modal
|
|
if (this.currentHostId) {
|
|
this.switchTab('host-alerts');
|
|
}
|
|
},
|
|
|
|
updateHostCard(hostData) {
|
|
const card = document.querySelector(`[data-host-id="${hostData.host_id}"]`);
|
|
if (card) {
|
|
card.outerHTML = this.renderHostCard(hostData);
|
|
}
|
|
},
|
|
|
|
async updateAlertsBadge() {
|
|
await this.loadStats();
|
|
},
|
|
|
|
showAlertNotification(alert) {
|
|
this.showToast(`Alerte Docker: ${alert.container_name} - ${alert.message}`, 'warning');
|
|
},
|
|
|
|
renderError(message) {
|
|
const grid = document.getElementById('docker-hosts-grid');
|
|
if (grid) {
|
|
grid.innerHTML = `
|
|
<div class="glass-card p-6 text-center col-span-full">
|
|
<i class="fas fa-exclamation-circle text-4xl text-red-500 mb-4"></i>
|
|
<p class="text-red-400">${message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
|
|
showToast(message, type = 'info') {
|
|
// Use existing toast system or create simple one
|
|
if (window.dashboard && window.dashboard.showToast) {
|
|
window.dashboard.showToast(message, type);
|
|
} else {
|
|
console.log(`[${type.toUpperCase()}] ${message}`);
|
|
}
|
|
},
|
|
|
|
formatRelativeTime(dateStr) {
|
|
if (!dateStr) return 'N/A';
|
|
|
|
const date = new Date(dateStr);
|
|
const now = new Date();
|
|
const diffMs = now - date;
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMins / 60);
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
|
|
if (diffMins < 1) return 'À l\'instant';
|
|
if (diffMins < 60) return `Il y a ${diffMins}min`;
|
|
if (diffHours < 24) return `Il y a ${diffHours}h`;
|
|
return `Il y a ${diffDays}j`;
|
|
},
|
|
|
|
formatBytes(bytes) {
|
|
if (!bytes) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
}
|
|
};
|
|
|
|
function setupDockerAutoInit() {
|
|
// Initialize when Docker page is shown
|
|
const dockerPage = document.getElementById('page-docker');
|
|
if (!dockerPage) return;
|
|
|
|
const observer = new MutationObserver((mutations) => {
|
|
mutations.forEach((mutation) => {
|
|
if (
|
|
mutation.target.id === 'page-docker' &&
|
|
mutation.target.classList.contains('active')
|
|
) {
|
|
dockerSection.ensureInit();
|
|
}
|
|
});
|
|
});
|
|
|
|
observer.observe(dockerPage, { attributes: true, attributeFilter: ['class'] });
|
|
|
|
// Also initialize if already on Docker page
|
|
if (dockerPage.classList.contains('active')) {
|
|
dockerSection.ensureInit();
|
|
}
|
|
}
|
|
|
|
// If this script is loaded after DOMContentLoaded (common when injected late),
|
|
// a DOMContentLoaded listener would never fire. Handle both cases.
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', setupDockerAutoInit);
|
|
} else {
|
|
setupDockerAutoInit();
|
|
}
|