261 lines
7.9 KiB
Vue
261 lines
7.9 KiB
Vue
<template>
|
|
<div class="p-8">
|
|
<h1 class="text-3xl font-bold mb-8 text-gray-100">🐳 Gestion Docker</h1>
|
|
|
|
<!-- Statut Docker -->
|
|
<div class="mb-6">
|
|
<div v-if="dockerStatus.connected" class="p-4 bg-green-500/20 border border-green-500/50 rounded-lg text-green-400">
|
|
✓ Docker est connecté et accessible
|
|
</div>
|
|
<div v-else class="p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400">
|
|
✗ Docker n'est pas accessible. Vérifiez la configuration.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Boutons d'action -->
|
|
<div class="mb-6 flex space-x-3">
|
|
<button
|
|
@click="fetchContainers"
|
|
:disabled="loading"
|
|
class="btn btn-primary"
|
|
>
|
|
🔄 Rafraîchir
|
|
</button>
|
|
<button
|
|
@click="toggleShowAll"
|
|
:class="showAll ? 'btn btn-secondary' : 'btn btn-primary'"
|
|
>
|
|
{{ showAll ? 'Afficher actifs' : 'Afficher tous' }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Liste des conteneurs -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<div
|
|
v-for="container in containers"
|
|
:key="container.id"
|
|
class="card hover:border-blue-500/50 transition"
|
|
>
|
|
<!-- En-tête du conteneur -->
|
|
<div class="flex items-start justify-between mb-4">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-blue-400">{{ container.name }}</h3>
|
|
<p class="text-gray-400 text-sm">{{ container.image }}</p>
|
|
<p class="text-gray-500 text-xs mt-1">ID: {{ container.id }}</p>
|
|
</div>
|
|
<span :class="getStatusBadgeClass(container.state)" class="px-3 py-1 rounded-full text-xs font-medium">
|
|
{{ container.state }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Ressources -->
|
|
<div class="grid grid-cols-2 gap-3 mb-4 text-sm">
|
|
<div class="bg-gray-700/50 p-2 rounded">
|
|
<p class="text-gray-400 text-xs">CPU</p>
|
|
<p class="text-blue-400 font-semibold">{{ container.cpu_percent }}%</p>
|
|
</div>
|
|
<div class="bg-gray-700/50 p-2 rounded">
|
|
<p class="text-gray-400 text-xs">Mémoire</p>
|
|
<p class="text-green-400 font-semibold">{{ container.memory_usage }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ports -->
|
|
<div v-if="container.ports.length > 0" class="mb-4">
|
|
<p class="text-gray-400 text-xs mb-2">Ports:</p>
|
|
<div class="space-y-1">
|
|
<div
|
|
v-for="(port, idx) in container.ports"
|
|
:key="idx"
|
|
class="text-xs text-gray-300 bg-gray-700/30 px-2 py-1 rounded"
|
|
>
|
|
{{ port.public_port || '-' }}:{{ port.private_port }}/{{ port.type }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Boutons d'action -->
|
|
<div class="flex space-x-2 pt-4 border-t border-gray-700">
|
|
<button
|
|
v-if="container.state !== 'running'"
|
|
@click="actionContainer(container.id, 'start')"
|
|
:disabled="actionLoading"
|
|
class="btn btn-secondary btn-small flex-1"
|
|
>
|
|
▶ Démarrer
|
|
</button>
|
|
<button
|
|
v-if="container.state === 'running'"
|
|
@click="actionContainer(container.id, 'stop')"
|
|
:disabled="actionLoading"
|
|
class="btn btn-danger btn-small flex-1"
|
|
>
|
|
⏹ Arrêter
|
|
</button>
|
|
<button
|
|
@click="actionContainer(container.id, 'restart')"
|
|
:disabled="actionLoading"
|
|
class="btn btn-primary btn-small"
|
|
>
|
|
🔄
|
|
</button>
|
|
<button
|
|
@click="actionContainer(container.id, 'delete')"
|
|
:disabled="actionLoading"
|
|
class="btn btn-danger btn-small"
|
|
>
|
|
🗑
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Message si aucun conteneur -->
|
|
<div v-if="containers.length === 0 && !loading" class="text-center py-12">
|
|
<p class="text-gray-400">Aucun conteneur {{ showAll ? '' : 'actif' }} trouvé</p>
|
|
</div>
|
|
|
|
<!-- Notification d'action -->
|
|
<div
|
|
v-if="notification.show"
|
|
:class="notification.type === 'success' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'"
|
|
class="fixed bottom-4 right-4 p-4 rounded-lg border transition"
|
|
>
|
|
{{ notification.message }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onActivated, onBeforeUnmount } from 'vue'
|
|
import api from '../api'
|
|
import { useApiCache } from '../composables/useApiCache'
|
|
|
|
const { fetchWithCache, invalidateCache } = useApiCache()
|
|
|
|
const containers = ref([])
|
|
const dockerStatus = ref({ connected: false })
|
|
const loading = ref(false)
|
|
const actionLoading = ref(false)
|
|
const showAll = ref(true)
|
|
const notification = ref({ show: false, message: '', type: 'success' })
|
|
|
|
let refreshInterval = null
|
|
|
|
const fetchContainers = async (forceRefresh = false) => {
|
|
loading.value = true
|
|
try {
|
|
const response = await fetchWithCache(
|
|
'/docker/containers',
|
|
() => api.get('/docker/containers', { params: { all: showAll.value } }),
|
|
{
|
|
ttl: 5000, // Cache de 5 secondes
|
|
forceRefresh,
|
|
params: { all: showAll.value }
|
|
}
|
|
)
|
|
containers.value = response.data
|
|
} catch (error) {
|
|
showNotification('Erreur lors du chargement des conteneurs', 'error')
|
|
console.error(error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const checkDockerStatus = async () => {
|
|
try {
|
|
const response = await fetchWithCache(
|
|
'/docker/status',
|
|
() => api.get('/docker/status'),
|
|
{ ttl: 10000 } // Cache de 10 secondes
|
|
)
|
|
dockerStatus.value = response.data
|
|
} catch (error) {
|
|
console.error('Erreur statut Docker:', error)
|
|
}
|
|
}
|
|
|
|
const actionContainer = async (containerId, action) => {
|
|
actionLoading.value = true
|
|
try {
|
|
if (action === 'start') {
|
|
await api.post(`/docker/containers/${containerId}/start`)
|
|
} else if (action === 'stop') {
|
|
await api.post(`/docker/containers/${containerId}/stop`)
|
|
} else if (action === 'restart') {
|
|
await api.post(`/docker/containers/${containerId}/restart`)
|
|
} else if (action === 'delete') {
|
|
if (confirm('Êtes-vous sûr de vouloir supprimer ce conteneur ?')) {
|
|
await api.delete(`/docker/containers/${containerId}`)
|
|
} else {
|
|
actionLoading.value = false
|
|
return
|
|
}
|
|
}
|
|
|
|
showNotification(`Conteneur ${action}é avec succès`, 'success')
|
|
|
|
// Invalider le cache et rafraîchir
|
|
invalidateCache('/docker/containers')
|
|
await fetchContainers(true)
|
|
} catch (error) {
|
|
showNotification(`Erreur lors du ${action} du conteneur`, 'error')
|
|
console.error(error)
|
|
} finally {
|
|
actionLoading.value = false
|
|
}
|
|
}
|
|
|
|
const toggleShowAll = () => {
|
|
showAll.value = !showAll.value
|
|
invalidateCache('/docker/containers')
|
|
fetchContainers(true)
|
|
}
|
|
|
|
const getStatusBadgeClass = (state) => {
|
|
const baseClass = 'px-2 py-1 rounded-full text-xs font-medium'
|
|
if (state === 'running') return baseClass + ' bg-green-500/30 text-green-400'
|
|
if (state === 'exited') return baseClass + ' bg-red-500/30 text-red-400'
|
|
return baseClass + ' bg-gray-600/30 text-gray-400'
|
|
}
|
|
|
|
const showNotification = (message, type = 'success') => {
|
|
notification.value = { show: true, message, type }
|
|
setTimeout(() => {
|
|
notification.value.show = false
|
|
}, 3000)
|
|
}
|
|
|
|
// Auto-refresh toutes les 10 secondes quand le composant est actif
|
|
const startAutoRefresh = () => {
|
|
refreshInterval = setInterval(() => {
|
|
fetchContainers()
|
|
}, 10000)
|
|
}
|
|
|
|
const stopAutoRefresh = () => {
|
|
if (refreshInterval) {
|
|
clearInterval(refreshInterval)
|
|
refreshInterval = null
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
checkDockerStatus()
|
|
fetchContainers()
|
|
startAutoRefresh()
|
|
})
|
|
|
|
// Appelé quand le composant est réactivé depuis keep-alive
|
|
onActivated(() => {
|
|
fetchContainers()
|
|
startAutoRefresh()
|
|
})
|
|
|
|
// Nettoyage
|
|
onBeforeUnmount(() => {
|
|
stopAutoRefresh()
|
|
})
|
|
</script>
|