Compare commits
2 Commits
d870219136
...
27f93959ff
| Author | SHA1 | Date | |
|---|---|---|---|
| 27f93959ff | |||
| f9dc811239 |
@@ -1,67 +0,0 @@
|
||||
# Theme Improvements - Better Contrast and Readability
|
||||
|
||||
## Issues Fixed
|
||||
|
||||
### 1. **Too Bright Light Theme**
|
||||
- **Before**: Very bright white backgrounds that were harsh on the eyes
|
||||
- **After**: Softer, more muted backgrounds using `#f1f5f9`, `#e2e8f0`, and `#cbd5e1`
|
||||
|
||||
### 2. **Poor Text Contrast**
|
||||
- **Before**: Many text elements had the same color as background, making them invisible
|
||||
- **After**: Strong contrast with dark text (`#0f172a`, `#1e293b`, `#334155`) on light backgrounds
|
||||
|
||||
### 3. **Specific Text Color Issues Fixed**
|
||||
- Member hostnames and IPs now use `var(--text-primary)` for maximum readability
|
||||
- Latency labels and values have proper contrast
|
||||
- Navigation tabs are clearly visible in both themes
|
||||
- Form inputs and dropdowns have proper text contrast
|
||||
- Status indicators have appropriate colors with good contrast
|
||||
|
||||
## Color Palette Updates
|
||||
|
||||
### Light Theme (Improved)
|
||||
- **Background**: Softer gradients (`#f1f5f9` → `#e2e8f0` → `#cbd5e1`)
|
||||
- **Text Primary**: `#0f172a` (very dark for maximum contrast)
|
||||
- **Text Secondary**: `#1e293b` (dark gray)
|
||||
- **Text Tertiary**: `#334155` (medium gray)
|
||||
- **Text Muted**: `#475569` (lighter gray)
|
||||
- **Borders**: Stronger borders (`rgba(30, 41, 59, 0.2)`) for better definition
|
||||
|
||||
### Dark Theme (Unchanged)
|
||||
- Maintains the original dark theme for users who prefer it
|
||||
- All existing functionality preserved
|
||||
|
||||
## Technical Improvements
|
||||
|
||||
### CSS Specificity
|
||||
- Added `!important` rules for critical text elements to ensure they override any conflicting styles
|
||||
- Used `[data-theme="light"]` selectors for theme-specific overrides
|
||||
|
||||
### Contrast Ratios
|
||||
- All text now meets WCAG AA contrast requirements
|
||||
- Status indicators have clear, distinguishable colors
|
||||
- Interactive elements have proper hover states
|
||||
|
||||
### Readability Enhancements
|
||||
- Removed problematic opacity values that made text invisible
|
||||
- Ensured all form elements have proper text contrast
|
||||
- Fixed dropdown options to be readable
|
||||
- Improved error and success message visibility
|
||||
|
||||
## Testing
|
||||
|
||||
To test the improvements:
|
||||
|
||||
1. Open `http://localhost:8080` in your browser
|
||||
2. Click the theme toggle to switch to light theme
|
||||
3. Verify all text is clearly readable
|
||||
4. Check that the background is not too bright
|
||||
5. Test form interactions and dropdowns
|
||||
6. Verify status indicators are clearly visible
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `public/styles/theme.css` - Updated color palette and added contrast fixes
|
||||
- `public/styles/main.css` - Fixed text color issues and opacity problems
|
||||
|
||||
The light theme is now much easier on the eyes with proper contrast for all text elements.
|
||||
144
THEME_README.md
144
THEME_README.md
@@ -1,144 +0,0 @@
|
||||
# SPORE UI Theme System
|
||||
|
||||
This document describes the theme system implementation for SPORE UI, which provides both dark and light themes with a user-friendly theme switcher.
|
||||
|
||||
## Features
|
||||
|
||||
- **Dark Theme (Default)**: The original dark theme with dark backgrounds and light text
|
||||
- **Light Theme**: A new light theme with light backgrounds and dark text
|
||||
- **Theme Switcher**: A toggle button in the header to switch between themes
|
||||
- **Persistence**: Theme preference is saved in localStorage
|
||||
- **Smooth Transitions**: CSS transitions for smooth theme switching
|
||||
- **Responsive Design**: Theme switcher adapts to mobile screens
|
||||
|
||||
## Files Added/Modified
|
||||
|
||||
### New Files
|
||||
- `public/styles/theme.css` - CSS variables and theme definitions
|
||||
- `public/scripts/theme-manager.js` - JavaScript theme management
|
||||
- `THEME_README.md` - This documentation
|
||||
|
||||
### Modified Files
|
||||
- `public/index.html` - Added theme switcher to header and theme CSS link
|
||||
- `public/styles/main.css` - Updated to use CSS variables instead of hardcoded colors
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### CSS Variables System
|
||||
|
||||
The theme system uses CSS custom properties (variables) defined in `theme.css`:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Dark theme variables */
|
||||
--bg-primary: linear-gradient(135deg, #2c3e50 0%, #34495e 50%, #1a252f 100%);
|
||||
--text-primary: #ecf0f1;
|
||||
--border-primary: rgba(255, 255, 255, 0.1);
|
||||
/* ... more variables */
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
/* Light theme overrides */
|
||||
--bg-primary: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%);
|
||||
--text-primary: #1e293b;
|
||||
--border-primary: rgba(30, 41, 59, 0.1);
|
||||
/* ... more variables */
|
||||
}
|
||||
```
|
||||
|
||||
### Theme Switcher
|
||||
|
||||
The theme switcher is located in the header's right section and includes:
|
||||
- A label indicating "Theme:"
|
||||
- A toggle button with sun/moon icons
|
||||
- Hover effects and smooth animations
|
||||
- Mobile-responsive design
|
||||
|
||||
### JavaScript Theme Manager
|
||||
|
||||
The `ThemeManager` class handles:
|
||||
- Theme persistence in localStorage
|
||||
- Theme switching logic
|
||||
- Icon updates (sun for light, moon for dark)
|
||||
- Event dispatching for other components
|
||||
- System theme detection (future enhancement)
|
||||
|
||||
## Usage
|
||||
|
||||
### For Users
|
||||
1. Click the theme toggle button in the header
|
||||
2. The theme will switch immediately with smooth transitions
|
||||
3. Your preference is automatically saved
|
||||
|
||||
### For Developers
|
||||
|
||||
#### Accessing Current Theme
|
||||
```javascript
|
||||
// Get current theme
|
||||
const currentTheme = window.themeManager.getCurrentTheme();
|
||||
|
||||
// Listen for theme changes
|
||||
window.addEventListener('themeChanged', (event) => {
|
||||
console.log('Theme changed to:', event.detail.theme);
|
||||
});
|
||||
```
|
||||
|
||||
#### Programmatic Theme Setting
|
||||
```javascript
|
||||
// Set theme programmatically
|
||||
window.themeManager.setTheme('light'); // or 'dark'
|
||||
```
|
||||
|
||||
## Color Palette
|
||||
|
||||
### Dark Theme
|
||||
- **Background**: Dark gradients (#2c3e50, #34495e, #1a252f)
|
||||
- **Text**: Light colors (#ecf0f1, rgba(255,255,255,0.8))
|
||||
- **Accents**: Bright colors (#4ade80, #60a5fa, #fbbf24)
|
||||
- **Borders**: Subtle white borders with low opacity
|
||||
|
||||
### Light Theme
|
||||
- **Background**: Light gradients (#f8fafc, #e2e8f0, #cbd5e1)
|
||||
- **Text**: Dark colors (#1e293b, rgba(30,41,59,0.8))
|
||||
- **Accents**: Muted colors (#059669, #2563eb, #d97706)
|
||||
- **Borders**: Subtle dark borders with low opacity
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Modern browsers with CSS custom properties support
|
||||
- localStorage support for theme persistence
|
||||
- Graceful degradation for older browsers
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **System Theme Detection**: Automatically detect user's system preference
|
||||
2. **More Themes**: Additional theme options (e.g., high contrast, colorblind-friendly)
|
||||
3. **Theme Customization**: Allow users to customize accent colors
|
||||
4. **Animation Preferences**: Respect user's motion preferences
|
||||
|
||||
## Testing
|
||||
|
||||
To test the theme system:
|
||||
|
||||
1. Start the development server: `cd public && python3 -m http.server 8080`
|
||||
2. Open `http://localhost:8080` in your browser
|
||||
3. Click the theme toggle button in the header
|
||||
4. Verify smooth transitions and proper color contrast
|
||||
5. Refresh the page to verify theme persistence
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Theme Not Switching
|
||||
- Check browser console for JavaScript errors
|
||||
- Verify `theme-manager.js` is loaded
|
||||
- Check if localStorage is available and not disabled
|
||||
|
||||
### Colors Not Updating
|
||||
- Verify `theme.css` is loaded after `main.css`
|
||||
- Check if CSS variables are supported in your browser
|
||||
- Ensure the `data-theme` attribute is being set on the `<html>` element
|
||||
|
||||
### Mobile Issues
|
||||
- Test on actual mobile devices, not just browser dev tools
|
||||
- Verify touch events are working properly
|
||||
- Check responsive CSS rules for theme switcher
|
||||
@@ -146,6 +146,7 @@
|
||||
<script src="./scripts/api-client.js"></script>
|
||||
<script src="./scripts/view-models.js"></script>
|
||||
<!-- Base/leaf components first -->
|
||||
<script src="./scripts/components/DrawerComponent.js"></script>
|
||||
<script src="./scripts/components/PrimaryNodeComponent.js"></script>
|
||||
<script src="./scripts/components/NodeDetailsComponent.js"></script>
|
||||
<script src="./scripts/components/ClusterMembersComponent.js"></script>
|
||||
|
||||
@@ -21,108 +21,58 @@ class ClusterMembersComponent extends Component {
|
||||
}, 200);
|
||||
|
||||
// Drawer state for desktop
|
||||
this.detailsDrawer = null;
|
||||
this.detailsDrawerContent = null;
|
||||
this.detailsDrawerBackdrop = null;
|
||||
this.activeDrawerComponent = null;
|
||||
this.drawer = new DrawerComponent();
|
||||
}
|
||||
|
||||
// Determine if we should use desktop drawer behavior
|
||||
isDesktop() {
|
||||
try {
|
||||
return window && window.innerWidth >= 1024; // desktop threshold
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
return this.drawer.isDesktop();
|
||||
}
|
||||
|
||||
ensureDrawer() {
|
||||
if (this.detailsDrawer) return;
|
||||
// Create backdrop
|
||||
this.detailsDrawerBackdrop = document.createElement('div');
|
||||
this.detailsDrawerBackdrop.className = 'details-drawer-backdrop';
|
||||
document.body.appendChild(this.detailsDrawerBackdrop);
|
||||
|
||||
// Create drawer
|
||||
this.detailsDrawer = document.createElement('div');
|
||||
this.detailsDrawer.className = 'details-drawer';
|
||||
|
||||
// Header with close button
|
||||
const header = document.createElement('div');
|
||||
header.className = 'details-drawer-header';
|
||||
header.innerHTML = `
|
||||
<div class="drawer-title">Node Details</div>
|
||||
<button class="drawer-close" aria-label="Close">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
this.detailsDrawer.appendChild(header);
|
||||
|
||||
// Content container
|
||||
this.detailsDrawerContent = document.createElement('div');
|
||||
this.detailsDrawerContent.className = 'details-drawer-content';
|
||||
this.detailsDrawer.appendChild(this.detailsDrawerContent);
|
||||
|
||||
document.body.appendChild(this.detailsDrawer);
|
||||
|
||||
// Close handlers
|
||||
const close = () => this.closeDrawer();
|
||||
header.querySelector('.drawer-close').addEventListener('click', close);
|
||||
this.detailsDrawerBackdrop.addEventListener('click', close);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') close();
|
||||
});
|
||||
}
|
||||
|
||||
openDrawerForMember(memberIp) {
|
||||
this.ensureDrawer();
|
||||
|
||||
// Set drawer title to member name (hostname) or IP
|
||||
// Get display name for drawer title
|
||||
let displayName = memberIp;
|
||||
try {
|
||||
const members = (this.viewModel && typeof this.viewModel.get === 'function') ? this.viewModel.get('members') : [];
|
||||
const member = Array.isArray(members) ? members.find(m => m && m.ip === memberIp) : null;
|
||||
const displayName = (member && member.hostname) ? member.hostname : (member && member.ip) ? member.ip : memberIp;
|
||||
const titleEl = this.detailsDrawer.querySelector('.drawer-title');
|
||||
if (titleEl) {
|
||||
titleEl.textContent = displayName;
|
||||
const hostname = (member && member.hostname) ? member.hostname : '';
|
||||
const ip = (member && member.ip) ? member.ip : memberIp;
|
||||
|
||||
if (hostname && ip) {
|
||||
displayName = `${hostname} - ${ip}`;
|
||||
} else if (hostname) {
|
||||
displayName = hostname;
|
||||
} else if (ip) {
|
||||
displayName = ip;
|
||||
}
|
||||
} catch (_) {
|
||||
// no-op if anything goes wrong, default title remains
|
||||
}
|
||||
|
||||
// Clear previous component if any
|
||||
if (this.activeDrawerComponent && typeof this.activeDrawerComponent.unmount === 'function') {
|
||||
try { this.activeDrawerComponent.unmount(); } catch (_) {}
|
||||
}
|
||||
this.detailsDrawerContent.innerHTML = '<div class="loading-details">Loading detailed information...</div>';
|
||||
// Open drawer with content callback
|
||||
this.drawer.openDrawer(displayName, (contentContainer, setActiveComponent) => {
|
||||
// Load and mount NodeDetails into drawer
|
||||
const nodeDetailsVM = new NodeDetailsViewModel();
|
||||
const nodeDetailsComponent = new NodeDetailsComponent(contentContainer, nodeDetailsVM, this.eventBus);
|
||||
setActiveComponent(nodeDetailsComponent);
|
||||
|
||||
// Load and mount NodeDetails into drawer
|
||||
const nodeDetailsVM = new NodeDetailsViewModel();
|
||||
const nodeDetailsComponent = new NodeDetailsComponent(this.detailsDrawerContent, nodeDetailsVM, this.eventBus);
|
||||
this.activeDrawerComponent = nodeDetailsComponent;
|
||||
|
||||
nodeDetailsVM.loadNodeDetails(memberIp).then(() => {
|
||||
nodeDetailsComponent.mount();
|
||||
}).catch((error) => {
|
||||
logger.error('Failed to load node details for drawer:', error);
|
||||
this.detailsDrawerContent.innerHTML = `
|
||||
<div class="error">
|
||||
<strong>Error loading node details:</strong><br>
|
||||
${this.escapeHtml(error.message)}
|
||||
</div>
|
||||
`;
|
||||
nodeDetailsVM.loadNodeDetails(memberIp).then(() => {
|
||||
nodeDetailsComponent.mount();
|
||||
}).catch((error) => {
|
||||
logger.error('Failed to load node details for drawer:', error);
|
||||
contentContainer.innerHTML = `
|
||||
<div class="error">
|
||||
<strong>Error loading node details:</strong><br>
|
||||
${this.escapeHtml(error.message)}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
});
|
||||
|
||||
// Open drawer
|
||||
this.detailsDrawer.classList.add('open');
|
||||
this.detailsDrawerBackdrop.classList.add('visible');
|
||||
}
|
||||
|
||||
closeDrawer() {
|
||||
if (this.detailsDrawer) this.detailsDrawer.classList.remove('open');
|
||||
if (this.detailsDrawerBackdrop) this.detailsDrawerBackdrop.classList.remove('visible');
|
||||
this.drawer.closeDrawer();
|
||||
}
|
||||
|
||||
mount() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
(function(){
|
||||
// Simple readiness flag once all component constructors are present
|
||||
function allReady(){
|
||||
return !!(window.PrimaryNodeComponent && window.ClusterMembersComponent && window.NodeDetailsComponent && window.FirmwareComponent && window.ClusterViewComponent && window.FirmwareViewComponent && window.TopologyGraphComponent && window.MemberCardOverlayComponent && window.ClusterStatusComponent);
|
||||
return !!(window.PrimaryNodeComponent && window.ClusterMembersComponent && window.NodeDetailsComponent && window.FirmwareComponent && window.ClusterViewComponent && window.FirmwareViewComponent && window.TopologyGraphComponent && window.MemberCardOverlayComponent && window.ClusterStatusComponent && window.DrawerComponent);
|
||||
}
|
||||
window.waitForComponentsReady = function(timeoutMs = 5000){
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
130
public/scripts/components/DrawerComponent.js
Normal file
130
public/scripts/components/DrawerComponent.js
Normal file
@@ -0,0 +1,130 @@
|
||||
// Reusable Drawer Component for desktop slide-in panels
|
||||
class DrawerComponent {
|
||||
constructor() {
|
||||
this.detailsDrawer = null;
|
||||
this.detailsDrawerContent = null;
|
||||
this.detailsDrawerBackdrop = null;
|
||||
this.activeDrawerComponent = null;
|
||||
}
|
||||
|
||||
// Determine if we should use desktop drawer behavior
|
||||
isDesktop() {
|
||||
try {
|
||||
return window && window.innerWidth >= 1024; // desktop threshold
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ensureDrawer() {
|
||||
if (this.detailsDrawer) return;
|
||||
|
||||
// Create backdrop
|
||||
this.detailsDrawerBackdrop = document.createElement('div');
|
||||
this.detailsDrawerBackdrop.className = 'details-drawer-backdrop';
|
||||
document.body.appendChild(this.detailsDrawerBackdrop);
|
||||
|
||||
// Create drawer
|
||||
this.detailsDrawer = document.createElement('div');
|
||||
this.detailsDrawer.className = 'details-drawer';
|
||||
|
||||
// Header with close button
|
||||
const header = document.createElement('div');
|
||||
header.className = 'details-drawer-header';
|
||||
header.innerHTML = `
|
||||
<div class="drawer-title">Node Details</div>
|
||||
<button class="drawer-close" aria-label="Close">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
this.detailsDrawer.appendChild(header);
|
||||
|
||||
// Content container
|
||||
this.detailsDrawerContent = document.createElement('div');
|
||||
this.detailsDrawerContent.className = 'details-drawer-content';
|
||||
this.detailsDrawer.appendChild(this.detailsDrawerContent);
|
||||
|
||||
document.body.appendChild(this.detailsDrawer);
|
||||
|
||||
// Close handlers
|
||||
const close = () => this.closeDrawer();
|
||||
header.querySelector('.drawer-close').addEventListener('click', close);
|
||||
this.detailsDrawerBackdrop.addEventListener('click', close);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') close();
|
||||
});
|
||||
}
|
||||
|
||||
openDrawer(title, contentCallback, errorCallback) {
|
||||
this.ensureDrawer();
|
||||
|
||||
// Set drawer title
|
||||
const titleEl = this.detailsDrawer.querySelector('.drawer-title');
|
||||
if (titleEl) {
|
||||
titleEl.textContent = title;
|
||||
}
|
||||
|
||||
// Clear previous component if any
|
||||
if (this.activeDrawerComponent && typeof this.activeDrawerComponent.unmount === 'function') {
|
||||
try {
|
||||
this.activeDrawerComponent.unmount();
|
||||
} catch (_) {}
|
||||
}
|
||||
this.detailsDrawerContent.innerHTML = '<div class="loading-details">Loading detailed information...</div>';
|
||||
|
||||
// Execute content callback
|
||||
try {
|
||||
contentCallback(this.detailsDrawerContent, (component) => {
|
||||
this.activeDrawerComponent = component;
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to load drawer content:', error);
|
||||
if (errorCallback) {
|
||||
errorCallback(error);
|
||||
} else {
|
||||
this.detailsDrawerContent.innerHTML = `
|
||||
<div class="error">
|
||||
<strong>Error loading content:</strong><br>
|
||||
${this.escapeHtml ? this.escapeHtml(error.message) : error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Open drawer
|
||||
this.detailsDrawer.classList.add('open');
|
||||
this.detailsDrawerBackdrop.classList.add('visible');
|
||||
}
|
||||
|
||||
closeDrawer() {
|
||||
if (this.detailsDrawer) this.detailsDrawer.classList.remove('open');
|
||||
if (this.detailsDrawerBackdrop) this.detailsDrawerBackdrop.classList.remove('visible');
|
||||
}
|
||||
|
||||
// Clean up drawer elements
|
||||
destroy() {
|
||||
if (this.detailsDrawer && this.detailsDrawer.parentNode) {
|
||||
this.detailsDrawer.parentNode.removeChild(this.detailsDrawer);
|
||||
}
|
||||
if (this.detailsDrawerBackdrop && this.detailsDrawerBackdrop.parentNode) {
|
||||
this.detailsDrawerBackdrop.parentNode.removeChild(this.detailsDrawerBackdrop);
|
||||
}
|
||||
|
||||
this.detailsDrawer = null;
|
||||
this.detailsDrawerContent = null;
|
||||
this.detailsDrawerBackdrop = null;
|
||||
this.activeDrawerComponent = null;
|
||||
}
|
||||
|
||||
// Helper method for HTML escaping (can be overridden)
|
||||
escapeHtml(text) {
|
||||
if (typeof text !== 'string') return text;
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
window.DrawerComponent = DrawerComponent;
|
||||
@@ -11,10 +11,7 @@ class TopologyGraphComponent extends Component {
|
||||
this.isInitialized = false;
|
||||
|
||||
// Drawer state for desktop reuse (same pattern as ClusterMembersComponent)
|
||||
this.detailsDrawer = null;
|
||||
this.detailsDrawerContent = null;
|
||||
this.detailsDrawerBackdrop = null;
|
||||
this.activeDrawerComponent = null;
|
||||
this.drawer = new DrawerComponent();
|
||||
|
||||
// Tooltip for labels on hover
|
||||
this.tooltipEl = null;
|
||||
@@ -22,86 +19,50 @@ class TopologyGraphComponent extends Component {
|
||||
|
||||
// Determine desktop threshold
|
||||
isDesktop() {
|
||||
try { return window && window.innerWidth >= 1024; } catch (_) { return false; }
|
||||
return this.drawer.isDesktop();
|
||||
}
|
||||
|
||||
ensureDrawer() {
|
||||
if (this.detailsDrawer) return;
|
||||
// Backdrop
|
||||
this.detailsDrawerBackdrop = document.createElement('div');
|
||||
this.detailsDrawerBackdrop.className = 'details-drawer-backdrop';
|
||||
document.body.appendChild(this.detailsDrawerBackdrop);
|
||||
|
||||
// Drawer
|
||||
this.detailsDrawer = document.createElement('div');
|
||||
this.detailsDrawer.className = 'details-drawer';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'details-drawer-header';
|
||||
header.innerHTML = `
|
||||
<div class="drawer-title">Node Details</div>
|
||||
<button class="drawer-close" aria-label="Close">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
this.detailsDrawer.appendChild(header);
|
||||
|
||||
this.detailsDrawerContent = document.createElement('div');
|
||||
this.detailsDrawerContent.className = 'details-drawer-content';
|
||||
this.detailsDrawer.appendChild(this.detailsDrawerContent);
|
||||
|
||||
document.body.appendChild(this.detailsDrawer);
|
||||
|
||||
const close = () => this.closeDrawer();
|
||||
header.querySelector('.drawer-close').addEventListener('click', close);
|
||||
this.detailsDrawerBackdrop.addEventListener('click', close);
|
||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); });
|
||||
}
|
||||
|
||||
openDrawerForNode(nodeData) {
|
||||
this.ensureDrawer();
|
||||
|
||||
// Title from hostname or IP
|
||||
// Get display name for drawer title
|
||||
let displayName = 'Node Details';
|
||||
try {
|
||||
const displayName = nodeData.hostname || nodeData.ip || 'Node Details';
|
||||
const titleEl = this.detailsDrawer.querySelector('.drawer-title');
|
||||
if (titleEl) titleEl.textContent = displayName;
|
||||
const hostname = nodeData.hostname || '';
|
||||
const ip = nodeData.ip || '';
|
||||
|
||||
if (hostname && ip) {
|
||||
displayName = `${hostname} - ${ip}`;
|
||||
} else if (hostname) {
|
||||
displayName = hostname;
|
||||
} else if (ip) {
|
||||
displayName = ip;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Clear previous component
|
||||
if (this.activeDrawerComponent && typeof this.activeDrawerComponent.unmount === 'function') {
|
||||
try { this.activeDrawerComponent.unmount(); } catch (_) {}
|
||||
}
|
||||
this.detailsDrawerContent.innerHTML = '<div class="loading-details">Loading detailed information...</div>';
|
||||
// Open drawer with content callback
|
||||
this.drawer.openDrawer(displayName, (contentContainer, setActiveComponent) => {
|
||||
// Mount NodeDetailsComponent
|
||||
const nodeDetailsVM = new NodeDetailsViewModel();
|
||||
const nodeDetailsComponent = new NodeDetailsComponent(contentContainer, nodeDetailsVM, this.eventBus);
|
||||
setActiveComponent(nodeDetailsComponent);
|
||||
|
||||
// Mount NodeDetailsComponent
|
||||
const nodeDetailsVM = new NodeDetailsViewModel();
|
||||
const nodeDetailsComponent = new NodeDetailsComponent(this.detailsDrawerContent, nodeDetailsVM, this.eventBus);
|
||||
this.activeDrawerComponent = nodeDetailsComponent;
|
||||
|
||||
const ip = nodeData.ip || nodeData.id;
|
||||
nodeDetailsVM.loadNodeDetails(ip).then(() => {
|
||||
nodeDetailsComponent.mount();
|
||||
}).catch((error) => {
|
||||
logger.error('Failed to load node details (topology drawer):', error);
|
||||
this.detailsDrawerContent.innerHTML = `
|
||||
<div class="error">
|
||||
<strong>Error loading node details:</strong><br>
|
||||
${this.escapeHtml(error.message)}
|
||||
</div>
|
||||
`;
|
||||
const ip = nodeData.ip || nodeData.id;
|
||||
nodeDetailsVM.loadNodeDetails(ip).then(() => {
|
||||
nodeDetailsComponent.mount();
|
||||
}).catch((error) => {
|
||||
logger.error('Failed to load node details (topology drawer):', error);
|
||||
contentContainer.innerHTML = `
|
||||
<div class="error">
|
||||
<strong>Error loading node details:</strong><br>
|
||||
${this.escapeHtml(error.message)}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
});
|
||||
|
||||
// Open
|
||||
this.detailsDrawer.classList.add('open');
|
||||
this.detailsDrawerBackdrop.classList.add('visible');
|
||||
}
|
||||
|
||||
closeDrawer() {
|
||||
if (this.detailsDrawer) this.detailsDrawer.classList.remove('open');
|
||||
if (this.detailsDrawerBackdrop) this.detailsDrawerBackdrop.classList.remove('visible');
|
||||
this.drawer.closeDrawer();
|
||||
}
|
||||
|
||||
// Tooltip helpers
|
||||
|
||||
Reference in New Issue
Block a user