/**
* Containers Page - All containers across all Docker hosts
* Features: search, filter, sort, group, actions, drawer details
*/
const containersPage = {
// State
containers: [],
filteredContainers: [],
hosts: [],
selectedIds: new Set(),
currentContainer: null,
inspectData: null,
// View settings
viewMode: 'comfortable', // 'comfortable', 'compact', 'grouped'
currentPage: 1,
perPage: 50,
// Filter state
filters: {
search: '',
status: '',
host: '',
health: ''
},
sortBy: 'name-asc',
// ========== INITIALIZATION ==========
async init() {
this.setupEventListeners();
this.setupKeyboardShortcuts();
await this.loadData();
},
setupEventListeners() {
// Search input
const searchInput = document.getElementById('containers-search');
if (searchInput) {
searchInput.addEventListener('input', this.debounce((e) => {
this.filters.search = e.target.value;
this.applyFilters();
}, 200));
}
// Clear search button
const clearBtn = document.getElementById('containers-search-clear');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
document.getElementById('containers-search').value = '';
this.filters.search = '';
this.applyFilters();
});
}
// Filter dropdowns
['status', 'host', 'health'].forEach(filter => {
const el = document.getElementById(`containers-filter-${filter}`);
if (el) {
el.addEventListener('change', (e) => {
this.filters[filter] = e.target.value;
this.applyFilters();
});
}
});
// Sort dropdown
const sortEl = document.getElementById('containers-sort');
if (sortEl) {
sortEl.addEventListener('change', (e) => {
this.sortBy = e.target.value;
this.applyFilters();
});
}
// View mode buttons
['comfortable', 'compact', 'grouped'].forEach(mode => {
const btn = document.getElementById(`containers-view-${mode}`);
if (btn) {
btn.addEventListener('click', () => this.setViewMode(mode));
}
});
// Per page select
const perPageEl = document.getElementById('containers-per-page');
if (perPageEl) {
perPageEl.addEventListener('change', (e) => {
this.perPage = parseInt(e.target.value);
this.currentPage = 1;
this.render();
});
}
// Select all checkbox
const selectAllEl = document.getElementById('containers-select-all');
if (selectAllEl) {
selectAllEl.addEventListener('change', (e) => {
this.selectAll(e.target.checked);
});
}
// Drawer tabs
document.querySelectorAll('.drawer-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
const tabName = e.currentTarget.dataset.tab;
this.switchDrawerTab(tabName);
});
});
},
setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Only active when on containers page
if (currentPage !== 'docker-containers') return;
// Ignore if in input
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'TEXTAREA') {
// Escape clears search
if (e.key === 'Escape') {
e.target.blur();
}
return;
}
switch (e.key) {
case '/':
e.preventDefault();
document.getElementById('containers-search')?.focus();
break;
case 'Escape':
if (this.isDrawerOpen()) {
this.closeDrawer();
}
break;
case 'r':
if (!e.ctrlKey && !e.metaKey) {
e.preventDefault();
this.refresh();
}
break;
}
});
},
// ========== DATA LOADING ==========
async loadData() {
this.showLoading();
try {
const [containersRes, hostsRes] = await Promise.all([
this.fetchAPI('/api/docker/containers'),
this.fetchAPI('/api/docker/hosts')
]);
this.containers = containersRes.containers || [];
this.hosts = hostsRes.hosts || [];
// Update host filter dropdown
this.populateHostFilter();
// Update stats
this.updateStats(containersRes);
// Apply filters and render
this.applyFilters();
} catch (error) {
console.error('Error loading containers:', error);
this.showError(error.message);
}
},
async refresh() {
const icon = document.getElementById('containers-refresh-icon');
if (icon) icon.classList.add('fa-spin');
await this.loadData();
if (icon) icon.classList.remove('fa-spin');
this.showToast('Données actualisées', 'success');
},
async fetchAPI(endpoint, options = {}) {
const token = localStorage.getItem('accessToken');
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${window.location.origin}${endpoint}`, {
...options,
headers
});
if (!response.ok) {
if (response.status === 401) {
this.showToast('Session expirée', 'error');
if (window.dashboard?.logout) window.dashboard.logout();
throw new Error('Unauthorized');
}
throw new Error(`API Error: ${response.status}`);
}
return response.json();
},
// ========== FILTERING & SORTING ==========
applyFilters() {
let result = [...this.containers];
// Text search with smart tokens
if (this.filters.search) {
result = this.smartSearch(result, this.filters.search);
}
// Status filter
if (this.filters.status) {
result = result.filter(c => c.state === this.filters.status);
}
// Host filter
if (this.filters.host) {
result = result.filter(c => c.host_id === this.filters.host);
}
// Health filter
if (this.filters.health) {
if (this.filters.health === 'none') {
result = result.filter(c => !c.health || c.health === 'none');
} else {
result = result.filter(c => c.health === this.filters.health);
}
}
// Sort
result = this.sortContainers(result, this.sortBy);
this.filteredContainers = result;
this.currentPage = 1;
this.updateActiveFilters();
this.render();
},
smartSearch(containers, query) {
const tokens = this.parseSearchTokens(query);
return containers.filter(c => {
// Check token filters
for (const token of tokens.filters) {
switch (token.key) {
case 'host':
if (!c.host_name.toLowerCase().includes(token.value.toLowerCase())) return false;
break;
case 'status':
case 'state':
if (c.state !== token.value.toLowerCase()) return false;
break;
case 'health':
if (c.health !== token.value.toLowerCase()) return false;
break;
case 'image':
if (!c.image?.toLowerCase().includes(token.value.toLowerCase())) return false;
break;
case 'port':
if (!this.containerHasPort(c, token.value)) return false;
break;
case 'project':
case 'stack':
if (!c.compose_project?.toLowerCase().includes(token.value.toLowerCase())) return false;
break;
}
}
// Free text search
if (tokens.freeText) {
const searchStr = tokens.freeText.toLowerCase();
const searchable = [
c.name,
c.host_name,
c.image,
c.compose_project,
c.container_id?.substring(0, 12)
].filter(Boolean).join(' ').toLowerCase();
if (!searchable.includes(searchStr)) return false;
}
return true;
});
},
parseSearchTokens(query) {
const filters = [];
let freeText = query;
// Match tokens like "host:value" or "status:running"
const tokenRegex = /(\w+):(\S+)/g;
let match;
while ((match = tokenRegex.exec(query)) !== null) {
filters.push({ key: match[1], value: match[2] });
freeText = freeText.replace(match[0], '').trim();
}
return { filters, freeText };
},
containerHasPort(container, port) {
if (!container.ports) return false;
const portStr = container.ports.raw || JSON.stringify(container.ports);
return portStr.includes(port);
},
sortContainers(containers, sortBy) {
const [field, direction] = sortBy.split('-');
const mult = direction === 'desc' ? -1 : 1;
return containers.sort((a, b) => {
let valA, valB;
switch (field) {
case 'name':
valA = a.name.toLowerCase();
valB = b.name.toLowerCase();
break;
case 'host':
valA = a.host_name.toLowerCase();
valB = b.host_name.toLowerCase();
break;
case 'status':
const statusOrder = { running: 0, restarting: 1, paused: 2, created: 3, exited: 4, dead: 5 };
valA = statusOrder[a.state] ?? 99;
valB = statusOrder[b.state] ?? 99;
break;
case 'updated':
valA = new Date(a.last_update_at || 0).getTime();
valB = new Date(b.last_update_at || 0).getTime();
break;
default:
valA = a.name.toLowerCase();
valB = b.name.toLowerCase();
}
if (valA < valB) return -1 * mult;
if (valA > valB) return 1 * mult;
return 0;
});
},
// ========== RENDERING ==========
render() {
const list = document.getElementById('containers-list');
const empty = document.getElementById('containers-empty');
const error = document.getElementById('containers-error');
const pagination = document.getElementById('containers-pagination');
// Hide error
error?.classList.add('hidden');
// Check empty
if (this.filteredContainers.length === 0) {
list.innerHTML = '';
empty?.classList.remove('hidden');
pagination?.classList.add('hidden');
return;
}
empty?.classList.add('hidden');
// Paginate
const start = (this.currentPage - 1) * this.perPage;
const end = Math.min(start + this.perPage, this.filteredContainers.length);
const pageContainers = this.filteredContainers.slice(start, end);
// Render based on view mode
if (this.viewMode === 'grouped') {
list.innerHTML = this.renderGroupedView(pageContainers);
} else {
list.innerHTML = pageContainers.map(c => this.renderContainerRow(c)).join('');
}
// Update pagination
this.updatePagination(start, end);
// Update search clear button visibility
const clearBtn = document.getElementById('containers-search-clear');
if (clearBtn) {
clearBtn.classList.toggle('hidden', !this.filters.search);
}
},
renderContainerRow(c) {
const stateColors = {
running: 'green',
exited: 'red',
paused: 'yellow',
created: 'blue',
restarting: 'orange',
dead: 'red'
};
const stateColor = stateColors[c.state] || 'gray';
const isCompact = this.viewMode === 'compact';
const healthBadge = c.health && c.health !== 'none' ? `
${c.health}
` : '';
const projectBadge = c.compose_project ? `
${this.escapeHtml(c.compose_project)}
` : '';
const portLinks = this.renderPortLinks(c);
if (isCompact) {
return `
${this.escapeHtml(c.name)}
${this.escapeHtml(c.host_name)}
${this.escapeHtml(c.image || '—')}
${this.renderQuickActions(c)}
`;
}
return `
${this.escapeHtml(c.name)}
${projectBadge}
${healthBadge}
${portLinks}
${this.escapeHtml(c.host_name)}
${this.escapeHtml(c.image || '—')}
${c.status || c.state}
${this.renderQuickActions(c)}
`;
},
renderGroupedView(containers) {
// Group by host
const groups = {};
containers.forEach(c => {
if (!groups[c.host_id]) {
groups[c.host_id] = {
host_name: c.host_name,
host_ip: c.host_ip,
containers: []
};
}
groups[c.host_id].containers.push(c);
});
return Object.entries(groups).map(([hostId, group]) => `
${this.escapeHtml(group.host_name)}
${this.escapeHtml(group.host_ip)}
${group.containers.length} container(s)
${group.containers.map(c => this.renderContainerRow(c)).join('')}
`).join('');
},
renderQuickActions(c) {
const isRunning = c.state === 'running';
return `
${!isRunning ? `
` : `
`}
`;
},
renderPortLinks(c) {
if (!c.ports) return '';
const portStr = c.ports.raw || (typeof c.ports === 'string' ? c.ports : '');
if (!portStr) return '';
const portRegex = /(?:([\d.]+):)?(\d+)->\d+\/tcp/g;
const links = [];
const seenPorts = new Set();
let match;
while ((match = portRegex.exec(portStr)) !== null) {
const bindIp = match[1] || '0.0.0.0';
const hostPort = match[2];
if (bindIp === '127.0.0.1' || bindIp === '::1') continue;
if (seenPorts.has(hostPort)) continue;
seenPorts.add(hostPort);
const protocol = ['443', '8443', '9443'].includes(hostPort) ? 'https' : 'http';
const url = `${protocol}://${c.host_ip}:${hostPort}`;
links.push(`
${hostPort}
`);
}
return links.slice(0, 3).join(''); // Limit to 3 port links
},
// ========== UI STATE UPDATES ==========
showLoading() {
const list = document.getElementById('containers-list');
if (list) {
list.innerHTML = `
Chargement des containers...
`;
}
document.getElementById('containers-empty')?.classList.add('hidden');
document.getElementById('containers-error')?.classList.add('hidden');
},
showError(message) {
document.getElementById('containers-list').innerHTML = '';
document.getElementById('containers-empty')?.classList.add('hidden');
const errorEl = document.getElementById('containers-error');
if (errorEl) {
errorEl.classList.remove('hidden');
document.getElementById('containers-error-message').textContent = message;
}
},
updateStats(data) {
const el = (id, val) => {
const elem = document.getElementById(id);
if (elem) elem.textContent = val;
};
el('containers-total', data.total || 0);
el('containers-running', data.running || 0);
el('containers-stopped', data.stopped || 0);
el('containers-paused', data.paused || 0);
el('containers-hosts-count', data.hosts_count || 0);
// Update last update time
if (data.last_update) {
const date = new Date(data.last_update);
document.getElementById('containers-last-update').textContent =
`Mis à jour ${this.formatRelativeTime(date)}`;
}
},
updatePagination(start, end) {
const pagination = document.getElementById('containers-pagination');
if (!pagination) return;
if (this.filteredContainers.length <= this.perPage) {
pagination.classList.add('hidden');
return;
}
pagination.classList.remove('hidden');
document.getElementById('containers-showing-start').textContent = start + 1;
document.getElementById('containers-showing-end').textContent = end;
document.getElementById('containers-showing-total').textContent = this.filteredContainers.length;
},
updateActiveFilters() {
const container = document.getElementById('containers-active-filters');
if (!container) return;
const activeFilters = [];
if (this.filters.status) {
activeFilters.push({ key: 'status', value: this.filters.status, label: `Status: ${this.filters.status}` });
}
if (this.filters.host) {
const host = this.hosts.find(h => h.host_id === this.filters.host);
activeFilters.push({ key: 'host', value: this.filters.host, label: `Host: ${host?.host_name || this.filters.host}` });
}
if (this.filters.health) {
activeFilters.push({ key: 'health', value: this.filters.health, label: `Health: ${this.filters.health}` });
}
if (activeFilters.length === 0) {
container.classList.add('hidden');
return;
}
container.classList.remove('hidden');
container.innerHTML = activeFilters.map(f => `
${f.label}
`).join('') + `
`;
},
populateHostFilter() {
const select = document.getElementById('containers-filter-host');
if (!select) return;
// Keep first option
select.innerHTML = '';
this.hosts.forEach(host => {
const option = document.createElement('option');
option.value = host.host_id;
option.textContent = host.host_name;
select.appendChild(option);
});
},
setViewMode(mode) {
this.viewMode = mode;
// Update button states
['comfortable', 'compact', 'grouped'].forEach(m => {
const btn = document.getElementById(`containers-view-${m}`);
if (btn) {
btn.classList.toggle('bg-purple-600', m === mode);
btn.classList.toggle('bg-gray-700', m !== mode);
btn.setAttribute('aria-pressed', m === mode ? 'true' : 'false');
}
});
this.render();
},
clearFilter(key) {
this.filters[key] = '';
const el = document.getElementById(`containers-filter-${key}`);
if (el) el.value = '';
this.applyFilters();
},
clearAllFilters() {
this.filters = { search: '', status: '', host: '', health: '' };
document.getElementById('containers-search').value = '';
document.getElementById('containers-filter-status').value = '';
document.getElementById('containers-filter-host').value = '';
document.getElementById('containers-filter-health').value = '';
this.applyFilters();
},
// ========== CONTAINER ACTIONS ==========
async containerAction(hostId, containerId, action) {
try {
this.showToast(`${action}...`, 'info');
const response = await this.fetchAPI(`/api/docker/containers/${hostId}/${containerId}/${action}`, {
method: 'POST'
});
if (response.success) {
this.showToast(`Container ${action} réussi`, 'success');
// Refresh after a short delay
setTimeout(() => this.refresh(), 1000);
} else {
this.showToast(`Erreur: ${response.error || response.message}`, 'error');
}
} catch (error) {
this.showToast(`Erreur: ${error.message}`, 'error');
}
},
async bulkAction(action) {
if (this.selectedIds.size === 0) {
this.showToast('Aucun container sélectionné', 'warning');
return;
}
const confirmMsg = `Êtes-vous sûr de vouloir ${action} ${this.selectedIds.size} container(s) ?`;
if (!confirm(confirmMsg)) return;
this.showToast(`${action} en cours...`, 'info');
const results = await Promise.allSettled(
Array.from(this.selectedIds).map(id => {
const [hostId, containerId] = id.split('::');
return this.fetchAPI(`/api/docker/containers/${hostId}/${containerId}/${action}`, {
method: 'POST'
});
})
);
const successful = results.filter(r => r.status === 'fulfilled' && r.value.success).length;
this.showToast(`${successful}/${this.selectedIds.size} container(s) ${action}`,
successful === this.selectedIds.size ? 'success' : 'warning');
this.selectedIds.clear();
this.updateBulkActionsBar();
setTimeout(() => this.refresh(), 1000);
},
selectAll(checked) {
if (checked) {
this.filteredContainers.forEach(c => {
this.selectedIds.add(`${c.host_id}::${c.container_id}`);
});
} else {
this.selectedIds.clear();
}
this.updateBulkActionsBar();
},
toggleSelection(hostId, containerId) {
const id = `${hostId}::${containerId}`;
if (this.selectedIds.has(id)) {
this.selectedIds.delete(id);
} else {
this.selectedIds.add(id);
}
this.updateBulkActionsBar();
},
updateBulkActionsBar() {
const bar = document.getElementById('containers-bulk-actions');
const count = document.getElementById('containers-selected-count');
const selectAll = document.getElementById('containers-select-all');
if (bar) {
bar.classList.toggle('hidden', this.selectedIds.size === 0);
}
if (count) {
count.textContent = this.selectedIds.size;
}
if (selectAll) {
selectAll.checked = this.selectedIds.size > 0 &&
this.selectedIds.size === this.filteredContainers.length;
}
},
// ========== DRAWER ==========
async openDrawer(hostId, containerId, tab = 'overview') {
this.currentContainer = this.containers.find(
c => c.host_id === hostId && c.container_id === containerId
);
if (!this.currentContainer) {
this.showToast('Container non trouvé', 'error');
return;
}
const drawer = document.getElementById('container-drawer');
const backdrop = document.getElementById('container-drawer-backdrop');
// Populate drawer
this.populateDrawer();
// Show drawer
drawer.classList.remove('translate-x-full');
backdrop.classList.remove('hidden');
document.body.style.overflow = 'hidden';
// Switch to requested tab
this.switchDrawerTab(tab);
// Load tab-specific data
if (tab === 'logs') {
await this.loadLogs();
} else if (tab === 'inspect') {
await this.loadInspect();
}
},
closeDrawer() {
const drawer = document.getElementById('container-drawer');
const backdrop = document.getElementById('container-drawer-backdrop');
drawer.classList.add('translate-x-full');
backdrop.classList.add('hidden');
document.body.style.overflow = '';
this.currentContainer = null;
this.inspectData = null;
},
isDrawerOpen() {
const drawer = document.getElementById('container-drawer');
return drawer && !drawer.classList.contains('translate-x-full');
},
populateDrawer() {
const c = this.currentContainer;
if (!c) return;
const stateColors = {
running: 'bg-green-500',
exited: 'bg-red-500',
paused: 'bg-yellow-500',
created: 'bg-blue-500',
restarting: 'bg-orange-500',
dead: 'bg-red-500'
};
// Header
document.getElementById('drawer-container-name').textContent = c.name;
document.getElementById('drawer-container-state').className =
`w-3 h-3 rounded-full ${stateColors[c.state] || 'bg-gray-500'}`;
// Overview
document.getElementById('drawer-host-name').textContent = c.host_name;
document.getElementById('drawer-host-ip').textContent = c.host_ip;
document.getElementById('drawer-status').textContent = c.status || c.state;
document.getElementById('drawer-health').textContent = c.health || 'No healthcheck';
document.getElementById('drawer-image').textContent = c.image || '—';
document.getElementById('drawer-container-id').textContent = c.container_id;
// Ports
const portsEl = document.getElementById('drawer-ports');
if (c.ports) {
portsEl.innerHTML = this.renderPortLinks(c) || 'Aucun port exposé';
} else {
portsEl.innerHTML = 'Aucun port exposé';
}
// Labels
const labelsEl = document.getElementById('drawer-labels');
if (c.labels && Object.keys(c.labels).length > 0) {
labelsEl.innerHTML = Object.entries(c.labels)
.slice(0, 10)
.map(([k, v]) => `
${this.escapeHtml(k.split('.').pop())}
`).join('');
} else {
labelsEl.innerHTML = 'Aucun label';
}
// Update action buttons based on state
const isRunning = c.state === 'running';
document.getElementById('drawer-btn-start')?.classList.toggle('hidden', isRunning);
document.getElementById('drawer-btn-stop')?.classList.toggle('hidden', !isRunning);
},
switchDrawerTab(tabName) {
// Update tab buttons
document.querySelectorAll('.drawer-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('.drawer-tab-content').forEach(content => {
content.classList.toggle('hidden', !content.id.endsWith(tabName));
});
// Load content if needed
if (tabName === 'logs' && this.currentContainer) {
this.loadLogs();
} else if (tabName === 'inspect' && this.currentContainer) {
this.loadInspect();
}
},
async loadLogs() {
if (!this.currentContainer) return;
const logsEl = document.getElementById('drawer-logs-content');
logsEl.textContent = 'Chargement...';
try {
const tail = document.getElementById('drawer-logs-tail')?.value || 200;
const timestamps = document.getElementById('drawer-logs-timestamps')?.checked || false;
const response = await this.fetchAPI(
`/api/docker/containers/${this.currentContainer.host_id}/${this.currentContainer.container_id}/logs?tail=${tail}×tamps=${timestamps}`
);
logsEl.textContent = response.logs || 'Aucun log disponible';
} catch (error) {
logsEl.textContent = `Erreur: ${error.message}`;
}
},
async loadInspect() {
if (!this.currentContainer) return;
const inspectEl = document.getElementById('drawer-inspect-content');
inspectEl.textContent = 'Chargement...';
try {
const response = await this.fetchAPI(
`/api/docker/containers/${this.currentContainer.host_id}/${this.currentContainer.container_id}/inspect`
);
this.inspectData = response.inspect_data || {};
inspectEl.textContent = JSON.stringify(this.inspectData, null, 2);
} catch (error) {
inspectEl.textContent = `Erreur: ${error.message}`;
}
},
async copyInspect() {
if (!this.inspectData) {
this.showToast('Aucune donnée à copier', 'warning');
return;
}
try {
await navigator.clipboard.writeText(JSON.stringify(this.inspectData, null, 2));
this.showToast('JSON copié !', 'success');
} catch (error) {
this.showToast('Erreur lors de la copie', 'error');
}
},
async drawerAction(action) {
if (!this.currentContainer) return;
await this.containerAction(
this.currentContainer.host_id,
this.currentContainer.container_id,
action
);
},
// ========== UTILITIES ==========
debounce(fn, delay) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(this, args), delay);
};
},
escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
formatRelativeTime(date) {
if (!date) return '—';
const now = new Date();
const d = new Date(date);
const diff = Math.floor((now - d) / 1000);
if (diff < 60) return 'À l\'instant';
if (diff < 3600) return `il y a ${Math.floor(diff / 60)} min`;
if (diff < 86400) return `il y a ${Math.floor(diff / 3600)} h`;
return `il y a ${Math.floor(diff / 86400)} j`;
},
showToast(message, type = 'info') {
// Use existing toast system if available
if (window.dockerSection?.showToast) {
window.dockerSection.showToast(message, type);
} else if (window.showNotification) {
window.showNotification(message, type);
} else {
console.log(`[${type}] ${message}`);
}
}
};
// Initialize when page is shown
document.addEventListener('DOMContentLoaded', () => {
// Listen for page navigation
const originalNavigateTo = window.navigateTo;
if (originalNavigateTo) {
window.navigateTo = function(pageName) {
originalNavigateTo(pageName);
if (pageName === 'docker-containers') {
containersPage.init();
}
};
}
});
// Export for global access
window.containersPage = containersPage;