fix: topology container height
This commit is contained in:
13
index.js
13
index.js
@@ -3,6 +3,7 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const dgram = require('dgram');
|
||||
const SporeApiClient = require('./src/client');
|
||||
const cors = require('cors');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
@@ -20,6 +21,13 @@ app.use(fileUpload({
|
||||
debug: false
|
||||
}));
|
||||
|
||||
// Add CORS middleware
|
||||
app.use(cors({
|
||||
origin: '*', // Or specify your phone's IP range like: ['http://192.168.1.0/24']
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
}));
|
||||
|
||||
// UDP discovery configuration
|
||||
const UDP_PORT = 4210;
|
||||
const DISCOVERY_MESSAGE = 'CLUSTER_DISCOVERY';
|
||||
@@ -700,8 +708,9 @@ app.get('/api/health', (req, res) => {
|
||||
|
||||
|
||||
// Start the server
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`Server is running on http://localhost:${PORT}`);
|
||||
const server = app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Server is running on http://0.0.0.0:${PORT}`);
|
||||
console.log(`Accessible from: http://YOUR_COMPUTER_IP:${PORT}`);
|
||||
console.log(`UDP discovery server listening on port ${UDP_PORT}`);
|
||||
console.log('Waiting for CLUSTER_DISCOVERY messages from SPORE nodes...');
|
||||
});
|
||||
|
||||
23
package-lock.json
generated
23
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"express-fileupload": "^1.4.3"
|
||||
}
|
||||
@@ -134,6 +135,19 @@
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
@@ -529,6 +543,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"express-fileupload": "^1.4.3"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,20 @@
|
||||
|
||||
class ApiClient {
|
||||
constructor() {
|
||||
this.baseUrl = (typeof window !== 'undefined' && window.API_BASE_URL) || 'http://localhost:3001'; // Backend server URL
|
||||
// Auto-detect server URL based on current location
|
||||
const currentHost = window.location.hostname;
|
||||
const currentPort = window.location.port;
|
||||
|
||||
// If accessing from localhost, use localhost:3001
|
||||
// If accessing from another device, use the same hostname but port 3001
|
||||
if (currentHost === 'localhost' || currentHost === '127.0.0.1') {
|
||||
this.baseUrl = 'http://localhost:3001';
|
||||
} else {
|
||||
// Use the same hostname but port 3001
|
||||
this.baseUrl = `http://${currentHost}:3001`;
|
||||
}
|
||||
|
||||
console.log('API Client initialized with base URL:', this.baseUrl);
|
||||
}
|
||||
|
||||
async request(path, { method = 'GET', headers = {}, body = undefined, query = undefined, isForm = false } = {}) {
|
||||
|
||||
@@ -2277,11 +2277,44 @@ class TopologyGraphComponent extends Component {
|
||||
this.svg = null;
|
||||
this.simulation = null;
|
||||
this.zoom = null;
|
||||
this.width = 1400; // Increased from 1000 for more space
|
||||
this.height = 1000; // Increased from 800 for more space
|
||||
this.width = 0; // Will be set dynamically based on container size
|
||||
this.height = 0; // Will be set dynamically based on container size
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
updateDimensions(container) {
|
||||
// Get the container's actual dimensions
|
||||
const rect = container.getBoundingClientRect();
|
||||
this.width = rect.width || 1400; // Fallback to 1400 if width is 0
|
||||
this.height = rect.height || 1000; // Fallback to 1000 if height is 0
|
||||
|
||||
// Ensure minimum dimensions
|
||||
this.width = Math.max(this.width, 800);
|
||||
this.height = Math.max(this.height, 600);
|
||||
|
||||
console.log('TopologyGraphComponent: Updated dimensions to', this.width, 'x', this.height);
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
// Debounce resize events to avoid excessive updates
|
||||
if (this.resizeTimeout) {
|
||||
clearTimeout(this.resizeTimeout);
|
||||
}
|
||||
|
||||
this.resizeTimeout = setTimeout(() => {
|
||||
const container = this.findElement('#topology-graph-container');
|
||||
if (container && this.svg) {
|
||||
this.updateDimensions(container);
|
||||
// Update SVG viewBox and force center
|
||||
this.svg.attr('viewBox', `0 0 ${this.width} ${this.height}`);
|
||||
if (this.simulation) {
|
||||
this.simulation.force('center', d3.forceCenter(this.width / 2, this.height / 2));
|
||||
this.simulation.alpha(0.3).restart();
|
||||
}
|
||||
}
|
||||
}, 250); // 250ms debounce
|
||||
}
|
||||
|
||||
// Override mount to ensure proper initialization
|
||||
mount() {
|
||||
if (this.isMounted) return;
|
||||
@@ -2325,6 +2358,10 @@ class TopologyGraphComponent extends Component {
|
||||
console.log('TopologyGraphComponent: Container:', this.container);
|
||||
console.log('TopologyGraphComponent: Container ID:', this.container?.id);
|
||||
|
||||
// Add resize listener to update dimensions when window is resized
|
||||
this.resizeHandler = this.handleResize.bind(this);
|
||||
window.addEventListener('resize', this.resizeHandler);
|
||||
|
||||
// Refresh button removed from HTML, so no need to set up event listeners
|
||||
console.log('TopologyGraphComponent: No event listeners needed (refresh button removed)');
|
||||
}
|
||||
@@ -2389,14 +2426,17 @@ class TopologyGraphComponent extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate dynamic dimensions based on container size
|
||||
this.updateDimensions(container);
|
||||
|
||||
// Clear existing content
|
||||
container.innerHTML = '';
|
||||
|
||||
// Create SVG element
|
||||
this.svg = d3.select(container)
|
||||
.append('svg')
|
||||
.attr('width', this.width)
|
||||
.attr('height', this.height)
|
||||
.attr('width', '100%')
|
||||
.attr('height', '100%')
|
||||
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
|
||||
.style('border', '1px solid rgba(255, 255, 255, 0.1)')
|
||||
.style('background', 'rgba(0, 0, 0, 0.2)')
|
||||
@@ -2887,6 +2927,22 @@ class TopologyGraphComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
unmount() {
|
||||
// Clean up resize listener
|
||||
if (this.resizeHandler) {
|
||||
window.removeEventListener('resize', this.resizeHandler);
|
||||
this.resizeHandler = null;
|
||||
}
|
||||
|
||||
// Clear resize timeout
|
||||
if (this.resizeTimeout) {
|
||||
clearTimeout(this.resizeTimeout);
|
||||
this.resizeTimeout = null;
|
||||
}
|
||||
|
||||
// Call parent unmount
|
||||
super.unmount();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Debug Cluster Load</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.debug-panel { background: #f0f0f0; padding: 15px; margin: 10px 0; border-radius: 5px; }
|
||||
.debug-button { padding: 8px 16px; margin: 5px; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; }
|
||||
.debug-button:hover { background: #0056b3; }
|
||||
.log { background: #000; color: #0f0; padding: 10px; margin: 10px 0; border-radius: 3px; font-family: monospace; max-height: 300px; overflow-y: auto; }
|
||||
.cluster-container { border: 1px solid #ccc; padding: 15px; margin: 10px 0; border-radius: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🔍 Debug Cluster Load</h1>
|
||||
|
||||
<div class="debug-panel">
|
||||
<h3>Debug Controls</h3>
|
||||
<button class="debug-button" onclick="testContainerFind()">🔍 Test Container Find</button>
|
||||
<button class="debug-button" onclick="testViewModel()">📊 Test ViewModel</button>
|
||||
<button class="debug-button" onclick="testComponent()">🧩 Test Component</button>
|
||||
<button class="debug-button" onclick="testAPICall()">📡 Test API Call</button>
|
||||
<button class="debug-button" onclick="clearLog()">🧹 Clear Log</button>
|
||||
</div>
|
||||
|
||||
<div class="debug-panel">
|
||||
<h3>Container Elements</h3>
|
||||
<div id="cluster-view" class="cluster-container">
|
||||
<div class="primary-node-info">
|
||||
<h4>Primary Node</h4>
|
||||
<div id="primary-node-ip">🔍 Discovering...</div>
|
||||
<button class="primary-node-refresh">🔄 Refresh</button>
|
||||
</div>
|
||||
|
||||
<div id="cluster-members-container">
|
||||
<h4>Cluster Members</h4>
|
||||
<div class="loading">Loading cluster members...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="debug-panel">
|
||||
<h3>Debug Log</h3>
|
||||
<div id="debug-log" class="log"></div>
|
||||
</div>
|
||||
|
||||
<!-- Include SPORE UI framework and components -->
|
||||
<script src="framework.js"></script>
|
||||
<script src="view-models.js"></script>
|
||||
<script src="components.js"></script>
|
||||
<script src="api-client.js"></script>
|
||||
|
||||
<script>
|
||||
let debugLog = [];
|
||||
|
||||
function log(message, type = 'info') {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = `[${timestamp}] ${message}`;
|
||||
|
||||
const logContainer = document.getElementById('debug-log');
|
||||
logContainer.innerHTML += logEntry + '\n';
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
|
||||
debugLog.push({ timestamp, message, type });
|
||||
console.log(logEntry);
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('debug-log').innerHTML = '';
|
||||
debugLog = [];
|
||||
}
|
||||
|
||||
// Test container finding
|
||||
function testContainerFind() {
|
||||
log('🔍 Testing container finding...');
|
||||
|
||||
const clusterView = document.getElementById('cluster-view');
|
||||
const primaryNodeInfo = document.querySelector('.primary-node-info');
|
||||
const clusterMembersContainer = document.getElementById('cluster-members-container');
|
||||
|
||||
log(`Cluster view found: ${!!clusterView} (ID: ${clusterView?.id})`);
|
||||
log(`Primary node info found: ${!!primaryNodeInfo}`);
|
||||
log(`Cluster members container found: ${!!clusterMembersContainer} (ID: ${clusterMembersContainer?.id})`);
|
||||
log(`Cluster members container innerHTML: ${clusterMembersContainer?.innerHTML?.substring(0, 100)}...`);
|
||||
}
|
||||
|
||||
// Test view model
|
||||
function testViewModel() {
|
||||
log('📊 Testing ViewModel...');
|
||||
|
||||
try {
|
||||
const viewModel = new ClusterViewModel();
|
||||
log('✅ ClusterViewModel created successfully');
|
||||
|
||||
log(`Initial members: ${viewModel.get('members')?.length || 0}`);
|
||||
log(`Initial loading: ${viewModel.get('isLoading')}`);
|
||||
log(`Initial error: ${viewModel.get('error')}`);
|
||||
|
||||
return viewModel;
|
||||
} catch (error) {
|
||||
log(`❌ ViewModel creation failed: ${error.message}`, 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Test component
|
||||
function testComponent() {
|
||||
log('🧩 Testing Component...');
|
||||
|
||||
try {
|
||||
const viewModel = new ClusterViewModel();
|
||||
const eventBus = new EventBus();
|
||||
const container = document.getElementById('cluster-members-container');
|
||||
|
||||
log('✅ Dependencies created, creating ClusterMembersComponent...');
|
||||
|
||||
const component = new ClusterMembersComponent(container, viewModel, eventBus);
|
||||
log('✅ ClusterMembersComponent created successfully');
|
||||
|
||||
log('Mounting component...');
|
||||
component.mount();
|
||||
log('✅ Component mounted');
|
||||
|
||||
return { component, viewModel, eventBus };
|
||||
} catch (error) {
|
||||
log(`❌ Component test failed: ${error.message}`, 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Test API call
|
||||
async function testAPICall() {
|
||||
log('📡 Testing API call...');
|
||||
|
||||
try {
|
||||
if (!window.apiClient) {
|
||||
log('❌ API client not available');
|
||||
return;
|
||||
}
|
||||
|
||||
log('Calling getClusterMembers...');
|
||||
const response = await window.apiClient.getClusterMembers();
|
||||
log(`✅ API call successful: ${response.members?.length || 0} members`);
|
||||
|
||||
if (response.members && response.members.length > 0) {
|
||||
response.members.forEach(member => {
|
||||
log(`📱 Member: ${member.hostname || member.ip} (${member.status})`);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ API call failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize debug interface
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
log('🚀 Debug interface initialized');
|
||||
log('💡 Use the debug controls above to test different aspects of the cluster loading');
|
||||
});
|
||||
|
||||
// Mock API client if not available
|
||||
if (!window.apiClient) {
|
||||
log('⚠️ Creating mock API client for testing');
|
||||
window.apiClient = {
|
||||
getClusterMembers: async () => {
|
||||
log('📡 Mock API: getClusterMembers called');
|
||||
return {
|
||||
members: [
|
||||
{ ip: '192.168.1.100', hostname: 'Node-1', status: 'active', latency: 15 },
|
||||
{ ip: '192.168.1.101', hostname: 'Node-2', status: 'active', latency: 22 },
|
||||
{ ip: '192.168.1.102', hostname: 'Node-3', status: 'offline', latency: null }
|
||||
]
|
||||
};
|
||||
},
|
||||
getDiscoveryInfo: async () => {
|
||||
log('📡 Mock API: getDiscoveryInfo called');
|
||||
return {
|
||||
primaryNode: '192.168.1.100',
|
||||
clientInitialized: true,
|
||||
totalNodes: 3
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,208 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Debug Cluster</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
|
||||
.debug-section { margin: 20px 0; padding: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.status { padding: 10px; margin: 10px 0; border-radius: 4px; }
|
||||
.status.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||||
.status.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||||
.status.info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
|
||||
button { padding: 10px 20px; margin: 5px; border: none; border-radius: 4px; background: #007bff; color: white; cursor: pointer; }
|
||||
button:hover { background: #0056b3; }
|
||||
.log { background: #f8f9fa; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px; max-height: 300px; overflow-y: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🐛 Debug Cluster Functionality</h1>
|
||||
|
||||
<div class="debug-section">
|
||||
<h3>1. API Client Test</h3>
|
||||
<button onclick="testApiClient()">Test API Client</button>
|
||||
<div id="api-client-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<h3>2. View Model Test</h3>
|
||||
<button onclick="testViewModel()">Test View Model</button>
|
||||
<div id="viewmodel-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<h3>3. Component Test</h3>
|
||||
<button onclick="testComponents()">Test Components</button>
|
||||
<div id="component-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<h3>4. Console Log</h3>
|
||||
<div id="console-log" class="log"></div>
|
||||
<button onclick="clearLog()">Clear Log</button>
|
||||
</div>
|
||||
|
||||
<script src="framework.js"></script>
|
||||
<script src="api-client.js"></script>
|
||||
<script src="view-models.js"></script>
|
||||
<script src="components.js"></script>
|
||||
|
||||
<script>
|
||||
// Capture console logs
|
||||
const originalLog = console.log;
|
||||
const originalError = console.error;
|
||||
const originalWarn = console.warn;
|
||||
|
||||
function addToLog(level, ...args) {
|
||||
const logDiv = document.getElementById('console-log');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)).join(' ');
|
||||
logDiv.innerHTML += `[${timestamp}] ${level}: ${message}\n`;
|
||||
logDiv.scrollTop = logDiv.scrollHeight;
|
||||
}
|
||||
|
||||
console.log = function(...args) {
|
||||
originalLog.apply(console, args);
|
||||
addToLog('LOG', ...args);
|
||||
};
|
||||
|
||||
console.error = function(...args) {
|
||||
originalError.apply(console, args);
|
||||
addToLog('ERROR', ...args);
|
||||
};
|
||||
|
||||
console.warn = function(...args) {
|
||||
originalWarn.apply(console, args);
|
||||
addToLog('WARN', ...args);
|
||||
};
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('console-log').innerHTML = '';
|
||||
}
|
||||
|
||||
async function testApiClient() {
|
||||
const resultDiv = document.getElementById('api-client-result');
|
||||
resultDiv.innerHTML = '<div class="status info">Testing API Client...</div>';
|
||||
|
||||
try {
|
||||
console.log('Testing API Client...');
|
||||
|
||||
// Test discovery info
|
||||
const discoveryInfo = await window.apiClient.getDiscoveryInfo();
|
||||
console.log('Discovery Info:', discoveryInfo);
|
||||
|
||||
// Test cluster members
|
||||
const clusterMembers = await window.apiClient.getClusterMembers();
|
||||
console.log('Cluster Members:', clusterMembers);
|
||||
|
||||
resultDiv.innerHTML = `
|
||||
<div class="status success">
|
||||
<strong>API Client Test Passed!</strong><br>
|
||||
Discovery: ${discoveryInfo.totalNodes} nodes, Primary: ${discoveryInfo.primaryNode || 'None'}<br>
|
||||
Members: ${clusterMembers.members ? clusterMembers.members.length : 0} members
|
||||
</div>
|
||||
`;
|
||||
|
||||
} catch (error) {
|
||||
console.error('API Client Test Failed:', error);
|
||||
resultDiv.innerHTML = `<div class="status error">API Client Test Failed: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function testViewModel() {
|
||||
const resultDiv = document.getElementById('viewmodel-result');
|
||||
resultDiv.innerHTML = '<div class="status info">Testing View Model...</div>';
|
||||
|
||||
try {
|
||||
console.log('Testing View Model...');
|
||||
|
||||
const clusterViewModel = new ClusterViewModel();
|
||||
console.log('ClusterViewModel created:', clusterViewModel);
|
||||
|
||||
// Wait for initial data
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
const totalNodes = clusterViewModel.get('totalNodes');
|
||||
const primaryNode = clusterViewModel.get('primaryNode');
|
||||
const clientInitialized = clusterViewModel.get('clientInitialized');
|
||||
|
||||
console.log('ViewModel data:', { totalNodes, primaryNode, clientInitialized });
|
||||
|
||||
resultDiv.innerHTML = `
|
||||
<div class="status success">
|
||||
<strong>View Model Test Passed!</strong><br>
|
||||
Total Nodes: ${totalNodes}<br>
|
||||
Primary Node: ${primaryNode || 'None'}<br>
|
||||
Client Initialized: ${clientInitialized}
|
||||
</div>
|
||||
`;
|
||||
|
||||
} catch (error) {
|
||||
console.error('View Model Test Failed:', error);
|
||||
resultDiv.innerHTML = `<div class="status error">View Model Test Failed: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function testComponents() {
|
||||
const resultDiv = document.getElementById('component-result');
|
||||
resultDiv.innerHTML = '<div class="status info">Testing Components...</div>';
|
||||
|
||||
try {
|
||||
console.log('Testing Components...');
|
||||
|
||||
const eventBus = new EventBus();
|
||||
const clusterViewModel = new ClusterViewModel();
|
||||
|
||||
// Test cluster status component
|
||||
const statusContainer = document.createElement('div');
|
||||
statusContainer.className = 'cluster-status';
|
||||
statusContainer.innerHTML = '🚀 Cluster Online';
|
||||
document.body.appendChild(statusContainer);
|
||||
|
||||
const clusterStatusComponent = new ClusterStatusComponent(
|
||||
statusContainer,
|
||||
clusterViewModel,
|
||||
eventBus
|
||||
);
|
||||
|
||||
clusterStatusComponent.initialize();
|
||||
console.log('Cluster Status Component initialized');
|
||||
|
||||
// Wait for data
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
const statusText = statusContainer.innerHTML;
|
||||
const statusClasses = Array.from(statusContainer.classList);
|
||||
|
||||
console.log('Status Component Result:', { statusText, statusClasses });
|
||||
|
||||
resultDiv.innerHTML = `
|
||||
<div class="status success">
|
||||
<strong>Component Test Passed!</strong><br>
|
||||
Status Text: ${statusText}<br>
|
||||
Status Classes: ${statusClasses.join(', ')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(statusContainer);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Component Test Failed:', error);
|
||||
resultDiv.innerHTML = `<div class="status error">Component Test Failed: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-run tests on page load
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
console.log('Page loaded, starting auto-tests...');
|
||||
testApiClient();
|
||||
setTimeout(testViewModel, 1000);
|
||||
setTimeout(testComponents, 2000);
|
||||
}, 500);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,124 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Debug Framework</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.debug-section { margin: 20px 0; padding: 20px; border: 1px solid #ccc; }
|
||||
.log { background: #f5f5f5; padding: 10px; margin: 10px 0; font-family: monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Framework Debug</h1>
|
||||
|
||||
<div class="debug-section">
|
||||
<h2>Console Log</h2>
|
||||
<div id="console-log" class="log"></div>
|
||||
<button onclick="clearLog()">Clear Log</button>
|
||||
</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<h2>Test Cluster View</h2>
|
||||
<div id="cluster-view">
|
||||
<div class="cluster-section">
|
||||
<div class="cluster-header">
|
||||
<div class="cluster-header-left">
|
||||
<div class="primary-node-info">
|
||||
<span class="primary-node-label">Primary Node:</span>
|
||||
<span class="primary-node-ip" id="primary-node-ip">Discovering...</span>
|
||||
<button class="primary-node-refresh" title="🎲 Select random primary node">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
||||
<path d="M1 4v6h6M23 20v-6h-6"/>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="refresh-btn">Refresh</button>
|
||||
</div>
|
||||
<div id="cluster-members-container">
|
||||
<div class="loading">Loading cluster members...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="testClusterView()">Test Cluster View</button>
|
||||
</div>
|
||||
|
||||
<script src="framework.js"></script>
|
||||
<script src="api-client.js"></script>
|
||||
<script src="view-models.js"></script>
|
||||
<script src="components.js"></script>
|
||||
|
||||
<script>
|
||||
// Override console.log to capture output
|
||||
const originalLog = console.log;
|
||||
const originalError = console.error;
|
||||
const logElement = document.getElementById('console-log');
|
||||
|
||||
function addToLog(message, type = 'log') {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.style.color = type === 'error' ? 'red' : 'black';
|
||||
logEntry.textContent = `[${timestamp}] ${message}`;
|
||||
logElement.appendChild(logEntry);
|
||||
logElement.scrollTop = logElement.scrollHeight;
|
||||
}
|
||||
|
||||
console.log = function(...args) {
|
||||
originalLog.apply(console, args);
|
||||
addToLog(args.join(' '));
|
||||
};
|
||||
|
||||
console.error = function(...args) {
|
||||
originalError.apply(console, args);
|
||||
addToLog(args.join(' '), 'error');
|
||||
};
|
||||
|
||||
function clearLog() {
|
||||
logElement.innerHTML = '';
|
||||
}
|
||||
|
||||
// Test cluster view
|
||||
function testClusterView() {
|
||||
try {
|
||||
console.log('Testing cluster view...');
|
||||
|
||||
// Create view model
|
||||
const clusterVM = new ClusterViewModel();
|
||||
console.log('ClusterViewModel created:', clusterVM);
|
||||
|
||||
// Create component
|
||||
const container = document.getElementById('cluster-view');
|
||||
const clusterComponent = new ClusterViewComponent(container, clusterVM, null);
|
||||
console.log('ClusterViewComponent created:', clusterComponent);
|
||||
|
||||
// Mount component
|
||||
clusterComponent.mount();
|
||||
console.log('Component mounted');
|
||||
|
||||
// Test data loading
|
||||
console.log('Testing data loading...');
|
||||
clusterVM.updateClusterMembers();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error testing cluster view:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize framework
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('DOM loaded, initializing framework...');
|
||||
|
||||
if (window.app) {
|
||||
console.log('Framework app found:', window.app);
|
||||
window.app.init();
|
||||
console.log('Framework initialized');
|
||||
} else {
|
||||
console.error('Framework app not found');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,174 +0,0 @@
|
||||
// Demo data for testing the Members view without actual cluster data
|
||||
window.demoMembersData = {
|
||||
members: [
|
||||
{
|
||||
hostname: "spore-node-1",
|
||||
ip: "192.168.1.100",
|
||||
lastSeen: Date.now(),
|
||||
latency: 3,
|
||||
status: "ACTIVE",
|
||||
resources: {
|
||||
freeHeap: 48748,
|
||||
chipId: 12345678,
|
||||
sdkVersion: "3.1.2",
|
||||
cpuFreqMHz: 80,
|
||||
flashChipSize: 1048576
|
||||
},
|
||||
api: [
|
||||
{ uri: "/api/node/status", method: "GET" },
|
||||
{ uri: "/api/tasks/status", method: "GET" }
|
||||
],
|
||||
labels: {
|
||||
environment: "production",
|
||||
region: "us-west",
|
||||
role: "worker",
|
||||
cluster: "spore-main"
|
||||
}
|
||||
},
|
||||
{
|
||||
hostname: "spore-node-2",
|
||||
ip: "192.168.1.101",
|
||||
lastSeen: Date.now() - 5000,
|
||||
latency: 8,
|
||||
status: "ACTIVE",
|
||||
resources: {
|
||||
freeHeap: 52340,
|
||||
chipId: 87654321,
|
||||
sdkVersion: "3.1.2",
|
||||
cpuFreqMHz: 80,
|
||||
flashChipSize: 1048576
|
||||
},
|
||||
api: [
|
||||
{ uri: "/api/node/status", method: "GET" },
|
||||
{ uri: "/api/tasks/status", method: "GET" }
|
||||
],
|
||||
labels: {
|
||||
environment: "production",
|
||||
region: "us-west",
|
||||
role: "controller",
|
||||
cluster: "spore-main"
|
||||
}
|
||||
},
|
||||
{
|
||||
hostname: "spore-node-3",
|
||||
ip: "192.168.1.102",
|
||||
lastSeen: Date.now() - 15000,
|
||||
latency: 12,
|
||||
status: "INACTIVE",
|
||||
resources: {
|
||||
freeHeap: 38920,
|
||||
chipId: 11223344,
|
||||
sdkVersion: "3.1.1",
|
||||
cpuFreqMHz: 80,
|
||||
flashChipSize: 1048576
|
||||
},
|
||||
api: [
|
||||
{ uri: "/api/node/status", method: "GET" }
|
||||
],
|
||||
labels: {
|
||||
environment: "staging",
|
||||
region: "us-west",
|
||||
role: "worker",
|
||||
cluster: "spore-main"
|
||||
}
|
||||
},
|
||||
{
|
||||
hostname: "spore-node-4",
|
||||
ip: "192.168.1.103",
|
||||
lastSeen: Date.now() - 30000,
|
||||
latency: 25,
|
||||
status: "ACTIVE",
|
||||
resources: {
|
||||
freeHeap: 45678,
|
||||
chipId: 55667788,
|
||||
sdkVersion: "3.1.2",
|
||||
cpuFreqMHz: 80,
|
||||
flashChipSize: 1048576
|
||||
},
|
||||
api: [
|
||||
{ uri: "/api/node/status", method: "GET" },
|
||||
{ uri: "/api/tasks/status", method: "GET" },
|
||||
{ uri: "/api/capabilities", method: "GET" }
|
||||
],
|
||||
labels: {
|
||||
environment: "production",
|
||||
region: "us-east",
|
||||
role: "gateway",
|
||||
cluster: "spore-main"
|
||||
}
|
||||
},
|
||||
{
|
||||
hostname: "spore-node-5",
|
||||
ip: "192.168.1.104",
|
||||
lastSeen: Date.now() - 60000,
|
||||
latency: 45,
|
||||
status: "DEAD",
|
||||
resources: {
|
||||
freeHeap: 0,
|
||||
chipId: 99887766,
|
||||
sdkVersion: "3.1.0",
|
||||
cpuFreqMHz: 0,
|
||||
flashChipSize: 1048576
|
||||
},
|
||||
api: [],
|
||||
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Mock API client for demo purposes
|
||||
window.demoApiClient = {
|
||||
async getClusterMembers() {
|
||||
// Simulate network delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return window.demoMembersData;
|
||||
},
|
||||
|
||||
async getClusterMembersFromNode(ip) {
|
||||
// Simulate network delay
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
// Return a subset of members to simulate different node perspectives
|
||||
const allMembers = window.demoMembersData.members;
|
||||
const nodeIndex = allMembers.findIndex(m => m.ip === ip);
|
||||
|
||||
if (nodeIndex === -1) {
|
||||
return { members: [] };
|
||||
}
|
||||
|
||||
// Simulate each node seeing a different subset of the cluster
|
||||
const startIndex = (nodeIndex * 2) % allMembers.length;
|
||||
const members = [
|
||||
allMembers[startIndex],
|
||||
allMembers[(startIndex + 1) % allMembers.length],
|
||||
allMembers[(startIndex + 2) % allMembers.length]
|
||||
];
|
||||
|
||||
return { members: members.filter(m => m.ip !== ip) };
|
||||
},
|
||||
|
||||
async getNodeStatus(ip) {
|
||||
// Simulate network delay
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
// Find the member by IP
|
||||
const member = window.demoMembersData.members.find(m => m.ip === ip);
|
||||
if (!member) {
|
||||
throw new Error('Node not found');
|
||||
}
|
||||
|
||||
// Return node status with labels
|
||||
return {
|
||||
...member.resources,
|
||||
api: member.api,
|
||||
labels: {
|
||||
environment: ip.includes('103') ? 'production' : 'production',
|
||||
region: ip.includes('103') ? 'us-east' : 'us-west',
|
||||
role: ip.includes('101') ? 'controller' : ip.includes('103') ? 'gateway' : 'worker',
|
||||
cluster: 'spore-main',
|
||||
nodeType: ip.includes('102') ? 'staging' : 'production',
|
||||
location: ip.includes('103') ? 'datacenter-2' : 'datacenter-1'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,641 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Demo Members View - SPORE UI</title>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<script src="demo-members-data.js"></script>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="main-navigation">
|
||||
<div class="nav-left">
|
||||
<button class="nav-tab" onclick="showCluster()">🌐 Cluster</button>
|
||||
<button class="nav-tab active" onclick="showTopology()">🌐 Topology</button>
|
||||
<button class="nav-tab" onclick="showFirmware()">📦 Firmware</button>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="cluster-status">🚀 Demo Mode</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cluster-view" class="view-content">
|
||||
<div class="cluster-section">
|
||||
<h2>🌐 Cluster View (Demo)</h2>
|
||||
<p>This is a demo of the cluster view. Switch to Members to see the network topology visualization.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="topology-view" class="view-content active">
|
||||
<div class="topology-section">
|
||||
<div class="topology-header">
|
||||
<h2>🌐 Network Topology (Demo)</h2>
|
||||
<button class="refresh-btn" onclick="refreshTopologyView()">
|
||||
<svg class="refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 4v6h6M23 20v-6h-6"/>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/>
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="topology-graph-container">
|
||||
<div class="loading">
|
||||
<div>Loading network topology...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="firmware-view" class="view-content">
|
||||
<div class="firmware-section">
|
||||
<h2>📦 Firmware View (Demo)</h2>
|
||||
<p>This is a demo of the firmware view. Switch to Members to see the network topology visualization.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Demo Topology View Implementation
|
||||
class DemoTopologyViewModel {
|
||||
constructor() {
|
||||
this.nodes = [];
|
||||
this.links = [];
|
||||
this.isLoading = false;
|
||||
this.error = null;
|
||||
this.selectedNode = null;
|
||||
}
|
||||
|
||||
async updateNetworkTopology() {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
this.error = null;
|
||||
|
||||
// Use demo API client
|
||||
const response = await window.demoApiClient.getClusterMembers();
|
||||
const members = response.members || [];
|
||||
|
||||
// Build enhanced graph data
|
||||
const { nodes, links } = await this.buildEnhancedGraphData(members);
|
||||
|
||||
this.nodes = nodes;
|
||||
this.links = links;
|
||||
|
||||
// Trigger render
|
||||
if (window.topologyGraphComponent) {
|
||||
window.topologyGraphComponent.renderGraph();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch network topology:', error);
|
||||
this.error = error.message;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async buildEnhancedGraphData(members) {
|
||||
const nodes = [];
|
||||
const links = [];
|
||||
const nodeConnections = new Map();
|
||||
|
||||
// Create nodes from members
|
||||
members.forEach((member, index) => {
|
||||
if (member && member.ip) {
|
||||
nodes.push({
|
||||
id: member.ip,
|
||||
hostname: member.hostname || member.ip,
|
||||
ip: member.ip,
|
||||
status: member.status || 'UNKNOWN',
|
||||
latency: member.latency || 0,
|
||||
resources: member.resources || {},
|
||||
x: Math.random() * 800 + 100,
|
||||
y: Math.random() * 600 + 100
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Try to get cluster members from each node to build actual connections
|
||||
for (const node of nodes) {
|
||||
try {
|
||||
const nodeResponse = await window.demoApiClient.getClusterMembersFromNode(node.ip);
|
||||
if (nodeResponse && nodeResponse.members) {
|
||||
nodeConnections.set(node.ip, nodeResponse.members);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to get cluster members from node ${node.ip}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Build links based on actual connections
|
||||
for (const [sourceIp, sourceMembers] of nodeConnections) {
|
||||
for (const targetMember of sourceMembers) {
|
||||
if (targetMember.ip && targetMember.ip !== sourceIp) {
|
||||
const existingLink = links.find(link =>
|
||||
(link.source === sourceIp && link.target === targetMember.ip) ||
|
||||
(link.source === targetMember.ip && link.target === sourceIp)
|
||||
);
|
||||
|
||||
if (!existingLink) {
|
||||
const sourceNode = nodes.find(n => n.id === sourceIp);
|
||||
const targetNode = nodes.find(n => n.id === targetMember.ip);
|
||||
|
||||
if (sourceNode && targetNode) {
|
||||
const latency = targetMember.latency || this.estimateLatency(sourceNode, targetNode);
|
||||
|
||||
links.push({
|
||||
source: sourceIp,
|
||||
target: targetMember.ip,
|
||||
latency: latency,
|
||||
sourceNode: sourceNode,
|
||||
targetNode: targetNode,
|
||||
bidirectional: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no actual connections found, create a basic mesh
|
||||
if (links.length === 0) {
|
||||
console.log('No actual connections found, creating basic mesh');
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
for (let j = i + 1; j < nodes.length; j++) {
|
||||
const sourceNode = nodes[i];
|
||||
const targetNode = nodes[j];
|
||||
|
||||
const estimatedLatency = this.estimateLatency(sourceNode, targetNode);
|
||||
|
||||
links.push({
|
||||
source: sourceNode.id,
|
||||
target: targetNode.id,
|
||||
latency: estimatedLatency,
|
||||
sourceNode: sourceNode,
|
||||
targetNode: targetNode,
|
||||
bidirectional: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes, links };
|
||||
}
|
||||
|
||||
estimateLatency(sourceNode, targetNode) {
|
||||
const baseLatency = 5;
|
||||
const randomVariation = Math.random() * 10;
|
||||
return Math.round(baseLatency + randomVariation);
|
||||
}
|
||||
|
||||
selectNode(nodeId) {
|
||||
this.selectedNode = nodeId;
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.selectedNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Demo Members Graph Component
|
||||
class DemoTopologyGraphComponent {
|
||||
constructor(container, viewModel) {
|
||||
this.container = container;
|
||||
this.viewModel = viewModel;
|
||||
this.svg = null;
|
||||
this.simulation = null;
|
||||
this.zoom = null;
|
||||
this.width = 800;
|
||||
this.height = 600;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
console.log('DemoTopologyGraphComponent: Initializing...');
|
||||
|
||||
// Set up the SVG container
|
||||
this.setupSVG();
|
||||
|
||||
// Initial data load
|
||||
await this.viewModel.updateNetworkTopology();
|
||||
}
|
||||
|
||||
setupSVG() {
|
||||
const container = this.findElement('#members-graph-container');
|
||||
if (!container) {
|
||||
console.error('Graph container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing content
|
||||
container.innerHTML = '';
|
||||
|
||||
// Create SVG element
|
||||
this.svg = d3.select(container)
|
||||
.append('svg')
|
||||
.attr('width', this.width)
|
||||
.attr('height', this.height)
|
||||
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
|
||||
.style('border', '1px solid #ddd')
|
||||
.style('background', '#f9f9f9');
|
||||
|
||||
// Add zoom behavior
|
||||
this.zoom = d3.zoom()
|
||||
.scaleExtent([0.1, 4])
|
||||
.on('zoom', (event) => {
|
||||
this.svg.select('g').attr('transform', event.transform);
|
||||
});
|
||||
|
||||
this.svg.call(this.zoom);
|
||||
|
||||
// Create main group for zoom
|
||||
this.svg.append('g');
|
||||
}
|
||||
|
||||
renderGraph() {
|
||||
const nodes = this.viewModel.nodes;
|
||||
const links = this.viewModel.links;
|
||||
|
||||
if (!nodes || nodes.length === 0) {
|
||||
this.showNoData();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Rendering graph with', nodes.length, 'nodes and', links.length, 'links');
|
||||
|
||||
const svgGroup = this.svg.select('g');
|
||||
|
||||
// Clear existing elements
|
||||
svgGroup.selectAll('*').remove();
|
||||
|
||||
// Create arrow marker for links
|
||||
svgGroup.append('defs').selectAll('marker')
|
||||
.data(['end'])
|
||||
.enter().append('marker')
|
||||
.attr('id', 'arrowhead')
|
||||
.attr('viewBox', '0 -5 10 10')
|
||||
.attr('refX', 20)
|
||||
.attr('refY', 0)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M0,-5L10,0L0,5')
|
||||
.attr('fill', '#999');
|
||||
|
||||
// Create links with better styling
|
||||
const link = svgGroup.append('g')
|
||||
.selectAll('line')
|
||||
.data(links)
|
||||
.enter().append('line')
|
||||
.attr('stroke', d => this.getLinkColor(d.latency))
|
||||
.attr('stroke-opacity', 0.7)
|
||||
.attr('stroke-width', d => Math.max(2, Math.min(8, d.latency / 3)))
|
||||
.attr('marker-end', 'url(#arrowhead)');
|
||||
|
||||
// Create nodes
|
||||
const node = svgGroup.append('g')
|
||||
.selectAll('g')
|
||||
.data(nodes)
|
||||
.enter().append('g')
|
||||
.attr('class', 'node')
|
||||
.call(this.drag(this.simulation));
|
||||
|
||||
// Add circles to nodes with size based on status
|
||||
node.append('circle')
|
||||
.attr('r', d => this.getNodeRadius(d.status))
|
||||
.attr('fill', d => this.getNodeColor(d.status))
|
||||
.attr('stroke', '#fff')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Add status indicator
|
||||
node.append('circle')
|
||||
.attr('r', 3)
|
||||
.attr('fill', d => this.getStatusIndicatorColor(d.status))
|
||||
.attr('cx', -8)
|
||||
.attr('cy', -8);
|
||||
|
||||
// Add labels to nodes
|
||||
node.append('text')
|
||||
.text(d => d.hostname.length > 15 ? d.hostname.substring(0, 15) + '...' : d.hostname)
|
||||
.attr('x', 15)
|
||||
.attr('y', 4)
|
||||
.attr('font-size', '12px')
|
||||
.attr('fill', '#333')
|
||||
.attr('font-weight', '500');
|
||||
|
||||
// Add IP address labels
|
||||
node.append('text')
|
||||
.text(d => d.ip)
|
||||
.attr('x', 15)
|
||||
.attr('y', 20)
|
||||
.attr('font-size', '10px')
|
||||
.attr('fill', '#666');
|
||||
|
||||
// Add status labels
|
||||
node.append('text')
|
||||
.text(d => d.status)
|
||||
.attr('x', 15)
|
||||
.attr('y', 35)
|
||||
.attr('font-size', '10px')
|
||||
.attr('fill', d => this.getNodeColor(d.status))
|
||||
.attr('font-weight', '600');
|
||||
|
||||
// Add latency labels on links with better positioning
|
||||
const linkLabels = svgGroup.append('g')
|
||||
.selectAll('text')
|
||||
.data(links)
|
||||
.enter().append('text')
|
||||
.attr('font-size', '11px')
|
||||
.attr('fill', '#333')
|
||||
.attr('font-weight', '500')
|
||||
.attr('text-anchor', 'middle')
|
||||
.text(d => `${d.latency}ms`);
|
||||
|
||||
// Add background for link labels
|
||||
const linkLabelBackgrounds = svgGroup.append('g')
|
||||
.selectAll('rect')
|
||||
.data(links)
|
||||
.enter().append('rect')
|
||||
.attr('width', d => `${d.latency}ms`.length * 6 + 4)
|
||||
.attr('height', 16)
|
||||
.attr('fill', '#fff')
|
||||
.attr('opacity', 0.8)
|
||||
.attr('rx', 3);
|
||||
|
||||
// Set up force simulation with better parameters
|
||||
this.simulation = d3.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(links).id(d => d.id).distance(120))
|
||||
.force('charge', d3.forceManyBody().strength(-400))
|
||||
.force('center', d3.forceCenter(this.width / 2, this.height / 2))
|
||||
.force('collision', d3.forceCollide().radius(40));
|
||||
|
||||
// Update positions on simulation tick
|
||||
this.simulation.on('tick', () => {
|
||||
link
|
||||
.attr('x1', d => d.source.x)
|
||||
.attr('y1', d => d.source.y)
|
||||
.attr('x2', d => d.target.x)
|
||||
.attr('y2', d => d.target.y);
|
||||
|
||||
// Update link labels
|
||||
linkLabels
|
||||
.attr('x', d => (d.source.x + d.target.x) / 2)
|
||||
.attr('y', d => (d.source.y + d.target.y) / 2 - 5);
|
||||
|
||||
// Update link label backgrounds
|
||||
linkLabelBackgrounds
|
||||
.attr('x', d => (d.source.x + d.target.x) / 2 - (d.latency.toString().length * 6 + 4) / 2)
|
||||
.attr('y', d => (d.source.y + d.target.y) / 2 - 12);
|
||||
|
||||
node
|
||||
.attr('transform', d => `translate(${d.x},${d.y})`);
|
||||
});
|
||||
|
||||
// Add click handlers for node selection
|
||||
node.on('click', (event, d) => {
|
||||
this.viewModel.selectNode(d.id);
|
||||
this.updateSelection(d.id);
|
||||
});
|
||||
|
||||
// Add hover effects
|
||||
node.on('mouseover', (event, d) => {
|
||||
d3.select(event.currentTarget).select('circle')
|
||||
.attr('r', d => this.getNodeRadius(d.status) + 4)
|
||||
.attr('stroke-width', 3);
|
||||
});
|
||||
|
||||
node.on('mouseout', (event, d) => {
|
||||
d3.select(event.currentTarget).select('circle')
|
||||
.attr('r', d => this.getNodeRadius(d.status))
|
||||
.attr('stroke-width', 2);
|
||||
});
|
||||
|
||||
// Add tooltip for links
|
||||
link.on('mouseover', (event, d) => {
|
||||
d3.select(event.currentTarget)
|
||||
.attr('stroke-width', d => Math.max(3, Math.min(10, d.latency / 2)))
|
||||
.attr('stroke-opacity', 1);
|
||||
});
|
||||
|
||||
link.on('mouseout', (event, d) => {
|
||||
d3.select(event.currentTarget)
|
||||
.attr('stroke-width', d => Math.max(2, Math.min(8, d.latency / 3)))
|
||||
.attr('stroke-opacity', 0.7);
|
||||
});
|
||||
|
||||
// Add legend
|
||||
this.addLegend(svgGroup);
|
||||
}
|
||||
|
||||
getNodeRadius(status) {
|
||||
switch (status?.toUpperCase()) {
|
||||
case 'ACTIVE':
|
||||
return 10;
|
||||
case 'INACTIVE':
|
||||
return 8;
|
||||
case 'DEAD':
|
||||
return 6;
|
||||
default:
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
getStatusIndicatorColor(status) {
|
||||
switch (status?.toUpperCase()) {
|
||||
case 'ACTIVE':
|
||||
return '#4CAF50';
|
||||
case 'INACTIVE':
|
||||
return '#FF9800';
|
||||
case 'DEAD':
|
||||
return '#F44336';
|
||||
default:
|
||||
return '#9E9E9E';
|
||||
}
|
||||
}
|
||||
|
||||
getNodeColor(status) {
|
||||
switch (status?.toUpperCase()) {
|
||||
case 'ACTIVE':
|
||||
return '#4CAF50';
|
||||
case 'INACTIVE':
|
||||
return '#FF9800';
|
||||
case 'DEAD':
|
||||
return '#F44336';
|
||||
default:
|
||||
return '#9E9E9E';
|
||||
}
|
||||
}
|
||||
|
||||
getLinkColor(latency) {
|
||||
if (latency <= 5) return '#4CAF50'; // Green for low latency
|
||||
if (latency <= 15) return '#FF9800'; // Orange for medium latency
|
||||
if (latency <= 30) return '#FF5722'; // Red-orange for high latency
|
||||
return '#F44336'; // Red for very high latency
|
||||
}
|
||||
|
||||
drag(simulation) {
|
||||
function dragstarted(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
}
|
||||
|
||||
function dragged(event, d) {
|
||||
d.fx = event.x;
|
||||
d.fy = event.y;
|
||||
}
|
||||
|
||||
function dragended(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
d.fx = null;
|
||||
d.fy = null;
|
||||
}
|
||||
|
||||
return d3.drag()
|
||||
.on('start', dragstarted)
|
||||
.on('drag', dragged)
|
||||
.on('end', dragended);
|
||||
}
|
||||
|
||||
updateSelection(selectedNodeId) {
|
||||
// Update visual selection
|
||||
this.svg.selectAll('.node').select('circle')
|
||||
.attr('stroke-width', d => d.id === selectedNodeId ? 4 : 2)
|
||||
.attr('stroke', d => d.id === selectedNodeId ? '#2196F3' : '#fff');
|
||||
}
|
||||
|
||||
addLegend(svgGroup) {
|
||||
const legend = svgGroup.append('g')
|
||||
.attr('class', 'legend')
|
||||
.attr('transform', `translate(20, 20)`);
|
||||
|
||||
// Node status legend
|
||||
const nodeLegend = legend.append('g');
|
||||
nodeLegend.append('text')
|
||||
.text('Node Status:')
|
||||
.attr('x', 0)
|
||||
.attr('y', 0)
|
||||
.attr('font-size', '12px')
|
||||
.attr('font-weight', '600')
|
||||
.attr('fill', '#333');
|
||||
|
||||
const statuses = [
|
||||
{ status: 'ACTIVE', color: '#4CAF50', y: 20 },
|
||||
{ status: 'INACTIVE', color: '#FF9800', y: 40 },
|
||||
{ status: 'DEAD', color: '#F44336', y: 60 }
|
||||
];
|
||||
|
||||
statuses.forEach(item => {
|
||||
nodeLegend.append('circle')
|
||||
.attr('r', 6)
|
||||
.attr('cx', 0)
|
||||
.attr('cy', item.y)
|
||||
.attr('fill', item.color);
|
||||
|
||||
nodeLegend.append('text')
|
||||
.text(item.status)
|
||||
.attr('x', 15)
|
||||
.attr('y', item.y + 4)
|
||||
.attr('font-size', '10px')
|
||||
.attr('fill', '#333');
|
||||
});
|
||||
|
||||
// Link latency legend
|
||||
const linkLegend = legend.append('g')
|
||||
.attr('transform', 'translate(120, 0)');
|
||||
|
||||
linkLegend.append('text')
|
||||
.text('Link Latency:')
|
||||
.attr('x', 0)
|
||||
.attr('y', 0)
|
||||
.attr('font-size', '12px')
|
||||
.attr('font-weight', '600')
|
||||
.attr('fill', '#333');
|
||||
|
||||
const latencies = [
|
||||
{ range: '≤5ms', color: '#4CAF50', y: 20 },
|
||||
{ range: '6-15ms', color: '#FF9800', y: 40 },
|
||||
{ range: '16-30ms', color: '#FF5722', y: 60 },
|
||||
{ range: '>30ms', color: '#F44336', y: 80 }
|
||||
];
|
||||
|
||||
latencies.forEach(item => {
|
||||
linkLegend.append('line')
|
||||
.attr('x1', 0)
|
||||
.attr('y1', item.y)
|
||||
.attr('x2', 20)
|
||||
.attr('y2', item.y)
|
||||
.attr('stroke', item.color)
|
||||
.attr('stroke-width', 3);
|
||||
|
||||
linkLegend.append('text')
|
||||
.text(item.range)
|
||||
.attr('x', 25)
|
||||
.attr('y', item.y + 4)
|
||||
.attr('font-size', '10px')
|
||||
.attr('fill', '#333');
|
||||
});
|
||||
}
|
||||
|
||||
findElement(selector) {
|
||||
return document.querySelector(selector);
|
||||
}
|
||||
|
||||
showNoData() {
|
||||
const container = this.findElement('#members-graph-container');
|
||||
container.innerHTML = '<div class="no-data"><div>No cluster members found</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation functions
|
||||
function showCluster() {
|
||||
document.querySelectorAll('.view-content').forEach(v => v.classList.remove('active'));
|
||||
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
|
||||
document.getElementById('cluster-view').classList.add('active');
|
||||
document.querySelector('[onclick="showCluster()"]').classList.add('active');
|
||||
}
|
||||
|
||||
function showTopology() {
|
||||
document.querySelectorAll('.view-content').forEach(v => v.classList.remove('active'));
|
||||
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
|
||||
document.getElementById('topology-view').classList.add('active');
|
||||
document.querySelector('[onclick="showTopology()"]').classList.add('active');
|
||||
}
|
||||
|
||||
function showFirmware() {
|
||||
document.querySelectorAll('.view-content').forEach(v => v.classList.remove('active'));
|
||||
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
|
||||
document.getElementById('firmware-view').classList.add('active');
|
||||
document.querySelector('[onclick="showFirmware()"]').classList.add('active');
|
||||
}
|
||||
|
||||
function refreshTopologyView() {
|
||||
if (window.topologyGraphComponent) {
|
||||
window.topologyGraphComponent.viewModel.updateNetworkTopology();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the demo
|
||||
window.addEventListener('DOMContentLoaded', async function() {
|
||||
console.log('Demo Members View: Initializing...');
|
||||
|
||||
// Create view model and component
|
||||
const topologyViewModel = new DemoTopologyViewModel();
|
||||
const topologyGraphComponent = new DemoTopologyGraphComponent(
|
||||
document.getElementById('topology-graph-container'),
|
||||
topologyViewModel
|
||||
);
|
||||
|
||||
// Store globally for refresh function
|
||||
window.topologyGraphComponent = topologyGraphComponent;
|
||||
|
||||
// Initialize
|
||||
await topologyGraphComponent.initialize();
|
||||
|
||||
console.log('Demo Topology View: Initialization completed');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,346 +0,0 @@
|
||||
<!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 - Isolated</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;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.debug-info {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin: 20px 0;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🚀 Deploy Button Test - Isolated</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Test Scenario: Deploy Button State</h2>
|
||||
<p>This test isolates the deploy button functionality to debug the issue.</p>
|
||||
</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="debug-info">
|
||||
<h3>Debug Information</h3>
|
||||
<div id="debug-output">Waiting for actions...</div>
|
||||
</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,
|
||||
labels: nodeCount % 2 === 0 ? { app: 'demo', role: 'worker' } : { device: 'sensor', zone: `Z${nodeCount}` }
|
||||
};
|
||||
testNodes.push(newNode);
|
||||
displayClusterMembers();
|
||||
populateNodeSelect();
|
||||
updateDeployButton();
|
||||
updateDebugInfo();
|
||||
}
|
||||
|
||||
function removeAllNodes() {
|
||||
testNodes = [];
|
||||
displayClusterMembers();
|
||||
populateNodeSelect();
|
||||
updateDeployButton();
|
||||
updateDebugInfo();
|
||||
}
|
||||
|
||||
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>
|
||||
${node.labels ? `<div class=\"member-labels\">${Object.entries(node.labels).map(([k,v]) => `<span class=\\\"label-chip\\\">${k}: ${v}</span>`).join('')}</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 !== "";
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
const debugInfo = {
|
||||
hasFile,
|
||||
targetType: targetType?.value,
|
||||
hasAvailableNodes,
|
||||
specificNodeValue: specificNodeSelect?.value,
|
||||
isValidTarget,
|
||||
memberCardsCount: testNodes.length
|
||||
};
|
||||
|
||||
console.log('updateDeployButton debug:', debugInfo);
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
updateDebugInfo();
|
||||
}
|
||||
|
||||
function updateDebugInfo() {
|
||||
const debugOutput = document.getElementById('debug-output');
|
||||
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');
|
||||
|
||||
const debugInfo = {
|
||||
hasFile: fileInput.files && fileInput.files.length > 0,
|
||||
targetType: targetType?.value,
|
||||
hasAvailableNodes: testNodes.length > 0,
|
||||
specificNodeValue: specificNodeSelect?.value,
|
||||
deployButtonDisabled: deployBtn.disabled,
|
||||
deployButtonText: deployBtn.textContent,
|
||||
testNodesCount: testNodes.length
|
||||
};
|
||||
|
||||
debugOutput.innerHTML = `<pre>${JSON.stringify(debugInfo, null, 2)}</pre>`;
|
||||
}
|
||||
|
||||
// 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', () => {
|
||||
console.log('Target radio changed to:', radio.value);
|
||||
|
||||
if (radio.value === 'specific') {
|
||||
specificNodeSelect.style.visibility = 'visible';
|
||||
specificNodeSelect.style.opacity = '1';
|
||||
populateNodeSelect();
|
||||
} else {
|
||||
specificNodeSelect.style.visibility = 'hidden';
|
||||
specificNodeSelect.style.opacity = '0';
|
||||
}
|
||||
|
||||
console.log('Calling updateDeployButton after target change');
|
||||
updateDeployButton();
|
||||
});
|
||||
});
|
||||
|
||||
// Setup specific node select change handler
|
||||
if (specificNodeSelect) {
|
||||
specificNodeSelect.addEventListener('change', (event) => {
|
||||
console.log('Specific node select changed to:', event.target.value);
|
||||
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>
|
||||
@@ -1,104 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Simple Framework Test</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.test-section { margin: 20px 0; padding: 20px; border: 1px solid #ccc; }
|
||||
.success { color: green; }
|
||||
.error { color: red; }
|
||||
button { margin: 5px; padding: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Simple Framework Test</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>API Test</h2>
|
||||
<button onclick="testAPI()">Test API Connection</button>
|
||||
<div id="api-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Framework Test</h2>
|
||||
<button onclick="testFramework()">Test Framework</button>
|
||||
<div id="framework-result"></div>
|
||||
</div>
|
||||
|
||||
<script src="framework.js"></script>
|
||||
<script src="api-client.js"></script>
|
||||
<script src="view-models.js"></script>
|
||||
|
||||
<script>
|
||||
async function testAPI() {
|
||||
const resultDiv = document.getElementById('api-result');
|
||||
resultDiv.innerHTML = 'Testing...';
|
||||
|
||||
try {
|
||||
// Test cluster members API
|
||||
const members = await window.apiClient.getClusterMembers();
|
||||
console.log('Members:', members);
|
||||
|
||||
// Test discovery API
|
||||
const discovery = await window.apiClient.getDiscoveryInfo();
|
||||
console.log('Discovery:', discovery);
|
||||
|
||||
resultDiv.innerHTML = `
|
||||
<div class="success">
|
||||
✅ API Test Successful!<br>
|
||||
Cluster Members: ${members.members?.length || 0}<br>
|
||||
Primary Node: ${discovery.primaryNode || 'None'}<br>
|
||||
Total Nodes: ${discovery.totalNodes || 0}
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error('API test failed:', error);
|
||||
resultDiv.innerHTML = `<div class="error">❌ API Test Failed: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function testFramework() {
|
||||
const resultDiv = document.getElementById('framework-result');
|
||||
resultDiv.innerHTML = 'Testing...';
|
||||
|
||||
try {
|
||||
// Test framework classes
|
||||
if (typeof EventBus !== 'undefined' &&
|
||||
typeof ViewModel !== 'undefined' &&
|
||||
typeof Component !== 'undefined') {
|
||||
|
||||
// Create a simple view model
|
||||
const vm = new ViewModel();
|
||||
vm.set('test', 'Hello World');
|
||||
|
||||
if (vm.get('test') === 'Hello World') {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="success">
|
||||
✅ Framework Test Successful!<br>
|
||||
EventBus: ${typeof EventBus}<br>
|
||||
ViewModel: ${typeof ViewModel}<br>
|
||||
Component: ${typeof Component}<br>
|
||||
ViewModel test: ${vm.get('test')}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
throw new Error('ViewModel get/set not working');
|
||||
}
|
||||
} else {
|
||||
throw new Error('Framework classes not found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Framework test failed:', error);
|
||||
resultDiv.innerHTML = `<div class="error">❌ Framework Test Failed: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Page loaded, framework ready');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,14 +7,24 @@
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #34495e 50%, #1a252f 100%);
|
||||
min-height: 100vh;
|
||||
height: 100vh; /* Fixed height instead of min-height */
|
||||
padding: 1rem;
|
||||
color: #ecf0f1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* Prevent body from growing beyond viewport */
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
max-width: none; /* Remove width constraint for full screen coverage */
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 0 2rem; /* Increase horizontal padding for better spacing */
|
||||
max-height: calc(100vh - 2rem); /* Constrain to viewport height minus body padding */
|
||||
overflow: hidden; /* Prevent container from growing beyond bounds */
|
||||
}
|
||||
|
||||
/* Header styles removed - integrated into navigation */
|
||||
@@ -233,7 +243,7 @@ p {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border-radius: 9999px;
|
||||
background: rgba(30, 58, 138, 0.35);
|
||||
border: 1px solid rgba(59, 130, 246, 0.4);
|
||||
@@ -804,10 +814,15 @@ p {
|
||||
}
|
||||
|
||||
.view-content.active {
|
||||
display: block;
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Special handling for cluster and firmware views to ensure proper width */
|
||||
#cluster-view.active, #firmware-view.active {
|
||||
display: block; /* Use block display for proper width inheritance */
|
||||
}
|
||||
|
||||
/* Firmware Section Styles */
|
||||
.firmware-section {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
@@ -2308,22 +2323,37 @@ p {
|
||||
|
||||
/* Topology View Styles */
|
||||
#topology-view {
|
||||
height: 100%;
|
||||
flex: 1; /* Take up remaining space */
|
||||
padding: 0;
|
||||
min-height: 100vh; /* Ensure full viewport height */
|
||||
margin: 0;
|
||||
position: relative;
|
||||
width: 100%; /* Use full container width */
|
||||
min-height: 0; /* Allow flex item to shrink */
|
||||
max-height: 100vh; /* Never exceed viewport height */
|
||||
overflow: hidden; /* Prevent content from exceeding bounds */
|
||||
}
|
||||
|
||||
/* Ensure other views work properly with flexbox */
|
||||
#cluster-view, #firmware-view {
|
||||
flex: 0 0 auto; /* Don't grow or shrink, use natural size */
|
||||
width: 100%; /* Use full container width */
|
||||
}
|
||||
|
||||
#topology-graph-container {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
min-height: 100%;
|
||||
height: 100vh; /* Use full viewport height */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-sizing: border-box;
|
||||
max-height: 100%; /* Ensure it doesn't exceed parent height */
|
||||
}
|
||||
|
||||
#topology-graph-container svg {
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SPORE UI - Component Caching Test</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<style>
|
||||
.test-info {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.test-button {
|
||||
background: rgba(74, 222, 128, 0.2);
|
||||
border: 1px solid rgba(74, 222, 128, 0.3);
|
||||
color: #4ade80;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
margin: 0.25rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.test-button:hover {
|
||||
background: rgba(74, 222, 128, 0.3);
|
||||
border-color: rgba(74, 222, 128, 0.5);
|
||||
}
|
||||
.test-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.test-results {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="test-info">
|
||||
<h3>🧪 Component Caching System Test</h3>
|
||||
<p>This page tests the new component caching system to verify that components are not re-rendered on view switches.</p>
|
||||
<p><strong>Note:</strong> Components now start with clean default state (collapsed cards, status tab) and don't restore previous UI state.</p>
|
||||
<div>
|
||||
<button class="test-button" onclick="testComponentCaching()">Test Component Caching</button>
|
||||
<button class="test-button" onclick="testDefaultState()">Test Default State</button>
|
||||
<button class="test-button" onclick="testPerformance()">Test Performance</button>
|
||||
<button class="test-button" onclick="clearTestData()">Clear Test Data</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-navigation">
|
||||
<div class="nav-left">
|
||||
<button class="nav-tab active" data-view="cluster">🌐 Cluster</button>
|
||||
<button class="nav-tab" data-view="firmware">📦 Firmware</button>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="cluster-status">🚀 Cluster Online</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cluster-view" class="view-content active">
|
||||
<div class="cluster-section">
|
||||
<div class="cluster-header">
|
||||
<div class="cluster-header-left">
|
||||
<div class="primary-node-info">
|
||||
<span class="primary-node-label">Primary Node:</span>
|
||||
<span class="primary-node-ip" id="primary-node-ip">Discovering...</span>
|
||||
<button class="primary-node-refresh" id="select-random-primary-btn" title="🎲 Select random primary node">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
||||
<path d="M1 4v6h6M23 20v-6h-6"/>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="refresh-btn" id="refresh-cluster-btn">
|
||||
<svg class="refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 4v6h6M23 20v-6h-6"/>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/>
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="cluster-members-container">
|
||||
<div class="loading">
|
||||
<div>Loading cluster members...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="firmware-view" class="view-content">
|
||||
<div class="firmware-section">
|
||||
<div class="firmware-overview">
|
||||
<div class="firmware-actions">
|
||||
<div class="action-group">
|
||||
<h3>🚀 Firmware Update</h3>
|
||||
<div class="firmware-upload-compact">
|
||||
<div class="compact-upload-row">
|
||||
<div class="file-upload-area">
|
||||
<div class="target-options">
|
||||
<label class="target-option">
|
||||
<input type="radio" name="target-type" value="all" checked>
|
||||
<span class="radio-custom"></span>
|
||||
<span class="target-label">All Nodes</span>
|
||||
</label>
|
||||
<label class="target-option specific-node-option">
|
||||
<input type="radio" name="target-type" value="specific">
|
||||
<span class="radio-custom"></span>
|
||||
<span class="target-label">Specific Node</span>
|
||||
<select id="specific-node-select" class="node-select">
|
||||
<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 class="upload-btn-compact" onclick="document.getElementById('global-firmware-file').click()">
|
||||
📁 Choose File
|
||||
</button>
|
||||
<span class="file-info" id="file-info">No file selected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="deploy-btn" id="deploy-btn" disabled>
|
||||
🚀 Deploy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="firmware-nodes-list" id="firmware-nodes-list">
|
||||
<!-- Nodes will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-results" id="test-results">
|
||||
<h4>Test Results:</h4>
|
||||
<div id="test-output">Run a test to see results...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="framework.js"></script>
|
||||
<script src="api-client.js"></script>
|
||||
<script src="view-models.js"></script>
|
||||
<script src="components.js"></script>
|
||||
<script src="app.js"></script>
|
||||
|
||||
<script>
|
||||
// Test tracking variables
|
||||
let componentCreationCount = 0;
|
||||
let componentMountCount = 0;
|
||||
let componentUnmountCount = 0;
|
||||
let componentPauseCount = 0;
|
||||
let componentResumeCount = 0;
|
||||
let testStartTime = 0;
|
||||
|
||||
// Override console.log to track component operations
|
||||
const originalLog = console.log;
|
||||
console.log = function(...args) {
|
||||
const message = args.join(' ');
|
||||
|
||||
// Track component operations
|
||||
if (message.includes('Constructor called')) {
|
||||
componentCreationCount++;
|
||||
} else if (message.includes('Mounting...')) {
|
||||
componentMountCount++;
|
||||
} else if (message.includes('Unmounting...')) {
|
||||
componentUnmountCount++;
|
||||
} else if (message.includes('Pausing...')) {
|
||||
componentPauseCount++;
|
||||
} else if (message.includes('Resuming...')) {
|
||||
componentResumeCount++;
|
||||
}
|
||||
|
||||
// Call original console.log
|
||||
originalLog.apply(console, args);
|
||||
};
|
||||
|
||||
// Test functions
|
||||
function testComponentCaching() {
|
||||
console.log('🧪 Testing component caching system...');
|
||||
resetTestCounts();
|
||||
|
||||
const results = document.getElementById('test-output');
|
||||
results.innerHTML = 'Testing component caching...<br>';
|
||||
|
||||
// Test rapid view switching
|
||||
const clusterTab = document.querySelector('[data-view="cluster"]');
|
||||
const firmwareTab = document.querySelector('[data-view="firmware"]');
|
||||
|
||||
let switchCount = 0;
|
||||
const maxSwitches = 10;
|
||||
|
||||
const rapidSwitch = setInterval(() => {
|
||||
if (switchCount >= maxSwitches) {
|
||||
clearInterval(rapidSwitch);
|
||||
analyzeResults();
|
||||
return;
|
||||
}
|
||||
|
||||
if (switchCount % 2 === 0) {
|
||||
firmwareTab.click();
|
||||
results.innerHTML += `Switch ${switchCount + 1}: Cluster → Firmware<br>`;
|
||||
} else {
|
||||
clusterTab.click();
|
||||
results.innerHTML += `Switch ${switchCount + 1}: Firmware → Cluster<br>`;
|
||||
}
|
||||
|
||||
switchCount++;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function testDefaultState() {
|
||||
console.log('🧪 Testing default state...');
|
||||
resetTestCounts();
|
||||
|
||||
const results = document.getElementById('test-output');
|
||||
results.innerHTML = 'Testing default state...<br>';
|
||||
|
||||
// Switch to cluster view
|
||||
const clusterTab = document.querySelector('[data-view="cluster"]');
|
||||
clusterTab.click();
|
||||
results.innerHTML += 'Switched to Cluster View.<br>';
|
||||
|
||||
// Check if default state is applied (collapsed cards, status tab)
|
||||
setTimeout(() => {
|
||||
const memberCards = document.querySelectorAll('.member-card');
|
||||
const statusTab = document.querySelector('.nav-tab.active[data-view="status"]');
|
||||
|
||||
if (memberCards.length > 0) {
|
||||
results.innerHTML += 'Checking default state:<br>';
|
||||
results.innerHTML += `- Member cards are collapsed: ${memberCards.every(card => !card.classList.contains('expanded'))}<br>`;
|
||||
results.innerHTML += `- Status tab is active: ${statusTab && statusTab.classList.contains('active')}<br>`;
|
||||
analyzeResults();
|
||||
} else {
|
||||
results.innerHTML += 'No member cards found to check default state<br>';
|
||||
analyzeResults();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function testPerformance() {
|
||||
console.log('🧪 Testing performance...');
|
||||
resetTestCounts();
|
||||
testStartTime = performance.now();
|
||||
|
||||
const results = document.getElementById('test-output');
|
||||
results.innerHTML = 'Testing performance with rapid switching...<br>';
|
||||
|
||||
// Perform rapid view switching
|
||||
const clusterTab = document.querySelector('[data-view="cluster"]');
|
||||
const firmwareTab = document.querySelector('[data-view="firmware"]');
|
||||
|
||||
let switchCount = 0;
|
||||
const maxSwitches = 20;
|
||||
|
||||
const performanceTest = setInterval(() => {
|
||||
if (switchCount >= maxSwitches) {
|
||||
clearInterval(performanceTest);
|
||||
const totalTime = performance.now() - testStartTime;
|
||||
results.innerHTML += `Performance test completed in ${totalTime.toFixed(2)}ms<br>`;
|
||||
analyzeResults();
|
||||
return;
|
||||
}
|
||||
|
||||
if (switchCount % 2 === 0) {
|
||||
firmwareTab.click();
|
||||
} else {
|
||||
clusterTab.click();
|
||||
}
|
||||
|
||||
switchCount++;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function resetTestCounts() {
|
||||
componentCreationCount = 0;
|
||||
componentMountCount = 0;
|
||||
componentUnmountCount = 0;
|
||||
componentPauseCount = 0;
|
||||
componentResumeCount = 0;
|
||||
}
|
||||
|
||||
function analyzeResults() {
|
||||
const results = document.getElementById('test-output');
|
||||
results.innerHTML += '<br><strong>Test Analysis:</strong><br>';
|
||||
results.innerHTML += `Component Creations: ${componentCreationCount}<br>`;
|
||||
results.innerHTML += `Component Mounts: ${componentMountCount}<br>`;
|
||||
results.innerHTML += `Component Unmounts: ${componentUnmountCount}<br>`;
|
||||
results.innerHTML += `Component Pauses: ${componentPauseCount}<br>`;
|
||||
results.innerHTML += `Component Resumes: ${componentResumeCount}<br><br>`;
|
||||
|
||||
// Analyze results
|
||||
if (componentCreationCount <= 2) {
|
||||
results.innerHTML += '✅ <strong>PASS:</strong> Components are properly cached (not re-created)<br>';
|
||||
} else {
|
||||
results.innerHTML += '❌ <strong>FAIL:</strong> Components are being re-created on view switches<br>';
|
||||
}
|
||||
|
||||
if (componentUnmountCount === 0) {
|
||||
results.innerHTML += '✅ <strong>PASS:</strong> Components are never unmounted during view switches<br>';
|
||||
} else {
|
||||
results.innerHTML += '❌ <strong>FAIL:</strong> Components are being unmounted during view switches<br>';
|
||||
}
|
||||
|
||||
if (componentPauseCount > 0 && componentResumeCount > 0) {
|
||||
results.innerHTML += '✅ <strong>PASS:</strong> Pause/Resume pattern is working correctly<br>';
|
||||
} else {
|
||||
results.innerHTML += '❌ <strong>FAIL:</strong> Pause/Resume pattern is not working<br>';
|
||||
}
|
||||
|
||||
// New test for default state behavior
|
||||
if (componentCreationCount <= 2 && componentUnmountCount === 0) {
|
||||
results.innerHTML += '✅ <strong>PASS:</strong> Component caching system is working correctly<br>';
|
||||
results.innerHTML += '✅ <strong>PASS:</strong> Components start with clean default state<br>';
|
||||
results.innerHTML += '✅ <strong>PASS:</strong> No complex state restoration causing issues<br>';
|
||||
}
|
||||
}
|
||||
|
||||
function clearTestData() {
|
||||
console.log('🧪 Clearing test data...');
|
||||
localStorage.removeItem('spore_cluster_expanded_cards');
|
||||
localStorage.removeItem('spore_cluster_active_tabs');
|
||||
console.log('🧪 Test data cleared');
|
||||
|
||||
const results = document.getElementById('test-output');
|
||||
results.innerHTML = 'Test data cleared. Run a test to see results...';
|
||||
}
|
||||
|
||||
// Add test info to console
|
||||
console.log('🧪 SPORE UI Component Caching Test Page Loaded');
|
||||
console.log('🧪 Use the test buttons above to verify the caching system works');
|
||||
console.log('🧪 Expected: Components should be created once and cached, never re-created');
|
||||
console.log('🧪 Expected: Components start with clean default state (collapsed cards, status tab)');
|
||||
console.log('🧪 Expected: No complex state restoration causing incorrect behavior');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,353 +0,0 @@
|
||||
<!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,
|
||||
labels: nodeCount % 2 === 0 ? { app: 'demo', role: 'worker' } : { device: 'sensor', zone: `Z${nodeCount}` }
|
||||
};
|
||||
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>
|
||||
${node.labels ? `<div class=\"member-labels\">${Object.entries(node.labels).map(([k,v]) => `<span class=\\\"label-chip\\\">${k}: ${v}</span>`).join('')}</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>
|
||||
@@ -1,131 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Framework Test</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.test-section { margin: 20px 0; padding: 20px; border: 1px solid #ccc; }
|
||||
.success { color: green; }
|
||||
.error { color: red; }
|
||||
button { margin: 5px; padding: 10px; }
|
||||
input { margin: 5px; padding: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SPORE UI Framework Test</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Framework Initialization Test</h2>
|
||||
<div id="framework-status">Checking...</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Event Bus Test</h2>
|
||||
<button id="publish-btn">Publish Test Event</button>
|
||||
<div id="event-log"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>View Model Test</h2>
|
||||
<input type="text" id="name-input" placeholder="Enter name">
|
||||
<button id="update-btn">Update Name</button>
|
||||
<div id="name-display">Name: (not set)</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Component Test</h2>
|
||||
<div id="test-component">
|
||||
<div class="loading">Loading...</div>
|
||||
</div>
|
||||
<button id="refresh-btn">Refresh Component</button>
|
||||
</div>
|
||||
|
||||
<script src="framework.js"></script>
|
||||
<script>
|
||||
// Test framework initialization
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Testing framework...');
|
||||
|
||||
// Test 1: Framework initialization
|
||||
if (window.app && window.app.eventBus) {
|
||||
document.getElementById('framework-status').innerHTML =
|
||||
'<span class="success">✅ Framework initialized successfully</span>';
|
||||
} else {
|
||||
document.getElementById('framework-status').innerHTML =
|
||||
'<span class="error">❌ Framework failed to initialize</span>';
|
||||
}
|
||||
|
||||
// Test 2: Event Bus
|
||||
const eventLog = document.getElementById('event-log');
|
||||
const unsubscribe = window.app.eventBus.subscribe('test-event', (data) => {
|
||||
eventLog.innerHTML += `<div>📡 Event received: ${JSON.stringify(data)}</div>`;
|
||||
});
|
||||
|
||||
document.getElementById('publish-btn').addEventListener('click', () => {
|
||||
window.app.eventBus.publish('test-event', {
|
||||
message: 'Hello from test!',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Test 3: View Model
|
||||
const testVM = new ViewModel();
|
||||
testVM.setEventBus(window.app.eventBus);
|
||||
|
||||
testVM.subscribe('name', (value) => {
|
||||
document.getElementById('name-display').textContent = `Name: ${value || '(not set)'}`;
|
||||
});
|
||||
|
||||
document.getElementById('update-btn').addEventListener('click', () => {
|
||||
const name = document.getElementById('name-input').value;
|
||||
testVM.set('name', name);
|
||||
});
|
||||
|
||||
// Test 4: Component
|
||||
class TestComponent extends Component {
|
||||
constructor(container, viewModel, eventBus) {
|
||||
super(container, viewModel, eventBus);
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const refreshBtn = document.getElementById('refresh-btn');
|
||||
if (refreshBtn) {
|
||||
this.addEventListener(refreshBtn, 'click', this.handleRefresh.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = this.viewModel ? this.viewModel.get('data') : null;
|
||||
if (data) {
|
||||
this.setHTML('', `<div class="success">✅ Component data: ${data}</div>`);
|
||||
} else {
|
||||
this.setHTML('', `<div class="loading">Loading component data...</div>`);
|
||||
}
|
||||
}
|
||||
|
||||
handleRefresh() {
|
||||
if (this.viewModel) {
|
||||
this.viewModel.set('data', `Refreshed at ${new Date().toLocaleTimeString()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const testComponentVM = new ViewModel();
|
||||
testComponentVM.setEventBus(window.app.eventBus);
|
||||
testComponentVM.set('data', 'Initial component data');
|
||||
|
||||
const testComponent = new TestComponent(
|
||||
document.getElementById('test-component'),
|
||||
testComponentVM,
|
||||
window.app.eventBus
|
||||
);
|
||||
|
||||
testComponent.mount();
|
||||
|
||||
console.log('Framework test completed');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,58 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Member Card Overlay</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="./d3.v7.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Test Member Card Overlay</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Test Data</h2>
|
||||
<button id="test-overlay-btn">Show Test Member Overlay</button>
|
||||
</div>
|
||||
|
||||
<div id="member-card-overlay" class="member-card-overlay"></div>
|
||||
</div>
|
||||
|
||||
<script src="./framework.js"></script>
|
||||
<script src="./components.js"></script>
|
||||
<script>
|
||||
// Test the member card overlay
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const testBtn = document.getElementById('test-overlay-btn');
|
||||
const overlayContainer = document.getElementById('member-card-overlay');
|
||||
|
||||
// Create test view model and event bus
|
||||
const testVM = new ViewModel();
|
||||
const testEventBus = new EventBus();
|
||||
|
||||
// Create the overlay component
|
||||
const overlayComponent = new MemberCardOverlayComponent(overlayContainer, testVM, testEventBus);
|
||||
overlayComponent.mount();
|
||||
|
||||
// Test data
|
||||
const testMember = {
|
||||
ip: '192.168.1.100',
|
||||
hostname: 'test-node-01',
|
||||
status: 'active',
|
||||
latency: 15,
|
||||
labels: {
|
||||
'environment': 'production',
|
||||
'region': 'us-west',
|
||||
'role': 'worker'
|
||||
}
|
||||
};
|
||||
|
||||
// Show overlay on button click
|
||||
testBtn.addEventListener('click', function() {
|
||||
overlayComponent.show(testMember);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,296 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Debug Members Component</title>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
.test-section { margin: 20px 0; padding: 20px; border: 1px solid #ddd; }
|
||||
button { padding: 10px 20px; margin: 5px; background: #007bff; color: white; border: none; cursor: pointer; }
|
||||
button:hover { background: #0056b3; }
|
||||
#graph-container { width: 600px; height: 400px; border: 1px solid #ccc; margin: 20px 0; }
|
||||
.status { padding: 10px; margin: 10px 0; border-radius: 4px; }
|
||||
.success { background: #d4edda; color: #155724; }
|
||||
.error { background: #f8d7da; color: #721c24; }
|
||||
.info { background: #d1ecf1; color: #0c5460; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🐛 Debug Members Component</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Component Test</h3>
|
||||
<p>Testing if the TopologyGraphComponent can be created and initialized.</p>
|
||||
<button onclick="testComponentCreation()">Test Component Creation</button>
|
||||
<button onclick="testInitialization()">Test Initialization</button>
|
||||
<button onclick="testMount()">Test Mount</button>
|
||||
<button onclick="clearTest()">Clear Test</button>
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Graph Container</h3>
|
||||
<div id="graph-container">
|
||||
<div style="text-align: center; padding: 50px; color: #666;">
|
||||
Graph will appear here after testing
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let testComponent = null;
|
||||
|
||||
function showStatus(message, type = 'info') {
|
||||
const statusDiv = document.getElementById('status');
|
||||
statusDiv.innerHTML = `<div class="status ${type}">${message}</div>`;
|
||||
}
|
||||
|
||||
// Mock base Component class
|
||||
class MockComponent {
|
||||
constructor(container, viewModel, eventBus) {
|
||||
this.container = container;
|
||||
this.viewModel = viewModel;
|
||||
this.eventBus = eventBus;
|
||||
this.isMounted = false;
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
console.log('MockComponent: initialize called');
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
mount() {
|
||||
console.log('MockComponent: mount called');
|
||||
this.isMounted = true;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
console.log('MockComponent: setupEventListeners called');
|
||||
}
|
||||
|
||||
setupViewModelListeners() {
|
||||
console.log('MockComponent: setupViewModelListeners called');
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log('MockComponent: render called');
|
||||
}
|
||||
}
|
||||
|
||||
// Mock ViewModel
|
||||
class MockViewModel {
|
||||
constructor() {
|
||||
this._data = {
|
||||
nodes: [],
|
||||
links: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
get(property) {
|
||||
return this._data[property];
|
||||
}
|
||||
|
||||
set(property, value) {
|
||||
this._data[property] = value;
|
||||
}
|
||||
|
||||
subscribe(property, callback) {
|
||||
console.log(`MockViewModel: Subscribing to ${property}`);
|
||||
}
|
||||
|
||||
async updateNetworkTopology() {
|
||||
console.log('MockViewModel: updateNetworkTopology called');
|
||||
// Simulate some data
|
||||
this.set('nodes', [
|
||||
{ id: '1', hostname: 'Test Node 1', ip: '192.168.1.1', status: 'ACTIVE' },
|
||||
{ id: '2', hostname: 'Test Node 2', ip: '192.168.1.2', status: 'ACTIVE' }
|
||||
]);
|
||||
this.set('links', [
|
||||
{ source: '1', target: '2', latency: 5 }
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Test TopologyGraphComponent (simplified version)
|
||||
class TestTopologyGraphComponent extends MockComponent {
|
||||
constructor(container, viewModel, eventBus) {
|
||||
super(container, viewModel, eventBus);
|
||||
console.log('TestTopologyGraphComponent: Constructor called');
|
||||
this.svg = null;
|
||||
this.simulation = null;
|
||||
this.zoom = null;
|
||||
this.width = 600;
|
||||
this.height = 400;
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
// Override mount to ensure proper initialization
|
||||
mount() {
|
||||
if (this.isMounted) return;
|
||||
|
||||
console.log('TestTopologyGraphComponent: Starting mount...');
|
||||
|
||||
// Call initialize if not already done
|
||||
if (!this.isInitialized) {
|
||||
console.log('TestTopologyGraphComponent: Initializing during mount...');
|
||||
this.initialize().then(() => {
|
||||
// Complete mount after initialization
|
||||
this.completeMount();
|
||||
}).catch(error => {
|
||||
console.error('TestTopologyGraphComponent: Initialization failed during mount:', error);
|
||||
// Still complete mount to prevent blocking
|
||||
this.completeMount();
|
||||
});
|
||||
} else {
|
||||
this.completeMount();
|
||||
}
|
||||
}
|
||||
|
||||
completeMount() {
|
||||
this.isMounted = true;
|
||||
this.setupEventListeners();
|
||||
this.setupViewModelListeners();
|
||||
this.render();
|
||||
|
||||
console.log('TestTopologyGraphComponent: Mounted successfully');
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
console.log('TestTopologyGraphComponent: Initializing...');
|
||||
await super.initialize();
|
||||
|
||||
// Set up the SVG container
|
||||
this.setupSVG();
|
||||
|
||||
// Mark as initialized
|
||||
this.isInitialized = true;
|
||||
|
||||
console.log('TestTopologyGraphComponent: Initialization completed');
|
||||
}
|
||||
|
||||
setupSVG() {
|
||||
const container = document.getElementById('graph-container');
|
||||
if (!container) {
|
||||
console.error('TestTopologyGraphComponent: Graph container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing content
|
||||
container.innerHTML = '';
|
||||
|
||||
// Create SVG element
|
||||
this.svg = d3.select(container)
|
||||
.append('svg')
|
||||
.attr('width', this.width)
|
||||
.attr('height', this.height)
|
||||
.style('border', '1px solid #ddd')
|
||||
.style('background', '#f9f9f9');
|
||||
|
||||
this.svg.append('g');
|
||||
|
||||
console.log('TestTopologyGraphComponent: SVG setup completed');
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log('TestTopologyGraphComponent: render called');
|
||||
// Simple render for testing
|
||||
if (this.svg) {
|
||||
const svgGroup = this.svg.select('g');
|
||||
svgGroup.append('text')
|
||||
.attr('x', 300)
|
||||
.attr('y', 200)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', '16px')
|
||||
.text('Component Rendered Successfully!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function testComponentCreation() {
|
||||
try {
|
||||
showStatus('🔄 Testing component creation...', 'info');
|
||||
|
||||
const mockViewModel = new MockViewModel();
|
||||
const mockEventBus = {};
|
||||
|
||||
testComponent = new TestTopologyGraphComponent(
|
||||
document.getElementById('graph-container'),
|
||||
mockViewModel,
|
||||
mockEventBus
|
||||
);
|
||||
|
||||
showStatus('✅ Component created successfully', 'success');
|
||||
console.log('Component created:', testComponent);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Component creation failed:', error);
|
||||
showStatus(`❌ Component creation failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function testInitialization() {
|
||||
if (!testComponent) {
|
||||
showStatus('❌ No component created. Run component creation test first.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showStatus('🔄 Testing initialization...', 'info');
|
||||
|
||||
testComponent.initialize().then(() => {
|
||||
showStatus('✅ Component initialized successfully', 'success');
|
||||
}).catch(error => {
|
||||
console.error('Initialization failed:', error);
|
||||
showStatus(`❌ Initialization failed: ${error.message}`, 'error');
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Initialization test failed:', error);
|
||||
showStatus(`❌ Initialization test failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function testMount() {
|
||||
if (!testComponent) {
|
||||
showStatus('❌ No component created. Run component creation test first.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showStatus('🔄 Testing mount...', 'info');
|
||||
|
||||
testComponent.mount();
|
||||
|
||||
showStatus('✅ Component mounted successfully', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Mount test failed:', error);
|
||||
showStatus(`❌ Mount test failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function clearTest() {
|
||||
if (testComponent) {
|
||||
testComponent = null;
|
||||
}
|
||||
document.getElementById('graph-container').innerHTML =
|
||||
'<div style="text-align: center; padding: 50px; color: #666;">Graph will appear here after testing</div>';
|
||||
document.getElementById('status').innerHTML = '';
|
||||
showStatus('🧹 Test cleared', 'info');
|
||||
}
|
||||
|
||||
// Auto-test on load
|
||||
window.addEventListener('load', () => {
|
||||
showStatus('🚀 Debug page loaded. Click "Test Component Creation" to begin.', 'info');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,192 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Refresh Button</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; background: #1a1a1a; color: white; }
|
||||
.test-section { margin: 20px 0; padding: 20px; border: 1px solid #333; border-radius: 8px; }
|
||||
.log { background: #2a2a2a; padding: 10px; margin: 10px 0; font-family: monospace; border-radius: 4px; max-height: 300px; overflow-y: auto; }
|
||||
.test-button { background: #4a90e2; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; margin: 5px; }
|
||||
.test-button:hover { background: #357abd; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🔍 Test Refresh Button Functionality</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Test Controls</h3>
|
||||
<button class="test-button" onclick="testRefreshButton()">🧪 Test Refresh Button</button>
|
||||
<button class="test-button" onclick="testAPICall()">📡 Test API Call</button>
|
||||
<button class="test-button" onclick="testComponent()">🧩 Test Component</button>
|
||||
<button class="test-button" onclick="clearLog()">🧹 Clear Log</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Cluster View (Simplified)</h3>
|
||||
<div id="cluster-view" class="cluster-container">
|
||||
<div class="cluster-header">
|
||||
<div class="cluster-header-left">
|
||||
<div class="primary-node-info">
|
||||
<span class="primary-node-label">Primary Node:</span>
|
||||
<span class="primary-node-ip" id="primary-node-ip">🔍 Discovering...</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="refresh-btn" id="refresh-cluster-btn">
|
||||
<svg class="refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 4v6h6M23 20v-6h-6"/>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/>
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="cluster-members-container">
|
||||
<div class="loading">Loading cluster members...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Debug Log</h3>
|
||||
<div id="debug-log" class="log"></div>
|
||||
</div>
|
||||
|
||||
<!-- Include SPORE UI framework and components -->
|
||||
<script src="framework.js"></script>
|
||||
<script src="view-models.js"></script>
|
||||
<script src="components.js"></script>
|
||||
<script src="api-client.js"></script>
|
||||
|
||||
<script>
|
||||
let debugLog = [];
|
||||
|
||||
function log(message, type = 'info') {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = `[${timestamp}] ${type.toUpperCase()}: ${message}`;
|
||||
debugLog.push(logEntry);
|
||||
|
||||
const logElement = document.getElementById('debug-log');
|
||||
if (logElement) {
|
||||
logElement.innerHTML = debugLog.map(entry => `<div>${entry}</div>`).join('');
|
||||
logElement.scrollTop = logElement.scrollHeight;
|
||||
}
|
||||
|
||||
console.log(logEntry);
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
debugLog = [];
|
||||
document.getElementById('debug-log').innerHTML = '';
|
||||
}
|
||||
|
||||
function testRefreshButton() {
|
||||
log('Testing refresh button functionality...');
|
||||
|
||||
const refreshBtn = document.getElementById('refresh-cluster-btn');
|
||||
if (refreshBtn) {
|
||||
log('Found refresh button, testing click event...');
|
||||
|
||||
// Test if the button is clickable
|
||||
refreshBtn.click();
|
||||
log('Refresh button clicked');
|
||||
|
||||
// Check if the button state changed
|
||||
setTimeout(() => {
|
||||
if (refreshBtn.disabled) {
|
||||
log('Button was disabled (good sign)', 'success');
|
||||
} else {
|
||||
log('Button was not disabled (potential issue)', 'warning');
|
||||
}
|
||||
|
||||
// Check button text
|
||||
if (refreshBtn.innerHTML.includes('Refreshing')) {
|
||||
log('Button text changed to "Refreshing" (good sign)', 'success');
|
||||
} else {
|
||||
log('Button text did not change (potential issue)', 'warning');
|
||||
}
|
||||
}, 100);
|
||||
|
||||
} else {
|
||||
log('Refresh button not found!', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function testAPICall() {
|
||||
log('Testing API call to cluster members endpoint...');
|
||||
|
||||
fetch('http://localhost:3001/api/cluster/members')
|
||||
.then(response => {
|
||||
log(`API response status: ${response.status}`, 'info');
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
log(`API response data: ${JSON.stringify(data, null, 2)}`, 'success');
|
||||
})
|
||||
.catch(error => {
|
||||
log(`API call failed: ${error.message}`, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function testComponent() {
|
||||
log('Testing component initialization...');
|
||||
|
||||
try {
|
||||
// Create a simple test component
|
||||
const container = document.getElementById('cluster-view');
|
||||
const viewModel = new ClusterViewModel();
|
||||
const component = new ClusterMembersComponent(container, viewModel, new EventBus());
|
||||
|
||||
log('Component created successfully', 'success');
|
||||
log(`Component container: ${!!component.container}`, 'info');
|
||||
log(`Component viewModel: ${!!component.viewModel}`, 'info');
|
||||
|
||||
// Test mounting
|
||||
component.mount();
|
||||
log('Component mounted successfully', 'success');
|
||||
|
||||
// Test finding elements
|
||||
const refreshBtn = component.findElement('.refresh-btn');
|
||||
log(`Found refresh button: ${!!refreshBtn}`, 'info');
|
||||
|
||||
// Test event listener setup
|
||||
component.setupEventListeners();
|
||||
log('Event listeners set up successfully', 'success');
|
||||
|
||||
// Clean up
|
||||
component.unmount();
|
||||
log('Component unmounted successfully', 'success');
|
||||
|
||||
} catch (error) {
|
||||
log(`Component test failed: ${error.message}`, 'error');
|
||||
console.error('Component test error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
log('Page loaded, ready for testing');
|
||||
|
||||
// Test if the refresh button exists
|
||||
const refreshBtn = document.getElementById('refresh-cluster-btn');
|
||||
if (refreshBtn) {
|
||||
log('Refresh button found on page load', 'success');
|
||||
} else {
|
||||
log('Refresh button NOT found on page load', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Global error handler
|
||||
window.addEventListener('error', function(event) {
|
||||
log(`Global error: ${event.error}`, 'error');
|
||||
});
|
||||
|
||||
// Global unhandled promise rejection handler
|
||||
window.addEventListener('unhandledrejection', function(event) {
|
||||
log(`Unhandled promise rejection: ${event.reason}`, 'error');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,419 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SPORE UI - State Preservation Test</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<style>
|
||||
.test-panel {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.test-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.test-button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.test-button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.test-button.danger {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.test-button.danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.test-button.success {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.test-button.success:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.test-info {
|
||||
background: #e9ecef;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.test-info h4 {
|
||||
margin-top: 0;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.state-indicator {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.state-preserved {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.state-lost {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin: 5px 0;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.log-info { background: #d1ecf1; color: #0c5460; }
|
||||
.log-success { background: #d4edda; color: #155724; }
|
||||
.log-warning { background: #fff3cd; color: #856404; }
|
||||
.log-error { background: #f8d7da; color: #721c24; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🧪 SPORE UI State Preservation Test</h1>
|
||||
|
||||
<div class="test-panel">
|
||||
<h3>Test Controls</h3>
|
||||
<div class="test-controls">
|
||||
<button class="test-button" onclick="testStatePreservation()">
|
||||
🔄 Test Data Refresh (Preserve State)
|
||||
</button>
|
||||
<button class="test-button danger" onclick="testFullRerender()">
|
||||
🗑️ Test Full Re-render (Lose State)
|
||||
</button>
|
||||
<button class="test-button success" onclick="expandAllCards()">
|
||||
📖 Expand All Cards
|
||||
</button>
|
||||
<button class="test-button" onclick="changeAllTabs()">
|
||||
🏷️ Change All Tabs
|
||||
</button>
|
||||
<button class="test-button" onclick="testManualDataLoad()">
|
||||
📡 Test Manual Data Load
|
||||
</button>
|
||||
<button class="test-button" onclick="debugComponentState()">
|
||||
🐛 Debug Component State
|
||||
</button>
|
||||
<button class="test-button" onclick="testManualRefresh()">
|
||||
🔧 Test Manual Refresh
|
||||
</button>
|
||||
<button class="test-button" onclick="clearLog()">
|
||||
🧹 Clear Log
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="test-info">
|
||||
<h4>What This Test Demonstrates:</h4>
|
||||
<ul>
|
||||
<li><strong>State Preservation:</strong> When data is refreshed, expanded cards and active tabs are maintained</li>
|
||||
<li><strong>Partial Updates:</strong> Only changed data is updated, not entire components</li>
|
||||
<li><strong>UI State Persistence:</strong> User interactions (expanded cards, active tabs) are preserved across refreshes</li>
|
||||
<li><strong>Smart Updates:</strong> The system detects when data has actually changed and only updates what's necessary</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-panel">
|
||||
<h3>Current State Indicators</h3>
|
||||
<div>
|
||||
<strong>Expanded Cards:</strong>
|
||||
<span class="state-indicator" id="expanded-count">0</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Active Tabs:</strong>
|
||||
<span class="state-indicator" id="active-tabs-count">0</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Last Update:</strong>
|
||||
<span class="state-indicator" id="last-update">Never</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-panel">
|
||||
<h3>Test Log</h3>
|
||||
<div class="log-container" id="test-log">
|
||||
<div class="log-entry log-info">Test log initialized. Use the test controls above to test state preservation.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include the actual SPORE UI components for testing -->
|
||||
<div id="cluster-view" class="view-content active">
|
||||
<div class="primary-node-info">
|
||||
<h3>Primary Node</h3>
|
||||
<div id="primary-node-ip">🔍 Discovering...</div>
|
||||
<button class="primary-node-refresh">🔄 Refresh</button>
|
||||
</div>
|
||||
|
||||
<div id="cluster-members-container">
|
||||
<h3>Cluster Members</h3>
|
||||
<button class="refresh-btn">🔄 Refresh Members</button>
|
||||
<div id="members-list">
|
||||
<!-- Members will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include SPORE UI framework and components -->
|
||||
<script src="framework.js"></script>
|
||||
<script src="view-models.js"></script>
|
||||
<script src="components.js"></script>
|
||||
<script src="api-client.js"></script>
|
||||
|
||||
<script>
|
||||
// Test state preservation functionality
|
||||
let testLog = [];
|
||||
let expandedCardsCount = 0;
|
||||
let activeTabsCount = 0;
|
||||
|
||||
function log(message, type = 'info') {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = `log-entry log-${type}`;
|
||||
logEntry.textContent = `[${timestamp}] ${message}`;
|
||||
|
||||
const logContainer = document.getElementById('test-log');
|
||||
logContainer.appendChild(logEntry);
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
|
||||
testLog.push({ timestamp, message, type });
|
||||
}
|
||||
|
||||
function updateStateIndicators() {
|
||||
document.getElementById('expanded-count').textContent = expandedCardsCount;
|
||||
document.getElementById('active-tabs-count').textContent = activeTabsCount;
|
||||
document.getElementById('last-update').textContent = new Date().toLocaleTimeString();
|
||||
}
|
||||
|
||||
function testStatePreservation() {
|
||||
log('🧪 Testing state preservation during data refresh...', 'info');
|
||||
|
||||
// Simulate a data refresh that preserves state
|
||||
setTimeout(() => {
|
||||
log('✅ Data refresh completed with state preservation', 'success');
|
||||
log('📊 Expanded cards maintained: ' + expandedCardsCount, 'info');
|
||||
log('🏷️ Active tabs maintained: ' + activeTabsCount, 'info');
|
||||
updateStateIndicators();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function testFullRerender() {
|
||||
log('🗑️ Testing full re-render (this would lose state in old system)...', 'warning');
|
||||
|
||||
// Simulate what would happen in the old system
|
||||
setTimeout(() => {
|
||||
log('❌ Full re-render completed - state would be lost in old system', 'error');
|
||||
log('💡 In new system, this preserves state automatically', 'info');
|
||||
updateStateIndicators();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function expandAllCards() {
|
||||
log('📖 Expanding all cluster member cards...', 'info');
|
||||
expandedCardsCount = 3; // Simulate 3 expanded cards
|
||||
updateStateIndicators();
|
||||
log('✅ All cards expanded. State will be preserved during refreshes.', 'success');
|
||||
}
|
||||
|
||||
function changeAllTabs() {
|
||||
log('🏷️ Changing all active tabs to different values...', 'info');
|
||||
activeTabsCount = 3; // Simulate 3 active tabs
|
||||
updateStateIndicators();
|
||||
log('✅ All tabs changed. Active tab states will be preserved during refreshes.', 'success');
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('test-log').innerHTML = '';
|
||||
testLog = [];
|
||||
log('🧹 Test log cleared', 'info');
|
||||
}
|
||||
|
||||
// Test manual data loading
|
||||
async function testManualDataLoad() {
|
||||
log('📡 Testing manual data load...', 'info');
|
||||
|
||||
try {
|
||||
// Test if we can manually trigger the cluster view model
|
||||
if (window.app && window.app.currentView && window.app.currentView.viewModel) {
|
||||
const viewModel = window.app.currentView.viewModel;
|
||||
log('✅ Found cluster view model, attempting to load data...', 'info');
|
||||
|
||||
if (viewModel.updateClusterMembers && typeof viewModel.updateClusterMembers === 'function') {
|
||||
await viewModel.updateClusterMembers();
|
||||
log('✅ Manual data load completed', 'success');
|
||||
} else {
|
||||
log('❌ updateClusterMembers method not found on view model', 'error');
|
||||
}
|
||||
} else {
|
||||
log('❌ No cluster view model found', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
log(`❌ Manual data load failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Debug component state
|
||||
function debugComponentState() {
|
||||
log('🐛 Debugging component state...', 'info');
|
||||
|
||||
try {
|
||||
if (window.app && window.app.currentView && window.app.currentView.clusterMembersComponent) {
|
||||
const component = window.app.currentView.clusterMembersComponent;
|
||||
log('✅ Found cluster members component, checking state...', 'info');
|
||||
|
||||
if (component.debugState && typeof component.debugState === 'function') {
|
||||
const state = component.debugState();
|
||||
log('📊 Component state:', 'info');
|
||||
log(` - Members: ${state.members?.length || 0}`, 'info');
|
||||
log(` - Loading: ${state.isLoading}`, 'info');
|
||||
log(` - Error: ${state.error || 'none'}`, 'info');
|
||||
log(` - Expanded cards: ${state.expandedCards?.size || 0}`, 'info');
|
||||
log(` - Active tabs: ${state.activeTabs?.size || 0}`, 'info');
|
||||
} else {
|
||||
log('❌ debugState method not found on component', 'error');
|
||||
}
|
||||
} else {
|
||||
log('❌ No cluster members component found', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
log(`❌ Debug failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test manual refresh
|
||||
async function testManualRefresh() {
|
||||
log('🔧 Testing manual refresh...', 'info');
|
||||
|
||||
try {
|
||||
if (window.app && window.app.currentView && window.app.currentView.clusterMembersComponent) {
|
||||
const component = window.app.currentView.clusterMembersComponent;
|
||||
log('✅ Found cluster members component, testing manual refresh...', 'info');
|
||||
|
||||
if (component.manualRefresh && typeof component.manualRefresh === 'function') {
|
||||
await component.manualRefresh();
|
||||
log('✅ Manual refresh completed', 'success');
|
||||
} else {
|
||||
log('❌ manualRefresh method not found on component', 'error');
|
||||
}
|
||||
} else {
|
||||
log('❌ No cluster members component found', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
log(`❌ Manual refresh failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize test
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
log('🚀 SPORE UI State Preservation Test initialized', 'success');
|
||||
log('💡 This demonstrates how the new system preserves UI state during data refreshes', 'info');
|
||||
updateStateIndicators();
|
||||
|
||||
// Test API client functionality
|
||||
testAPIClient();
|
||||
});
|
||||
|
||||
// Test API client functionality
|
||||
async function testAPIClient() {
|
||||
try {
|
||||
log('🧪 Testing API client functionality...', 'info');
|
||||
|
||||
const response = await window.apiClient.getClusterMembers();
|
||||
log(`✅ API client test successful. Found ${response.members?.length || 0} cluster members`, 'success');
|
||||
|
||||
if (response.members && response.members.length > 0) {
|
||||
response.members.forEach(member => {
|
||||
log(`📱 Member: ${member.hostname || member.ip} (${member.status})`, 'info');
|
||||
});
|
||||
}
|
||||
|
||||
// Test discovery info
|
||||
const discoveryInfo = await window.apiClient.getDiscoveryInfo();
|
||||
log(`🔍 Discovery info: Primary node ${discoveryInfo.primaryNode || 'none'}, Total nodes: ${discoveryInfo.totalNodes}`, 'info');
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ API client test failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Mock API client for testing
|
||||
if (!window.apiClient) {
|
||||
window.apiClient = {
|
||||
getClusterMembers: async () => {
|
||||
return {
|
||||
members: [
|
||||
{ ip: '192.168.1.100', hostname: 'Node-1', status: 'active', latency: 15 },
|
||||
{ ip: '192.168.1.101', hostname: 'Node-2', status: 'active', latency: 22 },
|
||||
{ ip: '192.168.1.102', hostname: 'Node-3', status: 'offline', latency: null }
|
||||
]
|
||||
};
|
||||
},
|
||||
getDiscoveryInfo: async () => {
|
||||
return {
|
||||
primaryNode: '192.168.1.100',
|
||||
clientInitialized: true,
|
||||
totalNodes: 3
|
||||
};
|
||||
},
|
||||
getNodeStatus: async (ip) => {
|
||||
return {
|
||||
freeHeap: 102400,
|
||||
chipId: 'ESP32-' + ip.split('.').pop(),
|
||||
sdkVersion: 'v4.4.2',
|
||||
cpuFreqMHz: 240,
|
||||
flashChipSize: 4194304,
|
||||
api: [
|
||||
{ method: 1, uri: '/status' },
|
||||
{ method: 2, uri: '/config' }
|
||||
]
|
||||
};
|
||||
},
|
||||
getTasksStatus: async () => {
|
||||
return [
|
||||
{ name: 'Heartbeat', running: true, interval: 5000, enabled: true },
|
||||
{ name: 'DataSync', running: false, interval: 30000, enabled: true }
|
||||
];
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,72 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tab Test</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Tab Active State Test</h1>
|
||||
|
||||
<div class="tabs-container">
|
||||
<div class="tabs-header">
|
||||
<button class="tab-button active" data-tab="status">Status</button>
|
||||
<button class="tab-button" data-tab="endpoints">Endpoints</button>
|
||||
<button class="tab-button" data-tab="tasks">Tasks</button>
|
||||
<button class="tab-button" data-tab="firmware">Firmware</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="status-tab">
|
||||
<h3>Status Tab</h3>
|
||||
<p>This is the status tab content.</p>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="endpoints-tab">
|
||||
<h3>Endpoints Tab</h3>
|
||||
<p>This is the endpoints tab content.</p>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tasks-tab">
|
||||
<h3>Tasks Tab</h3>
|
||||
<p>This is the tasks tab content.</p>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="firmware-tab">
|
||||
<h3>Firmware Tab</h3>
|
||||
<p>This is the firmware tab content.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Simple tab functionality test
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const tabButtons = document.querySelectorAll('.tab-button');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const targetTab = this.dataset.tab;
|
||||
|
||||
// Remove active class from all buttons and contents
|
||||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
||||
tabContents.forEach(content => content.classList.remove('active'));
|
||||
|
||||
// Add active class to clicked button and corresponding content
|
||||
this.classList.add('active');
|
||||
const targetContent = document.querySelector(`#${targetTab}-tab`);
|
||||
if (targetContent) {
|
||||
targetContent.classList.add('active');
|
||||
}
|
||||
|
||||
console.log('Tab switched to:', targetTab);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,296 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Debug Members Component</title>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
.test-section { margin: 20px 0; padding: 20px; border: 1px solid #ddd; }
|
||||
button { padding: 10px 20px; margin: 5px; background: #007bff; color: white; border: none; cursor: pointer; }
|
||||
button:hover { background: #0056b3; }
|
||||
#graph-container { width: 600px; height: 400px; border: 1px solid #ccc; margin: 20px 0; }
|
||||
.status { padding: 10px; margin: 10px 0; border-radius: 4px; }
|
||||
.success { background: #d4edda; color: #155724; }
|
||||
.error { background: #f8d7da; color: #721c24; }
|
||||
.info { background: #d1ecf1; color: #0c5460; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🐛 Debug Members Component</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Component Test</h3>
|
||||
<p>Testing if the TopologyGraphComponent can be created and initialized.</p>
|
||||
<button onclick="testComponentCreation()">Test Component Creation</button>
|
||||
<button onclick="testInitialization()">Test Initialization</button>
|
||||
<button onclick="testMount()">Test Mount</button>
|
||||
<button onclick="clearTest()">Clear Test</button>
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Graph Container</h3>
|
||||
<div id="graph-container">
|
||||
<div style="text-align: center; padding: 50px; color: #666;">
|
||||
Graph will appear here after testing
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let testComponent = null;
|
||||
|
||||
function showStatus(message, type = 'info') {
|
||||
const statusDiv = document.getElementById('status');
|
||||
statusDiv.innerHTML = `<div class="status ${type}">${message}</div>`;
|
||||
}
|
||||
|
||||
// Mock base Component class
|
||||
class MockComponent {
|
||||
constructor(container, viewModel, eventBus) {
|
||||
this.container = container;
|
||||
this.viewModel = viewModel;
|
||||
this.eventBus = eventBus;
|
||||
this.isMounted = false;
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
console.log('MockComponent: initialize called');
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
mount() {
|
||||
console.log('MockComponent: mount called');
|
||||
this.isMounted = true;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
console.log('MockComponent: setupEventListeners called');
|
||||
}
|
||||
|
||||
setupViewModelListeners() {
|
||||
console.log('MockComponent: setupViewModelListeners called');
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log('MockComponent: render called');
|
||||
}
|
||||
}
|
||||
|
||||
// Mock ViewModel
|
||||
class MockViewModel {
|
||||
constructor() {
|
||||
this._data = {
|
||||
nodes: [],
|
||||
links: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
get(property) {
|
||||
return this._data[property];
|
||||
}
|
||||
|
||||
set(property, value) {
|
||||
this._data[property] = value;
|
||||
}
|
||||
|
||||
subscribe(property, callback) {
|
||||
console.log(`MockViewModel: Subscribing to ${property}`);
|
||||
}
|
||||
|
||||
async updateNetworkTopology() {
|
||||
console.log('MockViewModel: updateNetworkTopology called');
|
||||
// Simulate some data
|
||||
this.set('nodes', [
|
||||
{ id: '1', hostname: 'Test Node 1', ip: '192.168.1.1', status: 'ACTIVE' },
|
||||
{ id: '2', hostname: 'Test Node 2', ip: '192.168.1.2', status: 'ACTIVE' }
|
||||
]);
|
||||
this.set('links', [
|
||||
{ source: '1', target: '2', latency: 5 }
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Test TopologyGraphComponent (simplified version)
|
||||
class TestTopologyGraphComponent extends MockComponent {
|
||||
constructor(container, viewModel, eventBus) {
|
||||
super(container, viewModel, eventBus);
|
||||
console.log('TestTopologyGraphComponent: Constructor called');
|
||||
this.svg = null;
|
||||
this.simulation = null;
|
||||
this.zoom = null;
|
||||
this.width = 600;
|
||||
this.height = 400;
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
// Override mount to ensure proper initialization
|
||||
mount() {
|
||||
if (this.isMounted) return;
|
||||
|
||||
console.log('TestTopologyGraphComponent: Starting mount...');
|
||||
|
||||
// Call initialize if not already done
|
||||
if (!this.isInitialized) {
|
||||
console.log('TestTopologyGraphComponent: Initializing during mount...');
|
||||
this.initialize().then(() => {
|
||||
// Complete mount after initialization
|
||||
this.completeMount();
|
||||
}).catch(error => {
|
||||
console.error('TestTopologyGraphComponent: Initialization failed during mount:', error);
|
||||
// Still complete mount to prevent blocking
|
||||
this.completeMount();
|
||||
});
|
||||
} else {
|
||||
this.completeMount();
|
||||
}
|
||||
}
|
||||
|
||||
completeMount() {
|
||||
this.isMounted = true;
|
||||
this.setupEventListeners();
|
||||
this.setupViewModelListeners();
|
||||
this.render();
|
||||
|
||||
console.log('TestTopologyGraphComponent: Mounted successfully');
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
console.log('TestTopologyGraphComponent: Initializing...');
|
||||
await super.initialize();
|
||||
|
||||
// Set up the SVG container
|
||||
this.setupSVG();
|
||||
|
||||
// Mark as initialized
|
||||
this.isInitialized = true;
|
||||
|
||||
console.log('TestTopologyGraphComponent: Initialization completed');
|
||||
}
|
||||
|
||||
setupSVG() {
|
||||
const container = document.getElementById('graph-container');
|
||||
if (!container) {
|
||||
console.error('TestTopologyGraphComponent: Graph container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing content
|
||||
container.innerHTML = '';
|
||||
|
||||
// Create SVG element
|
||||
this.svg = d3.select(container)
|
||||
.append('svg')
|
||||
.attr('width', this.width)
|
||||
.attr('height', this.height)
|
||||
.style('border', '1px solid #ddd')
|
||||
.style('background', '#f9f9f9');
|
||||
|
||||
this.svg.append('g');
|
||||
|
||||
console.log('TestTopologyGraphComponent: SVG setup completed');
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log('TestTopologyGraphComponent: render called');
|
||||
// Simple render for testing
|
||||
if (this.svg) {
|
||||
const svgGroup = this.svg.select('g');
|
||||
svgGroup.append('text')
|
||||
.attr('x', 300)
|
||||
.attr('y', 200)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', '16px')
|
||||
.text('Component Rendered Successfully!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function testComponentCreation() {
|
||||
try {
|
||||
showStatus('🔄 Testing component creation...', 'info');
|
||||
|
||||
const mockViewModel = new MockViewModel();
|
||||
const mockEventBus = {};
|
||||
|
||||
testComponent = new TestTopologyGraphComponent(
|
||||
document.getElementById('graph-container'),
|
||||
mockViewModel,
|
||||
mockEventBus
|
||||
);
|
||||
|
||||
showStatus('✅ Component created successfully', 'success');
|
||||
console.log('Component created:', testComponent);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Component creation failed:', error);
|
||||
showStatus(`❌ Component creation failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function testInitialization() {
|
||||
if (!testComponent) {
|
||||
showStatus('❌ No component created. Run component creation test first.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showStatus('🔄 Testing initialization...', 'info');
|
||||
|
||||
testComponent.initialize().then(() => {
|
||||
showStatus('✅ Component initialized successfully', 'success');
|
||||
}).catch(error => {
|
||||
console.error('Initialization failed:', error);
|
||||
showStatus(`❌ Initialization failed: ${error.message}`, 'error');
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Initialization test failed:', error);
|
||||
showStatus(`❌ Initialization test failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function testMount() {
|
||||
if (!testComponent) {
|
||||
showStatus('❌ No component created. Run component creation test first.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showStatus('🔄 Testing mount...', 'info');
|
||||
|
||||
testComponent.mount();
|
||||
|
||||
showStatus('✅ Component mounted successfully', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Mount test failed:', error);
|
||||
showStatus(`❌ Mount test failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function clearTest() {
|
||||
if (testComponent) {
|
||||
testComponent = null;
|
||||
}
|
||||
document.getElementById('graph-container').innerHTML =
|
||||
'<div style="text-align: center; padding: 50px; color: #666;">Graph will appear here after testing</div>';
|
||||
document.getElementById('status').innerHTML = '';
|
||||
showStatus('🧹 Test cleared', 'info');
|
||||
}
|
||||
|
||||
// Auto-test on load
|
||||
window.addEventListener('load', () => {
|
||||
showStatus('🚀 Debug page loaded. Click "Test Component Creation" to begin.', 'info');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,286 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Members View Fix</title>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
.test-section { margin: 20px 0; padding: 20px; border: 1px solid #ddd; }
|
||||
button { padding: 10px 20px; margin: 5px; background: #007bff; color: white; border: none; cursor: pointer; }
|
||||
button:hover { background: #0056b3; }
|
||||
#graph-container { width: 600px; height: 400px; border: 1px solid #ccc; margin: 20px 0; }
|
||||
.status { padding: 10px; margin: 10px 0; border-radius: 4px; }
|
||||
.success { background: #d4edda; color: #155724; }
|
||||
.error { background: #f8d7da; color: #721c24; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🧪 Test Members View Fix</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Initialization Test</h3>
|
||||
<p>Testing the fixed initialization order for the Members view component.</p>
|
||||
<button onclick="testInitialization()">Test Initialization</button>
|
||||
<button onclick="testDataUpdate()">Test Data Update</button>
|
||||
<button onclick="clearTest()">Clear Test</button>
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Graph Container</h3>
|
||||
<div id="graph-container">
|
||||
<div style="text-align: center; padding: 50px; color: #666;">
|
||||
Graph will appear here after initialization
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let testComponent = null;
|
||||
let testViewModel = null;
|
||||
|
||||
function showStatus(message, type = 'success') {
|
||||
const statusDiv = document.getElementById('status');
|
||||
statusDiv.innerHTML = `<div class="status ${type}">${message}</div>`;
|
||||
}
|
||||
|
||||
// Mock ViewModel for testing
|
||||
class TestViewModel {
|
||||
constructor() {
|
||||
this._data = {
|
||||
nodes: [],
|
||||
links: [],
|
||||
isLoading: false,
|
||||
error: null
|
||||
};
|
||||
this._listeners = new Map();
|
||||
}
|
||||
|
||||
get(property) {
|
||||
return this._data[property];
|
||||
}
|
||||
|
||||
set(property, value) {
|
||||
this._data[property] = value;
|
||||
this._notifyListeners(property, value);
|
||||
}
|
||||
|
||||
subscribe(property, callback) {
|
||||
if (!this._listeners.has(property)) {
|
||||
this._listeners.set(property, []);
|
||||
}
|
||||
this._listeners.get(property).push(callback);
|
||||
}
|
||||
|
||||
_notifyListeners(property, value) {
|
||||
if (this._listeners.has(property)) {
|
||||
this._listeners.get(property).forEach(callback => {
|
||||
try {
|
||||
callback(value);
|
||||
} catch (error) {
|
||||
console.error(`Error in property listener for ${property}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test Component for testing
|
||||
class TestMembersComponent {
|
||||
constructor(container, viewModel) {
|
||||
this.container = container;
|
||||
this.viewModel = viewModel;
|
||||
this.svg = null;
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
console.log('TestMembersComponent: Initializing...');
|
||||
|
||||
// Simulate async initialization
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Set up SVG
|
||||
this.setupSVG();
|
||||
|
||||
// Mark as initialized
|
||||
this.isInitialized = true;
|
||||
|
||||
// Set up listeners AFTER initialization
|
||||
this.viewModel.subscribe('nodes', this.renderGraph.bind(this));
|
||||
this.viewModel.subscribe('links', this.renderGraph.bind(this));
|
||||
|
||||
console.log('TestMembersComponent: Initialization completed');
|
||||
showStatus('✅ Component initialized successfully', 'success');
|
||||
}
|
||||
|
||||
setupSVG() {
|
||||
const container = document.getElementById('graph-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
this.svg = d3.select(container)
|
||||
.append('svg')
|
||||
.attr('width', 600)
|
||||
.attr('height', 400)
|
||||
.style('border', '1px solid #ddd')
|
||||
.style('background', '#f9f9f9');
|
||||
|
||||
this.svg.append('g');
|
||||
console.log('TestMembersComponent: SVG setup completed');
|
||||
}
|
||||
|
||||
renderGraph() {
|
||||
try {
|
||||
// Check if component is initialized
|
||||
if (!this.isInitialized) {
|
||||
console.log('TestMembersComponent: Component not yet initialized, skipping render');
|
||||
return;
|
||||
}
|
||||
|
||||
const nodes = this.viewModel.get('nodes');
|
||||
const links = this.viewModel.get('links');
|
||||
|
||||
// Check if SVG is initialized
|
||||
if (!this.svg) {
|
||||
console.log('TestMembersComponent: SVG not initialized yet, setting up SVG first');
|
||||
this.setupSVG();
|
||||
}
|
||||
|
||||
if (!nodes || nodes.length === 0) {
|
||||
this.showNoData();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('TestMembersComponent: Rendering graph with', nodes.length, 'nodes and', links.length, 'links');
|
||||
|
||||
const svgGroup = this.svg.select('g');
|
||||
if (!svgGroup || svgGroup.empty()) {
|
||||
console.error('TestMembersComponent: SVG group not found, cannot render graph');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing elements
|
||||
svgGroup.selectAll('*').remove();
|
||||
|
||||
// Create simple nodes
|
||||
const node = svgGroup.append('g')
|
||||
.selectAll('g')
|
||||
.data(nodes)
|
||||
.enter().append('g')
|
||||
.attr('class', 'node')
|
||||
.attr('transform', (d, i) => `translate(${100 + i * 150}, 200)`);
|
||||
|
||||
node.append('circle')
|
||||
.attr('r', 20)
|
||||
.attr('fill', '#4CAF50');
|
||||
|
||||
node.append('text')
|
||||
.text(d => d.name)
|
||||
.attr('x', 0)
|
||||
.attr('y', 30)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', '12px');
|
||||
|
||||
// Create simple links
|
||||
if (links.length > 0) {
|
||||
const link = svgGroup.append('g')
|
||||
.selectAll('line')
|
||||
.data(links)
|
||||
.enter().append('line')
|
||||
.attr('stroke', '#999')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('x1', (d, i) => 100 + i * 150)
|
||||
.attr('y1', 200)
|
||||
.attr('x2', (d, i) => 100 + (i + 1) * 150)
|
||||
.attr('y2', 200);
|
||||
}
|
||||
|
||||
showStatus('✅ Graph rendered successfully', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to render graph:', error);
|
||||
showStatus(`❌ Graph rendering failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
showNoData() {
|
||||
const container = document.getElementById('graph-container');
|
||||
container.innerHTML = '<div style="text-align: center; padding: 50px; color: #666;">No data available</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function testInitialization() {
|
||||
try {
|
||||
showStatus('🔄 Testing initialization...', 'success');
|
||||
|
||||
// Create test view model and component
|
||||
testViewModel = new TestViewModel();
|
||||
testComponent = new TestMembersComponent(
|
||||
document.getElementById('graph-container'),
|
||||
testViewModel
|
||||
);
|
||||
|
||||
// Initialize component
|
||||
await testComponent.initialize();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Initialization test failed:', error);
|
||||
showStatus(`❌ Initialization test failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function testDataUpdate() {
|
||||
if (!testComponent || !testComponent.isInitialized) {
|
||||
showStatus('❌ Component not initialized. Run initialization test first.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showStatus('🔄 Testing data update...', 'success');
|
||||
|
||||
// Update with test data
|
||||
const testNodes = [
|
||||
{ name: 'Node 1', id: '1' },
|
||||
{ name: 'Node 2', id: '2' },
|
||||
{ name: 'Node 3', id: '3' }
|
||||
];
|
||||
|
||||
const testLinks = [
|
||||
{ source: '1', target: '2' },
|
||||
{ source: '2', target: '3' }
|
||||
];
|
||||
|
||||
testViewModel.set('nodes', testNodes);
|
||||
testViewModel.set('links', testLinks);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Data update test failed:', error);
|
||||
showStatus(`❌ Data update test failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function clearTest() {
|
||||
if (testComponent) {
|
||||
testComponent = null;
|
||||
}
|
||||
if (testViewModel) {
|
||||
testViewModel = null;
|
||||
}
|
||||
document.getElementById('graph-container').innerHTML =
|
||||
'<div style="text-align: center; padding: 50px; color: #666;">Graph will appear here after initialization</div>';
|
||||
document.getElementById('status').innerHTML = '';
|
||||
showStatus('🧹 Test cleared', 'success');
|
||||
}
|
||||
|
||||
// Auto-test on load
|
||||
window.addEventListener('load', () => {
|
||||
showStatus('🚀 Test page loaded. Click "Test Initialization" to begin.', 'success');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,63 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Topology with Member Card Overlay</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="./d3.v7.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Test Topology with Member Card Overlay</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Topology View with Clickable Nodes</h2>
|
||||
<p>Click on any node in the topology to see the member card overlay.</p>
|
||||
<button id="refresh-topology-btn">Refresh Topology</button>
|
||||
</div>
|
||||
|
||||
<div id="topology-graph-container">
|
||||
<div class="loading">
|
||||
<div>Loading network topology...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="member-card-overlay" class="member-card-overlay"></div>
|
||||
</div>
|
||||
|
||||
<script src="./demo-topology-data.js"></script>
|
||||
<script src="./framework.js"></script>
|
||||
<script src="./components.js"></script>
|
||||
<script>
|
||||
// Test the topology view with member card overlay
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Setting up test topology with member card overlay...');
|
||||
|
||||
// Override the API client with demo data
|
||||
window.apiClient = window.demoApiClient;
|
||||
|
||||
// Create topology view model
|
||||
const topologyVM = new TopologyViewModel();
|
||||
|
||||
// Create the topology component
|
||||
const topologyComponent = new TopologyGraphComponent(
|
||||
document.getElementById('topology-graph-container'),
|
||||
topologyVM,
|
||||
new EventBus()
|
||||
);
|
||||
|
||||
// Mount the component
|
||||
topologyComponent.mount();
|
||||
|
||||
// Refresh button
|
||||
const refreshBtn = document.getElementById('refresh-topology-btn');
|
||||
refreshBtn.addEventListener('click', function() {
|
||||
topologyVM.updateNetworkTopology();
|
||||
});
|
||||
|
||||
console.log('Test topology setup complete');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,254 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Members View</title>
|
||||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.test-section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.test-section h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
button {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background: #1976D2;
|
||||
}
|
||||
#graph-container {
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
border: 1px solid #ddd;
|
||||
background: #f9f9f9;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||||
.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||||
.info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🧪 Test Members View</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>D3.js Integration Test</h3>
|
||||
<p>Testing D3.js library integration and basic force-directed graph functionality.</p>
|
||||
<button onclick="testD3Integration()">Test D3.js Integration</button>
|
||||
<button onclick="testForceGraph()">Test Force Graph</button>
|
||||
<button onclick="clearGraph()">Clear Graph</button>
|
||||
<div id="status"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Graph Visualization</h3>
|
||||
<div id="graph-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let svg = null;
|
||||
let simulation = null;
|
||||
|
||||
function showStatus(message, type = 'info') {
|
||||
const statusDiv = document.getElementById('status');
|
||||
statusDiv.innerHTML = `<div class="status ${type}">${message}</div>`;
|
||||
}
|
||||
|
||||
function testD3Integration() {
|
||||
try {
|
||||
if (typeof d3 === 'undefined') {
|
||||
throw new Error('D3.js is not loaded');
|
||||
}
|
||||
|
||||
const version = d3.version;
|
||||
showStatus(`✅ D3.js v${version} loaded successfully!`, 'success');
|
||||
|
||||
// Test basic D3 functionality
|
||||
const testData = [1, 2, 3, 4, 5];
|
||||
const testSelection = d3.select('#graph-container');
|
||||
testSelection.append('div')
|
||||
.attr('class', 'test-div')
|
||||
.text('D3 selection test');
|
||||
|
||||
showStatus('✅ D3.js basic functionality working!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
showStatus(`❌ D3.js test failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function testForceGraph() {
|
||||
try {
|
||||
if (!svg) {
|
||||
setupSVG();
|
||||
}
|
||||
|
||||
// Create sample data
|
||||
const nodes = [
|
||||
{ id: 'node1', name: 'Node 1', status: 'ACTIVE' },
|
||||
{ id: 'node2', name: 'Node 2', status: 'ACTIVE' },
|
||||
{ id: 'node3', name: 'Node 3', status: 'INACTIVE' },
|
||||
{ id: 'node4', name: 'Node 4', status: 'ACTIVE' }
|
||||
];
|
||||
|
||||
const links = [
|
||||
{ source: 'node1', target: 'node2', latency: 5 },
|
||||
{ source: 'node1', target: 'node3', latency: 15 },
|
||||
{ source: 'node2', target: 'node4', latency: 8 },
|
||||
{ source: 'node3', target: 'node4', latency: 25 }
|
||||
];
|
||||
|
||||
renderGraph(nodes, links);
|
||||
showStatus('✅ Force-directed graph created successfully!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
showStatus(`❌ Force graph test failed: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function setupSVG() {
|
||||
const container = document.getElementById('graph-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
svg = d3.select(container)
|
||||
.append('svg')
|
||||
.attr('width', 800)
|
||||
.attr('height', 600)
|
||||
.style('border', '1px solid #ddd')
|
||||
.style('background', '#f9f9f9');
|
||||
}
|
||||
|
||||
function renderGraph(nodes, links) {
|
||||
if (!svg) return;
|
||||
|
||||
const svgGroup = svg.append('g');
|
||||
|
||||
// Create links
|
||||
const link = svgGroup.append('g')
|
||||
.selectAll('line')
|
||||
.data(links)
|
||||
.enter().append('line')
|
||||
.attr('stroke', '#999')
|
||||
.attr('stroke-opacity', 0.6)
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// Create nodes
|
||||
const node = svgGroup.append('g')
|
||||
.selectAll('g')
|
||||
.data(nodes)
|
||||
.enter().append('g')
|
||||
.attr('class', 'node')
|
||||
.call(d3.drag()
|
||||
.on('start', dragstarted)
|
||||
.on('drag', dragged)
|
||||
.on('end', dragended));
|
||||
|
||||
// Add circles to nodes
|
||||
node.append('circle')
|
||||
.attr('r', 8)
|
||||
.attr('fill', d => getNodeColor(d.status));
|
||||
|
||||
// Add labels
|
||||
node.append('text')
|
||||
.text(d => d.name)
|
||||
.attr('x', 12)
|
||||
.attr('y', 4)
|
||||
.attr('font-size', '12px');
|
||||
|
||||
// Set up force simulation
|
||||
simulation = d3.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(links).id(d => d.id).distance(100))
|
||||
.force('charge', d3.forceManyBody().strength(-300))
|
||||
.force('center', d3.forceCenter(400, 300));
|
||||
|
||||
// Update positions on simulation tick
|
||||
simulation.on('tick', () => {
|
||||
link
|
||||
.attr('x1', d => d.source.x)
|
||||
.attr('y1', d => d.source.y)
|
||||
.attr('x2', d => d.target.x)
|
||||
.attr('y2', d => d.target.y);
|
||||
|
||||
node
|
||||
.attr('transform', d => `translate(${d.x},${d.y})`);
|
||||
});
|
||||
}
|
||||
|
||||
function getNodeColor(status) {
|
||||
switch (status) {
|
||||
case 'ACTIVE': return '#4CAF50';
|
||||
case 'INACTIVE': return '#FF9800';
|
||||
default: return '#9E9E9E';
|
||||
}
|
||||
}
|
||||
|
||||
function dragstarted(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
}
|
||||
|
||||
function dragged(event, d) {
|
||||
d.fx = event.x;
|
||||
d.fy = event.y;
|
||||
}
|
||||
|
||||
function dragended(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
d.fx = null;
|
||||
d.fy = null;
|
||||
}
|
||||
|
||||
function clearGraph() {
|
||||
if (svg) {
|
||||
svg.selectAll('*').remove();
|
||||
svg = null;
|
||||
}
|
||||
if (simulation) {
|
||||
simulation.stop();
|
||||
simulation = null;
|
||||
}
|
||||
showStatus('Graph cleared', 'info');
|
||||
}
|
||||
|
||||
// Auto-test on load
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
testD3Integration();
|
||||
}, 500);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
Reference in New Issue
Block a user