Compare commits
9 Commits
d91eb4d5b6
...
83d252f3cc
| Author | SHA1 | Date | |
|---|---|---|---|
| 83d252f3cc | |||
| 948a8a1fab | |||
| 9dab498aa2 | |||
| 4ee209ef78 | |||
| ab03cd772d | |||
| 1bdaed9a2c | |||
| b757cb68da | |||
| f18907d9e4 | |||
| c0aef5b8d5 |
@@ -1,266 +0,0 @@
|
||||
# SPORE UI State Preservation System
|
||||
|
||||
## Overview
|
||||
|
||||
The SPORE UI framework now includes an advanced state preservation system that prevents UI state loss during data refreshes. This system ensures that user interactions like expanded cards, active tabs, and other UI state are maintained when data is updated from the server.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. **UI State Persistence**
|
||||
- **Expanded Cards**: When cluster member cards are expanded, their state is preserved across data refreshes
|
||||
- **Active Tabs**: Active tab selections within node detail views are maintained
|
||||
- **User Interactions**: All user-initiated UI changes are stored and restored automatically
|
||||
|
||||
### 2. **Smart Data Updates**
|
||||
- **Change Detection**: The system detects when data has actually changed and only updates what's necessary
|
||||
- **Partial Updates**: Components can update specific data without re-rendering the entire UI
|
||||
- **State Preservation**: UI state is automatically preserved during all data operations
|
||||
|
||||
### 3. **Efficient Rendering**
|
||||
- **No Full Re-renders**: Components avoid unnecessary full re-renders when only data changes
|
||||
- **Granular Updates**: Only changed properties trigger UI updates
|
||||
- **Performance Optimization**: Reduced DOM manipulation and improved user experience
|
||||
|
||||
## Architecture
|
||||
|
||||
### Enhanced ViewModel Class
|
||||
|
||||
The base `ViewModel` class now includes:
|
||||
|
||||
```javascript
|
||||
class ViewModel {
|
||||
// UI State Management
|
||||
setUIState(key, value) // Store UI state
|
||||
getUIState(key) // Retrieve UI state
|
||||
getAllUIState() // Get all stored UI state
|
||||
clearUIState(key) // Clear specific or all UI state
|
||||
|
||||
// Change Detection
|
||||
hasChanged(property) // Check if property changed
|
||||
getPrevious(property) // Get previous value
|
||||
|
||||
// Batch Updates
|
||||
batchUpdate(updates, options) // Update multiple properties with state preservation
|
||||
}
|
||||
```
|
||||
|
||||
### Enhanced Component Class
|
||||
|
||||
The base `Component` class now includes:
|
||||
|
||||
```javascript
|
||||
class Component {
|
||||
// UI State Management
|
||||
setUIState(key, value) // Store local UI state
|
||||
getUIState(key) // Get local or view model state
|
||||
getAllUIState() // Get merged state
|
||||
restoreUIState() // Restore state from view model
|
||||
|
||||
// Partial Updates
|
||||
updatePartial(property, newValue, previousValue) // Handle partial updates
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Examples
|
||||
|
||||
### 1. **Cluster Members Component**
|
||||
|
||||
The `ClusterMembersComponent` demonstrates state preservation:
|
||||
|
||||
```javascript
|
||||
class ClusterMembersComponent extends Component {
|
||||
setupViewModelListeners() {
|
||||
// Listen with change detection
|
||||
this.subscribeToProperty('members', this.handleMembersUpdate.bind(this));
|
||||
}
|
||||
|
||||
handleMembersUpdate(newMembers, previousMembers) {
|
||||
if (this.shouldPreserveState(newMembers, previousMembers)) {
|
||||
// Partial update preserves UI state
|
||||
this.updateMembersPartially(newMembers, previousMembers);
|
||||
} else {
|
||||
// Full re-render only when necessary
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
shouldPreserveState(newMembers, previousMembers) {
|
||||
// Check if member structure allows state preservation
|
||||
if (newMembers.length !== previousMembers.length) return false;
|
||||
|
||||
const newIps = new Set(newMembers.map(m => m.ip));
|
||||
const prevIps = new Set(previousMembers.map(m => m.ip));
|
||||
|
||||
return newIps.size === prevIps.size &&
|
||||
[...newIps].every(ip => prevIps.has(ip));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Node Details Component**
|
||||
|
||||
The `NodeDetailsComponent` preserves active tab state:
|
||||
|
||||
```javascript
|
||||
class NodeDetailsComponent extends Component {
|
||||
setupViewModelListeners() {
|
||||
this.subscribeToProperty('activeTab', this.handleActiveTabUpdate.bind(this));
|
||||
}
|
||||
|
||||
handleActiveTabUpdate(newTab, previousTab) {
|
||||
// Update tab UI without full re-render
|
||||
this.updateActiveTab(newTab, previousTab);
|
||||
}
|
||||
|
||||
updateActiveTab(newTab) {
|
||||
// Update only the tab UI, preserving other state
|
||||
const tabButtons = this.findAllElements('.tab-button');
|
||||
const tabContents = this.findAllElements('.tab-content');
|
||||
|
||||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
||||
tabContents.forEach(content => content.classList.remove('active'));
|
||||
|
||||
const activeButton = this.findElement(`[data-tab="${newTab}"]`);
|
||||
const activeContent = this.findElement(`#${newTab}-tab`);
|
||||
|
||||
if (activeButton) activeButton.classList.add('active');
|
||||
if (activeContent) activeContent.classList.add('active');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. **Storing UI State**
|
||||
|
||||
```javascript
|
||||
// In a component
|
||||
this.setUIState('expandedCard', memberIp);
|
||||
this.setUIState('activeTab', 'firmware');
|
||||
|
||||
// In a view model
|
||||
this.setUIState('userPreferences', { theme: 'dark', layout: 'compact' });
|
||||
```
|
||||
|
||||
### 2. **Retrieving UI State**
|
||||
|
||||
```javascript
|
||||
// Get specific state
|
||||
const expandedCard = this.getUIState('expandedCard');
|
||||
const activeTab = this.getUIState('activeTab');
|
||||
|
||||
// Get all state
|
||||
const allState = this.getAllUIState();
|
||||
```
|
||||
|
||||
### 3. **Batch Updates with State Preservation**
|
||||
|
||||
```javascript
|
||||
// Update data while preserving UI state
|
||||
this.viewModel.batchUpdate({
|
||||
members: newMembers,
|
||||
lastUpdateTime: new Date().toISOString()
|
||||
}, { preserveUIState: true });
|
||||
```
|
||||
|
||||
### 4. **Smart Updates**
|
||||
|
||||
```javascript
|
||||
// Use smart update to preserve state
|
||||
await this.viewModel.smartUpdate();
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
### 1. **Improved User Experience**
|
||||
- Users don't lose their place in the interface
|
||||
- Expanded cards remain expanded
|
||||
- Active tabs stay selected
|
||||
- No jarring UI resets
|
||||
|
||||
### 2. **Better Performance**
|
||||
- Reduced unnecessary DOM manipulation
|
||||
- Efficient partial updates
|
||||
- Optimized rendering cycles
|
||||
|
||||
### 3. **Maintainable Code**
|
||||
- Clear separation of concerns
|
||||
- Consistent state management patterns
|
||||
- Easy to extend and modify
|
||||
|
||||
## Testing
|
||||
|
||||
Use the `test-state-preservation.html` file to test the state preservation system:
|
||||
|
||||
1. **Expand cluster member cards**
|
||||
2. **Change active tabs in node details**
|
||||
3. **Trigger data refresh**
|
||||
4. **Verify state is preserved**
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Old System
|
||||
|
||||
If you're upgrading from the old system:
|
||||
|
||||
1. **Update ViewModel Listeners**: Change from `this.render.bind(this)` to specific update handlers
|
||||
2. **Add State Management**: Use `setUIState()` and `getUIState()` for UI state
|
||||
3. **Implement Partial Updates**: Override `updatePartial()` method for efficient updates
|
||||
4. **Use Smart Updates**: Replace direct data updates with `smartUpdate()` calls
|
||||
|
||||
### Example Migration
|
||||
|
||||
**Old Code:**
|
||||
```javascript
|
||||
this.subscribeToProperty('members', this.render.bind(this));
|
||||
|
||||
async handleRefresh() {
|
||||
await this.viewModel.updateClusterMembers();
|
||||
}
|
||||
```
|
||||
|
||||
**New Code:**
|
||||
```javascript
|
||||
this.subscribeToProperty('members', this.handleMembersUpdate.bind(this));
|
||||
|
||||
async handleRefresh() {
|
||||
await this.viewModel.smartUpdate();
|
||||
}
|
||||
|
||||
handleMembersUpdate(newMembers, previousMembers) {
|
||||
if (this.shouldPreserveState(newMembers, previousMembers)) {
|
||||
this.updateMembersPartially(newMembers, previousMembers);
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always Store UI State**: Use `setUIState()` for any user interaction
|
||||
2. **Implement Partial Updates**: Override `updatePartial()` for efficient updates
|
||||
3. **Use Change Detection**: Leverage `hasChanged()` to avoid unnecessary updates
|
||||
4. **Batch Related Updates**: Use `batchUpdate()` for multiple property changes
|
||||
5. **Test State Preservation**: Verify that UI state is maintained during data refreshes
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **State Not Preserved**: Ensure you're using `setUIState()` and `getUIState()`
|
||||
2. **Full Re-renders**: Check if `shouldPreserveState()` logic is correct
|
||||
3. **Performance Issues**: Verify you're using partial updates instead of full renders
|
||||
|
||||
### Debug Tips
|
||||
|
||||
1. **Enable Console Logging**: Check browser console for state preservation logs
|
||||
2. **Use State Indicators**: Monitor state changes in the test interface
|
||||
3. **Verify Change Detection**: Ensure `hasChanged()` is working correctly
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **State Synchronization**: Real-time state sync across multiple browser tabs
|
||||
- **Advanced Change Detection**: Deep object comparison for complex data structures
|
||||
- **State Persistence**: Save UI state to localStorage for session persistence
|
||||
- **State Rollback**: Ability to revert to previous UI states
|
||||
@@ -1,146 +0,0 @@
|
||||
# Topology View - Network Topology Visualization
|
||||
|
||||
## Overview
|
||||
|
||||
The Topology view provides an interactive, force-directed graph visualization of the SPORE cluster network topology. It displays each cluster member as a node and shows the connections (links) between them with latency information.
|
||||
|
||||
## Features
|
||||
|
||||
### 🎯 **Interactive Network Graph**
|
||||
- **Force-directed layout**: Nodes automatically arrange themselves based on connections
|
||||
- **Zoom and pan**: Navigate through large network topologies
|
||||
- **Drag and drop**: Reposition nodes manually for better visualization
|
||||
- **Responsive design**: Adapts to different screen sizes
|
||||
|
||||
### 📊 **Node Information**
|
||||
- **Status indicators**: Color-coded nodes based on member status (ACTIVE, INACTIVE, DEAD)
|
||||
- **Hostname display**: Shows the human-readable name of each node
|
||||
- **IP addresses**: Displays the network address of each member
|
||||
- **Resource information**: Access to system resources and capabilities
|
||||
|
||||
### 🔗 **Connection Visualization**
|
||||
- **Latency display**: Shows network latency between connected nodes
|
||||
- **Color-coded links**: Different colors indicate latency ranges:
|
||||
- 🟢 Green: ≤5ms (excellent)
|
||||
- 🟠 Orange: 6-15ms (good)
|
||||
- 🔴 Red-orange: 16-30ms (fair)
|
||||
- 🔴 Red: >30ms (poor)
|
||||
- **Bidirectional connections**: Shows actual network topology from each node's perspective
|
||||
|
||||
### 🎨 **Visual Enhancements**
|
||||
- **Legend**: Explains node status colors and latency ranges
|
||||
- **Hover effects**: Interactive feedback when hovering over nodes and links
|
||||
- **Selection highlighting**: Click nodes to select and highlight them
|
||||
- **Smooth animations**: Force simulation provides natural movement
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Architecture
|
||||
- **ViewModel**: `TopologyViewModel` manages data and state
|
||||
- **Component**: `TopologyGraphComponent` handles rendering and interactions
|
||||
- **Framework**: Integrates with the existing SPORE UI framework
|
||||
- **Library**: Uses D3.js v7 for graph visualization
|
||||
|
||||
### Data Flow
|
||||
1. **Primary node query**: Fetches cluster members from the primary node
|
||||
2. **Individual node queries**: Gets cluster view from each member node
|
||||
3. **Topology building**: Constructs network graph from actual connections
|
||||
4. **Fallback mesh**: Creates basic mesh if no actual connections found
|
||||
|
||||
### API Endpoints
|
||||
- `/api/cluster/members` - Get cluster membership from primary node
|
||||
- `/api/cluster/members?ip={nodeIP}` - Get cluster view from specific node
|
||||
|
||||
## Usage
|
||||
|
||||
### Navigation
|
||||
1. Click the "🌐 Topology" tab in the main navigation
|
||||
2. The view automatically loads and displays the network topology
|
||||
3. Use the refresh button to update the visualization
|
||||
|
||||
### Interaction
|
||||
- **Zoom**: Use mouse wheel or pinch gestures
|
||||
- **Pan**: Click and drag on empty space
|
||||
- **Select nodes**: Click on any node to highlight it
|
||||
- **Move nodes**: Drag nodes to reposition them
|
||||
- **Hover**: Hover over nodes and links for additional information
|
||||
|
||||
### Refresh
|
||||
- Click the "Refresh" button to reload network topology data
|
||||
- Useful after network changes or when adding/removing nodes
|
||||
|
||||
## Configuration
|
||||
|
||||
### Graph Parameters
|
||||
- **Node spacing**: 120px between connected nodes
|
||||
- **Repulsion force**: -400 strength for node separation
|
||||
- **Collision radius**: 40px minimum distance between nodes
|
||||
- **Zoom limits**: 0.1x to 4x zoom range
|
||||
|
||||
### Visual Settings
|
||||
- **Node sizes**: Vary based on status (ACTIVE: 10px, INACTIVE: 8px, DEAD: 6px)
|
||||
- **Link thickness**: Proportional to latency (2-8px range)
|
||||
- **Colors**: Semantic color scheme for status and latency
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### No Graph Displayed
|
||||
- Check browser console for JavaScript errors
|
||||
- Verify D3.js library is loading correctly
|
||||
- Ensure cluster has discovered nodes
|
||||
|
||||
#### Missing Connections
|
||||
- Verify nodes are responding to API calls
|
||||
- Check network connectivity between nodes
|
||||
- Review cluster discovery configuration
|
||||
|
||||
#### Performance Issues
|
||||
- Reduce number of displayed nodes
|
||||
- Adjust force simulation parameters
|
||||
- Use zoom to focus on specific areas
|
||||
|
||||
### Debug Information
|
||||
- Test file available at `test-topology-view.html`
|
||||
- Console logging provides detailed component lifecycle information
|
||||
- Network topology data is logged during updates
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
- **Real-time updates**: WebSocket integration for live topology changes
|
||||
- **Metrics overlay**: CPU, memory, and network usage display
|
||||
- **Path finding**: Show routes between specific nodes
|
||||
- **Export options**: Save graph as image or data file
|
||||
- **Custom layouts**: Alternative visualization algorithms
|
||||
|
||||
### Performance Optimizations
|
||||
- **Lazy loading**: Load node details on demand
|
||||
- **Virtualization**: Handle large numbers of nodes efficiently
|
||||
- **Caching**: Store topology data locally
|
||||
- **Web Workers**: Offload computation to background threads
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **D3.js v7**: Force-directed graph visualization
|
||||
- **SPORE UI Framework**: Component architecture and state management
|
||||
- **Modern Browser**: ES6+ support required
|
||||
- **Network Access**: Ability to reach cluster nodes
|
||||
|
||||
## Browser Support
|
||||
|
||||
- **Chrome**: 80+ (recommended)
|
||||
- **Firefox**: 75+
|
||||
- **Safari**: 13+
|
||||
- **Edge**: 80+
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute to the Members view:
|
||||
|
||||
1. Follow the existing code style and patterns
|
||||
2. Test with different cluster configurations
|
||||
3. Ensure responsive design works on mobile devices
|
||||
4. Add appropriate error handling and logging
|
||||
5. Update documentation for new features
|
||||
@@ -1,223 +0,0 @@
|
||||
# View Switching Fixes for Member Card Issues
|
||||
|
||||
## Problem Description
|
||||
|
||||
When switching between the cluster and firmware views, member cards were experiencing:
|
||||
- **Wrong UI state**: Expanded cards, active tabs, and other UI state was being lost
|
||||
- **Flickering**: Visual glitches and rapid re-rendering during view switches
|
||||
- **Broken functionality**: Member cards not working properly after view switches
|
||||
- **Inefficient rendering**: Components were completely unmounted and remounted on every view switch
|
||||
- **Incorrect state restoration**: UI state was incorrectly restored on first load (all cards expanded, wrong tabs active)
|
||||
|
||||
## Root Causes Identified
|
||||
|
||||
1. **Aggressive DOM Manipulation**: Complete component unmounting/remounting on every view switch
|
||||
2. **Race Conditions**: Multiple async operations and timeouts interfering with each other
|
||||
3. **State Loss**: UI state not properly preserved across view switches
|
||||
4. **Rapid Navigation**: Multiple rapid clicks could cause navigation conflicts
|
||||
5. **CSS Transition Conflicts**: Multiple transitions causing visual flickering
|
||||
6. **No Component Caching**: Every view switch created new component instances
|
||||
7. **Complex State Restoration**: Attempting to restore UI state caused incorrect behavior on first load
|
||||
|
||||
## Fixes Implemented
|
||||
|
||||
### 1. **Component Caching System** (`framework.js`)
|
||||
|
||||
- **Component Cache**: Components are created once and cached, never re-created
|
||||
- **Pause/Resume Pattern**: Components are paused (not unmounted) when switching away
|
||||
- **Pre-initialization**: Components are created during route registration for better performance
|
||||
- **Simple Show/Hide**: Components are just shown/hidden without touching UI state
|
||||
|
||||
### 2. **Enhanced Navigation System** (`framework.js`)
|
||||
|
||||
- **Debounced Navigation**: Added 300ms cooldown between navigation requests
|
||||
- **Navigation Queue**: Queues navigation requests when one is already in progress
|
||||
- **Smooth Transitions**: Added opacity transitions to prevent abrupt view changes
|
||||
- **No Component Destruction**: Components are kept alive and just paused/resumed
|
||||
|
||||
### 3. **Simplified State Management** (`view-models.js`)
|
||||
|
||||
- **No UI State Persistence**: Removed complex localStorage state restoration
|
||||
- **Clean State on Load**: Components start with default state (collapsed cards, status tab)
|
||||
- **No State Corruption**: Eliminates incorrect state restoration on first load
|
||||
|
||||
### 4. **Enhanced Component Lifecycle** (`components.js`)
|
||||
|
||||
- **Pause/Resume Methods**: Components can be paused and resumed without losing state
|
||||
- **Default State**: Member cards always start collapsed, tabs start on 'status'
|
||||
- **No State Restoration**: Components maintain their current state without external interference
|
||||
- **Render Guards**: Prevents multiple simultaneous render operations
|
||||
- **View Switch Detection**: Skips rendering during view transitions
|
||||
- **Improved Unmounting**: Better cleanup of timeouts and event listeners
|
||||
- **State Tracking**: Tracks if data has already been loaded to prevent unnecessary reloads
|
||||
|
||||
### 5. **CSS Improvements** (`styles.css`)
|
||||
|
||||
- **Smooth Transitions**: Added fade-in/fade-out animations for view switching
|
||||
- **Reduced Transition Times**: Shortened member card transitions from 0.3s to 0.2s
|
||||
- **Better Animations**: Improved expand/collapse animations for member cards
|
||||
- **Loading States**: Added fade-in animations for loading, error, and empty states
|
||||
|
||||
### 6. **View Model Enhancements**
|
||||
|
||||
- **Smart Updates**: Only updates changed data to minimize re-renders
|
||||
- **Change Detection**: Compares data before triggering updates
|
||||
- **Clean Initialization**: No complex state restoration logic
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Component Caching Flow
|
||||
|
||||
1. **Route Registration**: Components are created and cached during app initialization
|
||||
2. **Navigation**: When switching views, current component is paused (not unmounted)
|
||||
3. **State Preservation**: All component state, DOM, and event listeners remain intact
|
||||
4. **Resume**: When returning to a view, component is resumed from paused state
|
||||
5. **No Re-rendering**: Components maintain their exact state and appearance
|
||||
6. **Simple Show/Hide**: No complex state restoration, just show/hide components
|
||||
|
||||
### Pause/Resume Pattern
|
||||
|
||||
```javascript
|
||||
// Component is paused instead of unmounted
|
||||
onPause() {
|
||||
// Clear timers, pause operations
|
||||
// Component state and DOM remain intact
|
||||
}
|
||||
|
||||
onResume() {
|
||||
// Restore timers, resume operations
|
||||
// No re-rendering needed
|
||||
}
|
||||
```
|
||||
|
||||
### Navigation Flow
|
||||
|
||||
1. **Cooldown Check**: 300ms minimum between navigation requests
|
||||
2. **Queue Management**: Multiple requests queued and processed sequentially
|
||||
3. **Pause Current**: Current component paused (opacity: 0)
|
||||
4. **Show New View**: New view becomes visible with fade-in animation
|
||||
5. **Resume Component**: Cached component resumed from paused state
|
||||
6. **No Unmounting**: Components are never destroyed during view switches
|
||||
7. **No State Touch**: UI state is not modified during view switches
|
||||
|
||||
### State Management
|
||||
|
||||
- **Default State**: Member cards start collapsed, tabs start on 'status'
|
||||
- **No Persistence**: No localStorage state restoration
|
||||
- **Clean Initialization**: Components always start with predictable state
|
||||
- **No State Corruption**: Eliminates incorrect state restoration issues
|
||||
|
||||
### Render Optimization
|
||||
|
||||
- **No Re-rendering**: Components maintain their exact state across view switches
|
||||
- **Pause/Resume**: Components are paused instead of unmounted
|
||||
- **State Persistence**: All UI state preserved in memory (not localStorage)
|
||||
- **Change Detection**: Only updates changed data when resuming
|
||||
- **Default Behavior**: Always starts with clean, predictable state
|
||||
|
||||
## Testing
|
||||
|
||||
Use the test page `test-view-switching.html` to verify fixes:
|
||||
|
||||
1. **Rapid Switching Test**: Clicks navigation tabs rapidly to test cooldown
|
||||
2. **State Preservation Test**: Expands cards, switches views, verifies state restoration
|
||||
3. **Component Caching Test**: Verify components are not re-created on view switches
|
||||
4. **Default State Test**: Verify components start with correct default state
|
||||
5. **Console Monitoring**: Check console for detailed operation logs
|
||||
|
||||
## Expected Results
|
||||
|
||||
After implementing these fixes:
|
||||
|
||||
- ✅ **No More Re-rendering**: Components are cached and never re-created
|
||||
- ✅ **No More Flickering**: Smooth transitions between views
|
||||
- ✅ **Correct Default State**: Member cards start collapsed, tabs start on 'status'
|
||||
- ✅ **No State Corruption**: No incorrect state restoration on first load
|
||||
- ✅ **Stable Navigation**: No more broken member cards after view switches
|
||||
- ✅ **Better Performance**: No unnecessary component creation/destruction
|
||||
- ✅ **Improved UX**: Smoother, more professional feel
|
||||
- ✅ **Memory Efficiency**: Components reused instead of recreated
|
||||
- ✅ **Predictable Behavior**: Components always start with clean state
|
||||
|
||||
## Configuration
|
||||
|
||||
### Navigation Cooldown
|
||||
```javascript
|
||||
this.navigationCooldown = 300; // 300ms between navigation requests
|
||||
```
|
||||
|
||||
### Component Caching
|
||||
```javascript
|
||||
// Components are automatically cached during route registration
|
||||
app.registerRoute('cluster', ClusterViewComponent, 'cluster-view', clusterViewModel);
|
||||
```
|
||||
|
||||
### Transition Timing
|
||||
```css
|
||||
.view-content {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
```
|
||||
|
||||
### Member Card Transitions
|
||||
```css
|
||||
.member-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture Benefits
|
||||
|
||||
### 1. **Performance**
|
||||
- No component recreation on view switches
|
||||
- Faster view transitions
|
||||
- Reduced memory allocation/deallocation
|
||||
|
||||
### 2. **State Management**
|
||||
- Clean, predictable default state
|
||||
- No state corruption on first load
|
||||
- Consistent user experience
|
||||
|
||||
### 3. **Maintainability**
|
||||
- Cleaner component lifecycle
|
||||
- No complex state restoration logic
|
||||
- Easier debugging and testing
|
||||
- More predictable behavior
|
||||
|
||||
### 4. **User Experience**
|
||||
- No flickering or visual glitches
|
||||
- Instant view switching
|
||||
- Maintained user context
|
||||
- Predictable component behavior
|
||||
|
||||
## Key Changes Made
|
||||
|
||||
### Removed Complex State Restoration
|
||||
- ❌ `preserveUIState()` method
|
||||
- ❌ `restoreUIState()` method
|
||||
- ❌ localStorage state persistence
|
||||
- ❌ Complex tab state restoration
|
||||
- ❌ Expanded card state restoration
|
||||
|
||||
### Simplified Component Behavior
|
||||
- ✅ Components start with default state
|
||||
- ✅ Member cards always start collapsed
|
||||
- ✅ Tabs always start on 'status'
|
||||
- ✅ No external state interference
|
||||
- ✅ Clean, predictable initialization
|
||||
|
||||
### Maintained Performance Benefits
|
||||
- ✅ Component caching still works
|
||||
- ✅ No re-rendering on view switches
|
||||
- ✅ Smooth transitions
|
||||
- ✅ Better memory efficiency
|
||||
|
||||
## Future Improvements
|
||||
|
||||
1. **Virtual Scrolling**: For large numbers of member cards
|
||||
2. **Animation Preferences**: User-configurable transition speeds
|
||||
3. **State Sync**: Real-time state synchronization across multiple tabs
|
||||
4. **Performance Metrics**: Track and optimize render performance
|
||||
5. **Lazy Loading**: Load components only when first accessed
|
||||
6. **Memory Management**: Intelligent cache cleanup for unused components
|
||||
7. **User Preferences**: Allow users to set default states if desired
|
||||
@@ -143,6 +143,7 @@
|
||||
</div>
|
||||
|
||||
<script src="./vendor/d3.v7.min.js"></script>
|
||||
<script src="./scripts/constants.js"></script>
|
||||
<script src="./scripts/framework.js"></script>
|
||||
<script src="./scripts/api-client.js"></script>
|
||||
<script src="./scripts/view-models.js"></script>
|
||||
|
||||
@@ -15,7 +15,7 @@ class ApiClient {
|
||||
this.baseUrl = `http://${currentHost}:3001`;
|
||||
}
|
||||
|
||||
console.log('API Client initialized with base URL:', this.baseUrl);
|
||||
logger.debug('API Client initialized with base URL:', this.baseUrl);
|
||||
}
|
||||
|
||||
async request(path, { method = 'GET', headers = {}, body = undefined, query = undefined, isForm = false } = {}) {
|
||||
@@ -51,92 +51,56 @@ class ApiClient {
|
||||
}
|
||||
|
||||
async getClusterMembers() {
|
||||
try {
|
||||
return await this.request('/api/cluster/members', { method: 'GET' });
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request('/api/cluster/members', { method: 'GET' });
|
||||
}
|
||||
|
||||
async getClusterMembersFromNode(ip) {
|
||||
try {
|
||||
return await this.request(`/api/cluster/members`, {
|
||||
method: 'GET',
|
||||
query: { ip: ip }
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request(`/api/cluster/members`, {
|
||||
method: 'GET',
|
||||
query: { ip: ip }
|
||||
});
|
||||
}
|
||||
|
||||
async getDiscoveryInfo() {
|
||||
try {
|
||||
return await this.request('/api/discovery/nodes', { method: 'GET' });
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request('/api/discovery/nodes', { method: 'GET' });
|
||||
}
|
||||
|
||||
async selectRandomPrimaryNode() {
|
||||
try {
|
||||
return await this.request('/api/discovery/random-primary', {
|
||||
method: 'POST',
|
||||
body: { timestamp: new Date().toISOString() }
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request('/api/discovery/random-primary', {
|
||||
method: 'POST',
|
||||
body: { timestamp: new Date().toISOString() }
|
||||
});
|
||||
}
|
||||
|
||||
async getNodeStatus(ip) {
|
||||
try {
|
||||
return await this.request(`/api/node/status/${encodeURIComponent(ip)}`, { method: 'GET' });
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request(`/api/node/status/${encodeURIComponent(ip)}`, { method: 'GET' });
|
||||
}
|
||||
|
||||
async getTasksStatus(ip) {
|
||||
try {
|
||||
return await this.request('/api/tasks/status', { method: 'GET', query: ip ? { ip } : undefined });
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request('/api/tasks/status', { method: 'GET', query: ip ? { ip } : undefined });
|
||||
}
|
||||
|
||||
async getCapabilities(ip) {
|
||||
try {
|
||||
return await this.request('/api/capabilities', { method: 'GET', query: ip ? { ip } : undefined });
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request('/api/capabilities', { method: 'GET', query: ip ? { ip } : undefined });
|
||||
}
|
||||
|
||||
async callCapability({ ip, method, uri, params }) {
|
||||
try {
|
||||
return await this.request('/api/proxy-call', {
|
||||
method: 'POST',
|
||||
body: { ip, method, uri, params }
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Request failed: ${error.message}`);
|
||||
}
|
||||
return this.request('/api/proxy-call', {
|
||||
method: 'POST',
|
||||
body: { ip, method, uri, params }
|
||||
});
|
||||
}
|
||||
|
||||
async uploadFirmware(file, nodeIp) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return await this.request(`/api/node/update`, {
|
||||
method: 'POST',
|
||||
query: { ip: nodeIp },
|
||||
body: formData,
|
||||
isForm: true,
|
||||
headers: {},
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Upload failed: ${error.message}`);
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return this.request(`/api/node/update`, {
|
||||
method: 'POST',
|
||||
query: { ip: nodeIp },
|
||||
body: formData,
|
||||
isForm: true,
|
||||
headers: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
|
||||
// Initialize the application when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('=== SPORE UI Application Initialization ===');
|
||||
logger.debug('=== SPORE UI Application Initialization ===');
|
||||
|
||||
// Initialize the framework (but don't navigate yet)
|
||||
console.log('App: Creating framework instance...');
|
||||
logger.debug('App: Creating framework instance...');
|
||||
const app = window.app;
|
||||
|
||||
// Create view models
|
||||
console.log('App: Creating view models...');
|
||||
logger.debug('App: Creating view models...');
|
||||
const clusterViewModel = new ClusterViewModel();
|
||||
const firmwareViewModel = new FirmwareViewModel();
|
||||
const topologyViewModel = new TopologyViewModel();
|
||||
console.log('App: View models created:', { clusterViewModel, firmwareViewModel, topologyViewModel });
|
||||
logger.debug('App: View models created:', { clusterViewModel, firmwareViewModel, topologyViewModel });
|
||||
|
||||
// Connect firmware view model to cluster data
|
||||
clusterViewModel.subscribe('members', (members) => {
|
||||
console.log('App: Members subscription triggered:', members);
|
||||
logger.debug('App: Members subscription triggered:', members);
|
||||
if (members && members.length > 0) {
|
||||
// Extract node information for firmware view
|
||||
const nodes = members.map(member => ({
|
||||
@@ -26,48 +26,39 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
labels: member.labels || {}
|
||||
}));
|
||||
firmwareViewModel.updateAvailableNodes(nodes);
|
||||
console.log('App: Updated firmware view model with nodes:', nodes);
|
||||
logger.debug('App: Updated firmware view model with nodes:', nodes);
|
||||
} else {
|
||||
firmwareViewModel.updateAvailableNodes([]);
|
||||
console.log('App: Cleared firmware view model nodes');
|
||||
logger.debug('App: Cleared firmware view model nodes');
|
||||
}
|
||||
});
|
||||
|
||||
// Register routes with their view models
|
||||
console.log('App: Registering routes...');
|
||||
logger.debug('App: Registering routes...');
|
||||
app.registerRoute('cluster', ClusterViewComponent, 'cluster-view', clusterViewModel);
|
||||
app.registerRoute('topology', TopologyGraphComponent, 'topology-view', topologyViewModel);
|
||||
app.registerRoute('firmware', FirmwareViewComponent, 'firmware-view', firmwareViewModel);
|
||||
console.log('App: Routes registered and components pre-initialized');
|
||||
logger.debug('App: Routes registered and components pre-initialized');
|
||||
|
||||
// Initialize cluster status component for header badge AFTER main components
|
||||
// DISABLED - causes interference with main cluster functionality
|
||||
/*
|
||||
console.log('App: Initializing cluster status component...');
|
||||
// Initialize cluster status component for header badge
|
||||
logger.debug('App: Initializing cluster status component...');
|
||||
const clusterStatusComponent = new ClusterStatusComponent(
|
||||
document.querySelector('.cluster-status'),
|
||||
clusterViewModel,
|
||||
app.eventBus
|
||||
);
|
||||
clusterStatusComponent.initialize();
|
||||
console.log('App: Cluster status component initialized');
|
||||
*/
|
||||
clusterStatusComponent.mount();
|
||||
logger.debug('App: Cluster status component initialized');
|
||||
|
||||
// Set up navigation event listeners
|
||||
console.log('App: Setting up navigation...');
|
||||
logger.debug('App: Setting up navigation...');
|
||||
app.setupNavigation();
|
||||
|
||||
// Set up cluster status updates (simple approach without component interference)
|
||||
setupClusterStatusUpdates(clusterViewModel);
|
||||
|
||||
// Set up periodic updates for cluster view with state preservation
|
||||
// setupPeriodicUpdates(); // Disabled automatic refresh
|
||||
|
||||
// Now navigate to the default route
|
||||
console.log('App: Navigating to default route...');
|
||||
logger.debug('App: Navigating to default route...');
|
||||
app.navigateTo('cluster');
|
||||
|
||||
console.log('=== SPORE UI Application initialization completed ===');
|
||||
logger.debug('=== SPORE UI Application initialization completed ===');
|
||||
});
|
||||
|
||||
// Burger menu toggle for mobile
|
||||
@@ -106,10 +97,10 @@ function setupPeriodicUpdates() {
|
||||
|
||||
// Use smart update if available, otherwise fall back to regular update
|
||||
if (viewModel.smartUpdate && typeof viewModel.smartUpdate === 'function') {
|
||||
console.log('App: Performing smart update to preserve UI state...');
|
||||
logger.debug('App: Performing smart update to preserve UI state...');
|
||||
viewModel.smartUpdate();
|
||||
} else if (viewModel.updateClusterMembers && typeof viewModel.updateClusterMembers === 'function') {
|
||||
console.log('App: Performing regular update...');
|
||||
logger.debug('App: Performing regular update...');
|
||||
viewModel.updateClusterMembers();
|
||||
}
|
||||
}
|
||||
@@ -126,104 +117,20 @@ function setupPeriodicUpdates() {
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// Set up cluster status updates (simple approach without component interference)
|
||||
function setupClusterStatusUpdates(clusterViewModel) {
|
||||
// Set initial "discovering" state immediately
|
||||
updateClusterStatusBadge(undefined, undefined, undefined);
|
||||
|
||||
// Force a fresh fetch and keep showing "discovering" until we get real data
|
||||
let hasReceivedRealData = false;
|
||||
|
||||
// Subscribe to view model changes to update cluster status
|
||||
clusterViewModel.subscribe('totalNodes', (totalNodes) => {
|
||||
if (hasReceivedRealData) {
|
||||
updateClusterStatusBadge(totalNodes, clusterViewModel.get('clientInitialized'), clusterViewModel.get('error'));
|
||||
}
|
||||
});
|
||||
|
||||
clusterViewModel.subscribe('clientInitialized', (clientInitialized) => {
|
||||
if (hasReceivedRealData) {
|
||||
updateClusterStatusBadge(clusterViewModel.get('totalNodes'), clientInitialized, clusterViewModel.get('error'));
|
||||
}
|
||||
});
|
||||
|
||||
clusterViewModel.subscribe('error', (error) => {
|
||||
if (hasReceivedRealData) {
|
||||
updateClusterStatusBadge(clusterViewModel.get('totalNodes'), clusterViewModel.get('clientInitialized'), error);
|
||||
}
|
||||
});
|
||||
|
||||
// Force a fresh fetch and only update status after we get real data
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
console.log('Cluster Status: Forcing fresh fetch from backend...');
|
||||
const discoveryInfo = await window.apiClient.getDiscoveryInfo();
|
||||
console.log('Cluster Status: Got fresh data:', discoveryInfo);
|
||||
|
||||
// Now we have real data, mark it and update the status
|
||||
hasReceivedRealData = true;
|
||||
updateClusterStatusBadge(discoveryInfo.totalNodes, discoveryInfo.clientInitialized, null);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Cluster Status: Failed to fetch fresh data:', error);
|
||||
hasReceivedRealData = true;
|
||||
updateClusterStatusBadge(0, false, error.message);
|
||||
}
|
||||
}, 100); // Small delay to ensure view model is ready
|
||||
}
|
||||
|
||||
function updateClusterStatusBadge(totalNodes, clientInitialized, error) {
|
||||
const clusterStatusBadge = document.querySelector('.cluster-status');
|
||||
if (!clusterStatusBadge) return;
|
||||
|
||||
let statusText, statusIcon, statusClass;
|
||||
|
||||
// Check if we're still in initial state (no real data yet)
|
||||
const hasRealData = totalNodes !== undefined && clientInitialized !== undefined;
|
||||
|
||||
if (!hasRealData) {
|
||||
statusText = 'Cluster Discovering...';
|
||||
statusIcon = '🔍';
|
||||
statusClass = 'cluster-status-discovering';
|
||||
} else if (error || totalNodes === 0) {
|
||||
// Show "Cluster Offline" for both errors and when no nodes are discovered
|
||||
statusText = 'Cluster Offline';
|
||||
statusIcon = '🔴';
|
||||
statusClass = 'cluster-status-offline';
|
||||
} else if (clientInitialized) {
|
||||
statusText = 'Cluster Online';
|
||||
statusIcon = '🟢';
|
||||
statusClass = 'cluster-status-online';
|
||||
} else {
|
||||
statusText = 'Cluster Connecting';
|
||||
statusIcon = '🟡';
|
||||
statusClass = 'cluster-status-connecting';
|
||||
}
|
||||
|
||||
// Update the badge
|
||||
clusterStatusBadge.innerHTML = `${statusIcon} ${statusText}`;
|
||||
|
||||
// Remove all existing status classes
|
||||
clusterStatusBadge.classList.remove('cluster-status-online', 'cluster-status-offline', 'cluster-status-connecting', 'cluster-status-error', 'cluster-status-discovering');
|
||||
|
||||
// Add the appropriate status class
|
||||
clusterStatusBadge.classList.add(statusClass);
|
||||
}
|
||||
|
||||
// Global error handler
|
||||
window.addEventListener('error', function(event) {
|
||||
console.error('Global error:', event.error);
|
||||
logger.error('Global error:', event.error);
|
||||
});
|
||||
|
||||
// Global unhandled promise rejection handler
|
||||
window.addEventListener('unhandledrejection', function(event) {
|
||||
console.error('Unhandled promise rejection:', event.reason);
|
||||
logger.error('Unhandled promise rejection:', event.reason);
|
||||
});
|
||||
|
||||
// Clean up on page unload
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (window.app) {
|
||||
console.log('App: Cleaning up cached components...');
|
||||
logger.debug('App: Cleaning up cached components...');
|
||||
window.app.cleanup();
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
27
public/scripts/constants.js
Normal file
27
public/scripts/constants.js
Normal file
@@ -0,0 +1,27 @@
|
||||
(function(){
|
||||
const TIMING = {
|
||||
NAV_COOLDOWN_MS: 300,
|
||||
VIEW_FADE_OUT_MS: 150,
|
||||
VIEW_FADE_IN_MS: 200,
|
||||
VIEW_FADE_DELAY_MS: 50,
|
||||
AUTO_REFRESH_MS: 30000,
|
||||
PRIMARY_NODE_REFRESH_MS: 10000,
|
||||
LOAD_GUARD_MS: 10000
|
||||
};
|
||||
|
||||
const SELECTORS = {
|
||||
NAV_TAB: '.nav-tab',
|
||||
VIEW_CONTENT: '.view-content',
|
||||
CLUSTER_STATUS: '.cluster-status'
|
||||
};
|
||||
|
||||
const CLASSES = {
|
||||
CLUSTER_STATUS_ONLINE: 'cluster-status-online',
|
||||
CLUSTER_STATUS_OFFLINE: 'cluster-status-offline',
|
||||
CLUSTER_STATUS_CONNECTING: 'cluster-status-connecting',
|
||||
CLUSTER_STATUS_ERROR: 'cluster-status-error',
|
||||
CLUSTER_STATUS_DISCOVERING: 'cluster-status-discovering'
|
||||
};
|
||||
|
||||
window.CONSTANTS = window.CONSTANTS || { TIMING, SELECTORS, CLASSES };
|
||||
})();
|
||||
@@ -88,7 +88,7 @@ class ViewModel {
|
||||
|
||||
// Set data property and notify listeners
|
||||
set(property, value) {
|
||||
console.log(`ViewModel: Setting property '${property}' to:`, value);
|
||||
logger.debug(`ViewModel: Setting property '${property}' to:`, value);
|
||||
|
||||
// Check if the value has actually changed
|
||||
const hasChanged = this._data[property] !== value;
|
||||
@@ -100,10 +100,10 @@ class ViewModel {
|
||||
// Update the data
|
||||
this._data[property] = value;
|
||||
|
||||
console.log(`ViewModel: Property '${property}' changed, notifying listeners...`);
|
||||
logger.debug(`ViewModel: Property '${property}' changed, notifying listeners...`);
|
||||
this._notifyListeners(property, value, this._previousData[property]);
|
||||
} else {
|
||||
console.log(`ViewModel: Property '${property}' unchanged, skipping notification`);
|
||||
logger.debug(`ViewModel: Property '${property}' unchanged, skipping notification`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ class ViewModel {
|
||||
});
|
||||
|
||||
if (Object.keys(changedProperties).length > 0) {
|
||||
console.log(`ViewModel: Updated ${Object.keys(changedProperties).length} changed properties:`, Object.keys(changedProperties));
|
||||
logger.debug(`ViewModel: Updated ${Object.keys(changedProperties).length} changed properties:`, Object.keys(changedProperties));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,20 +157,20 @@ class ViewModel {
|
||||
|
||||
// Notify listeners of property changes
|
||||
_notifyListeners(property, value, previousValue) {
|
||||
console.log(`ViewModel: _notifyListeners called for property '${property}'`);
|
||||
logger.debug(`ViewModel: _notifyListeners called for property '${property}'`);
|
||||
if (this._listeners.has(property)) {
|
||||
const callbacks = this._listeners.get(property);
|
||||
console.log(`ViewModel: Found ${callbacks.length} listeners for property '${property}'`);
|
||||
logger.debug(`ViewModel: Found ${callbacks.length} listeners for property '${property}'`);
|
||||
callbacks.forEach((callback, index) => {
|
||||
try {
|
||||
console.log(`ViewModel: Calling listener ${index} for property '${property}'`);
|
||||
logger.debug(`ViewModel: Calling listener ${index} for property '${property}'`);
|
||||
callback(value, previousValue);
|
||||
} catch (error) {
|
||||
console.error(`Error in property listener for ${property}:`, error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log(`ViewModel: No listeners found for property '${property}'`);
|
||||
logger.debug(`ViewModel: No listeners found for property '${property}'`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,13 +285,13 @@ class Component {
|
||||
mount() {
|
||||
if (this.isMounted) return;
|
||||
|
||||
console.log(`${this.constructor.name}: Starting mount...`);
|
||||
logger.debug(`${this.constructor.name}: Starting mount...`);
|
||||
this.isMounted = true;
|
||||
this.setupEventListeners();
|
||||
this.setupViewModelListeners();
|
||||
this.render();
|
||||
|
||||
console.log(`${this.constructor.name}: Mounted successfully`);
|
||||
logger.debug(`${this.constructor.name}: Mounted successfully`);
|
||||
}
|
||||
|
||||
// Unmount the component
|
||||
@@ -302,14 +302,14 @@ class Component {
|
||||
this.cleanupEventListeners();
|
||||
this.cleanupViewModelListeners();
|
||||
|
||||
console.log(`${this.constructor.name} unmounted`);
|
||||
logger.debug(`${this.constructor.name} unmounted`);
|
||||
}
|
||||
|
||||
// Pause the component (keep alive but pause activity)
|
||||
pause() {
|
||||
if (!this.isMounted) return;
|
||||
|
||||
console.log(`${this.constructor.name}: Pausing component`);
|
||||
logger.debug(`${this.constructor.name}: Pausing component`);
|
||||
|
||||
// Pause any active timers or animations
|
||||
if (this.updateInterval) {
|
||||
@@ -328,7 +328,7 @@ class Component {
|
||||
resume() {
|
||||
if (!this.isMounted || !this.isPaused) return;
|
||||
|
||||
console.log(`${this.constructor.name}: Resuming component`);
|
||||
logger.debug(`${this.constructor.name}: Resuming component`);
|
||||
|
||||
this.isPaused = false;
|
||||
|
||||
@@ -385,7 +385,7 @@ class Component {
|
||||
// Partial update method for efficient data updates
|
||||
updatePartial(property, newValue, previousValue) {
|
||||
// Override in subclasses to implement partial updates
|
||||
console.log(`${this.constructor.name}: Partial update for '${property}':`, { newValue, previousValue });
|
||||
logger.debug(`${this.constructor.name}: Partial update for '${property}':`, { newValue, previousValue });
|
||||
}
|
||||
|
||||
// UI State Management Methods
|
||||
@@ -474,22 +474,22 @@ class Component {
|
||||
|
||||
// Helper method to set innerHTML safely
|
||||
setHTML(selector, html) {
|
||||
console.log(`${this.constructor.name}: setHTML called with selector '${selector}', html length: ${html.length}`);
|
||||
logger.debug(`${this.constructor.name}: setHTML called with selector '${selector}', html length: ${html.length}`);
|
||||
|
||||
let element;
|
||||
if (selector === '') {
|
||||
// Empty selector means set HTML on the component's container itself
|
||||
element = this.container;
|
||||
console.log(`${this.constructor.name}: Using component container for empty selector`);
|
||||
logger.debug(`${this.constructor.name}: Using component container for empty selector`);
|
||||
} else {
|
||||
// Find element within the component's container
|
||||
element = this.findElement(selector);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
console.log(`${this.constructor.name}: Element found, setting innerHTML`);
|
||||
logger.debug(`${this.constructor.name}: Element found, setting innerHTML`);
|
||||
element.innerHTML = html;
|
||||
console.log(`${this.constructor.name}: innerHTML set successfully`);
|
||||
logger.debug(`${this.constructor.name}: innerHTML set successfully`);
|
||||
} else {
|
||||
console.error(`${this.constructor.name}: Element not found for selector '${selector}'`);
|
||||
}
|
||||
@@ -550,7 +550,7 @@ class Component {
|
||||
}
|
||||
|
||||
renderError(message) {
|
||||
const safe = String(message || 'An error occurred');
|
||||
const safe = this.escapeHtml(String(message || 'An error occurred'));
|
||||
const html = `
|
||||
<div class="error">
|
||||
<strong>Error:</strong><br>
|
||||
@@ -569,8 +569,19 @@ class Component {
|
||||
this.setHTML('', html);
|
||||
}
|
||||
|
||||
// Basic HTML escaping for dynamic values
|
||||
escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Tab helpers
|
||||
setupTabs(container = this.container) {
|
||||
setupTabs(container = this.container, options = {}) {
|
||||
const { onChange } = options;
|
||||
const tabButtons = container.querySelectorAll('.tab-button');
|
||||
const tabContents = container.querySelectorAll('.tab-content');
|
||||
tabButtons.forEach(button => {
|
||||
@@ -578,6 +589,9 @@ class Component {
|
||||
e.stopPropagation();
|
||||
const targetTab = button.dataset.tab;
|
||||
this.setActiveTab(targetTab, container);
|
||||
if (typeof onChange === 'function') {
|
||||
try { onChange(targetTab); } catch (_) {}
|
||||
}
|
||||
});
|
||||
});
|
||||
tabContents.forEach(content => {
|
||||
@@ -610,7 +624,7 @@ class App {
|
||||
this.navigationInProgress = false;
|
||||
this.navigationQueue = [];
|
||||
this.lastNavigationTime = 0;
|
||||
this.navigationCooldown = 300; // 300ms cooldown between navigations
|
||||
this.navigationCooldown = (window.CONSTANTS && window.CONSTANTS.TIMING.NAV_COOLDOWN_MS) || 300; // cooldown between navigations
|
||||
|
||||
// Component cache to keep components alive
|
||||
this.componentCache = new Map();
|
||||
@@ -637,7 +651,7 @@ class App {
|
||||
|
||||
// Store in cache
|
||||
this.componentCache.set(name, component);
|
||||
console.log(`App: Pre-initialized component for route '${name}'`);
|
||||
logger.debug(`App: Pre-initialized component for route '${name}'`);
|
||||
}
|
||||
|
||||
// Navigate to a route
|
||||
@@ -645,13 +659,13 @@ class App {
|
||||
// Check cooldown period
|
||||
const now = Date.now();
|
||||
if (now - this.lastNavigationTime < this.navigationCooldown) {
|
||||
console.log(`App: Navigation cooldown active, skipping route '${routeName}'`);
|
||||
logger.debug(`App: Navigation cooldown active, skipping route '${routeName}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If navigation is already in progress, queue this request
|
||||
if (this.navigationInProgress) {
|
||||
console.log(`App: Navigation in progress, queuing route '${routeName}'`);
|
||||
logger.debug(`App: Navigation in progress, queuing route '${routeName}'`);
|
||||
if (!this.navigationQueue.includes(routeName)) {
|
||||
this.navigationQueue.push(routeName);
|
||||
}
|
||||
@@ -660,7 +674,7 @@ class App {
|
||||
|
||||
// If trying to navigate to the same route, do nothing
|
||||
if (this.currentView && this.currentView.routeName === routeName) {
|
||||
console.log(`App: Already on route '${routeName}', skipping navigation`);
|
||||
logger.debug(`App: Already on route '${routeName}', skipping navigation`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -673,19 +687,19 @@ class App {
|
||||
this.navigationInProgress = true;
|
||||
|
||||
try {
|
||||
console.log(`App: Navigating to route '${routeName}'`);
|
||||
logger.debug(`App: Navigating to route '${routeName}'`);
|
||||
const route = this.routes.get(routeName);
|
||||
if (!route) {
|
||||
console.error(`Route '${routeName}' not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`App: Route found, component: ${route.componentClass.name}, container: ${route.containerId}`);
|
||||
logger.debug(`App: Route found, component: ${route.componentClass.name}, container: ${route.containerId}`);
|
||||
|
||||
// Get or create component from cache
|
||||
let component = this.componentCache.get(routeName);
|
||||
if (!component) {
|
||||
console.log(`App: Component not in cache, creating new instance for '${routeName}'`);
|
||||
logger.debug(`App: Component not in cache, creating new instance for '${routeName}'`);
|
||||
const container = document.getElementById(route.containerId);
|
||||
if (!container) {
|
||||
console.error(`Container '${route.containerId}' not found`);
|
||||
@@ -700,12 +714,12 @@ class App {
|
||||
|
||||
// Hide current view smoothly
|
||||
if (this.currentView) {
|
||||
console.log('App: Hiding current view');
|
||||
logger.debug('App: Hiding current view');
|
||||
await this.hideCurrentView();
|
||||
}
|
||||
|
||||
// Show new view
|
||||
console.log(`App: Showing new view '${routeName}'`);
|
||||
logger.debug(`App: Showing new view '${routeName}'`);
|
||||
await this.showView(routeName, component);
|
||||
|
||||
// Update navigation state
|
||||
@@ -717,7 +731,7 @@ class App {
|
||||
// Mark view as cached for future use
|
||||
this.cachedViews.add(routeName);
|
||||
|
||||
console.log(`App: Navigation to '${routeName}' completed`);
|
||||
logger.debug(`App: Navigation to '${routeName}' completed`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('App: Navigation failed:', error);
|
||||
@@ -727,7 +741,7 @@ class App {
|
||||
// Process any queued navigation requests
|
||||
if (this.navigationQueue.length > 0) {
|
||||
const nextRoute = this.navigationQueue.shift();
|
||||
console.log(`App: Processing queued navigation to '${nextRoute}'`);
|
||||
logger.debug(`App: Processing queued navigation to '${nextRoute}'`);
|
||||
setTimeout(() => this.navigateTo(nextRoute), 100);
|
||||
}
|
||||
}
|
||||
@@ -739,18 +753,18 @@ class App {
|
||||
|
||||
// If component is mounted, pause it instead of unmounting
|
||||
if (this.currentView.isMounted) {
|
||||
console.log('App: Pausing current view instead of unmounting');
|
||||
logger.debug('App: Pausing current view instead of unmounting');
|
||||
this.currentView.pause();
|
||||
}
|
||||
|
||||
// Fade out the container
|
||||
if (this.currentView.container) {
|
||||
this.currentView.container.style.opacity = '0';
|
||||
this.currentView.container.style.transition = 'opacity 0.15s ease-out';
|
||||
this.currentView.container.style.transition = `opacity ${(window.CONSTANTS && window.CONSTANTS.TIMING.VIEW_FADE_OUT_MS) || 150}ms ease-out`;
|
||||
}
|
||||
|
||||
// Wait for fade out to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
await new Promise(resolve => setTimeout(resolve, (window.CONSTANTS && window.CONSTANTS.TIMING.VIEW_FADE_OUT_MS) || 150));
|
||||
}
|
||||
|
||||
// Show view smoothly
|
||||
@@ -759,19 +773,19 @@ class App {
|
||||
|
||||
// Ensure component is mounted (but not necessarily active)
|
||||
if (!component.isMounted) {
|
||||
console.log(`App: Mounting component for '${routeName}'`);
|
||||
logger.debug(`App: Mounting component for '${routeName}'`);
|
||||
component.mount();
|
||||
} else {
|
||||
console.log(`App: Resuming component for '${routeName}'`);
|
||||
logger.debug(`App: Resuming component for '${routeName}'`);
|
||||
component.resume();
|
||||
}
|
||||
|
||||
// Fade in the container
|
||||
container.style.opacity = '0';
|
||||
container.style.transition = 'opacity 0.2s ease-in';
|
||||
container.style.transition = `opacity ${(window.CONSTANTS && window.CONSTANTS.TIMING.VIEW_FADE_IN_MS) || 200}ms ease-in`;
|
||||
|
||||
// Small delay to ensure smooth transition
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await new Promise(resolve => setTimeout(resolve, (window.CONSTANTS && window.CONSTANTS.TIMING.VIEW_FADE_DELAY_MS) || 50));
|
||||
|
||||
// Fade in
|
||||
container.style.opacity = '1';
|
||||
@@ -780,7 +794,7 @@ class App {
|
||||
// Update navigation state
|
||||
updateNavigation(activeRoute) {
|
||||
// Remove active class from all nav tabs
|
||||
document.querySelectorAll('.nav-tab').forEach(tab => {
|
||||
document.querySelectorAll((window.CONSTANTS && window.CONSTANTS.SELECTORS.NAV_TAB) || '.nav-tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
|
||||
@@ -791,7 +805,7 @@ class App {
|
||||
}
|
||||
|
||||
// Hide all view contents with smooth transition
|
||||
document.querySelectorAll('.view-content').forEach(view => {
|
||||
document.querySelectorAll((window.CONSTANTS && window.CONSTANTS.SELECTORS.VIEW_CONTENT) || '.view-content').forEach(view => {
|
||||
view.classList.remove('active');
|
||||
view.style.opacity = '0';
|
||||
view.style.transition = 'opacity 0.15s ease-out';
|
||||
@@ -826,7 +840,7 @@ class App {
|
||||
|
||||
// Initialize the application
|
||||
init() {
|
||||
console.log('SPORE UI Framework initialized');
|
||||
logger.debug('SPORE UI Framework initialized');
|
||||
|
||||
// Note: Navigation is now handled by the app initialization
|
||||
// to ensure routes are registered before navigation
|
||||
@@ -834,7 +848,7 @@ class App {
|
||||
|
||||
// Setup navigation
|
||||
setupNavigation() {
|
||||
document.querySelectorAll('.nav-tab').forEach(tab => {
|
||||
document.querySelectorAll((window.CONSTANTS && window.CONSTANTS.SELECTORS.NAV_TAB) || '.nav-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const routeName = tab.dataset.view;
|
||||
this.navigateTo(routeName);
|
||||
@@ -844,11 +858,11 @@ class App {
|
||||
|
||||
// Clean up cached components (call when app is shutting down)
|
||||
cleanup() {
|
||||
console.log('App: Cleaning up cached components...');
|
||||
logger.debug('App: Cleaning up cached components...');
|
||||
|
||||
this.componentCache.forEach((component, routeName) => {
|
||||
if (component.isMounted) {
|
||||
console.log(`App: Unmounting cached component '${routeName}'`);
|
||||
logger.debug(`App: Unmounting cached component '${routeName}'`);
|
||||
component.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ class ClusterViewModel extends ViewModel {
|
||||
// Update cluster members with state preservation
|
||||
async updateClusterMembers() {
|
||||
try {
|
||||
console.log('ClusterViewModel: updateClusterMembers called');
|
||||
logger.debug('ClusterViewModel: updateClusterMembers called');
|
||||
|
||||
// Store current UI state before update
|
||||
const currentUIState = this.getAllUIState();
|
||||
@@ -36,9 +36,9 @@ class ClusterViewModel extends ViewModel {
|
||||
this.set('isLoading', true);
|
||||
this.set('error', null);
|
||||
|
||||
console.log('ClusterViewModel: Fetching cluster members...');
|
||||
logger.debug('ClusterViewModel: Fetching cluster members...');
|
||||
const response = await window.apiClient.getClusterMembers();
|
||||
console.log('ClusterViewModel: Got response:', response);
|
||||
logger.debug('ClusterViewModel: Got response:', response);
|
||||
|
||||
const members = response.members || [];
|
||||
const onlineNodes = Array.isArray(members)
|
||||
@@ -57,7 +57,7 @@ class ClusterViewModel extends ViewModel {
|
||||
this.set('activeTabs', currentActiveTabs);
|
||||
|
||||
// Update primary node display
|
||||
console.log('ClusterViewModel: Updating primary node display...');
|
||||
logger.debug('ClusterViewModel: Updating primary node display...');
|
||||
await this.updatePrimaryNodeDisplay();
|
||||
|
||||
} catch (error) {
|
||||
@@ -65,7 +65,7 @@ class ClusterViewModel extends ViewModel {
|
||||
this.set('error', error.message);
|
||||
} finally {
|
||||
this.set('isLoading', false);
|
||||
console.log('ClusterViewModel: updateClusterMembers completed');
|
||||
logger.debug('ClusterViewModel: updateClusterMembers completed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ class ClusterViewModel extends ViewModel {
|
||||
// Smart update that only updates changed data
|
||||
async smartUpdate() {
|
||||
try {
|
||||
console.log('ClusterViewModel: Performing smart update...');
|
||||
logger.debug('ClusterViewModel: Performing smart update...');
|
||||
|
||||
// Fetch new data
|
||||
const response = await window.apiClient.getClusterMembers();
|
||||
@@ -193,10 +193,10 @@ class ClusterViewModel extends ViewModel {
|
||||
|
||||
// Check if members data has actually changed
|
||||
if (this.hasDataChanged(newMembers, 'members')) {
|
||||
console.log('ClusterViewModel: Members data changed, updating...');
|
||||
logger.debug('ClusterViewModel: Members data changed, updating...');
|
||||
await this.updateClusterMembers();
|
||||
} else {
|
||||
console.log('ClusterViewModel: Members data unchanged, skipping update');
|
||||
logger.debug('ClusterViewModel: Members data unchanged, skipping update');
|
||||
// Still update primary node display as it might have changed
|
||||
await this.updatePrimaryNodeDisplay();
|
||||
}
|
||||
@@ -292,7 +292,7 @@ class NodeDetailsViewModel extends ViewModel {
|
||||
|
||||
// Set active tab with state persistence
|
||||
setActiveTab(tabName) {
|
||||
console.log('NodeDetailsViewModel: Setting activeTab to:', tabName);
|
||||
logger.debug('NodeDetailsViewModel: Setting activeTab to:', tabName);
|
||||
this.set('activeTab', tabName);
|
||||
|
||||
// Store in UI state for persistence
|
||||
@@ -492,14 +492,14 @@ class TopologyViewModel extends ViewModel {
|
||||
// Update network topology data
|
||||
async updateNetworkTopology() {
|
||||
try {
|
||||
console.log('TopologyViewModel: updateNetworkTopology called');
|
||||
logger.debug('TopologyViewModel: updateNetworkTopology called');
|
||||
|
||||
this.set('isLoading', true);
|
||||
this.set('error', null);
|
||||
|
||||
// Get cluster members from the primary node
|
||||
const response = await window.apiClient.getClusterMembers();
|
||||
console.log('TopologyViewModel: Got cluster members response:', response);
|
||||
logger.debug('TopologyViewModel: Got cluster members response:', response);
|
||||
|
||||
const members = response.members || [];
|
||||
|
||||
@@ -517,7 +517,7 @@ class TopologyViewModel extends ViewModel {
|
||||
this.set('error', error.message);
|
||||
} finally {
|
||||
this.set('isLoading', false);
|
||||
console.log('TopologyViewModel: updateNetworkTopology completed');
|
||||
logger.debug('TopologyViewModel: updateNetworkTopology completed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,7 +589,7 @@ class TopologyViewModel extends ViewModel {
|
||||
|
||||
// If no actual connections found, create a basic mesh
|
||||
if (links.length === 0) {
|
||||
console.log('TopologyViewModel: No actual connections found, creating basic mesh');
|
||||
logger.debug('TopologyViewModel: 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];
|
||||
|
||||
Reference in New Issue
Block a user