vm-placement / static /index.html
blackopsrepl's picture
.
e2dcb4d verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>VM Placement - SolverForge for Python</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
<link rel="stylesheet" href="/webjars/solverforge/css/solverforge-webui.css">
<link rel="icon" href="/webjars/solverforge/img/solverforge-favicon.svg" type="image/svg+xml">
<style>
/* Solving spinner */
#solvingSpinner {
display: none;
width: 1.25rem;
height: 1.25rem;
border: 2px solid #10b981;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.75s linear infinite;
vertical-align: middle;
}
#solvingSpinner.active {
display: inline-block;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Rack visualization */
.rack-container {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
padding: 1rem;
}
.rack {
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
border-radius: 8px;
padding: 1rem;
min-width: 280px;
flex: 1;
max-width: 400px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.rack-header {
color: #10b981;
font-weight: bold;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #10b981;
display: flex;
align-items: center;
gap: 0.5rem;
}
.rack-header i {
font-size: 1rem;
}
.server-blade {
background: linear-gradient(90deg, #2d3748 0%, #1a202c 100%);
border: 1px solid #4a5568;
border-radius: 4px;
padding: 0.75rem;
margin-bottom: 0.5rem;
transition: all 0.3s ease;
cursor: pointer;
}
.server-blade:hover {
border-color: #10b981;
box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
transform: translateX(4px);
}
.server-blade.empty {
opacity: 0.5;
}
.server-blade.overcommitted {
border-color: #ef4444;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4);
}
.server-blade-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.server-name {
color: #e2e8f0;
font-weight: 600;
font-size: 0.85rem;
}
.server-specs {
color: #718096;
font-size: 0.7rem;
}
.utilization-mini {
display: flex;
gap: 0.25rem;
margin-bottom: 0.5rem;
}
.util-mini-bar {
flex: 1;
height: 6px;
background: #4a5568;
border-radius: 3px;
overflow: hidden;
position: relative;
}
.util-mini-fill {
height: 100%;
border-radius: 3px;
transition: width 0.5s ease, background 0.3s ease;
}
.util-mini-fill.low { background: linear-gradient(90deg, #10b981, #34d399); }
.util-mini-fill.medium { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
.util-mini-fill.high { background: linear-gradient(90deg, #ef4444, #f87171); }
.util-mini-fill.over { background: linear-gradient(90deg, #991b1b, #dc2626); }
.util-mini-label {
position: absolute;
right: 2px;
top: -1px;
font-size: 0.5rem;
color: #fff;
text-shadow: 0 0 2px rgba(0,0,0,0.8);
}
.vm-chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.vm-chip {
font-size: 0.65rem;
padding: 2px 6px;
border-radius: 3px;
color: white;
font-weight: 500;
transition: transform 0.2s ease;
cursor: default;
}
.vm-chip:hover {
transform: scale(1.1);
z-index: 10;
}
.vm-chip.priority-5 { background: linear-gradient(135deg, #7c3aed, #a855f7); }
.vm-chip.priority-4 { background: linear-gradient(135deg, #2563eb, #3b82f6); }
.vm-chip.priority-3 { background: linear-gradient(135deg, #059669, #10b981); }
.vm-chip.priority-2 { background: linear-gradient(135deg, #4b5563, #6b7280); }
.vm-chip.priority-1 { background: linear-gradient(135deg, #9ca3af, #d1d5db); color: #1f2937; }
.vm-chip.affinity {
box-shadow: 0 0 0 2px #f59e0b;
}
.vm-chip.anti-affinity {
box-shadow: 0 0 0 2px #ef4444;
}
/* Summary cards */
.summary-card {
background: white;
border-radius: 12px;
padding: 1.25rem;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.summary-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.summary-card .value {
font-size: 2rem;
font-weight: 700;
color: #1f2937;
line-height: 1.2;
}
.summary-card .label {
font-size: 0.8rem;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.summary-card.highlight .value {
color: #10b981;
}
.summary-card.warning .value {
color: #f59e0b;
}
.summary-card.danger .value {
color: #ef4444;
}
/* Unassigned VMs panel */
.unassigned-panel {
background: white;
border-radius: 12px;
padding: 1rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
max-height: 400px;
overflow-y: auto;
}
.unassigned-vm {
background: linear-gradient(135deg, #fef3c7, #fde68a);
border-left: 4px solid #f59e0b;
padding: 0.75rem;
margin-bottom: 0.5rem;
border-radius: 0 8px 8px 0;
transition: transform 0.2s ease;
}
.unassigned-vm:hover {
transform: translateX(4px);
}
.unassigned-vm .name {
font-weight: 600;
color: #92400e;
}
.unassigned-vm .details {
font-size: 0.75rem;
color: #78716c;
margin-top: 0.25rem;
}
.all-assigned {
background: linear-gradient(135deg, #d1fae5, #a7f3d0);
border-left: 4px solid #10b981;
padding: 1rem;
border-radius: 0 8px 8px 0;
color: #065f46;
text-align: center;
}
.all-assigned i {
font-size: 1.5rem;
margin-bottom: 0.5rem;
display: block;
}
/* Legend */
.legend {
background: #f9fafb;
border-radius: 8px;
padding: 1rem;
margin-top: 1rem;
}
.legend h6 {
font-size: 0.8rem;
color: #6b7280;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.75rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
margin-bottom: 0.25rem;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 4px;
}
/* Constraint markers */
.constraint-marker {
font-size: 0.6rem;
padding: 1px 4px;
border-radius: 2px;
margin-left: 4px;
}
.constraint-marker.affinity {
background: #fef3c7;
color: #92400e;
}
.constraint-marker.anti-affinity {
background: #fee2e2;
color: #991b1b;
}
/* View toggle */
.view-toggle {
margin-bottom: 1rem;
}
.view-toggle .btn-check:checked + .btn {
background-color: #10b981;
border-color: #10b981;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.rack {
max-width: 100%;
}
}
/* Flying VM animation */
.flying-vm {
position: fixed;
z-index: 9999;
pointer-events: none;
transition: none;
}
.flying-vm.animate {
transition: transform 200ms ease-in-out, opacity 200ms ease;
}
/* Utilization bar pulse on change */
@keyframes utilPulse {
0%, 100% { box-shadow: none; }
50% { box-shadow: 0 0 8px 2px rgba(16, 185, 129, 0.6); }
}
.util-mini-fill.changed {
animation: utilPulse 300ms ease;
}
/* Server highlight when receiving VM */
@keyframes serverReceive {
0%, 100% { box-shadow: inset 0 0 0 0 rgba(16, 185, 129, 0); }
50% { box-shadow: inset 0 0 20px 5px rgba(16, 185, 129, 0.3); }
}
.server-blade.receiving {
animation: serverReceive 300ms ease;
}
/* Server highlight when losing VM */
@keyframes serverSend {
0%, 100% { box-shadow: inset 0 0 0 0 rgba(239, 68, 68, 0); }
50% { box-shadow: inset 0 0 15px 3px rgba(239, 68, 68, 0.2); }
}
.server-blade.sending {
animation: serverSend 200ms ease;
}
/* Score update flash */
@keyframes scoreFlash {
0%, 100% { background: transparent; }
50% { background: rgba(16, 185, 129, 0.2); }
}
.score.updated {
animation: scoreFlash 300ms ease;
border-radius: 4px;
padding: 2px 8px;
}
/* Summary card value change */
@keyframes valueChange {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.summary-card .value.changed {
animation: valueChange 300ms ease;
}
/* Progress animation during solving */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.solving-pulse {
animation: pulse 1.5s ease-in-out infinite;
}
/* Advanced settings panel */
.settings-card {
background: white;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.settings-card .card-body {
padding: 1.25rem;
}
.form-range::-webkit-slider-thumb {
background: #10b981;
}
.form-range::-moz-range-thumb {
background: #10b981;
}
.config-value {
font-weight: 600;
color: #10b981;
}
.preset-description {
font-size: 0.85rem;
color: #64748b;
}
</style>
</head>
<body>
<header id="solverforge-auto-header">
<!-- Filled in by app.js -->
</header>
<div class="tab-content">
<div id="demo" class="tab-pane fade show active container-fluid">
<div class="sticky-top d-flex justify-content-center align-items-center" aria-live="polite" aria-atomic="true">
<div id="notificationPanel" style="position: absolute; top: .5rem;"></div>
</div>
<h1>VM Placement Optimizer</h1>
<p>Optimize virtual machine placement across servers to maximize resource utilization while respecting constraints.</p>
<div class="mb-4">
<button id="solveButton" type="button" class="btn btn-success">
<i class="fas fa-play"></i> Solve
</button>
<button id="stopSolvingButton" type="button" class="btn btn-danger" style="display: none;">
<i class="fas fa-stop"></i> Stop solving
</button>
<span id="solvingSpinner" class="ms-2"></span>
<span id="score" class="score ms-2 align-middle fw-bold">Score: ?</span>
<button id="analyzeButton" type="button" class="ms-2 btn btn-secondary">
<i class="fas fa-question"></i>
</button>
<div class="float-end">
<button class="btn btn-outline-secondary btn-sm me-2" type="button"
data-bs-toggle="collapse" data-bs-target="#advancedSettings"
aria-expanded="false" aria-controls="advancedSettings">
<i class="fas fa-cog"></i> Advanced
</button>
<div class="btn-group view-toggle me-2" role="group" aria-label="View toggle">
<input type="radio" class="btn-check" name="viewToggle" id="rackView" autocomplete="off" checked>
<label class="btn btn-outline-secondary btn-sm" for="rackView">
<i class="fas fa-server"></i> Rack View
</label>
<input type="radio" class="btn-check" name="viewToggle" id="cardView" autocomplete="off">
<label class="btn btn-outline-secondary btn-sm" for="cardView">
<i class="fas fa-th-large"></i> Card View
</label>
</div>
</div>
</div>
<!-- Advanced Settings Panel -->
<div class="collapse mb-4" id="advancedSettings">
<div class="settings-card">
<div class="card-body">
<div class="row g-4">
<!-- Infrastructure Settings -->
<div class="col-md-6">
<h6 class="text-muted mb-3"><i class="fas fa-server me-2"></i>Infrastructure</h6>
<div class="row g-3">
<div class="col-6">
<label class="form-label">
Racks: <span id="rackCountValue" class="config-value">3</span>
</label>
<input type="range" class="form-range" id="rackCountSlider"
min="1" max="8" step="1" value="3">
<div class="d-flex justify-content-between text-muted" style="font-size: 0.7rem;">
<span>1</span>
<span>8</span>
</div>
</div>
<div class="col-6">
<label class="form-label">
Servers/Rack: <span id="serversPerRackValue" class="config-value">4</span>
</label>
<input type="range" class="form-range" id="serversPerRackSlider"
min="2" max="10" step="1" value="4">
<div class="d-flex justify-content-between text-muted" style="font-size: 0.7rem;">
<span>2</span>
<span>10</span>
</div>
</div>
</div>
</div>
<!-- Workload Settings -->
<div class="col-md-6">
<h6 class="text-muted mb-3"><i class="fas fa-cubes me-2"></i>Workload</h6>
<div class="row g-3">
<div class="col-6">
<label class="form-label">
VMs: <span id="vmCountValue" class="config-value">20</span>
</label>
<input type="range" class="form-range" id="vmCountSlider"
min="5" max="200" step="5" value="20">
<div class="d-flex justify-content-between text-muted" style="font-size: 0.7rem;">
<span>5</span>
<span>200</span>
</div>
</div>
<div class="col-6">
<label class="form-label">
Solver Time: <span id="solverTimeValue" class="config-value">30s</span>
</label>
<input type="range" class="form-range" id="solverTimeSlider"
min="5" max="120" step="5" value="30">
<div class="d-flex justify-content-between text-muted" style="font-size: 0.7rem;">
<span>5s</span>
<span>2min</span>
</div>
</div>
</div>
</div>
</div>
<!-- Action buttons -->
<div class="mt-3 d-flex justify-content-between align-items-center">
<div class="preset-description">
<i class="fas fa-info-circle me-1"></i>
<span id="configSummary">12 servers across 3 racks, 20 VMs to place</span>
</div>
<button id="generateDataBtn" class="btn btn-primary btn-sm">
<i class="fas fa-sync-alt me-1"></i> Generate New Data
</button>
</div>
</div>
</div>
</div>
<!-- Summary Cards -->
<div class="row mb-4 g-3" id="summaryCards">
<div class="col-6 col-md-4 col-lg-2">
<div class="summary-card">
<div class="value" id="totalServers">-</div>
<div class="label">Total Servers</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg-2">
<div class="summary-card highlight">
<div class="value" id="activeServers">-</div>
<div class="label">Active Servers</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg-2">
<div class="summary-card">
<div class="value" id="totalVms">-</div>
<div class="label">Total VMs</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg-2">
<div class="summary-card" id="unassignedCard">
<div class="value" id="unassignedVms">-</div>
<div class="label">Unassigned</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg-2">
<div class="summary-card">
<div class="value" id="cpuUtil">-</div>
<div class="label">CPU Util</div>
</div>
</div>
<div class="col-6 col-md-4 col-lg-2">
<div class="summary-card">
<div class="value" id="memUtil">-</div>
<div class="label">Memory Util</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="row">
<!-- Rack/Card View -->
<div class="col-lg-9">
<div id="rackViewContainer" class="rack-container">
<p class="text-muted">Select a dataset to see server rack visualization</p>
</div>
<div id="cardViewContainer" class="row g-3" style="display: none;">
<p class="text-muted">Select a dataset to see server cards</p>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-3">
<div class="unassigned-panel mb-3">
<h5 class="mb-3"><i class="fas fa-exclamation-triangle text-warning me-2"></i>Unassigned VMs</h5>
<div id="unassignedList">
<p class="text-muted small">No data loaded</p>
</div>
</div>
<div class="legend">
<h6><i class="fas fa-palette me-1"></i>Priority Legend</h6>
<div class="legend-item">
<div class="legend-color" style="background: linear-gradient(135deg, #7c3aed, #a855f7);"></div>
<span>Critical (5)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: linear-gradient(135deg, #2563eb, #3b82f6);"></div>
<span>High (4)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: linear-gradient(135deg, #059669, #10b981);"></div>
<span>Medium (3)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: linear-gradient(135deg, #4b5563, #6b7280);"></div>
<span>Low (2)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: linear-gradient(135deg, #9ca3af, #d1d5db);"></div>
<span>Lowest (1)</span>
</div>
<hr class="my-2">
<h6><i class="fas fa-link me-1"></i>Constraints</h6>
<div class="legend-item">
<div class="legend-color" style="background: #fef3c7; box-shadow: 0 0 0 2px #f59e0b;"></div>
<span>Affinity Group</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #fee2e2; box-shadow: 0 0 0 2px #ef4444;"></div>
<span>Anti-Affinity Group</span>
</div>
</div>
</div>
</div>
</div>
<div id="rest" class="tab-pane fade container-fluid">
<h1>REST API Guide</h1>
<h2>VM Placement solver integration via cURL</h2>
<h3>1. Download demo data</h3>
<pre>
<button class="btn btn-outline-dark btn-sm float-end" onclick="copyTextToClipboard('curl1')">Copy</button>
<code id="curl1">curl -X GET -H 'Accept:application/json' http://localhost:8080/demo-data/SMALL -o sample.json</code>
</pre>
<h3>2. Post the sample data for solving</h3>
<p>The POST operation returns a <code>jobId</code> that should be used in subsequent commands.</p>
<pre>
<button class="btn btn-outline-dark btn-sm float-end" onclick="copyTextToClipboard('curl2')">Copy</button>
<code id="curl2">curl -X POST -H 'Content-Type:application/json' http://localhost:8080/placements -d@sample.json</code>
</pre>
<h3>3. Get the current status and score</h3>
<pre>
<button class="btn btn-outline-dark btn-sm float-end" onclick="copyTextToClipboard('curl3')">Copy</button>
<code id="curl3">curl -X GET -H 'Accept:application/json' http://localhost:8080/placements/{jobId}/status</code>
</pre>
<h3>4. Get the complete solution</h3>
<pre>
<button class="btn btn-outline-dark btn-sm float-end" onclick="copyTextToClipboard('curl4')">Copy</button>
<code id="curl4">curl -X GET -H 'Accept:application/json' http://localhost:8080/placements/{jobId}</code>
</pre>
<h3>5. Terminate solving early</h3>
<pre>
<button class="btn btn-outline-dark btn-sm float-end" onclick="copyTextToClipboard('curl5')">Copy</button>
<code id="curl5">curl -X DELETE -H 'Accept:application/json' http://localhost:8080/placements/{jobId}</code>
</pre>
</div>
<div id="openapi" class="tab-pane fade container-fluid">
<h1>REST API Reference</h1>
<div class="ratio ratio-1x1">
<iframe src="/q/swagger-ui" style="overflow:hidden;" scrolling="no"></iframe>
</div>
</div>
</div>
<!-- Score Analysis Modal -->
<div class="modal fade" id="scoreAnalysisModal" tabindex="-1" aria-labelledby="scoreAnalysisModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="scoreAnalysisModalLabel">Score Analysis <span id="scoreAnalysisScoreLabel"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="scoreAnalysisModalContent">
<!-- Filled in by app.js -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<!-- Server Detail Modal -->
<div class="modal fade" id="serverDetailModal" tabindex="-1" aria-labelledby="serverDetailModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="serverDetailModalLabel"><i class="fas fa-server me-2"></i>Server Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="serverDetailModalContent">
<!-- Filled in by app.js -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<footer id="solverforge-auto-footer"></footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.8/umd/popper.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.min.js"></script>
<script src="/webjars/solverforge/js/solverforge-webui.js"></script>
<script src="/app.js"></script>
<script src="/config.js"></script>
</body>
</html>