init projectmycelium

This commit is contained in:
mik-tf
2025-09-01 21:37:01 -04:00
commit b41efb0e99
319 changed files with 128160 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
# Project Mycelium - Modal-Based Checkout Flow Requirements
## Project Context
We are building a checkout flow for the Project Mycelium where users can assign compute slices (resources) to either Individual VMs or Kubernetes clusters. The current implementation has issues with the modal-based assignment system.
## Required Flow Behavior
### Initial State
- **All slices added to cart start as "Individual VM"** (gray badges)
- **No clusters exist initially** - the clusters section should be empty
- Each slice shows: Provider name, size (e.g., "4x Base Unit"), specs, location, and price
### Assignment Modal Flow
When a user clicks on any slice:
1. **Modal Opens** showing:
- Slice details (provider, specs, price)
- Two assignment options:
- "Individual VM" (selected if current state, and by default)
- "Assign to Cluster"
2. **Individual VM Selection**:
- Assigns by default
- Assign or close do the same: no state change
- Slice badge updates to gray "Individual VM" if was from cluster before
3. **Cluster Assignment Selection**:
- Shows cluster role selector with:
- Cluster dropdown (initially only shows "+ Create New Cluster")
- Role selection: Master or Worker (Worker selected by default)
- Cluster name input field (only visible when "Create New Cluster" selected)
- Default cluster name: "Cluster #1", "Cluster #2", etc.
- User can enter custom cluster name
- Clusters have default name e.g. Cluster #1
4. **Assignment Execution**:
- Click "Assign Slice" button
- If creating new cluster: creates cluster display section
- Slice badge updates to show cluster name + role (e.g., "Cluster #1 Master" in red, "Cluster #1 Worker" in green)
- Modal closes automatically
- Deployment overview updates with cluster statistics
### State Persistence
When reopening a slice modal:
- Shows current assignment state (Individual or Cluster)
- If assigned to cluster: shows selected cluster and role
- Allows changing assignment or role
### Cluster Management
- **Dynamic Creation**: Clusters only appear when first slice is assigned
- **Auto-Cleanup**: Empty clusters are automatically removed
- **Naming**: Default "Cluster #1, #2..." with custom name option
- **Statistics**: Show master count, worker count, total cores per cluster
### Visual Feedback
- **Individual VM**: Gray left border, gray badge
- **Master Node**: Red left border, red badge
- **Worker Node**: Green left border, green badge
- **Cluster Display**: Blue-themed containers with editable names and statistics
## Technical Requirements
### Key Functions Needed
1. `openAssignmentModal(sliceElement)` - Opens modal with current state
2. `selectAssignment(optionElement)` - Handles assignment option selection
3. `assignSlice()` - Executes the assignment and closes modal
4. `updateSliceAssignment()` - Updates slice visual state
5. `createClusterDisplay()` - Creates cluster UI section
6. `deleteCluster()` - Removes empty clusters
7. `updateDeploymentOverview()` - Updates statistics
### Data Structure
```javascript
clusters = {
'cluster1': {
name: 'Cluster #1',
customName: 'Production', // if user renamed it
masters: ['slice1', 'slice3'],
workers: ['slice2', 'slice5']
}
}
```
### Modal Behavior
- Must close automatically after successful assignment
- Must show current state when reopened
- Must handle cluster dropdown population
- Must show/hide cluster name input based on selection
## Success Criteria
1. All slices start as Individual VMs
2. Clicking slice opens modal with correct current state
3. Assigning to new cluster creates cluster display and assigns slice
4. Modal closes automatically after assignment
5. Slice badges update correctly with cluster name and role
6. Reopening slice modal shows current assignment
7. Empty clusters are automatically removed
8. Statistics update correctly in deployment overview

View File

@@ -0,0 +1,646 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Project Mycelium - Checkout Flow</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.slice-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin: 10px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.slice-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transform: translateY(-2px);
border-color: #0d6efd;
}
.slice-card.assigned-individual {
border-left: 4px solid #6c757d;
}
.slice-card.assigned-master {
border-left: 4px solid #dc3545;
}
.slice-card.assigned-worker {
border-left: 4px solid #28a745;
}
.assignment-badge {
position: absolute;
top: 10px;
right: 10px;
font-size: 0.75rem;
padding: 4px 8px;
border-radius: 12px;
}
.badge-individual {
background: #6c757d;
color: white;
}
.badge-master {
background: #dc3545;
color: white;
}
.badge-worker {
background: #28a745;
color: white;
}
.assignment-option {
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.2s ease;
}
.assignment-option:hover {
border-color: #0d6efd;
background: #f8f9fa;
}
.assignment-option.selected {
border-color: #0d6efd;
background: #e7f3ff;
}
</style>
</head>
<body class="bg-light">
<div class="container py-4">
<h3>Project Mycelium - Checkout Flow</h3>
<!-- Compute Slices -->
<div class="row">
<div class="col-md-6">
<div class="slice-card assigned-individual" data-slice-id="1" data-assignment="individual" data-cluster="" data-role="" onclick="openAssignmentModal(this)">
<div class="assignment-badge badge-individual">Individual VM</div>
<div>
<strong>slice-1</strong> <span class="text-muted">4x Base Unit</span><br>
<small>4 cores, 16GB, 800GB</small>
</div>
<div class="text-end mt-2">
<strong>4.00 TFP</strong><br>
<small class="text-muted">per hour</small>
</div>
</div>
</div>
<div class="col-md-6">
<div class="slice-card assigned-individual" data-slice-id="2" data-assignment="individual" data-cluster="" data-role="" onclick="openAssignmentModal(this)">
<div class="assignment-badge badge-individual">Individual VM</div>
<div>
<strong>slice-2</strong> <span class="text-muted">2x Base Unit</span><br>
<small>2 cores, 8GB, 400GB</small>
</div>
<div class="text-end mt-2">
<strong>2.00 TFP</strong><br>
<small class="text-muted">per hour</small>
</div>
</div>
</div>
<div class="col-md-6">
<div class="slice-card assigned-individual" data-slice-id="3" data-assignment="individual" data-cluster="" data-role="" onclick="openAssignmentModal(this)">
<div class="assignment-badge badge-individual">Individual VM</div>
<div>
<strong>slice-3</strong> <span class="text-muted">8x Base Unit</span><br>
<small>8 cores, 32GB, 1600GB</small>
</div>
<div class="text-end mt-2">
<strong>8.00 TFP</strong><br>
<small class="text-muted">per hour</small>
</div>
</div>
</div>
<div class="col-md-6">
<div class="slice-card assigned-individual" data-slice-id="4" data-assignment="individual" data-cluster="" data-role="" onclick="openAssignmentModal(this)">
<div class="assignment-badge badge-individual">Individual VM</div>
<div>
<strong>slice-4</strong> <span class="text-muted">1x Base Unit</span><br>
<small>1 core, 4GB, 200GB</small>
</div>
<div class="text-end mt-2">
<strong>1.00 TFP</strong><br>
<small class="text-muted">per hour</small>
</div>
</div>
</div>
<div class="col-md-6">
<div class="slice-card assigned-individual" data-slice-id="5" data-assignment="individual" data-cluster="" data-role="" onclick="openAssignmentModal(this)">
<div class="assignment-badge badge-individual">Individual VM</div>
<div>
<strong>slice-5</strong> <span class="text-muted">4x Base Unit</span><br>
<small>4 cores, 16GB, 800GB</small>
</div>
<div class="text-end mt-2">
<strong>4.00 TFP</strong><br>
<small class="text-muted">per hour</small>
</div>
</div>
</div>
<div class="col-md-6">
<div class="slice-card assigned-individual" data-slice-id="6" data-assignment="individual" data-cluster="" data-role="" onclick="openAssignmentModal(this)">
<div class="assignment-badge badge-individual">Individual VM</div>
<div>
<strong>slice-6</strong> <span class="text-muted">2x Base Unit</span><br>
<small>2 cores, 8GB, 400GB</small>
</div>
<div class="text-end mt-2">
<strong>2.00 TFP</strong><br>
<small class="text-muted">per hour</small>
</div>
</div>
</div>
<div class="col-md-6">
<div class="slice-card assigned-individual" data-slice-id="7" data-assignment="individual" data-cluster="" data-role="" onclick="openAssignmentModal(this)">
<div class="assignment-badge badge-individual">Individual VM</div>
<div>
<strong>slice-7</strong> <span class="text-muted">4x Base Unit</span><br>
<small>4 cores, 16GB, 800GB</small>
</div>
<div class="text-end mt-2">
<strong>4.00 TFP</strong><br>
<small class="text-muted">per hour</small>
</div>
</div>
</div>
<div class="col-md-6">
<div class="slice-card assigned-individual" data-slice-id="8" data-assignment="individual" data-cluster="" data-role="" onclick="openAssignmentModal(this)">
<div class="assignment-badge badge-individual">Individual VM</div>
<div>
<strong>slice-8</strong> <span class="text-muted">8x Base Unit</span><br>
<small>8 cores, 32GB, 1600GB</small>
</div>
<div class="text-end mt-2">
<strong>8.00 TFP</strong><br>
<small class="text-muted">per hour</small>
</div>
</div>
</div>
</div>
<!-- Deployment Overview -->
<div class="mt-5">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-graph-up me-2"></i>Deployment Overview
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="text-center p-3 bg-light rounded">
<div class="h4 mb-1" id="totalSlices">8</div>
<small class="text-muted">Total Slices</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center p-3 bg-light rounded">
<div class="h4 mb-1" id="individualCount">8</div>
<small class="text-muted">Individual VMs</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center p-3 bg-light rounded">
<div class="h4 mb-1" id="clusterCount">0</div>
<small class="text-muted">Clusters</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center p-3 bg-primary text-white rounded">
<div class="h4 mb-1" id="totalCost">25.00 TFP</div>
<small>Total Cost/Hour</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Clusters Container -->
<div id="clustersContainer" class="mt-4"></div>
</div>
<!-- Assignment Modal -->
<div class="modal fade" id="assignmentModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Assign Slice</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<h6>Choose Assignment:</h6>
<!-- Individual VM Option -->
<div class="assignment-option" data-assignment="individual" onclick="selectAssignment(this)">
<strong>Individual VM</strong>
<div class="text-muted">Deploy as a standalone virtual machine</div>
</div>
<!-- Cluster Assignment Option -->
<div class="assignment-option" data-assignment="cluster" onclick="selectAssignment(this)">
<strong>Assign to Cluster</strong>
<div class="text-muted">Add to a Kubernetes cluster</div>
<!-- Cluster Role Selector -->
<div class="mt-3" id="clusterRoleSelector" style="display: none;">
<div class="row">
<div class="col-md-6">
<label class="form-label">Select Cluster:</label>
<select class="form-select" id="clusterSelect">
<option value="new">+ Create New Cluster</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Node Role:</label>
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="nodeRole" id="masterRole" value="master">
<label class="btn btn-outline-danger" for="masterRole">Master</label>
<input type="radio" class="btn-check" name="nodeRole" id="workerRole" value="worker" checked>
<label class="btn btn-outline-success" for="workerRole">Worker</label>
</div>
</div>
</div>
<!-- New Cluster Name -->
<div class="mt-3" id="newClusterName" style="display: block;">
<label class="form-label">Cluster Name:</label>
<input type="text" class="form-control" id="newClusterNameInput" value="Cluster #1">
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="assignSlice()">Assign Slice</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let currentSliceElement = null;
let clusters = {};
let clusterCounter = 0;
function openAssignmentModal(sliceElement) {
currentSliceElement = sliceElement;
// Reset selections
document.querySelectorAll('.assignment-option').forEach(opt => opt.classList.remove('selected'));
document.getElementById('clusterRoleSelector').style.display = 'none';
document.getElementById('newClusterName').style.display = 'none';
// Populate cluster dropdown
populateClusterDropdown();
// Set current state
const currentAssignment = sliceElement.dataset.assignment;
const currentCluster = sliceElement.dataset.cluster;
const currentRole = sliceElement.dataset.role;
if (currentAssignment === 'individual') {
document.querySelector('[data-assignment="individual"]').classList.add('selected');
} else if (currentAssignment === 'cluster') {
document.querySelector('[data-assignment="cluster"]').classList.add('selected');
document.getElementById('clusterRoleSelector').style.display = 'block';
if (currentCluster) {
document.getElementById('clusterSelect').value = currentCluster;
document.getElementById('newClusterName').style.display = 'none';
}
if (currentRole === 'master') {
document.getElementById('masterRole').checked = true;
} else {
document.getElementById('workerRole').checked = true;
}
}
new bootstrap.Modal(document.getElementById('assignmentModal')).show();
}
function selectAssignment(optionElement) {
document.querySelectorAll('.assignment-option').forEach(opt => opt.classList.remove('selected'));
optionElement.classList.add('selected');
if (optionElement.dataset.assignment === 'cluster') {
document.getElementById('clusterRoleSelector').style.display = 'block';
updateClusterNameVisibility();
} else {
document.getElementById('clusterRoleSelector').style.display = 'none';
}
}
function populateClusterDropdown() {
const select = document.getElementById('clusterSelect');
select.innerHTML = '<option value="new">+ Create New Cluster</option>';
Object.keys(clusters).forEach(clusterId => {
const option = document.createElement('option');
option.value = clusterId;
option.textContent = clusters[clusterId].name;
select.appendChild(option);
});
select.addEventListener('change', updateClusterNameVisibility);
}
function updateClusterNameVisibility() {
const select = document.getElementById('clusterSelect');
const newClusterDiv = document.getElementById('newClusterName');
if (select.value === 'new') {
newClusterDiv.style.display = 'block';
clusterCounter++;
document.getElementById('newClusterNameInput').value = `Cluster #${clusterCounter}`;
} else {
newClusterDiv.style.display = 'none';
}
}
function assignSlice() {
const selectedOption = document.querySelector('.assignment-option.selected');
if (!selectedOption) return;
const assignment = selectedOption.dataset.assignment;
if (assignment === 'individual') {
updateSliceAssignment(currentSliceElement, 'individual', '', '');
} else if (assignment === 'cluster') {
const clusterSelect = document.getElementById('clusterSelect');
const roleInputs = document.querySelectorAll('input[name="nodeRole"]');
let selectedRole = '';
roleInputs.forEach(input => {
if (input.checked) selectedRole = input.value;
});
let clusterId = clusterSelect.value;
let clusterName = '';
if (clusterId === 'new') {
clusterName = document.getElementById('newClusterNameInput').value || `Cluster #${clusterCounter}`;
clusterId = 'cluster_' + Date.now();
clusters[clusterId] = {
name: clusterName,
slices: []
};
createClusterDisplay(clusterId, clusterName);
} else {
clusterName = clusters[clusterId].name;
}
updateSliceAssignment(currentSliceElement, 'cluster', clusterId, selectedRole);
clusters[clusterId].slices.push(currentSliceElement.dataset.sliceId);
}
updateDeploymentOverview();
bootstrap.Modal.getInstance(document.getElementById('assignmentModal')).hide();
}
function updateSliceAssignment(sliceElement, assignment, clusterId, role) {
sliceElement.dataset.assignment = assignment;
sliceElement.dataset.cluster = clusterId;
sliceElement.dataset.role = role;
const badge = sliceElement.querySelector('.assignment-badge');
sliceElement.className = 'slice-card';
if (assignment === 'individual') {
sliceElement.classList.add('assigned-individual');
badge.className = 'assignment-badge badge-individual';
badge.textContent = 'Individual VM';
} else if (assignment === 'cluster') {
if (role === 'master') {
sliceElement.classList.add('assigned-master');
badge.className = 'assignment-badge badge-master';
badge.textContent = `${clusters[clusterId].name} (Master)`;
} else {
sliceElement.classList.add('assigned-worker');
badge.className = 'assignment-badge badge-worker';
badge.textContent = `${clusters[clusterId].name} (Worker)`;
}
}
}
function createClusterDisplay(clusterId, clusterName) {
const container = document.getElementById('clustersContainer');
const clusterDiv = document.createElement('div');
clusterDiv.className = 'card mt-3';
clusterDiv.id = `cluster_${clusterId}`;
clusterDiv.innerHTML = `
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">${clusterName}</h6>
<button class="btn btn-sm btn-outline-danger" onclick="deleteCluster('${clusterId}')">Delete</button>
</div>
<div class="card-body">
<small class="text-muted">Cluster created</small>
</div>
`;
container.appendChild(clusterDiv);
}
function deleteCluster(clusterId) {
// Reassign all slices in this cluster to individual
clusters[clusterId].slices.forEach(sliceId => {
const sliceElement = document.querySelector(`[data-slice-id="${sliceId}"]`);
if (sliceElement) {
updateSliceAssignment(sliceElement, 'individual', '', '');
}
});
// Remove cluster
delete clusters[clusterId];
document.getElementById(`cluster_${clusterId}`).remove();
updateDeploymentOverview();
}
function updateDeploymentOverview() {
const allSlices = document.querySelectorAll('.slice-card');
let individualCount = 0;
let clusterCount = Object.keys(clusters).length;
let totalCost = 0;
allSlices.forEach(slice => {
if (slice.dataset.assignment === 'individual') {
individualCount++;
}
// Calculate cost from slice text
const priceText = slice.querySelector('strong:last-child').textContent;
const price = parseFloat(priceText.replace(' TFP', ''));
totalCost += price;
});
document.getElementById('individualCount').textContent = individualCount;
document.getElementById('clusterCount').textContent = clusterCount;
document.getElementById('totalCost').textContent = totalCost.toFixed(2) + ' TFP';
}
function updateClusterNameVisibility() {
const select = document.getElementById('clusterSelect');
const newClusterDiv = document.getElementById('newClusterName');
if (select.value === 'new') {
newClusterDiv.style.display = 'block';
clusterCounter++;
document.getElementById('newClusterNameInput').value = `Cluster #${clusterCounter}`;
} else {
newClusterDiv.style.display = 'none';
}
}
function assignSlice() {
const selectedOption = document.querySelector('.assignment-option.selected');
if (!selectedOption) return;
const assignment = selectedOption.dataset.assignment;
if (assignment === 'individual') {
updateSliceAssignment(currentSliceElement, 'individual', '', '');
} else if (assignment === 'cluster') {
const clusterSelect = document.getElementById('clusterSelect');
const roleInputs = document.querySelectorAll('input[name="nodeRole"]');
let selectedRole = '';
roleInputs.forEach(input => {
if (input.checked) selectedRole = input.value;
});
let clusterId = clusterSelect.value;
let clusterName = '';
if (clusterId === 'new') {
clusterName = document.getElementById('newClusterNameInput').value || `Cluster #${clusterCounter}`;
clusterId = 'cluster_' + Date.now();
clusters[clusterId] = {
name: clusterName,
slices: []
};
createClusterDisplay(clusterId, clusterName);
} else {
clusterName = clusters[clusterId].name;
}
updateSliceAssignment(currentSliceElement, 'cluster', clusterId, selectedRole);
clusters[clusterId].slices.push(currentSliceElement.dataset.sliceId);
}
bootstrap.Modal.getInstance(document.getElementById('assignmentModal')).hide();
}
function updateSliceAssignment(sliceElement, assignment, clusterId, role) {
sliceElement.dataset.assignment = assignment;
sliceElement.dataset.cluster = clusterId;
sliceElement.dataset.role = role;
const badge = sliceElement.querySelector('.assignment-badge');
sliceElement.className = 'slice-card';
if (assignment === 'individual') {
sliceElement.classList.add('assigned-individual');
badge.className = 'assignment-badge badge-individual';
badge.textContent = 'Individual VM';
} else if (assignment === 'cluster') {
if (role === 'master') {
sliceElement.classList.add('assigned-master');
badge.className = 'assignment-badge badge-master';
badge.textContent = `${clusters[clusterId].name} (Master)`;
} else {
sliceElement.classList.add('assigned-worker');
badge.className = 'assignment-badge badge-worker';
badge.textContent = `${clusters[clusterId].name} (Worker)`;
}
}
}
function createClusterDisplay(clusterId, clusterName) {
const container = document.getElementById('clustersContainer');
const clusterDiv = document.createElement('div');
clusterDiv.className = 'card mt-3';
clusterDiv.id = `cluster_${clusterId}`;
clusterDiv.innerHTML = `
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">${clusterName}</h6>
<button class="btn btn-sm btn-outline-danger" onclick="deleteCluster('${clusterId}')">Delete</button>
</div>
<div class="card-body">
<small class="text-muted">Cluster created</small>
</div>
`;
container.appendChild(clusterDiv);
}
function deleteCluster(clusterId) {
// Reassign all slices in this cluster to individual
clusters[clusterId].slices.forEach(sliceId => {
const sliceElement = document.querySelector(`[data-slice-id="${sliceId}"]`);
if (sliceElement) {
updateSliceAssignment(sliceElement, 'individual', '', '');
}
});
// Remove cluster
delete clusters[clusterId];
document.getElementById(`cluster_${clusterId}`).remove();
updateDeploymentOverview();
}
function updateDeploymentOverview() {
const allSlices = document.querySelectorAll('.slice-card');
let individualCount = 0;
let clusterCount = Object.keys(clusters).length;
let totalCost = 0;
allSlices.forEach(slice => {
if (slice.dataset.assignment === 'individual') {
individualCount++;
}
// Calculate cost from slice text
const priceText = slice.querySelector('strong:last-child').textContent;
const price = parseFloat(priceText.replace(' TFP', ''));
totalCost += price;
});
document.getElementById('individualCount').textContent = individualCount;
document.getElementById('clusterCount').textContent = clusterCount;
document.getElementById('totalCost').textContent = totalCost.toFixed(2) + ' TFP';
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,380 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Modal Flow</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.slice-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin: 10px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.slice-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transform: translateY(-2px);
border-color: #0d6efd;
}
.slice-card.assigned-individual {
border-left: 4px solid #6c757d;
}
.slice-card.assigned-master {
border-left: 4px solid #dc3545;
}
.slice-card.assigned-worker {
border-left: 4px solid #28a745;
}
.assignment-badge {
position: absolute;
top: 10px;
right: 10px;
font-size: 0.75rem;
padding: 4px 8px;
border-radius: 12px;
}
.badge-individual {
background: #6c757d;
color: white;
}
.badge-master {
background: #dc3545;
color: white;
}
.badge-worker {
background: #28a745;
color: white;
}
.assignment-option {
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.2s ease;
}
.assignment-option:hover {
border-color: #0d6efd;
background: #f8f9fa;
}
.assignment-option.selected {
border-color: #0d6efd;
background: #e7f3ff;
}
</style>
</head>
<body class="bg-light">
<div class="container py-4">
<h3>Test Modal Flow</h3>
<!-- Test Slice -->
<div class="slice-card assigned-individual" data-slice-id="1" data-assignment="individual" data-cluster="" data-role="" onclick="openAssignmentModal(this)">
<div class="assignment-badge badge-individual">Individual VM</div>
<div>
<strong>test-slice</strong> <span class="text-muted">4x Base Unit</span><br>
<small>4 cores, 16GB, 800GB</small>
</div>
<div class="text-end mt-2">
<strong>4.00 TFP</strong><br>
<small class="text-muted">per hour</small>
</div>
</div>
<!-- Debug Info -->
<div class="mt-4 p-3 bg-info bg-opacity-10 border border-info rounded">
<h6>Debug Info:</h6>
<div id="debugInfo">Ready to test...</div>
</div>
<!-- Clusters Container -->
<div id="clustersContainer" class="mt-4"></div>
</div>
<!-- Assignment Modal -->
<div class="modal fade" id="assignmentModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Assign Slice</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<h6>Choose Assignment:</h6>
<!-- Individual VM Option -->
<div class="assignment-option" data-assignment="individual" onclick="selectAssignment(this)">
<strong>Individual VM</strong>
<div class="text-muted">Deploy as a standalone virtual machine</div>
</div>
<!-- Cluster Assignment Option -->
<div class="assignment-option" data-assignment="cluster" onclick="selectAssignment(this)">
<strong>Assign to Cluster</strong>
<div class="text-muted">Add to a Kubernetes cluster</div>
<!-- Cluster Role Selector -->
<div class="mt-3" id="clusterRoleSelector" style="display: none;">
<div class="row">
<div class="col-md-6">
<label class="form-label">Select Cluster:</label>
<select class="form-select" id="clusterSelect">
<option value="new">+ Create New Cluster</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Node Role:</label>
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="nodeRole" id="masterRole" value="master">
<label class="btn btn-outline-danger" for="masterRole">Master</label>
<input type="radio" class="btn-check" name="nodeRole" id="workerRole" value="worker" checked>
<label class="btn btn-outline-success" for="workerRole">Worker</label>
</div>
</div>
</div>
<!-- New Cluster Name -->
<div class="mt-3" id="newClusterName" style="display: block;">
<label class="form-label">Cluster Name:</label>
<input type="text" class="form-control" id="newClusterNameInput" value="Cluster #1">
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="assignSlice()">Assign Slice</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let currentSliceElement = null;
let clusters = {};
let clusterCounter = 0;
function debug(message) {
document.getElementById('debugInfo').innerHTML += '<br>' + message;
console.log(message);
}
function openAssignmentModal(sliceElement) {
debug('Opening modal for slice: ' + sliceElement.dataset.sliceId);
currentSliceElement = sliceElement;
// Reset selections
document.querySelectorAll('.assignment-option').forEach(opt => opt.classList.remove('selected'));
document.getElementById('clusterRoleSelector').style.display = 'none';
document.getElementById('newClusterName').style.display = 'none';
// Populate cluster dropdown
populateClusterDropdown();
// Set current state
const currentAssignment = sliceElement.dataset.assignment;
const currentCluster = sliceElement.dataset.cluster;
const currentRole = sliceElement.dataset.role;
debug('Current state - Assignment: ' + currentAssignment + ', Cluster: ' + currentCluster + ', Role: ' + currentRole);
if (currentAssignment === 'individual') {
document.querySelector('[data-assignment="individual"]').classList.add('selected');
} else if (currentAssignment === 'cluster') {
document.querySelector('[data-assignment="cluster"]').classList.add('selected');
document.getElementById('clusterRoleSelector').style.display = 'block';
// Set cluster dropdown
document.getElementById('clusterSelect').value = currentCluster;
// Set role radio button
document.querySelector(`input[name="nodeRole"][value="${currentRole}"]`).checked = true;
// Don't show new cluster name field since we're editing existing assignment
document.getElementById('newClusterName').style.display = 'none';
}
// Show modal
new bootstrap.Modal(document.getElementById('assignmentModal')).show();
}
function populateClusterDropdown() {
const select = document.getElementById('clusterSelect');
select.innerHTML = '';
// Add existing clusters
Object.keys(clusters).forEach(clusterId => {
const cluster = clusters[clusterId];
const displayName = cluster.customName || cluster.name;
const option = document.createElement('option');
option.value = clusterId;
option.textContent = displayName;
select.appendChild(option);
});
// Add "Create New Cluster" option
const newOption = document.createElement('option');
newOption.value = 'new';
newOption.textContent = '+ Create New Cluster';
select.appendChild(newOption);
}
function selectAssignment(optionElement) {
debug('Selected assignment: ' + optionElement.dataset.assignment);
// Clear previous selections
document.querySelectorAll('.assignment-option').forEach(opt => opt.classList.remove('selected'));
// Select current option
optionElement.classList.add('selected');
// Show/hide cluster options
const isCluster = optionElement.dataset.assignment === 'cluster';
document.getElementById('clusterRoleSelector').style.display = isCluster ? 'block' : 'none';
if (isCluster) {
debug('Showing cluster options');
// Check if we should show new cluster name field
const clusterSelect = document.getElementById('clusterSelect');
const newClusterDiv = document.getElementById('newClusterName');
if (clusterSelect.value === 'new') {
newClusterDiv.style.display = 'block';
// Generate default name
const nextNumber = clusterCounter + 1;
document.getElementById('newClusterNameInput').value = `Cluster #${nextNumber}`;
} else {
newClusterDiv.style.display = 'none';
}
}
}
// Add event listener for cluster select change
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('clusterSelect').addEventListener('change', function() {
const newClusterDiv = document.getElementById('newClusterName');
if (this.value === 'new') {
newClusterDiv.style.display = 'block';
// Generate default name
const nextNumber = clusterCounter + 1;
document.getElementById('newClusterNameInput').value = `Cluster #${nextNumber}`;
} else {
newClusterDiv.style.display = 'none';
}
});
});
function assignSlice() {
const selectedOption = document.querySelector('.assignment-option.selected');
if (!selectedOption) {
debug('ERROR: No assignment option selected');
alert('Please select an assignment option');
return;
}
debug('Assigning slice with option: ' + selectedOption.dataset.assignment);
if (selectedOption.dataset.assignment === 'individual') {
debug('Assigning as Individual VM');
updateSliceAssignment(currentSliceElement, 'individual', 'Individual VM', '', '');
} else if (selectedOption.dataset.assignment === 'cluster') {
const roleRadio = document.querySelector('input[name="nodeRole"]:checked');
if (!roleRadio) {
debug('ERROR: No role selected');
alert('Please select a node role (Master or Worker)');
return;
}
const role = roleRadio.value;
debug('Creating new cluster with role: ' + role);
// Create new cluster
clusterCounter++;
const clusterId = 'cluster' + clusterCounter;
const clusterName = document.getElementById('newClusterNameInput').value.trim() || `Cluster #${clusterCounter}`;
clusters[clusterId] = {
name: `Cluster #${clusterCounter}`,
customName: clusterName !== `Cluster #${clusterCounter}` ? clusterName : '',
masters: [],
workers: []
};
// Add slice to cluster
clusters[clusterId][role + 's'].push(currentSliceElement.dataset.sliceId);
debug('Created cluster: ' + clusterId + ' with name: ' + clusterName);
const displayName = clusters[clusterId].customName || clusters[clusterId].name;
const roleText = role === 'master' ? 'Master' : 'Worker';
updateSliceAssignment(currentSliceElement, role, `${displayName} ${roleText}`, clusterId, role);
createClusterDisplay(clusterId);
}
// Close modal
debug('Closing modal');
const modal = bootstrap.Modal.getInstance(document.getElementById('assignmentModal'));
if (modal) {
modal.hide();
}
}
function updateSliceAssignment(sliceElement, type, badgeText, clusterId, role) {
debug('Updating slice assignment: ' + type + ' - ' + badgeText);
// Remove old classes
sliceElement.classList.remove('assigned-individual', 'assigned-master', 'assigned-worker');
// Add new class
sliceElement.classList.add(`assigned-${type}`);
// Update badge
const badge = sliceElement.querySelector('.assignment-badge');
badge.className = `assignment-badge badge-${type}`;
badge.textContent = badgeText;
// Update data attributes
sliceElement.dataset.assignment = type === 'individual' ? 'individual' : 'cluster';
sliceElement.dataset.cluster = clusterId;
sliceElement.dataset.role = role;
}
function createClusterDisplay(clusterId) {
const cluster = clusters[clusterId];
const clustersContainer = document.getElementById('clustersContainer');
debug('Creating cluster display for: ' + clusterId);
const clusterDiv = document.createElement('div');
clusterDiv.className = 'mt-3 p-3 bg-success bg-opacity-10 border border-success rounded';
clusterDiv.id = `cluster-display-${clusterId}`;
clusterDiv.innerHTML = `
<h6>Cluster: ${cluster.customName || cluster.name}</h6>
<p>Masters: ${cluster.masters.length}, Workers: ${cluster.workers.length}</p>
`;
clustersContainer.appendChild(clusterDiv);
}
</script>
</body>
</html>