|
|
@@ -0,0 +1,517 @@
|
|
|
+const API_BASE = window.location.origin;
|
|
|
+let selectedVM = null;
|
|
|
+
|
|
|
+// Initialize dashboard
|
|
|
+document.addEventListener('DOMContentLoaded', () => {
|
|
|
+ loadVMs();
|
|
|
+ loadISOsForSelect();
|
|
|
+
|
|
|
+ // Create VM Form
|
|
|
+ document.getElementById('createVMForm').addEventListener('submit', createVM);
|
|
|
+});
|
|
|
+
|
|
|
+// Get badge class based on VM state
|
|
|
+function getStateClass(state) {
|
|
|
+ switch (state) {
|
|
|
+ case 'running': return 'badge-running';
|
|
|
+ case 'shut off': return 'badge-off';
|
|
|
+ case 'paused': return 'badge-paused';
|
|
|
+ case 'crashed': return 'badge-error';
|
|
|
+ default: return 'bg-light text-dark';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Get icon for state
|
|
|
+function getStateIcon(state) {
|
|
|
+ switch (state) {
|
|
|
+ case 'running': return '<i class="bi bi-play-circle-fill"></i>';
|
|
|
+ case 'shut off': return '<i class="bi bi-power"></i>';
|
|
|
+ case 'paused': return '<i class="bi bi-pause-circle-fill"></i>';
|
|
|
+ default: return '<i class="bi bi-question-circle"></i>';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Load VMs from API
|
|
|
+async function loadVMs() {
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE}/vms`);
|
|
|
+ const data = await response.json();
|
|
|
+ const vmList = document.getElementById('vmList');
|
|
|
+
|
|
|
+ if (!data.vms || data.vms.length === 0) {
|
|
|
+ vmList.innerHTML = '<p style="text-align: center; color: rgba(255,255,255,0.7); margin-top: 20px;">No hay máquinas virtuales</p>';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ vmList.innerHTML = data.vms.map(vm => `
|
|
|
+ <div class="vm-item ${selectedVM === vm.name ? 'active' : ''}" onclick="selectVM('${vm.name}')">
|
|
|
+ <div class="vm-item-name">
|
|
|
+ <div>${vm.name}</div>
|
|
|
+ <small style="opacity: 0.9;">${vm.id !== -1 ? 'ID: ' + vm.id : 'Detenida'}</small>
|
|
|
+ </div>
|
|
|
+ <span class="vm-item-badge badge ${getStateClass(vm.state)}">${vm.state}</span>
|
|
|
+ </div>
|
|
|
+ `).join('');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error loading VMs:', error);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Select a VM and show details
|
|
|
+async function selectVM(vmName) {
|
|
|
+ selectedVM = vmName;
|
|
|
+ loadVMs(); // Refresh to highlight selected
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE}/vms/${vmName}/info`);
|
|
|
+ const vm = await response.json();
|
|
|
+
|
|
|
+ if (vm.error) {
|
|
|
+ showError(vm.error);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ displayVMDetails(vm, vmName);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error loading VM info:', error);
|
|
|
+ showError('Error al cargar información de la VM');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Display VM details
|
|
|
+function displayVMDetails(vm, vmName) {
|
|
|
+ const contentArea = document.getElementById('contentArea');
|
|
|
+ const topBar = document.getElementById('currentVMName');
|
|
|
+ const topBarActions = document.getElementById('topBarActions');
|
|
|
+
|
|
|
+ topBar.textContent = vmName;
|
|
|
+
|
|
|
+ // Build status badge
|
|
|
+ const statusBadge = `<span class="badge ${getStateClass(vm.state)} p-2">${vm.state}</span>`;
|
|
|
+
|
|
|
+ // Build action buttons
|
|
|
+ const actionButtons = `
|
|
|
+ <div>
|
|
|
+ ${vm.state === 'running' ? `
|
|
|
+ <button class="btn btn-sm btn-warning" onclick="stopVM('${vmName}')">
|
|
|
+ <i class="bi bi-stop-circle"></i> Detener
|
|
|
+ </button>
|
|
|
+ <button class="btn btn-sm btn-info" onclick="openConsole('${vmName}')">
|
|
|
+ <i class="bi bi-display"></i> Consola
|
|
|
+ </button>
|
|
|
+ ` : `
|
|
|
+ <button class="btn btn-sm btn-success" onclick="startVM('${vmName}')">
|
|
|
+ <i class="bi bi-play-circle"></i> Iniciar
|
|
|
+ </button>
|
|
|
+ `}
|
|
|
+ <button class="btn btn-sm btn-danger" onclick="deleteVMConfirm('${vmName}')">
|
|
|
+ <i class="bi bi-trash"></i> Eliminar
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ topBarActions.innerHTML = actionButtons;
|
|
|
+
|
|
|
+ // Build content
|
|
|
+ const disksHTML = vm.disks && vm.disks.length > 0 ? vm.disks.map(disk => `
|
|
|
+ <li>
|
|
|
+ <strong>${disk.target}</strong> (${disk.driver}) - ${disk.source}
|
|
|
+ </li>
|
|
|
+ `).join('') : '<li>Sin discos</li>';
|
|
|
+
|
|
|
+ const interfacesHTML = vm.interfaces && vm.interfaces.length > 0 ? vm.interfaces.map(iface => `
|
|
|
+ <li>
|
|
|
+ <strong>MAC:</strong> ${iface.mac}<br>
|
|
|
+ <strong>Red:</strong> ${iface.network}
|
|
|
+ </li>
|
|
|
+ `).join('') : '<li>Sin interfaces</li>';
|
|
|
+
|
|
|
+ contentArea.innerHTML = `
|
|
|
+ <ul class="nav nav-tabs" role="tablist">
|
|
|
+ <li class="nav-item">
|
|
|
+ <a class="nav-link active" id="overview-tab" data-bs-toggle="tab" href="#overview">Resumen</a>
|
|
|
+ </li>
|
|
|
+ <li class="nav-item">
|
|
|
+ <a class="nav-link" id="disks-tab" data-bs-toggle="tab" href="#disks">Discos</a>
|
|
|
+ </li>
|
|
|
+ <li class="nav-item">
|
|
|
+ <a class="nav-link" id="network-tab" data-bs-toggle="tab" href="#network">Red</a>
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+
|
|
|
+ <div class="tab-content">
|
|
|
+ <!-- Overview Tab -->
|
|
|
+ <div class="tab-pane fade show active" id="overview">
|
|
|
+ <div class="vm-details">
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-header">
|
|
|
+ <h6 class="card-title mb-0"><i class="bi bi-gear"></i> Configuración</h6>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <div class="info-item">
|
|
|
+ <div class="info-label">ID de Dominio</div>
|
|
|
+ <div class="info-value">${vm.id !== -1 ? vm.id : 'N/A (Parada)'}</div>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <div class="info-label">Estado</div>
|
|
|
+ <div class="info-value">${statusBadge}</div>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <div class="info-label">Procesadores</div>
|
|
|
+ <div class="info-value">${vm.cpus} CPU${vm.cpus !== 1 ? 's' : ''}</div>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <div class="info-label">Memoria RAM</div>
|
|
|
+ <div class="info-value">${vm.memory} MiB</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-header">
|
|
|
+ <h6 class="card-title mb-0"><i class="bi bi-display"></i> Display</h6>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <div class="info-item">
|
|
|
+ <div class="info-label">Puerto VNC</div>
|
|
|
+ <div class="info-value">${vm.vnc_port ? vm.vnc_port : 'No asignado'}</div>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <div class="info-label">Dirección</div>
|
|
|
+ <div class="info-value">${vm.vnc_port ? `localhost:${vm.vnc_port}` : 'N/A'}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="actions-group">
|
|
|
+ <h5><i class="bi bi-lightning"></i> Acciones</h5>
|
|
|
+ <button class="btn btn-sm btn-primary btn-action" onclick="openBootOrderModal('${vmName}')">
|
|
|
+ <i class="bi bi-arrow-repeat"></i> Configurar Arranque
|
|
|
+ </button>
|
|
|
+ <button class="btn btn-sm btn-info btn-action" onclick="openAddDiskModal('${vmName}')" ${vm.state !== 'shut off' ? 'disabled' : ''}>
|
|
|
+ <i class="bi bi-plus-circle"></i> Agregar Disco
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Disks Tab -->
|
|
|
+ <div class="tab-pane fade" id="disks">
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-header">
|
|
|
+ <h6 class="card-title mb-0"><i class="bi bi-hdd"></i> Discos</h6>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <ul class="list-unstyled">
|
|
|
+ ${disksHTML}
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Network Tab -->
|
|
|
+ <div class="tab-pane fade" id="network">
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-header">
|
|
|
+ <h6 class="card-title mb-0"><i class="bi bi-diagram-3"></i> Interfaces de Red</h6>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <ul class="list-unstyled">
|
|
|
+ ${interfacesHTML}
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+}
|
|
|
+
|
|
|
+// Show error message
|
|
|
+function showError(message) {
|
|
|
+ const contentArea = document.getElementById('contentArea');
|
|
|
+ contentArea.innerHTML = `
|
|
|
+ <div class="alert alert-danger" role="alert">
|
|
|
+ <i class="bi bi-exclamation-circle"></i> ${message}
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+}
|
|
|
+
|
|
|
+// Open Create VM Modal
|
|
|
+function openCreateVMModal() {
|
|
|
+ const modal = new bootstrap.Modal(document.getElementById('createVMModal'));
|
|
|
+ modal.show();
|
|
|
+}
|
|
|
+
|
|
|
+// Create VM
|
|
|
+async function createVM(e) {
|
|
|
+ e.preventDefault();
|
|
|
+
|
|
|
+ const name = document.getElementById('name').value;
|
|
|
+ const memory = document.getElementById('memory').value;
|
|
|
+ const disk_size = document.getElementById('disk_size').value;
|
|
|
+ const cpus = document.getElementById('cpus').value;
|
|
|
+ const iso = document.getElementById('iso').value;
|
|
|
+
|
|
|
+ if (!iso) {
|
|
|
+ alert('Por favor selecciona una ISO');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE}/vms?name=${name}&memory=${memory}&disk_size=${disk_size}&cpus=${cpus}&iso=${iso}`, {
|
|
|
+ method: 'POST'
|
|
|
+ });
|
|
|
+
|
|
|
+ let result;
|
|
|
+ try {
|
|
|
+ result = await response.json();
|
|
|
+ } catch {
|
|
|
+ result = { error: `Server error: ${response.status}` };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ alert(result.message || 'VM creada correctamente');
|
|
|
+ document.getElementById('createVMForm').reset();
|
|
|
+ bootstrap.Modal.getInstance(document.getElementById('createVMModal')).hide();
|
|
|
+ loadVMs();
|
|
|
+ } else {
|
|
|
+ alert(result.error || 'Error al crear VM');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error:', error);
|
|
|
+ alert(`Error: ${error.message}`);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Load ISOs for select
|
|
|
+async function loadISOsForSelect() {
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE}/isos`);
|
|
|
+ const data = await response.json();
|
|
|
+ const select = document.getElementById('iso');
|
|
|
+
|
|
|
+ select.innerHTML = '<option value="">Selecciona una ISO</option>';
|
|
|
+ if (data.isos && data.isos.length > 0) {
|
|
|
+ data.isos.forEach(iso => {
|
|
|
+ select.innerHTML += `<option value="${iso.path}">${iso.name}</option>`;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error loading ISOs:', error);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Start VM
|
|
|
+async function startVM(vmName) {
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE}/vms/${vmName}/start`);
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ loadVMs();
|
|
|
+ if (selectedVM === vmName) {
|
|
|
+ selectVM(vmName);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ alert(result.error || 'Error al iniciar VM');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error:', error);
|
|
|
+ alert(`Error: ${error.message}`);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Stop VM
|
|
|
+async function stopVM(vmName) {
|
|
|
+ if (confirm(`¿Estás seguro de que quieres detener ${vmName}?`)) {
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE}/vms/${vmName}/stop`);
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ loadVMs();
|
|
|
+ if (selectedVM === vmName) {
|
|
|
+ selectVM(vmName);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ alert(result.error || 'Error al detener VM');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error:', error);
|
|
|
+ alert(`Error: ${error.message}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Delete VM
|
|
|
+async function deleteVMConfirm(vmName) {
|
|
|
+ if (confirm(`¿Estás seguro de que quieres eliminar ${vmName}? Esta acción no se puede deshacer.`)) {
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE}/vms/${vmName}`, { method: 'DELETE' });
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ alert('VM eliminada correctamente');
|
|
|
+ selectedVM = null;
|
|
|
+ loadVMs();
|
|
|
+ document.getElementById('contentArea').innerHTML = `
|
|
|
+ <div class="empty-state">
|
|
|
+ <i class="bi bi-inbox"></i>
|
|
|
+ <h5>Selecciona una máquina virtual</h5>
|
|
|
+ <p>Haz clic en una VM del panel lateral para ver sus detalles</p>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ } else {
|
|
|
+ alert(result.error || 'Error al eliminar VM');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error:', error);
|
|
|
+ alert(`Error: ${error.message}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Open Boot Order Modal
|
|
|
+function openBootOrderModal(vmName) {
|
|
|
+ const html = `
|
|
|
+ <div class="modal fade" id="bootOrderModal" tabindex="-1">
|
|
|
+ <div class="modal-dialog">
|
|
|
+ <div class="modal-content">
|
|
|
+ <div class="modal-header">
|
|
|
+ <h5 class="modal-title">Configurar Orden de Arranque - ${vmName}</h5>
|
|
|
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
|
+ </div>
|
|
|
+ <form id="bootOrderForm">
|
|
|
+ <div class="modal-body">
|
|
|
+ <div class="mb-3">
|
|
|
+ <label for="bootOrderInput" class="form-label">Orden de Arranque</label>
|
|
|
+ <input type="text" class="form-control" id="bootOrderInput" placeholder="hd,cdrom" required>
|
|
|
+ <small class="form-text text-muted">Separados por comas: hd, cdrom, network</small>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="modal-footer">
|
|
|
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
|
|
+ <button type="submit" class="btn btn-primary">Guardar</button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ document.body.insertAdjacentHTML('beforeend', html);
|
|
|
+ const modal = new bootstrap.Modal(document.getElementById('bootOrderModal'));
|
|
|
+
|
|
|
+ document.getElementById('bootOrderForm').addEventListener('submit', async (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ const order = document.getElementById('bootOrderInput').value.split(',').map(s => s.trim());
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE}/vms/${vmName}/boot-order`, {
|
|
|
+ method: 'PUT',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ order })
|
|
|
+ });
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+ if (response.ok) {
|
|
|
+ alert('Orden de arranque actualizado');
|
|
|
+ modal.hide();
|
|
|
+ document.getElementById('bootOrderModal').remove();
|
|
|
+ } else {
|
|
|
+ alert(result.error || 'Error al actualizar');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ alert(`Error: ${error.message}`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ modal.show();
|
|
|
+}
|
|
|
+
|
|
|
+// Open Add Disk Modal
|
|
|
+function openAddDiskModal(vmName) {
|
|
|
+ const html = `
|
|
|
+ <div class="modal fade" id="addDiskModal" tabindex="-1">
|
|
|
+ <div class="modal-dialog">
|
|
|
+ <div class="modal-content">
|
|
|
+ <div class="modal-header">
|
|
|
+ <h5 class="modal-title">Agregar Disco - ${vmName}</h5>
|
|
|
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
|
+ </div>
|
|
|
+ <div class="alert alert-warning m-3">
|
|
|
+ <small><strong>⚠️ Importante:</strong> La VM debe estar parada y el nombre del disco no debe existir</small>
|
|
|
+ </div>
|
|
|
+ <form id="addDiskFormModal">
|
|
|
+ <div class="modal-body">
|
|
|
+ <div class="mb-3">
|
|
|
+ <label for="diskNameInput" class="form-label">Nombre del Disco</label>
|
|
|
+ <input type="text" class="form-control" id="diskNameInput" placeholder="datos" required>
|
|
|
+ </div>
|
|
|
+ <div class="mb-3">
|
|
|
+ <label for="diskSizeInput" class="form-label">Tamaño (GB)</label>
|
|
|
+ <input type="number" class="form-control" id="diskSizeInput" value="10" min="1" required>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="modal-footer">
|
|
|
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
|
|
+ <button type="submit" class="btn btn-primary">Agregar</button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ document.body.insertAdjacentHTML('beforeend', html);
|
|
|
+ const modal = new bootstrap.Modal(document.getElementById('addDiskModal'));
|
|
|
+
|
|
|
+ document.getElementById('addDiskFormModal').addEventListener('submit', async (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ const disk_name = document.getElementById('diskNameInput').value;
|
|
|
+ const disk_size = document.getElementById('diskSizeInput').value;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch(`${API_BASE}/vms/${vmName}/add-disk?disk_name=${disk_name}&disk_size=${disk_size}`, {
|
|
|
+ method: 'POST'
|
|
|
+ });
|
|
|
+
|
|
|
+ let result;
|
|
|
+ try {
|
|
|
+ result = await response.json();
|
|
|
+ } catch {
|
|
|
+ result = { error: 'Error del servidor' };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (response.ok) {
|
|
|
+ alert(result.message || 'Disco agregado correctamente');
|
|
|
+ modal.hide();
|
|
|
+ document.getElementById('addDiskModal').remove();
|
|
|
+ selectVM(vmName); // Refresh
|
|
|
+ } else {
|
|
|
+ alert(result.error || 'Error al agregar disco');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ alert(`Error: ${error.message}`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ modal.show();
|
|
|
+}
|
|
|
+
|
|
|
+// Open Console
|
|
|
+function openConsole(vmName) {
|
|
|
+ fetch(`${API_BASE}/console/${vmName}`)
|
|
|
+ .then(response => response.json())
|
|
|
+ .then(data => {
|
|
|
+ if (data.url) {
|
|
|
+ window.open(data.url);
|
|
|
+ } else {
|
|
|
+ alert(data.error || 'Error al abrir consola');
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ console.error('Error:', error);
|
|
|
+ alert('Error al abrir consola');
|
|
|
+ });
|
|
|
+}
|