feat: première version fonctionnelle
This commit is contained in:
46
frontend/src/App.vue
Normal file
46
frontend/src/App.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-100">
|
||||
<nav class="bg-white shadow-sm">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center space-x-2">
|
||||
<img src="/tomato.svg" alt="Zebra Power" class="w-8 h-8" />
|
||||
<h1 class="text-xl font-bold text-gray-900">Zebra Power</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-8">
|
||||
<router-link
|
||||
to="/"
|
||||
class="inline-flex items-center px-1 pt-1 text-sm font-medium"
|
||||
:class="$route.name === 'Dashboard' ? 'border-b-2 border-blue-500 text-gray-900' : 'text-gray-500 hover:text-gray-700'"
|
||||
>
|
||||
Dashboard
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/servers"
|
||||
class="inline-flex items-center px-1 pt-1 text-sm font-medium"
|
||||
:class="$route.name === 'Servers' ? 'border-b-2 border-blue-500 text-gray-900' : 'text-gray-500 hover:text-gray-700'"
|
||||
>
|
||||
Serveurs
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/proxmox"
|
||||
class="inline-flex items-center px-1 pt-1 text-sm font-medium"
|
||||
:class="$route.name === 'Proxmox' ? 'border-b-2 border-blue-500 text-gray-900' : 'text-gray-500 hover:text-gray-700'"
|
||||
>
|
||||
Proxmox
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
27
frontend/src/main.js
Normal file
27
frontend/src/main.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
|
||||
import Dashboard from './views/Dashboard.vue'
|
||||
import Servers from './views/Servers.vue'
|
||||
import Proxmox from './views/Proxmox.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', name: 'Dashboard', component: Dashboard },
|
||||
{ path: '/servers', name: 'Servers', component: Servers },
|
||||
{ path: '/proxmox', name: 'Proxmox', component: Proxmox }
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
createApp(App)
|
||||
.use(pinia)
|
||||
.use(router)
|
||||
.mount('#app')
|
||||
46
frontend/src/services/api.js
Normal file
46
frontend/src/services/api.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: apiUrl,
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
console.error('API Error:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export const serversApi = {
|
||||
getAll: () => api.get('/servers'),
|
||||
create: (server) => api.post('/servers', server),
|
||||
update: (id, server) => api.put(`/servers/${id}`, server),
|
||||
delete: (id) => api.delete(`/servers/${id}`),
|
||||
checkStatus: () => api.post('/servers/check-status'),
|
||||
}
|
||||
|
||||
export const wolApi = {
|
||||
wake: (serverId) => api.post(`/wol/wake/${serverId}`),
|
||||
ping: (serverId) => api.post(`/wol/ping/${serverId}`),
|
||||
getLogs: (limit = 50) => api.get(`/wol/logs?limit=${limit}`),
|
||||
getServerLogs: (serverId, limit = 20) => api.get(`/wol/logs/${serverId}?limit=${limit}`),
|
||||
getAllLogs: (limit = 100) => api.get(`/wol/all-logs?limit=${limit}`),
|
||||
getLogsByType: (type, limit = 50) => api.get(`/wol/all-logs/${type}?limit=${limit}`),
|
||||
}
|
||||
|
||||
export const proxmoxApi = {
|
||||
getClusters: () => api.get('/proxmox/clusters'),
|
||||
createCluster: (cluster) => api.post('/proxmox/clusters', cluster),
|
||||
deleteCluster: (id) => api.delete(`/proxmox/clusters/${id}`),
|
||||
getVMs: (clusterId) => api.get(`/proxmox/clusters/${clusterId}/vms`),
|
||||
startVM: (clusterId, vmid, node, vmType = 'qemu') =>
|
||||
api.post(`/proxmox/clusters/${clusterId}/vms/${vmid}/start?node=${node}&vm_type=${vmType}`),
|
||||
stopVM: (clusterId, vmid, node, vmType = 'qemu') =>
|
||||
api.post(`/proxmox/clusters/${clusterId}/vms/${vmid}/stop?node=${node}&vm_type=${vmType}`),
|
||||
}
|
||||
|
||||
export default api
|
||||
3
frontend/src/style.css
Normal file
3
frontend/src/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
274
frontend/src/views/Dashboard.vue
Normal file
274
frontend/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div class="px-4 py-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
|
||||
<span class="text-white font-medium">S</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Serveurs</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ servers.length }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
|
||||
<span class="text-white font-medium">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">En ligne</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ onlineServers }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
|
||||
<span class="text-white font-medium">P</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">Clusters Proxmox</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">{{ clusters.length }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Serveurs</h3>
|
||||
<div class="space-y-3">
|
||||
<div v-for="server in servers.slice(0, 5)" :key="server.id" class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
:class="server.is_online ? 'bg-green-400' : 'bg-red-400'"
|
||||
></div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-gray-900">{{ server.name }}</p>
|
||||
<p class="text-sm text-gray-500">{{ server.ip_address }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="wakeServer(server.id)"
|
||||
:disabled="loading"
|
||||
class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
|
||||
>
|
||||
Wake
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="servers.length === 0" class="text-gray-500 text-sm">
|
||||
Aucun serveur configuré
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">VMs/Containers</h3>
|
||||
<div class="space-y-3">
|
||||
<div v-for="vm in allVMs.slice(0, 5)" :key="`${vm.clusterId}-${vm.node}-${vm.vmid}`" class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
:class="vm.status === 'running' ? 'bg-green-400' : 'bg-red-400'"
|
||||
></div>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-gray-900">{{ vm.name }}</p>
|
||||
<p class="text-sm text-gray-500">{{ vm.type.toUpperCase() }} #{{ vm.vmid }} - {{ vm.node }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-1">
|
||||
<button
|
||||
v-if="vm.status !== 'running'"
|
||||
@click="startVM(vm.clusterId, vm.vmid, vm.node, vm.type)"
|
||||
:disabled="loading"
|
||||
class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50"
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
<button
|
||||
v-if="vm.status === 'running'"
|
||||
@click="stopVM(vm.clusterId, vm.vmid, vm.node, vm.type)"
|
||||
:disabled="loading"
|
||||
class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="allVMs.length === 0" class="text-gray-500 text-sm">
|
||||
Aucune VM/Container disponible
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Logs récents</h3>
|
||||
<div class="space-y-3">
|
||||
<div v-for="log in logs.slice(0, 5)" :key="log.id" class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
:class="log.success ? 'bg-green-400' : 'bg-red-400'"
|
||||
></div>
|
||||
</div>
|
||||
<div class="ml-3 min-w-0 flex-1">
|
||||
<p class="text-sm text-gray-900">{{ getLogDisplayText(log) }}</p>
|
||||
<p class="text-sm text-gray-500">{{ formatDate(log.timestamp) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="logs.length === 0" class="text-gray-500 text-sm">
|
||||
Aucun log disponible
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
|
||||
import { serversApi, proxmoxApi, wolApi } from '../services/api'
|
||||
|
||||
const servers = ref([])
|
||||
const clusters = ref([])
|
||||
const logs = ref([])
|
||||
const allVMs = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
const onlineServers = computed(() =>
|
||||
servers.value.filter(server => server.is_online).length
|
||||
)
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString('fr-FR')
|
||||
}
|
||||
|
||||
const getLogDisplayText = (log) => {
|
||||
if (log.target_name) {
|
||||
return `${log.action} - ${log.target_name} (${log.action_type})`
|
||||
}
|
||||
// Fallback pour les anciens logs WOL
|
||||
return `${log.action} - Serveur #${log.server_id || log.target_id}`
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
// D'abord vérifier le statut des serveurs
|
||||
await serversApi.checkStatus()
|
||||
|
||||
const [serversResponse, clustersResponse, logsResponse] = await Promise.all([
|
||||
serversApi.getAll(),
|
||||
proxmoxApi.getClusters(),
|
||||
wolApi.getAllLogs(10)
|
||||
])
|
||||
|
||||
servers.value = serversResponse.data
|
||||
clusters.value = clustersResponse.data
|
||||
logs.value = logsResponse.data
|
||||
|
||||
// Charger les VMs de tous les clusters
|
||||
const vms = []
|
||||
for (const cluster of clustersResponse.data) {
|
||||
try {
|
||||
const vmsResponse = await proxmoxApi.getVMs(cluster.id)
|
||||
for (const vm of vmsResponse.data) {
|
||||
vms.push({
|
||||
...vm,
|
||||
clusterId: cluster.id
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors du chargement des VMs du cluster ${cluster.id}:`, error)
|
||||
}
|
||||
}
|
||||
allVMs.value = vms
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des données:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const wakeServer = async (serverId) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await wolApi.wake(serverId)
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du réveil du serveur:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startVM = async (clusterId, vmid, node, vmType) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await proxmoxApi.startVM(clusterId, vmid, node, vmType)
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du démarrage de la VM:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const stopVM = async (clusterId, vmid, node, vmType) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await proxmoxApi.stopVM(clusterId, vmid, node, vmType)
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'arrêt de la VM:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
|
||||
// Mise à jour automatique toutes les 30 secondes
|
||||
const interval = setInterval(() => {
|
||||
loadData()
|
||||
}, 30000)
|
||||
|
||||
// Nettoyer l'interval quand le composant est démonté
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(interval)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
305
frontend/src/views/Proxmox.vue
Normal file
305
frontend/src/views/Proxmox.vue
Normal file
@@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<div class="px-4 py-6">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Clusters Proxmox</h1>
|
||||
<p class="mt-2 text-sm text-gray-700">
|
||||
Gérez vos clusters Proxmox et contrôlez vos VMs/Containers
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<button
|
||||
@click="showAddModal = true"
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:w-auto"
|
||||
>
|
||||
Ajouter un cluster
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liste des clusters -->
|
||||
<div class="mt-8 space-y-6">
|
||||
<div v-for="cluster in clusters" :key="cluster.id" class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">{{ cluster.name }}</h3>
|
||||
<p class="text-sm text-gray-500">{{ cluster.host }}:{{ cluster.port }}</p>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
@click="loadVMs(cluster.id)"
|
||||
:disabled="loading"
|
||||
class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
Actualiser
|
||||
</button>
|
||||
<button
|
||||
@click="deleteCluster(cluster.id)"
|
||||
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liste des VMs -->
|
||||
<div v-if="clusterVMs[cluster.id]" class="mt-4">
|
||||
<h4 class="text-md font-medium text-gray-900 mb-3">VMs et Containers</h4>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
v-for="vm in clusterVMs[cluster.id]"
|
||||
:key="`${vm.node}-${vm.vmid}`"
|
||||
class="border rounded-lg p-4"
|
||||
:class="vm.status === 'running' ? 'border-green-200 bg-green-50' : 'border-gray-200 bg-gray-50'"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h5 class="text-sm font-medium text-gray-900">{{ vm.name }}</h5>
|
||||
<p class="text-xs text-gray-500">{{ vm.type.toUpperCase() }} #{{ vm.vmid }}</p>
|
||||
<p class="text-xs text-gray-500">Node: {{ vm.node }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class="inline-flex px-2 py-1 text-xs font-medium rounded-full"
|
||||
:class="vm.status === 'running' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
|
||||
>
|
||||
{{ vm.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex space-x-2">
|
||||
<button
|
||||
v-if="vm.status !== 'running'"
|
||||
@click="startVM(cluster.id, vm.vmid, vm.node, vm.type)"
|
||||
:disabled="loading"
|
||||
class="flex-1 inline-flex justify-center items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-white bg-green-600 hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
Démarrer
|
||||
</button>
|
||||
<button
|
||||
v-if="vm.status === 'running'"
|
||||
@click="stopVM(cluster.id, vm.vmid, vm.node, vm.type)"
|
||||
:disabled="loading"
|
||||
class="flex-1 inline-flex justify-center items-center px-2 py-1 border border-transparent text-xs font-medium rounded text-white bg-red-600 hover:bg-red-700 disabled:opacity-50"
|
||||
>
|
||||
Arrêter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="mt-4 text-center text-gray-500">
|
||||
<button
|
||||
@click="loadVMs(cluster.id)"
|
||||
:disabled="loading"
|
||||
class="text-blue-600 hover:text-blue-500 disabled:opacity-50"
|
||||
>
|
||||
Charger les VMs/Containers
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="clusters.length === 0" class="text-center text-gray-500 py-8">
|
||||
Aucun cluster Proxmox configuré
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal d'ajout de cluster -->
|
||||
<div v-if="showAddModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Ajouter un cluster Proxmox</h3>
|
||||
<form @submit.prevent="saveCluster">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Nom du cluster</label>
|
||||
<input
|
||||
v-model="clusterForm.name"
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Host</label>
|
||||
<input
|
||||
v-model="clusterForm.host"
|
||||
type="text"
|
||||
placeholder="192.168.1.100"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Nom d'utilisateur</label>
|
||||
<input
|
||||
v-model="clusterForm.username"
|
||||
type="text"
|
||||
placeholder="root@pam"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Mot de passe</label>
|
||||
<input
|
||||
v-model="clusterForm.password"
|
||||
type="password"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Port</label>
|
||||
<input
|
||||
v-model="clusterForm.port"
|
||||
type="number"
|
||||
value="8006"
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
v-model="clusterForm.verify_ssl"
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
>
|
||||
<span class="ml-2 text-sm text-gray-700">Vérifier SSL</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { proxmoxApi } from '../services/api'
|
||||
|
||||
const clusters = ref([])
|
||||
const clusterVMs = ref({})
|
||||
const loading = ref(false)
|
||||
const showAddModal = ref(false)
|
||||
|
||||
const clusterForm = reactive({
|
||||
name: '',
|
||||
host: '',
|
||||
username: '',
|
||||
password: '',
|
||||
port: 8006,
|
||||
verify_ssl: true
|
||||
})
|
||||
|
||||
const loadClusters = async () => {
|
||||
try {
|
||||
const response = await proxmoxApi.getClusters()
|
||||
clusters.value = response.data
|
||||
|
||||
// Auto-charger les VMs pour chaque cluster
|
||||
for (const cluster of response.data) {
|
||||
await loadVMs(cluster.id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des clusters:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const saveCluster = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await proxmoxApi.createCluster(clusterForm)
|
||||
await loadClusters()
|
||||
closeModal()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde:', error)
|
||||
alert('Erreur lors de la connexion au cluster Proxmox. Vérifiez les paramètres.')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteCluster = async (clusterId) => {
|
||||
if (confirm('Êtes-vous sûr de vouloir supprimer ce cluster ?')) {
|
||||
try {
|
||||
await proxmoxApi.deleteCluster(clusterId)
|
||||
delete clusterVMs.value[clusterId]
|
||||
await loadClusters()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadVMs = async (clusterId) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await proxmoxApi.getVMs(clusterId)
|
||||
clusterVMs.value[clusterId] = response.data
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des VMs:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startVM = async (clusterId, vmid, node, vmType) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await proxmoxApi.startVM(clusterId, vmid, node, vmType)
|
||||
await loadVMs(clusterId)
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du démarrage de la VM:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const stopVM = async (clusterId, vmid, node, vmType) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await proxmoxApi.stopVM(clusterId, vmid, node, vmType)
|
||||
await loadVMs(clusterId)
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'arrêt de la VM:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
showAddModal.value = false
|
||||
Object.assign(clusterForm, {
|
||||
name: '',
|
||||
host: '',
|
||||
username: '',
|
||||
password: '',
|
||||
port: 8006,
|
||||
verify_ssl: true
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadClusters()
|
||||
})
|
||||
</script>
|
||||
261
frontend/src/views/Servers.vue
Normal file
261
frontend/src/views/Servers.vue
Normal file
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<div class="px-4 py-6">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Serveurs</h1>
|
||||
<p class="mt-2 text-sm text-gray-700">
|
||||
Gérez vos serveurs et envoyez des paquets Wake-on-LAN
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<button
|
||||
@click="showAddModal = true"
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:w-auto"
|
||||
>
|
||||
Ajouter un serveur
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flow-root">
|
||||
<div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-300">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">
|
||||
Nom
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||
IP
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||
MAC
|
||||
</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||
Statut
|
||||
</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
<tr v-for="server in servers" :key="server.id">
|
||||
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
||||
{{ server.name }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
{{ server.ip_address }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
{{ server.mac_address }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<span
|
||||
class="inline-flex px-2 py-1 text-xs font-medium rounded-full"
|
||||
:class="server.is_online ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'"
|
||||
>
|
||||
{{ server.is_online ? 'En ligne' : 'Hors ligne' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
||||
<button
|
||||
@click="wakeServer(server.id)"
|
||||
:disabled="loading"
|
||||
class="text-blue-600 hover:text-blue-900 mr-4 disabled:opacity-50"
|
||||
>
|
||||
Wake
|
||||
</button>
|
||||
<button
|
||||
@click="pingServer(server.id)"
|
||||
:disabled="loading"
|
||||
class="text-green-600 hover:text-green-900 mr-4 disabled:opacity-50"
|
||||
>
|
||||
Ping
|
||||
</button>
|
||||
<button
|
||||
@click="editServer(server)"
|
||||
class="text-indigo-600 hover:text-indigo-900 mr-4"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
<button
|
||||
@click="deleteServer(server.id)"
|
||||
class="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal d'ajout/modification -->
|
||||
<div v-if="showAddModal || editingServer" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
{{ editingServer ? 'Modifier le serveur' : 'Ajouter un serveur' }}
|
||||
</h3>
|
||||
<form @submit.prevent="saveServer">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Nom</label>
|
||||
<input
|
||||
v-model="serverForm.name"
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Adresse IP</label>
|
||||
<input
|
||||
v-model="serverForm.ip_address"
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Adresse MAC</label>
|
||||
<input
|
||||
v-model="serverForm.mac_address"
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea
|
||||
v-model="serverForm.description"
|
||||
class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="flex items-center justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{{ editingServer ? 'Modifier' : 'Ajouter' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { serversApi, wolApi } from '../services/api'
|
||||
|
||||
const servers = ref([])
|
||||
const loading = ref(false)
|
||||
const showAddModal = ref(false)
|
||||
const editingServer = ref(null)
|
||||
|
||||
const serverForm = reactive({
|
||||
name: '',
|
||||
ip_address: '',
|
||||
mac_address: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const loadServers = async () => {
|
||||
try {
|
||||
const response = await serversApi.getAll()
|
||||
servers.value = response.data
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des serveurs:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const saveServer = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
if (editingServer.value) {
|
||||
await serversApi.update(editingServer.value.id, serverForm)
|
||||
} else {
|
||||
await serversApi.create(serverForm)
|
||||
}
|
||||
await loadServers()
|
||||
closeModal()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const editServer = (server) => {
|
||||
editingServer.value = server
|
||||
Object.assign(serverForm, server)
|
||||
}
|
||||
|
||||
const deleteServer = async (serverId) => {
|
||||
if (confirm('Êtes-vous sûr de vouloir supprimer ce serveur ?')) {
|
||||
try {
|
||||
await serversApi.delete(serverId)
|
||||
await loadServers()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wakeServer = async (serverId) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await wolApi.wake(serverId)
|
||||
await loadServers()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du réveil du serveur:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const pingServer = async (serverId) => {
|
||||
loading.value = true
|
||||
try {
|
||||
await wolApi.ping(serverId)
|
||||
await loadServers()
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du ping:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
showAddModal.value = false
|
||||
editingServer.value = null
|
||||
Object.assign(serverForm, {
|
||||
name: '',
|
||||
ip_address: '',
|
||||
mac_address: '',
|
||||
description: ''
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadServers()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user