dashboard.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. const API_BASE = window.location.origin;
  2. let selectedVM = null;
  3. // Initialize dashboard
  4. document.addEventListener('DOMContentLoaded', () => {
  5. loadVMs();
  6. loadISOsForSelect();
  7. // Create VM Form
  8. document.getElementById('createVMForm').addEventListener('submit', createVM);
  9. });
  10. // Get badge class based on VM state
  11. function getStateClass(state) {
  12. switch (state) {
  13. case 'running': return 'badge-running';
  14. case 'shut off': return 'badge-off';
  15. case 'paused': return 'badge-paused';
  16. case 'crashed': return 'badge-error';
  17. default: return 'bg-light text-dark';
  18. }
  19. }
  20. // Get icon for state
  21. function getStateIcon(state) {
  22. switch (state) {
  23. case 'running': return '<i class="bi bi-play-circle-fill"></i>';
  24. case 'shut off': return '<i class="bi bi-power"></i>';
  25. case 'paused': return '<i class="bi bi-pause-circle-fill"></i>';
  26. default: return '<i class="bi bi-question-circle"></i>';
  27. }
  28. }
  29. // Load VMs from API
  30. async function loadVMs() {
  31. try {
  32. const response = await fetch(`${API_BASE}/vms`);
  33. const data = await response.json();
  34. const vmList = document.getElementById('vmList');
  35. if (!data.vms || data.vms.length === 0) {
  36. vmList.innerHTML = '<p style="text-align: center; color: rgba(255,255,255,0.7); margin-top: 20px;">No hay máquinas virtuales</p>';
  37. return;
  38. }
  39. vmList.innerHTML = data.vms.map(vm => `
  40. <div class="vm-item ${selectedVM === vm.name ? 'active' : ''}" onclick="selectVM('${vm.name}')">
  41. <div class="vm-item-name">
  42. <div>${vm.name}</div>
  43. <small style="opacity: 0.9;">${vm.id !== -1 ? 'ID: ' + vm.id : 'Detenida'}</small>
  44. </div>
  45. <span class="vm-item-badge badge ${getStateClass(vm.state)}">${vm.state}</span>
  46. </div>
  47. `).join('');
  48. } catch (error) {
  49. console.error('Error loading VMs:', error);
  50. }
  51. }
  52. // Select a VM and show details
  53. async function selectVM(vmName) {
  54. selectedVM = vmName;
  55. loadVMs(); // Refresh to highlight selected
  56. try {
  57. const response = await fetch(`${API_BASE}/vms/${vmName}/info`);
  58. const vm = await response.json();
  59. if (vm.error) {
  60. showError(vm.error);
  61. return;
  62. }
  63. displayVMDetails(vm, vmName);
  64. } catch (error) {
  65. console.error('Error loading VM info:', error);
  66. showError('Error al cargar información de la VM');
  67. }
  68. }
  69. // Display VM details
  70. function displayVMDetails(vm, vmName) {
  71. const contentArea = document.getElementById('contentArea');
  72. const topBar = document.getElementById('currentVMName');
  73. const topBarActions = document.getElementById('topBarActions');
  74. topBar.textContent = vmName;
  75. // Build status badge
  76. const statusBadge = `<span class="badge ${getStateClass(vm.state)} p-2">${vm.state}</span>`;
  77. // Build action buttons
  78. const actionButtons = `
  79. <div>
  80. ${vm.state === 'running' ? `
  81. <button class="btn btn-sm btn-warning" onclick="stopVM('${vmName}')">
  82. <i class="bi bi-stop-circle"></i> Detener
  83. </button>
  84. <button class="btn btn-sm btn-info" onclick="openConsole('${vmName}')">
  85. <i class="bi bi-display"></i> Consola
  86. </button>
  87. ` : `
  88. <button class="btn btn-sm btn-success" onclick="startVM('${vmName}')">
  89. <i class="bi bi-play-circle"></i> Iniciar
  90. </button>
  91. `}
  92. <button class="btn btn-sm btn-danger" onclick="deleteVMConfirm('${vmName}')">
  93. <i class="bi bi-trash"></i> Eliminar
  94. </button>
  95. </div>
  96. `;
  97. topBarActions.innerHTML = actionButtons;
  98. // Build content
  99. const disksHTML = vm.disks && vm.disks.length > 0 ? vm.disks.map(disk => `
  100. <li>
  101. <strong>${disk.target}</strong> (${disk.driver}) - ${disk.source}
  102. </li>
  103. `).join('') : '<li>Sin discos</li>';
  104. const interfacesHTML = vm.interfaces && vm.interfaces.length > 0 ? vm.interfaces.map(iface => `
  105. <li>
  106. <strong>MAC:</strong> ${iface.mac}<br>
  107. <strong>Red:</strong> ${iface.network}
  108. </li>
  109. `).join('') : '<li>Sin interfaces</li>';
  110. contentArea.innerHTML = `
  111. <ul class="nav nav-tabs" role="tablist">
  112. <li class="nav-item">
  113. <a class="nav-link active" id="overview-tab" data-bs-toggle="tab" href="#overview">Resumen</a>
  114. </li>
  115. <li class="nav-item">
  116. <a class="nav-link" id="disks-tab" data-bs-toggle="tab" href="#disks">Discos</a>
  117. </li>
  118. <li class="nav-item">
  119. <a class="nav-link" id="network-tab" data-bs-toggle="tab" href="#network">Red</a>
  120. </li>
  121. </ul>
  122. <div class="tab-content">
  123. <!-- Overview Tab -->
  124. <div class="tab-pane fade show active" id="overview">
  125. <div class="vm-details">
  126. <div class="card">
  127. <div class="card-header">
  128. <h6 class="card-title mb-0"><i class="bi bi-gear"></i> Configuración</h6>
  129. </div>
  130. <div class="card-body">
  131. <div class="info-item">
  132. <div class="info-label">ID de Dominio</div>
  133. <div class="info-value">${vm.id !== -1 ? vm.id : 'N/A (Parada)'}</div>
  134. </div>
  135. <div class="info-item">
  136. <div class="info-label">Estado</div>
  137. <div class="info-value">${statusBadge}</div>
  138. </div>
  139. <div class="info-item">
  140. <div class="info-label">Procesadores</div>
  141. <div class="info-value">${vm.cpus} CPU${vm.cpus !== 1 ? 's' : ''}</div>
  142. </div>
  143. <div class="info-item">
  144. <div class="info-label">Memoria RAM</div>
  145. <div class="info-value">${vm.memory} MiB</div>
  146. </div>
  147. </div>
  148. </div>
  149. <div class="card">
  150. <div class="card-header">
  151. <h6 class="card-title mb-0"><i class="bi bi-display"></i> Display</h6>
  152. </div>
  153. <div class="card-body">
  154. <div class="info-item">
  155. <div class="info-label">Puerto VNC</div>
  156. <div class="info-value">${vm.vnc_port ? vm.vnc_port : 'No asignado'}</div>
  157. </div>
  158. <div class="info-item">
  159. <div class="info-label">Dirección</div>
  160. <div class="info-value">${vm.vnc_port ? `localhost:${vm.vnc_port}` : 'N/A'}</div>
  161. </div>
  162. </div>
  163. </div>
  164. </div>
  165. <div class="actions-group">
  166. <h5><i class="bi bi-lightning"></i> Acciones</h5>
  167. <button class="btn btn-sm btn-primary btn-action" onclick="openBootOrderModal('${vmName}')">
  168. <i class="bi bi-arrow-repeat"></i> Configurar Arranque
  169. </button>
  170. <button class="btn btn-sm btn-info btn-action" onclick="openAddDiskModal('${vmName}')" ${vm.state !== 'shut off' ? 'disabled' : ''}>
  171. <i class="bi bi-plus-circle"></i> Agregar Disco
  172. </button>
  173. </div>
  174. </div>
  175. <!-- Disks Tab -->
  176. <div class="tab-pane fade" id="disks">
  177. <div class="card">
  178. <div class="card-header">
  179. <h6 class="card-title mb-0"><i class="bi bi-hdd"></i> Discos</h6>
  180. </div>
  181. <div class="card-body">
  182. <ul class="list-unstyled">
  183. ${disksHTML}
  184. </ul>
  185. </div>
  186. </div>
  187. </div>
  188. <!-- Network Tab -->
  189. <div class="tab-pane fade" id="network">
  190. <div class="card">
  191. <div class="card-header">
  192. <h6 class="card-title mb-0"><i class="bi bi-diagram-3"></i> Interfaces de Red</h6>
  193. </div>
  194. <div class="card-body">
  195. <ul class="list-unstyled">
  196. ${interfacesHTML}
  197. </ul>
  198. </div>
  199. </div>
  200. </div>
  201. </div>
  202. `;
  203. }
  204. // Show error message
  205. function showError(message) {
  206. const contentArea = document.getElementById('contentArea');
  207. contentArea.innerHTML = `
  208. <div class="alert alert-danger" role="alert">
  209. <i class="bi bi-exclamation-circle"></i> ${message}
  210. </div>
  211. `;
  212. }
  213. // Open Create VM Modal
  214. function openCreateVMModal() {
  215. const modal = new bootstrap.Modal(document.getElementById('createVMModal'));
  216. modal.show();
  217. }
  218. // Create VM
  219. async function createVM(e) {
  220. e.preventDefault();
  221. const name = document.getElementById('name').value;
  222. const memory = document.getElementById('memory').value;
  223. const disk_size = document.getElementById('disk_size').value;
  224. const cpus = document.getElementById('cpus').value;
  225. const iso = document.getElementById('iso').value;
  226. if (!iso) {
  227. alert('Por favor selecciona una ISO');
  228. return;
  229. }
  230. try {
  231. const response = await fetch(`${API_BASE}/vms?name=${name}&memory=${memory}&disk_size=${disk_size}&cpus=${cpus}&iso=${iso}`, {
  232. method: 'POST'
  233. });
  234. let result;
  235. try {
  236. result = await response.json();
  237. } catch {
  238. result = { error: `Server error: ${response.status}` };
  239. }
  240. if (response.ok) {
  241. alert(result.message || 'VM creada correctamente');
  242. document.getElementById('createVMForm').reset();
  243. bootstrap.Modal.getInstance(document.getElementById('createVMModal')).hide();
  244. loadVMs();
  245. } else {
  246. alert(result.error || 'Error al crear VM');
  247. }
  248. } catch (error) {
  249. console.error('Error:', error);
  250. alert(`Error: ${error.message}`);
  251. }
  252. }
  253. // Load ISOs for select
  254. async function loadISOsForSelect() {
  255. try {
  256. const response = await fetch(`${API_BASE}/isos`);
  257. const data = await response.json();
  258. const select = document.getElementById('iso');
  259. select.innerHTML = '<option value="">Selecciona una ISO</option>';
  260. if (data.isos && data.isos.length > 0) {
  261. data.isos.forEach(iso => {
  262. select.innerHTML += `<option value="${iso.path}">${iso.name}</option>`;
  263. });
  264. }
  265. } catch (error) {
  266. console.error('Error loading ISOs:', error);
  267. }
  268. }
  269. // Start VM
  270. async function startVM(vmName) {
  271. try {
  272. const response = await fetch(`${API_BASE}/vms/${vmName}/start`);
  273. const result = await response.json();
  274. if (response.ok) {
  275. loadVMs();
  276. if (selectedVM === vmName) {
  277. selectVM(vmName);
  278. }
  279. } else {
  280. alert(result.error || 'Error al iniciar VM');
  281. }
  282. } catch (error) {
  283. console.error('Error:', error);
  284. alert(`Error: ${error.message}`);
  285. }
  286. }
  287. // Stop VM
  288. async function stopVM(vmName) {
  289. if (confirm(`¿Estás seguro de que quieres detener ${vmName}?`)) {
  290. try {
  291. const response = await fetch(`${API_BASE}/vms/${vmName}/stop`);
  292. const result = await response.json();
  293. if (response.ok) {
  294. loadVMs();
  295. if (selectedVM === vmName) {
  296. selectVM(vmName);
  297. }
  298. } else {
  299. alert(result.error || 'Error al detener VM');
  300. }
  301. } catch (error) {
  302. console.error('Error:', error);
  303. alert(`Error: ${error.message}`);
  304. }
  305. }
  306. }
  307. // Delete VM
  308. async function deleteVMConfirm(vmName) {
  309. if (confirm(`¿Estás seguro de que quieres eliminar ${vmName}? Esta acción no se puede deshacer.`)) {
  310. try {
  311. const response = await fetch(`${API_BASE}/vms/${vmName}`, { method: 'DELETE' });
  312. const result = await response.json();
  313. if (response.ok) {
  314. alert('VM eliminada correctamente');
  315. selectedVM = null;
  316. loadVMs();
  317. document.getElementById('contentArea').innerHTML = `
  318. <div class="empty-state">
  319. <i class="bi bi-inbox"></i>
  320. <h5>Selecciona una máquina virtual</h5>
  321. <p>Haz clic en una VM del panel lateral para ver sus detalles</p>
  322. </div>
  323. `;
  324. } else {
  325. alert(result.error || 'Error al eliminar VM');
  326. }
  327. } catch (error) {
  328. console.error('Error:', error);
  329. alert(`Error: ${error.message}`);
  330. }
  331. }
  332. }
  333. // Open Boot Order Modal
  334. function openBootOrderModal(vmName) {
  335. const html = `
  336. <div class="modal fade" id="bootOrderModal" tabindex="-1">
  337. <div class="modal-dialog">
  338. <div class="modal-content">
  339. <div class="modal-header">
  340. <h5 class="modal-title">Configurar Orden de Arranque - ${vmName}</h5>
  341. <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
  342. </div>
  343. <form id="bootOrderForm">
  344. <div class="modal-body">
  345. <div class="mb-3">
  346. <label for="bootOrderInput" class="form-label">Orden de Arranque</label>
  347. <input type="text" class="form-control" id="bootOrderInput" placeholder="hd,cdrom" required>
  348. <small class="form-text text-muted">Separados por comas: hd, cdrom, network</small>
  349. </div>
  350. </div>
  351. <div class="modal-footer">
  352. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
  353. <button type="submit" class="btn btn-primary">Guardar</button>
  354. </div>
  355. </form>
  356. </div>
  357. </div>
  358. </div>
  359. `;
  360. document.body.insertAdjacentHTML('beforeend', html);
  361. const modal = new bootstrap.Modal(document.getElementById('bootOrderModal'));
  362. document.getElementById('bootOrderForm').addEventListener('submit', async (e) => {
  363. e.preventDefault();
  364. const order = document.getElementById('bootOrderInput').value.split(',').map(s => s.trim());
  365. try {
  366. const response = await fetch(`${API_BASE}/vms/${vmName}/boot-order`, {
  367. method: 'PUT',
  368. headers: { 'Content-Type': 'application/json' },
  369. body: JSON.stringify({ order })
  370. });
  371. const result = await response.json();
  372. if (response.ok) {
  373. alert('Orden de arranque actualizado');
  374. modal.hide();
  375. document.getElementById('bootOrderModal').remove();
  376. } else {
  377. alert(result.error || 'Error al actualizar');
  378. }
  379. } catch (error) {
  380. alert(`Error: ${error.message}`);
  381. }
  382. });
  383. modal.show();
  384. }
  385. // Open Add Disk Modal
  386. function openAddDiskModal(vmName) {
  387. const html = `
  388. <div class="modal fade" id="addDiskModal" tabindex="-1">
  389. <div class="modal-dialog">
  390. <div class="modal-content">
  391. <div class="modal-header">
  392. <h5 class="modal-title">Agregar Disco - ${vmName}</h5>
  393. <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
  394. </div>
  395. <div class="alert alert-warning m-3">
  396. <small><strong>⚠️ Importante:</strong> La VM debe estar parada y el nombre del disco no debe existir</small>
  397. </div>
  398. <form id="addDiskFormModal">
  399. <div class="modal-body">
  400. <div class="mb-3">
  401. <label for="diskNameInput" class="form-label">Nombre del Disco</label>
  402. <input type="text" class="form-control" id="diskNameInput" placeholder="datos" required>
  403. </div>
  404. <div class="mb-3">
  405. <label for="diskSizeInput" class="form-label">Tamaño (GB)</label>
  406. <input type="number" class="form-control" id="diskSizeInput" value="10" min="1" required>
  407. </div>
  408. </div>
  409. <div class="modal-footer">
  410. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
  411. <button type="submit" class="btn btn-primary">Agregar</button>
  412. </div>
  413. </form>
  414. </div>
  415. </div>
  416. </div>
  417. `;
  418. document.body.insertAdjacentHTML('beforeend', html);
  419. const modal = new bootstrap.Modal(document.getElementById('addDiskModal'));
  420. document.getElementById('addDiskFormModal').addEventListener('submit', async (e) => {
  421. e.preventDefault();
  422. const disk_name = document.getElementById('diskNameInput').value;
  423. const disk_size = document.getElementById('diskSizeInput').value;
  424. try {
  425. const response = await fetch(`${API_BASE}/vms/${vmName}/add-disk?disk_name=${disk_name}&disk_size=${disk_size}`, {
  426. method: 'POST'
  427. });
  428. let result;
  429. try {
  430. result = await response.json();
  431. } catch {
  432. result = { error: 'Error del servidor' };
  433. }
  434. if (response.ok) {
  435. alert(result.message || 'Disco agregado correctamente');
  436. modal.hide();
  437. document.getElementById('addDiskModal').remove();
  438. selectVM(vmName); // Refresh
  439. } else {
  440. alert(result.error || 'Error al agregar disco');
  441. }
  442. } catch (error) {
  443. alert(`Error: ${error.message}`);
  444. }
  445. });
  446. modal.show();
  447. }
  448. // Open Console
  449. function openConsole(vmName) {
  450. fetch(`${API_BASE}/console/${vmName}`)
  451. .then(response => response.json())
  452. .then(data => {
  453. if (data.url) {
  454. window.open(data.url);
  455. } else {
  456. alert(data.error || 'Error al abrir consola');
  457. }
  458. })
  459. .catch(error => {
  460. console.error('Error:', error);
  461. alert('Error al abrir consola');
  462. });
  463. }
  464. // Open ISO Management Modal
  465. function openISOManagementModal() {
  466. const modal = new bootstrap.Modal(document.getElementById('isoManagementModal'));
  467. loadISOs();
  468. setupUploadISOForm();
  469. modal.show();
  470. }
  471. // Load ISOs for management
  472. async function loadISOs() {
  473. try {
  474. const response = await fetch(`${API_BASE}/isos`);
  475. const data = await response.json();
  476. const tbody = document.getElementById('isoTableBody');
  477. if (!data.isos || data.isos.length === 0) {
  478. tbody.innerHTML = '<tr><td colspan="2" class="text-center text-muted">No hay ISOs subidas</td></tr>';
  479. return;
  480. }
  481. tbody.innerHTML = data.isos.map(iso => `
  482. <tr>
  483. <td>
  484. <i class="bi bi-disc"></i> ${iso.name}
  485. </td>
  486. <td>
  487. <button class="btn btn-sm btn-danger" onclick="deleteISO('${iso.name}')">
  488. <i class="bi bi-trash"></i> Eliminar
  489. </button>
  490. </td>
  491. </tr>
  492. `).join('');
  493. } catch (error) {
  494. console.error('Error loading ISOs:', error);
  495. document.getElementById('isoTableBody').innerHTML = '<tr><td colspan="2" class="text-center text-danger">Error al cargar ISOs</td></tr>';
  496. }
  497. }
  498. // Setup ISO upload form
  499. function setupUploadISOForm() {
  500. const form = document.getElementById('uploadIsoForm');
  501. if (!form.dataset.listenerAdded) {
  502. form.addEventListener('submit', uploadISO);
  503. form.dataset.listenerAdded = 'true';
  504. }
  505. }
  506. // Upload ISO
  507. async function uploadISO(e) {
  508. e.preventDefault();
  509. const isoFile = document.getElementById('isoFile');
  510. const uploadBtn = document.getElementById('uploadBtn');
  511. const progressContainer = document.getElementById('progressContainer');
  512. const uploadProgress = document.getElementById('uploadProgress');
  513. if (!isoFile.files.length) {
  514. alert('Por favor selecciona un archivo');
  515. return;
  516. }
  517. const formData = new FormData();
  518. formData.append('file', isoFile.files[0]);
  519. uploadBtn.disabled = true;
  520. progressContainer.style.display = 'block';
  521. try {
  522. const xhr = new XMLHttpRequest();
  523. xhr.upload.addEventListener('progress', (e) => {
  524. if (e.lengthComputable) {
  525. const percentComplete = (e.loaded / e.total) * 100;
  526. uploadProgress.style.width = percentComplete + '%';
  527. uploadProgress.setAttribute('aria-valuenow', percentComplete);
  528. uploadProgress.textContent = Math.round(percentComplete) + '%';
  529. }
  530. });
  531. xhr.addEventListener('load', () => {
  532. if (xhr.status === 200) {
  533. const result = JSON.parse(xhr.responseText);
  534. alert(result.message || 'ISO subida correctamente');
  535. document.getElementById('uploadIsoForm').reset();
  536. progressContainer.style.display = 'none';
  537. uploadProgress.style.width = '0%';
  538. uploadProgress.textContent = '0%';
  539. loadISOs();
  540. loadISOsForSelect();
  541. } else {
  542. const result = JSON.parse(xhr.responseText);
  543. alert(result.error || 'Error al subir ISO');
  544. progressContainer.style.display = 'none';
  545. }
  546. uploadBtn.disabled = false;
  547. });
  548. xhr.addEventListener('error', () => {
  549. alert('Error al subir ISO');
  550. progressContainer.style.display = 'none';
  551. uploadBtn.disabled = false;
  552. });
  553. xhr.open('POST', `${API_BASE}/upload-iso`);
  554. xhr.send(formData);
  555. } catch (error) {
  556. console.error('Error:', error);
  557. alert(`Error: ${error.message}`);
  558. uploadBtn.disabled = false;
  559. progressContainer.style.display = 'none';
  560. }
  561. }
  562. // Delete ISO
  563. async function deleteISO(name) {
  564. if (confirm(`¿Estás seguro de que quieres eliminar ${name}?`)) {
  565. try {
  566. const response = await fetch(`${API_BASE}/isos/${name}`, { method: 'DELETE' });
  567. const result = await response.json();
  568. if (response.ok) {
  569. alert('ISO eliminada correctamente');
  570. loadISOs();
  571. loadISOsForSelect();
  572. } else {
  573. alert(result.error || 'Error al eliminar ISO');
  574. }
  575. } catch (error) {
  576. console.error('Error:', error);
  577. alert(`Error: ${error.message}`);
  578. }
  579. }
  580. }
  581. // Mobile responsive - Toggle sidebar
  582. function toggleSidebar() {
  583. const sidebar = document.querySelector('.sidebar');
  584. // Para móviles (max-width 768px), mostrar/ocultar el sidebar
  585. if (window.innerWidth <= 768) {
  586. if (sidebar.classList.contains('show')) {
  587. sidebar.classList.remove('show');
  588. sidebar.classList.add('hidden');
  589. } else {
  590. sidebar.classList.remove('hidden');
  591. sidebar.classList.add('show');
  592. }
  593. }
  594. }
  595. // Cerrar sidebar cuando se selecciona una VM en móvil
  596. const originalSelectVM = window.selectVM;
  597. window.selectVM = function(vmName) {
  598. originalSelectVM(vmName);
  599. // Cerrar sidebar en móvil después de seleccionar
  600. if (window.innerWidth <= 768) {
  601. const sidebar = document.querySelector('.sidebar');
  602. sidebar.classList.remove('show');
  603. sidebar.classList.add('hidden');
  604. }
  605. };
  606. // Cerrar sidebar cuando se abre un modal
  607. document.addEventListener('shown.bs.modal', function() {
  608. if (window.innerWidth <= 768) {
  609. const sidebar = document.querySelector('.sidebar');
  610. sidebar.classList.remove('show');
  611. sidebar.classList.add('hidden');
  612. }
  613. });