// VM Placement Optimizer - Frontend Application with Real-Time Animations let autoRefreshIntervalId = null; let demoDataId = null; let placementId = null; let loadedPlacement = null; // Animation state tracking let vmPositionCache = {}; // { vmId: serverId | null } let isFirstRender = true; let previousScore = null; let serverLookup = {}; // For quick server access during animations $(document).ready(function () { let initialized = false; function safeInitialize() { if (!initialized) { initialized = true; initializeApp(); } } $(window).on('load', safeInitialize); setTimeout(safeInitialize, 100); }); function initializeApp() { replaceQuickstartSolverForgeAutoHeaderFooter(); $("#solveButton").click(function () { solve(); }); $("#stopSolvingButton").click(function () { stopSolving(); }); $("#analyzeButton").click(function () { analyze(); }); // View toggle $('input[name="viewToggle"]').change(function() { if ($('#rackView').is(':checked')) { $('#rackViewContainer').show(); $('#cardViewContainer').hide(); } else { $('#rackViewContainer').hide(); $('#cardViewContainer').show(); } }); setupAjax(); fetchDemoData(); } function setupAjax() { $.ajaxSetup({ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json,text/plain', } }); jQuery.each(["put", "delete"], function (i, method) { jQuery[method] = function (url, data, callback, type) { if (jQuery.isFunction(data)) { type = type || callback; callback = data; data = undefined; } return jQuery.ajax({ url: url, type: method, dataType: type, data: data, success: callback }); }; }); } function fetchDemoData() { $.get("/demo-data", function (data) { data.forEach(item => { $("#testDataButton").append($('' + item + '')); $("#" + item + "TestData").click(function () { switchDataDropDownItemActive(item); placementId = null; demoDataId = item; // Reset animation state for new dataset isFirstRender = true; vmPositionCache = {}; previousScore = null; refreshPlacement(); }); }); if (data.length > 0) { demoDataId = data[0]; switchDataDropDownItemActive(demoDataId); refreshPlacement(); } }).fail(function (xhr, ajaxOptions, thrownError) { let $demo = $("#demo"); $demo.empty(); $demo.html("
No test data available
"); }); } function switchDataDropDownItemActive(newItem) { const activeCssClass = "active"; $("#testDataButton > a." + activeCssClass).removeClass(activeCssClass); $("#" + newItem + "TestData").addClass(activeCssClass); } function refreshPlacement() { let path = "/placements/" + placementId; if (placementId === null) { if (demoDataId === null) { showSimpleError("Please select a test data set."); return; } path = "/demo-data/" + demoDataId; } $.getJSON(path, function (placement) { loadedPlacement = placement; renderPlacement(placement); }).fail(function (xhr, ajaxOptions, thrownError) { showError("Getting the placement has failed.", xhr); refreshSolvingButtons(false); }); } function renderPlacement(placement) { if (!placement) { console.error('No placement data provided'); return; } const isSolving = placement.solverStatus != null && placement.solverStatus !== "NOT_SOLVING"; refreshSolvingButtons(isSolving); // Update score with animation const newScore = placement.score == null ? "?" : placement.score; const scoreEl = $("#score"); if (previousScore !== newScore) { scoreEl.text("Score: " + newScore); if (previousScore !== null && newScore !== "?") { scoreEl.addClass("updated"); setTimeout(() => scoreEl.removeClass("updated"), 300); } previousScore = newScore; } // Update summary cards with change detection updateSummaryCard("#totalServers", placement.servers ? placement.servers.length : 0); updateSummaryCard("#activeServers", placement.activeServers || 0); updateSummaryCard("#totalVms", placement.vms ? placement.vms.length : 0); updateSummaryCard("#unassignedVms", placement.unassignedVms || 0); updateSummaryCard("#cpuUtil", Math.round((placement.totalCpuUtilization || 0) * 100) + "%"); updateSummaryCard("#memUtil", Math.round((placement.totalMemoryUtilization || 0) * 100) + "%"); // Update unassigned card styling const unassignedCard = $("#unassignedCard"); const unassignedVms = placement.unassignedVms || 0; unassignedCard.removeClass("warning danger"); if (unassignedVms > 0) { unassignedCard.addClass(unassignedVms > 5 ? "danger" : "warning"); } if (!placement.servers || !placement.vms) { return; } // Create VM and server lookups const vmById = {}; placement.vms.forEach(vm => vmById[vm.id] = vm); serverLookup = {}; placement.servers.forEach(s => serverLookup[s.id] = s); if (isFirstRender) { // Full render on first load renderRackView(placement, vmById); renderCardView(placement, vmById); renderUnassignedVMs(placement.vms); vmPositionCache = buildPositionCache(placement); isFirstRender = false; return; } // Incremental animated update const newPositions = buildPositionCache(placement); const changes = diffPositions(vmPositionCache, newPositions); if (changes.length > 0) { console.log(`Detected ${changes.length} VM position changes:`, changes.slice(0, 5)); animateVmChanges(changes, placement, vmById); } // Always update utilization bars smoothly updateUtilizationBars(placement); // Update unassigned list updateUnassignedList(placement.vms, vmById); vmPositionCache = newPositions; } function updateSummaryCard(selector, newValue) { const el = $(selector); const oldValue = el.text(); if (oldValue !== String(newValue)) { el.text(newValue); el.addClass("changed"); setTimeout(() => el.removeClass("changed"), 300); } } function buildPositionCache(placement) { const cache = {}; placement.vms.forEach(vm => { cache[vm.id] = vm.server || null; }); // Debug: log sample of cache const sample = Object.entries(cache).slice(0, 3); console.log('Position cache sample:', sample); return cache; } function diffPositions(oldCache, newCache) { const changes = []; for (const vmId in newCache) { const oldServer = oldCache[vmId]; const newServer = newCache[vmId]; if (oldServer !== newServer) { changes.push({ vmId, from: oldServer, to: newServer }); } } return changes; } function animateVmChanges(changes, placement, vmById) { changes.forEach(({ vmId, from, to }) => { const vm = vmById[vmId]; if (!vm) return; // Get source and target elements let sourceEl, targetEl; let sourceChipEl = null; // The actual chip element to hide during animation if (from) { sourceEl = $(`#server-blade-${from} .vm-chips`); sourceChipEl = $(`#server-blade-${from} #vm-chip-${vmId}`); // Highlight sending server $(`#server-blade-${from}`).addClass('sending'); setTimeout(() => $(`#server-blade-${from}`).removeClass('sending'), 200); } else { // VM is coming from unassigned list sourceEl = $(`#unassigned-${vmId}`); sourceChipEl = sourceEl; } if (to) { targetEl = $(`#server-blade-${to} .vm-chips`); // Highlight receiving server $(`#server-blade-${to}`).addClass('receiving'); setTimeout(() => $(`#server-blade-${to}`).removeClass('receiving'), 300); } else { targetEl = $('#unassignedList'); } // If elements not found, just update DOM directly if (!sourceEl.length || !targetEl.length) { console.log(`Animation fallback: sourceEl=${sourceEl.length}, targetEl=${targetEl.length} for VM ${vmId}`); updateVmPosition(vmId, from, to, vm); return; } // Create flying clone const flyingChip = createVmChip(vm); flyingChip.addClass('flying-vm'); // Get positions const sourceRect = sourceEl[0].getBoundingClientRect(); const targetRect = targetEl[0].getBoundingClientRect(); // Position at source flyingChip.css({ left: sourceRect.left + 'px', top: sourceRect.top + 'px', transform: 'translate(0, 0)' }); $('body').append(flyingChip); // Hide original element during animation if (sourceChipEl && sourceChipEl.length) { sourceChipEl.css('opacity', '0'); } // Trigger fly animation (use double RAF for reliable animation start) requestAnimationFrame(() => { requestAnimationFrame(() => { flyingChip.addClass('animate'); flyingChip.css({ transform: `translate(${targetRect.left - sourceRect.left}px, ${targetRect.top - sourceRect.top}px)` }); }); }); // Clean up and update DOM after animation setTimeout(() => { flyingChip.remove(); updateVmPosition(vmId, from, to, vm); }, 220); // Slightly longer than animation duration to ensure completion }); } function updateVmPosition(vmId, from, to, vm) { // Remove from source if (from) { $(`#server-blade-${from} #vm-chip-${vmId}`).remove(); } else { $(`#unassigned-${vmId}`).remove(); } // Add to target if (to) { let vmChipsContainer = $(`#server-blade-${to} .vm-chips`); if (!vmChipsContainer.length) { // Create vm-chips container if it doesn't exist vmChipsContainer = $(''); $(`#server-blade-${to}`).append(vmChipsContainer); } const newChip = createVmChip(vm); newChip.attr('id', `vm-chip-${vm.id}`); vmChipsContainer.append(newChip); // Update server blade state (empty/not empty) $(`#server-blade-${to}`).removeClass('empty'); } // Update source server empty state if (from) { const sourceChips = $(`#server-blade-${from} .vm-chips`).children(); if (sourceChips.length === 0) { $(`#server-blade-${from}`).addClass('empty'); } } } function updateUtilizationBars(placement) { placement.servers.forEach(server => { const cpuBar = $(`#util-${server.id}-cpu`); const memBar = $(`#util-${server.id}-mem`); const stoBar = $(`#util-${server.id}-sto`); const updates = [ { bar: cpuBar, util: server.cpuUtilization || 0 }, { bar: memBar, util: server.memoryUtilization || 0 }, { bar: stoBar, util: server.storageUtilization || 0 } ]; updates.forEach(({ bar, util }) => { if (bar.length) { const newPct = Math.min(util * 100, 100); const oldPct = parseFloat(bar[0].style.width) || 0; bar.css('width', newPct + '%'); bar.removeClass('low medium high over').addClass(getUtilClass(util)); if (Math.abs(oldPct - newPct) > 1) { bar.addClass('changed'); setTimeout(() => bar.removeClass('changed'), 300); } } }); // Update server blade empty/overcommitted state const blade = $(`#server-blade-${server.id}`); const hasVms = server.vms && server.vms.length > 0; const isOvercommitted = (server.cpuUtilization || 0) > 1 || (server.memoryUtilization || 0) > 1 || (server.storageUtilization || 0) > 1; blade.toggleClass('empty', !hasVms); blade.toggleClass('overcommitted', isOvercommitted); }); } function updateUnassignedList(vms, vmById) { const container = $("#unassignedList"); const unassigned = vms.filter(vm => !vm.server); if (unassigned.length === 0) { if (!container.find('.all-assigned').length) { container.empty(); container.append(`| Rack | ${server.rack || 'Unracked'} |
| CPU Cores | ${server.cpuCores} |
| Memory | ${server.memoryGb} GB |
| Storage | ${server.storageGb} GB |
| Name | CPU | Memory | Storage | Priority |
|---|---|---|---|---|
| ${vm.name} | ${vm.cpuCores} | ${vm.memoryGb} GB | ${vm.storageGb} GB | P${vm.priority || 1} |
No VMs assigned
'}