init projectmycelium
This commit is contained in:
98
html_template_tests/MODAL_FLOW_REQUIREMENTS.md
Normal file
98
html_template_tests/MODAL_FLOW_REQUIREMENTS.md
Normal 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
|
646
html_template_tests/checkout_flow_modal_v2.html
Normal file
646
html_template_tests/checkout_flow_modal_v2.html
Normal 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>
|
1219
html_template_tests/preview_resource_design.html
Normal file
1219
html_template_tests/preview_resource_design.html
Normal file
File diff suppressed because it is too large
Load Diff
380
html_template_tests/test_modal_flow.html
Normal file
380
html_template_tests/test_modal_flow.html
Normal 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>
|
Reference in New Issue
Block a user