feat: implement framework and refactor everything
This commit is contained in:
351
public/test-deploy-button.html
Normal file
351
public/test-deploy-button.html
Normal file
@@ -0,0 +1,351 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Deploy Button Test</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #1a202c;
|
||||
color: white;
|
||||
}
|
||||
.test-section {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.firmware-actions {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.target-options {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.target-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.file-input-wrapper {
|
||||
margin: 20px 0;
|
||||
}
|
||||
.deploy-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.deploy-btn:disabled {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.node-select {
|
||||
background: #2d3748;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.no-nodes-message {
|
||||
color: #fbbf24;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.25rem;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 0.25rem;
|
||||
border-radius: 4px;
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border: 1px solid rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
.cluster-members {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.member-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.status-online {
|
||||
color: #4ade80;
|
||||
}
|
||||
.status-offline {
|
||||
color: #f87171;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🚀 Deploy Button Test</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Test Scenario: Deploy Button State</h2>
|
||||
<p>This test demonstrates the deploy button behavior when:</p>
|
||||
<ul>
|
||||
<li>No file is selected</li>
|
||||
<li>No nodes are available</li>
|
||||
<li>File is selected but no target is chosen</li>
|
||||
<li>File is selected and target is chosen</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="firmware-actions">
|
||||
<h3>🚀 Firmware Update</h3>
|
||||
|
||||
<div class="target-options">
|
||||
<label class="target-option">
|
||||
<input type="radio" name="target-type" value="all" checked>
|
||||
<span>All Nodes</span>
|
||||
</label>
|
||||
<label class="target-option">
|
||||
<input type="radio" name="target-type" value="specific">
|
||||
<span>Specific Node</span>
|
||||
<select id="specific-node-select" class="node-select" style="visibility: hidden; opacity: 0;">
|
||||
<option value="">Select a node...</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="file-input-wrapper">
|
||||
<input type="file" id="global-firmware-file" accept=".bin,.hex" style="display: none;">
|
||||
<button onclick="document.getElementById('global-firmware-file').click()">
|
||||
📁 Choose File
|
||||
</button>
|
||||
<span id="file-info">No file selected</span>
|
||||
</div>
|
||||
|
||||
<button class="deploy-btn" id="deploy-btn" disabled>
|
||||
🚀 Deploy Firmware
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="cluster-members">
|
||||
<h3>Cluster Members</h3>
|
||||
<div id="cluster-members-container">
|
||||
<div class="loading">Loading cluster members...</div>
|
||||
</div>
|
||||
<button onclick="addTestNode()">Add Test Node</button>
|
||||
<button onclick="removeAllNodes()">Remove All Nodes</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Test Instructions</h2>
|
||||
<ol>
|
||||
<li>Select "Specific Node" radio button - notice the deploy button remains disabled</li>
|
||||
<li>Click "Add Test Node" to simulate cluster discovery</li>
|
||||
<li>Select "Specific Node" again - now you should see nodes in the dropdown</li>
|
||||
<li>Select a file - deploy button should remain disabled until you select a node</li>
|
||||
<li>Select a specific node - deploy button should now be enabled</li>
|
||||
<li>Click "Remove All Nodes" to test the "no nodes available" state</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Simulate the cluster members functionality
|
||||
let testNodes = [];
|
||||
|
||||
function addTestNode() {
|
||||
const nodeCount = testNodes.length + 1;
|
||||
const newNode = {
|
||||
ip: `192.168.1.${100 + nodeCount}`,
|
||||
hostname: `TestNode${nodeCount}`,
|
||||
status: 'active',
|
||||
latency: Math.floor(Math.random() * 50) + 10
|
||||
};
|
||||
testNodes.push(newNode);
|
||||
displayClusterMembers();
|
||||
populateNodeSelect();
|
||||
updateDeployButton();
|
||||
}
|
||||
|
||||
function removeAllNodes() {
|
||||
testNodes = [];
|
||||
displayClusterMembers();
|
||||
populateNodeSelect();
|
||||
updateDeployButton();
|
||||
}
|
||||
|
||||
function displayClusterMembers() {
|
||||
const container = document.getElementById('cluster-members-container');
|
||||
|
||||
if (testNodes.length === 0) {
|
||||
container.innerHTML = '<div class="loading">No cluster members found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const membersHTML = testNodes.map(node => {
|
||||
const statusClass = node.status === 'active' ? 'status-online' : 'status-offline';
|
||||
const statusText = node.status === 'active' ? 'Online' : 'Offline';
|
||||
const statusIcon = node.status === 'active' ? '🟢' : '🔴';
|
||||
|
||||
return `
|
||||
<div class="member-card" data-member-ip="${node.ip}">
|
||||
<div class="member-name">${node.hostname}</div>
|
||||
<div class="member-ip">${node.ip}</div>
|
||||
<div class="member-status ${statusClass}">
|
||||
${statusIcon} ${statusText}
|
||||
</div>
|
||||
<div class="member-latency">Latency: ${node.latency}ms</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = membersHTML;
|
||||
}
|
||||
|
||||
function populateNodeSelect() {
|
||||
const select = document.getElementById('specific-node-select');
|
||||
if (!select) return;
|
||||
|
||||
select.innerHTML = '<option value="">Select a node...</option>';
|
||||
|
||||
if (testNodes.length === 0) {
|
||||
const option = document.createElement('option');
|
||||
option.value = "";
|
||||
option.textContent = "No nodes available";
|
||||
option.disabled = true;
|
||||
select.appendChild(option);
|
||||
return;
|
||||
}
|
||||
|
||||
testNodes.forEach(node => {
|
||||
const option = document.createElement('option');
|
||||
option.value = node.ip;
|
||||
option.textContent = `${node.hostname} (${node.ip})`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function updateDeployButton() {
|
||||
const deployBtn = document.getElementById('deploy-btn');
|
||||
const fileInput = document.getElementById('global-firmware-file');
|
||||
const targetType = document.querySelector('input[name="target-type"]:checked');
|
||||
const specificNodeSelect = document.getElementById('specific-node-select');
|
||||
|
||||
if (!deployBtn || !fileInput) return;
|
||||
|
||||
const hasFile = fileInput.files && fileInput.files.length > 0;
|
||||
const hasAvailableNodes = testNodes.length > 0;
|
||||
|
||||
let isValidTarget = false;
|
||||
if (targetType.value === 'all') {
|
||||
isValidTarget = hasAvailableNodes;
|
||||
} else if (targetType.value === 'specific') {
|
||||
isValidTarget = hasAvailableNodes && specificNodeSelect.value && specificNodeSelect.value !== "";
|
||||
}
|
||||
|
||||
deployBtn.disabled = !hasFile || !isValidTarget;
|
||||
|
||||
// Update button text to provide better feedback
|
||||
if (!hasAvailableNodes) {
|
||||
deployBtn.textContent = '🚀 Deploy (No nodes available)';
|
||||
deployBtn.title = 'No cluster nodes are currently available for deployment';
|
||||
} else if (!hasFile) {
|
||||
deployBtn.textContent = '🚀 Deploy Firmware';
|
||||
deployBtn.title = 'Please select a firmware file to deploy';
|
||||
} else if (!isValidTarget) {
|
||||
deployBtn.textContent = '🚀 Deploy Firmware';
|
||||
deployBtn.title = 'Please select a valid target for deployment';
|
||||
} else {
|
||||
deployBtn.textContent = '🚀 Deploy Firmware';
|
||||
deployBtn.title = 'Ready to deploy firmware';
|
||||
}
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Setup target selection
|
||||
const targetRadios = document.querySelectorAll('input[name="target-type"]');
|
||||
const specificNodeSelect = document.getElementById('specific-node-select');
|
||||
|
||||
targetRadios.forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
if (radio.value === 'specific') {
|
||||
specificNodeSelect.style.visibility = 'visible';
|
||||
specificNodeSelect.style.opacity = '1';
|
||||
populateNodeSelect();
|
||||
|
||||
// Check if there are any nodes available and show appropriate message
|
||||
if (testNodes.length === 0) {
|
||||
// Show a message that no nodes are available
|
||||
const noNodesMsg = document.createElement('div');
|
||||
noNodesMsg.className = 'no-nodes-message';
|
||||
noNodesMsg.textContent = 'No cluster nodes are currently available';
|
||||
|
||||
// Remove any existing message
|
||||
const existingMsg = specificNodeSelect.parentNode.querySelector('.no-nodes-message');
|
||||
if (existingMsg) {
|
||||
existingMsg.remove();
|
||||
}
|
||||
|
||||
specificNodeSelect.parentNode.appendChild(noNodesMsg);
|
||||
} else {
|
||||
// Remove any existing no-nodes message
|
||||
const existingMsg = specificNodeSelect.parentNode.querySelector('.no-nodes-message');
|
||||
if (existingMsg) {
|
||||
existingMsg.remove();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
specificNodeSelect.style.visibility = 'hidden';
|
||||
specificNodeSelect.style.opacity = '0';
|
||||
|
||||
// Remove any no-nodes message when hiding
|
||||
const existingMsg = specificNodeSelect.parentNode.querySelector('.no-nodes-message');
|
||||
if (existingMsg) {
|
||||
existingMsg.remove();
|
||||
}
|
||||
}
|
||||
updateDeployButton();
|
||||
});
|
||||
});
|
||||
|
||||
// Setup specific node select change handler
|
||||
if (specificNodeSelect) {
|
||||
specificNodeSelect.addEventListener('change', updateDeployButton);
|
||||
}
|
||||
|
||||
// Setup file input change handler
|
||||
const fileInput = document.getElementById('global-firmware-file');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', (event) => {
|
||||
const file = event.target.files[0];
|
||||
const fileInfo = document.getElementById('file-info');
|
||||
|
||||
if (file) {
|
||||
fileInfo.textContent = `${file.name} (${(file.size / 1024).toFixed(1)}KB)`;
|
||||
} else {
|
||||
fileInfo.textContent = 'No file selected';
|
||||
}
|
||||
|
||||
updateDeployButton();
|
||||
});
|
||||
}
|
||||
|
||||
// Initial setup
|
||||
displayClusterMembers();
|
||||
populateNodeSelect();
|
||||
updateDeployButton();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user