Merge pull request 'feature/light-theme' (#4) from feature/light-theme into main
Reviewed-on: #4
This commit is contained in:
67
THEME_IMPROVEMENTS.md
Normal file
67
THEME_IMPROVEMENTS.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# 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
Normal file
144
THEME_README.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# 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
|
||||||
6
index.js
6
index.js
@@ -466,7 +466,7 @@ app.get('/api/node/status', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Proxy endpoint to get node capabilities (optionally for a specific node via ?ip=)
|
// Proxy endpoint to get node capabilities (optionally for a specific node via ?ip=)
|
||||||
app.get('/api/capabilities', async (req, res) => {
|
app.get('/api/node/endpoints', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { ip } = req.query;
|
const { ip } = req.query;
|
||||||
|
|
||||||
@@ -476,9 +476,9 @@ app.get('/api/capabilities', async (req, res) => {
|
|||||||
const caps = await nodeClient.getCapabilities();
|
const caps = await nodeClient.getCapabilities();
|
||||||
return res.json(caps);
|
return res.json(caps);
|
||||||
} catch (innerError) {
|
} catch (innerError) {
|
||||||
console.error('Error fetching capabilities from specific node:', innerError);
|
console.error('Error fetching endpoints from specific node:', innerError);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: 'Failed to fetch capabilities from node',
|
error: 'Failed to fetch endpoints from node',
|
||||||
message: innerError.message
|
message: innerError.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>SPORE UI</title>
|
<title>SPORE UI</title>
|
||||||
<link rel="stylesheet" href="styles/main.css">
|
<link rel="stylesheet" href="styles/main.css">
|
||||||
|
<link rel="stylesheet" href="styles/theme.css?v=1757159926">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -22,6 +23,14 @@
|
|||||||
<button class="nav-tab" data-view="firmware">📦 Firmware</button>
|
<button class="nav-tab" data-view="firmware">📦 Firmware</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-right">
|
<div class="nav-right">
|
||||||
|
<div class="theme-switcher">
|
||||||
|
<button class="theme-toggle" id="theme-toggle" title="Toggle theme">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="5"/>
|
||||||
|
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="cluster-status">🚀 Cluster Online</div>
|
<div class="cluster-status">🚀 Cluster Online</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,6 +156,7 @@
|
|||||||
<script src="./scripts/components/ClusterStatusComponent.js"></script>
|
<script src="./scripts/components/ClusterStatusComponent.js"></script>
|
||||||
<script src="./scripts/components/TopologyGraphComponent.js"></script>
|
<script src="./scripts/components/TopologyGraphComponent.js"></script>
|
||||||
<script src="./scripts/components/ComponentsLoader.js"></script>
|
<script src="./scripts/components/ComponentsLoader.js"></script>
|
||||||
|
<script src="./scripts/theme-manager.js"></script>
|
||||||
<script src="./scripts/app.js"></script>
|
<script src="./scripts/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -80,11 +80,11 @@ class ApiClient {
|
|||||||
return this.request('/api/tasks/status', { method: 'GET', query: ip ? { ip } : undefined });
|
return this.request('/api/tasks/status', { method: 'GET', query: ip ? { ip } : undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCapabilities(ip) {
|
async getEndpoints(ip) {
|
||||||
return this.request('/api/capabilities', { method: 'GET', query: ip ? { ip } : undefined });
|
return this.request('/api/node/endpoints', { method: 'GET', query: ip ? { ip } : undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
async callCapability({ ip, method, uri, params }) {
|
async callEndpoint({ ip, method, uri, params }) {
|
||||||
return this.request('/api/proxy-call', {
|
return this.request('/api/proxy-call', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { ip, method, uri, params }
|
body: { ip, method, uri, params }
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ class NodeDetailsComponent extends Component {
|
|||||||
this.subscribeToProperty('isLoading', this.handleLoadingUpdate.bind(this));
|
this.subscribeToProperty('isLoading', this.handleLoadingUpdate.bind(this));
|
||||||
this.subscribeToProperty('error', this.handleErrorUpdate.bind(this));
|
this.subscribeToProperty('error', this.handleErrorUpdate.bind(this));
|
||||||
this.subscribeToProperty('activeTab', this.handleActiveTabUpdate.bind(this));
|
this.subscribeToProperty('activeTab', this.handleActiveTabUpdate.bind(this));
|
||||||
this.subscribeToProperty('capabilities', this.handleCapabilitiesUpdate.bind(this));
|
this.subscribeToProperty('endpoints', this.handleEndpointsUpdate.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle node status update with state preservation
|
// Handle node status update with state preservation
|
||||||
handleNodeStatusUpdate(newStatus, previousStatus) {
|
handleNodeStatusUpdate(newStatus, previousStatus) {
|
||||||
if (newStatus && !this.viewModel.get('isLoading')) {
|
if (newStatus && !this.viewModel.get('isLoading')) {
|
||||||
this.renderNodeDetails(newStatus, this.viewModel.get('tasks'), this.viewModel.get('capabilities'));
|
this.renderNodeDetails(newStatus, this.viewModel.get('tasks'), this.viewModel.get('endpoints'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ class NodeDetailsComponent extends Component {
|
|||||||
handleTasksUpdate(newTasks, previousTasks) {
|
handleTasksUpdate(newTasks, previousTasks) {
|
||||||
const nodeStatus = this.viewModel.get('nodeStatus');
|
const nodeStatus = this.viewModel.get('nodeStatus');
|
||||||
if (nodeStatus && !this.viewModel.get('isLoading')) {
|
if (nodeStatus && !this.viewModel.get('isLoading')) {
|
||||||
this.renderNodeDetails(nodeStatus, newTasks, this.viewModel.get('capabilities'));
|
this.renderNodeDetails(nodeStatus, newTasks, this.viewModel.get('endpoints'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,12 +48,12 @@ class NodeDetailsComponent extends Component {
|
|||||||
this.updateActiveTab(newTab, previousTab);
|
this.updateActiveTab(newTab, previousTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle capabilities update with state preservation
|
// Handle endpoints update with state preservation
|
||||||
handleCapabilitiesUpdate(newCapabilities, previousCapabilities) {
|
handleEndpointsUpdate(newEndpoints, previousEndpoints) {
|
||||||
const nodeStatus = this.viewModel.get('nodeStatus');
|
const nodeStatus = this.viewModel.get('nodeStatus');
|
||||||
const tasks = this.viewModel.get('tasks');
|
const tasks = this.viewModel.get('tasks');
|
||||||
if (nodeStatus && !this.viewModel.get('isLoading')) {
|
if (nodeStatus && !this.viewModel.get('isLoading')) {
|
||||||
this.renderNodeDetails(nodeStatus, tasks, newCapabilities);
|
this.renderNodeDetails(nodeStatus, tasks, newEndpoints);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ class NodeDetailsComponent extends Component {
|
|||||||
const tasks = this.viewModel.get('tasks');
|
const tasks = this.viewModel.get('tasks');
|
||||||
const isLoading = this.viewModel.get('isLoading');
|
const isLoading = this.viewModel.get('isLoading');
|
||||||
const error = this.viewModel.get('error');
|
const error = this.viewModel.get('error');
|
||||||
const capabilities = this.viewModel.get('capabilities');
|
const endpoints = this.viewModel.get('endpoints');
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
this.renderLoading('<div class="loading-details">Loading detailed information...</div>');
|
this.renderLoading('<div class="loading-details">Loading detailed information...</div>');
|
||||||
@@ -79,10 +79,10 @@ class NodeDetailsComponent extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderNodeDetails(nodeStatus, tasks, capabilities);
|
this.renderNodeDetails(nodeStatus, tasks, endpoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNodeDetails(nodeStatus, tasks, capabilities) {
|
renderNodeDetails(nodeStatus, tasks, endpoints) {
|
||||||
// Use persisted active tab from the view model, default to 'status'
|
// Use persisted active tab from the view model, default to 'status'
|
||||||
const activeTab = (this.viewModel && typeof this.viewModel.get === 'function' && this.viewModel.get('activeTab')) || 'status';
|
const activeTab = (this.viewModel && typeof this.viewModel.get === 'function' && this.viewModel.get('activeTab')) || 'status';
|
||||||
logger.debug('NodeDetailsComponent: Rendering with activeTab:', activeTab);
|
logger.debug('NodeDetailsComponent: Rendering with activeTab:', activeTab);
|
||||||
@@ -92,7 +92,6 @@ class NodeDetailsComponent extends Component {
|
|||||||
<div class="tabs-header">
|
<div class="tabs-header">
|
||||||
<button class="tab-button ${activeTab === 'status' ? 'active' : ''}" data-tab="status">Status</button>
|
<button class="tab-button ${activeTab === 'status' ? 'active' : ''}" data-tab="status">Status</button>
|
||||||
<button class="tab-button ${activeTab === 'endpoints' ? 'active' : ''}" data-tab="endpoints">Endpoints</button>
|
<button class="tab-button ${activeTab === 'endpoints' ? 'active' : ''}" data-tab="endpoints">Endpoints</button>
|
||||||
<button class="tab-button ${activeTab === 'capabilities' ? 'active' : ''}" data-tab="capabilities">Capabilities</button>
|
|
||||||
<button class="tab-button ${activeTab === 'tasks' ? 'active' : ''}" data-tab="tasks">Tasks</button>
|
<button class="tab-button ${activeTab === 'tasks' ? 'active' : ''}" data-tab="tasks">Tasks</button>
|
||||||
<button class="tab-button ${activeTab === 'firmware' ? 'active' : ''}" data-tab="firmware">Firmware</button>
|
<button class="tab-button ${activeTab === 'firmware' ? 'active' : ''}" data-tab="firmware">Firmware</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,14 +120,9 @@ class NodeDetailsComponent extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content ${activeTab === 'endpoints' ? 'active' : ''}" id="endpoints-tab">
|
<div class="tab-content ${activeTab === 'endpoints' ? 'active' : ''}" id="endpoints-tab">
|
||||||
${nodeStatus.api ? nodeStatus.api.map(endpoint =>
|
${this.renderEndpointsTab(endpoints)}
|
||||||
`<div class="endpoint-item">${endpoint.method === 1 ? 'GET' : 'POST'} ${endpoint.uri}</div>`
|
|
||||||
).join('') : '<div class="endpoint-item">No API endpoints available</div>'}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content ${activeTab === 'capabilities' ? 'active' : ''}" id="capabilities-tab">
|
|
||||||
${this.renderCapabilitiesTab(capabilities)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content ${activeTab === 'tasks' ? 'active' : ''}" id="tasks-tab">
|
<div class="tab-content ${activeTab === 'tasks' ? 'active' : ''}" id="tasks-tab">
|
||||||
${this.renderTasksTab(tasks)}
|
${this.renderTasksTab(tasks)}
|
||||||
@@ -150,18 +144,18 @@ class NodeDetailsComponent extends Component {
|
|||||||
this.setupFirmwareUpload();
|
this.setupFirmwareUpload();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCapabilitiesTab(capabilities) {
|
renderEndpointsTab(endpoints) {
|
||||||
if (!capabilities || !Array.isArray(capabilities.endpoints) || capabilities.endpoints.length === 0) {
|
if (!endpoints || !Array.isArray(endpoints.endpoints) || endpoints.endpoints.length === 0) {
|
||||||
return `
|
return `
|
||||||
<div class="no-capabilities">
|
<div class="no-endpoints">
|
||||||
<div>🧩 No capabilities reported</div>
|
<div>🧩 No endpoints reported</div>
|
||||||
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.7;">This node did not return any capabilities</div>
|
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.7;">This node did not return any endpoints</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort endpoints by URI (name), then by method for stable ordering
|
// Sort endpoints by URI (name), then by method for stable ordering
|
||||||
const endpoints = [...capabilities.endpoints].sort((a, b) => {
|
const endpointsList = [...endpoints.endpoints].sort((a, b) => {
|
||||||
const aUri = String(a.uri || '').toLowerCase();
|
const aUri = String(a.uri || '').toLowerCase();
|
||||||
const bUri = String(b.uri || '').toLowerCase();
|
const bUri = String(b.uri || '').toLowerCase();
|
||||||
if (aUri < bUri) return -1;
|
if (aUri < bUri) return -1;
|
||||||
@@ -171,22 +165,22 @@ class NodeDetailsComponent extends Component {
|
|||||||
return aMethod.localeCompare(bMethod);
|
return aMethod.localeCompare(bMethod);
|
||||||
});
|
});
|
||||||
|
|
||||||
const total = endpoints.length;
|
const total = endpointsList.length;
|
||||||
|
|
||||||
// Preserve selection based on a stable key of method+uri if available
|
// Preserve selection based on a stable key of method+uri if available
|
||||||
const selectedKey = String(this.getUIState('capSelectedKey') || '');
|
const selectedKey = String(this.getUIState('endpointSelectedKey') || '');
|
||||||
let selectedIndex = endpoints.findIndex(ep => `${ep.method} ${ep.uri}` === selectedKey);
|
let selectedIndex = endpointsList.findIndex(ep => `${ep.method} ${ep.uri}` === selectedKey);
|
||||||
if (selectedIndex === -1) {
|
if (selectedIndex === -1) {
|
||||||
selectedIndex = Number(this.getUIState('capSelectedIndex'));
|
selectedIndex = Number(this.getUIState('endpointSelectedIndex'));
|
||||||
if (Number.isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= total) {
|
if (Number.isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= total) {
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute padding for aligned display in dropdown
|
// Compute padding for aligned display in dropdown
|
||||||
const maxMethodLen = endpoints.reduce((m, ep) => Math.max(m, String(ep.method || '').length), 0);
|
const maxMethodLen = endpointsList.reduce((m, ep) => Math.max(m, String(ep.method || '').length), 0);
|
||||||
|
|
||||||
const selectorOptions = endpoints.map((ep, idx) => {
|
const selectorOptions = endpointsList.map((ep, idx) => {
|
||||||
const method = String(ep.method || '');
|
const method = String(ep.method || '');
|
||||||
const uri = String(ep.uri || '');
|
const uri = String(ep.uri || '');
|
||||||
const padCount = Math.max(1, (maxMethodLen - method.length) + 2);
|
const padCount = Math.max(1, (maxMethodLen - method.length) + 2);
|
||||||
@@ -194,12 +188,12 @@ class NodeDetailsComponent extends Component {
|
|||||||
return `<option value="${idx}" data-method="${method}" data-uri="${uri}" ${idx === selectedIndex ? 'selected' : ''}>${method}${spacer}${uri}</option>`;
|
return `<option value="${idx}" data-method="${method}" data-uri="${uri}" ${idx === selectedIndex ? 'selected' : ''}>${method}${spacer}${uri}</option>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
const items = endpoints.map((ep, idx) => {
|
const items = endpointsList.map((ep, idx) => {
|
||||||
const formId = `cap-form-${idx}`;
|
const formId = `endpoint-form-${idx}`;
|
||||||
const resultId = `cap-result-${idx}`;
|
const resultId = `endpoint-result-${idx}`;
|
||||||
const params = Array.isArray(ep.params) && ep.params.length > 0
|
const params = Array.isArray(ep.params) && ep.params.length > 0
|
||||||
? `<div class="capability-params">${ep.params.map((p, pidx) => `
|
? `<div class="endpoint-params">${ep.params.map((p, pidx) => `
|
||||||
<label class="capability-param" for="${formId}-field-${pidx}">
|
<label class="endpoint-param" for="${formId}-field-${pidx}">
|
||||||
<span class="param-name">${p.name}${p.required ? ' *' : ''}</span>
|
<span class="param-name">${p.name}${p.required ? ' *' : ''}</span>
|
||||||
${ (Array.isArray(p.values) && p.values.length > 1)
|
${ (Array.isArray(p.values) && p.values.length > 1)
|
||||||
? `<select id="${formId}-field-${pidx}" data-param-name="${p.name}" data-param-location="${p.location || 'body'}" data-param-type="${p.type || 'string'}" data-param-required="${p.required ? '1' : '0'}" class="param-input">${p.values.map(v => `<option value="${v}">${v}</option>`).join('')}</select>`
|
? `<select id="${formId}-field-${pidx}" data-param-name="${p.name}" data-param-location="${p.location || 'body'}" data-param-type="${p.type || 'string'}" data-param-required="${p.required ? '1' : '0'}" class="param-input">${p.values.map(v => `<option value="${v}">${v}</option>`).join('')}</select>`
|
||||||
@@ -207,54 +201,54 @@ class NodeDetailsComponent extends Component {
|
|||||||
}
|
}
|
||||||
</label>
|
</label>
|
||||||
`).join('')}</div>`
|
`).join('')}</div>`
|
||||||
: '<div class="capability-params none">No parameters</div>';
|
: '<div class="endpoint-params none">No parameters</div>';
|
||||||
return `
|
return `
|
||||||
<div class="capability-item" data-cap-index="${idx}" style="display:${idx === selectedIndex ? '' : 'none'};">
|
<div class="endpoint-item" data-endpoint-index="${idx}" style="display:${idx === selectedIndex ? '' : 'none'};">
|
||||||
<div class="capability-header">
|
<div class="endpoint-header">
|
||||||
<span class="cap-method">${ep.method}</span>
|
<span class="endpoint-method">${ep.method}</span>
|
||||||
<span class="cap-uri">${ep.uri}</span>
|
<span class="endpoint-uri">${ep.uri}</span>
|
||||||
<button class="cap-call-btn" data-action="call-capability" data-method="${ep.method}" data-uri="${ep.uri}" data-form-id="${formId}" data-result-id="${resultId}">Call</button>
|
<button class="endpoint-call-btn" data-action="call-endpoint" data-method="${ep.method}" data-uri="${ep.uri}" data-form-id="${formId}" data-result-id="${resultId}">Call</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="${formId}" class="capability-form" onsubmit="return false;">
|
<form id="${formId}" class="endpoint-form" onsubmit="return false;">
|
||||||
${params}
|
${params}
|
||||||
</form>
|
</form>
|
||||||
<div id="${resultId}" class="capability-result" style="display:none;"></div>
|
<div id="${resultId}" class="endpoint-result" style="display:none;"></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
// Attach events after render in setupCapabilitiesEvents()
|
// Attach events after render in setupEndpointsEvents()
|
||||||
setTimeout(() => this.setupCapabilitiesEvents(), 0);
|
setTimeout(() => this.setupEndpointsEvents(), 0);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="capability-selector">
|
<div class="endpoint-selector">
|
||||||
<label class="param-name" for="capability-select">Capability</label>
|
<label class="param-name" for="endpoint-select">Endpoint</label>
|
||||||
<select id="capability-select" class="param-input" style="font-family: monospace;">${selectorOptions}</select>
|
<select id="endpoint-select" class="param-input" style="font-family: monospace;">${selectorOptions}</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="capabilities-list">${items}</div>
|
<div class="endpoints-list">${items}</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
setupCapabilitiesEvents() {
|
setupEndpointsEvents() {
|
||||||
const selector = this.findElement('#capability-select');
|
const selector = this.findElement('#endpoint-select');
|
||||||
if (selector) {
|
if (selector) {
|
||||||
this.addEventListener(selector, 'change', (e) => {
|
this.addEventListener(selector, 'change', (e) => {
|
||||||
const selected = Number(e.target.value);
|
const selected = Number(e.target.value);
|
||||||
const items = Array.from(this.findAllElements('.capability-item'));
|
const items = Array.from(this.findAllElements('.endpoint-item'));
|
||||||
items.forEach((el, idx) => {
|
items.forEach((el, idx) => {
|
||||||
el.style.display = (idx === selected) ? '' : 'none';
|
el.style.display = (idx === selected) ? '' : 'none';
|
||||||
});
|
});
|
||||||
this.setUIState('capSelectedIndex', selected);
|
this.setUIState('endpointSelectedIndex', selected);
|
||||||
const opt = e.target.selectedOptions && e.target.selectedOptions[0];
|
const opt = e.target.selectedOptions && e.target.selectedOptions[0];
|
||||||
if (opt) {
|
if (opt) {
|
||||||
const method = opt.dataset.method || '';
|
const method = opt.dataset.method || '';
|
||||||
const uri = opt.dataset.uri || '';
|
const uri = opt.dataset.uri || '';
|
||||||
this.setUIState('capSelectedKey', `${method} ${uri}`);
|
this.setUIState('endpointSelectedKey', `${method} ${uri}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttons = this.findAllElements('.cap-call-btn');
|
const buttons = this.findAllElements('.endpoint-call-btn');
|
||||||
buttons.forEach(btn => {
|
buttons.forEach(btn => {
|
||||||
this.addEventListener(btn, 'click', async (e) => {
|
this.addEventListener(btn, 'click', async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -281,7 +275,7 @@ class NodeDetailsComponent extends Component {
|
|||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
resultEl.style.display = 'block';
|
resultEl.style.display = 'block';
|
||||||
resultEl.innerHTML = `
|
resultEl.innerHTML = `
|
||||||
<div class="cap-call-error">
|
<div class="endpoint-call-error">
|
||||||
<div>❌ Missing required fields: ${missing.map(m => this.escapeHtml(m.name)).join(', ')}</div>
|
<div>❌ Missing required fields: ${missing.map(m => this.escapeHtml(m.name)).join(', ')}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -293,17 +287,17 @@ class NodeDetailsComponent extends Component {
|
|||||||
resultEl.innerHTML = '<div class="loading">Calling endpoint...</div>';
|
resultEl.innerHTML = '<div class="loading">Calling endpoint...</div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.viewModel.callCapability(method, uri, params);
|
const response = await this.viewModel.callEndpoint(method, uri, params);
|
||||||
const pretty = typeof response?.data === 'object' ? JSON.stringify(response.data, null, 2) : String(response?.data ?? '');
|
const pretty = typeof response?.data === 'object' ? JSON.stringify(response.data, null, 2) : String(response?.data ?? '');
|
||||||
resultEl.innerHTML = `
|
resultEl.innerHTML = `
|
||||||
<div class="cap-call-success">
|
<div class="endpoint-call-success">
|
||||||
<div>✅ Success</div>
|
<div>✅ Success</div>
|
||||||
<pre class="cap-result-pre">${this.escapeHtml(pretty)}</pre>
|
<pre class="endpoint-result-pre">${this.escapeHtml(pretty)}</pre>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
resultEl.innerHTML = `
|
resultEl.innerHTML = `
|
||||||
<div class="cap-call-error">
|
<div class="endpoint-call-error">
|
||||||
<div>❌ Error: ${this.escapeHtml(err.message || 'Request failed')}</div>
|
<div>❌ Error: ${this.escapeHtml(err.message || 'Request failed')}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ class TopologyGraphComponent extends Component {
|
|||||||
.attr('height', '100%')
|
.attr('height', '100%')
|
||||||
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
|
.attr('viewBox', `0 0 ${this.width} ${this.height}`)
|
||||||
.style('border', '1px solid rgba(255, 255, 255, 0.1)')
|
.style('border', '1px solid rgba(255, 255, 255, 0.1)')
|
||||||
.style('background', 'rgba(0, 0, 0, 0.2)')
|
//.style('background', 'rgba(0, 0, 0, 0.2)')
|
||||||
.style('border-radius', '12px');
|
.style('border-radius', '12px');
|
||||||
|
|
||||||
// Add zoom behavior
|
// Add zoom behavior
|
||||||
@@ -280,7 +280,7 @@ class TopologyGraphComponent extends Component {
|
|||||||
.attr('x', 15)
|
.attr('x', 15)
|
||||||
.attr('y', 4)
|
.attr('y', 4)
|
||||||
.attr('font-size', '13px')
|
.attr('font-size', '13px')
|
||||||
.attr('fill', '#ecf0f1')
|
.attr('fill', 'var(--text-primary)')
|
||||||
.attr('font-weight', '500');
|
.attr('font-weight', '500');
|
||||||
|
|
||||||
// IP
|
// IP
|
||||||
@@ -289,7 +289,7 @@ class TopologyGraphComponent extends Component {
|
|||||||
.attr('x', 15)
|
.attr('x', 15)
|
||||||
.attr('y', 20)
|
.attr('y', 20)
|
||||||
.attr('font-size', '11px')
|
.attr('font-size', '11px')
|
||||||
.attr('fill', 'rgba(255, 255, 255, 0.7)');
|
.attr('fill', 'var(--text-secondary)');
|
||||||
|
|
||||||
// Status text
|
// Status text
|
||||||
node.append('text')
|
node.append('text')
|
||||||
@@ -307,7 +307,7 @@ class TopologyGraphComponent extends Component {
|
|||||||
.data(links)
|
.data(links)
|
||||||
.enter().append('text')
|
.enter().append('text')
|
||||||
.attr('font-size', '12px')
|
.attr('font-size', '12px')
|
||||||
.attr('fill', '#ecf0f1')
|
.attr('fill', 'var(--text-primary)')
|
||||||
.attr('font-weight', '600')
|
.attr('font-weight', '600')
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
.style('text-shadow', '0 1px 2px rgba(0, 0, 0, 0.8)')
|
.style('text-shadow', '0 1px 2px rgba(0, 0, 0, 0.8)')
|
||||||
|
|||||||
120
public/scripts/theme-manager.js
Normal file
120
public/scripts/theme-manager.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
// Theme Manager - Handles theme switching and persistence
|
||||||
|
|
||||||
|
class ThemeManager {
|
||||||
|
constructor() {
|
||||||
|
this.currentTheme = this.getStoredTheme() || 'dark';
|
||||||
|
this.themeToggle = document.getElementById('theme-toggle');
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Apply stored theme on page load
|
||||||
|
this.applyTheme(this.currentTheme);
|
||||||
|
|
||||||
|
// Set up event listener for theme toggle
|
||||||
|
if (this.themeToggle) {
|
||||||
|
this.themeToggle.addEventListener('click', () => this.toggleTheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for system theme changes
|
||||||
|
if (window.matchMedia) {
|
||||||
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
mediaQuery.addListener((e) => {
|
||||||
|
if (this.getStoredTheme() === 'system') {
|
||||||
|
this.applyTheme(e.matches ? 'dark' : 'light');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStoredTheme() {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem('spore-ui-theme');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not access localStorage for theme preference');
|
||||||
|
return 'dark';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setStoredTheme(theme) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('spore-ui-theme', theme);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not save theme preference to localStorage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTheme(theme) {
|
||||||
|
// Update data attribute on html element
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
|
||||||
|
// Update theme toggle icon
|
||||||
|
this.updateThemeIcon(theme);
|
||||||
|
|
||||||
|
// Store the theme preference
|
||||||
|
this.setStoredTheme(theme);
|
||||||
|
|
||||||
|
this.currentTheme = theme;
|
||||||
|
|
||||||
|
// Dispatch custom event for other components
|
||||||
|
window.dispatchEvent(new CustomEvent('themeChanged', {
|
||||||
|
detail: { theme: theme }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateThemeIcon(theme) {
|
||||||
|
if (!this.themeToggle) return;
|
||||||
|
|
||||||
|
const svg = this.themeToggle.querySelector('svg');
|
||||||
|
if (!svg) return;
|
||||||
|
|
||||||
|
// Update the SVG content based on theme
|
||||||
|
if (theme === 'light') {
|
||||||
|
// Sun icon for light theme
|
||||||
|
svg.innerHTML = `
|
||||||
|
<circle cx="12" cy="12" r="5"/>
|
||||||
|
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Moon icon for dark theme
|
||||||
|
svg.innerHTML = `
|
||||||
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleTheme() {
|
||||||
|
const newTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
|
||||||
|
this.applyTheme(newTheme);
|
||||||
|
|
||||||
|
// Add a subtle animation to the toggle button
|
||||||
|
if (this.themeToggle) {
|
||||||
|
this.themeToggle.style.transform = 'scale(0.9)';
|
||||||
|
setTimeout(() => {
|
||||||
|
this.themeToggle.style.transform = 'scale(1)';
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to get current theme (useful for other components)
|
||||||
|
getCurrentTheme() {
|
||||||
|
return this.currentTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to set theme programmatically
|
||||||
|
setTheme(theme) {
|
||||||
|
if (['dark', 'light'].includes(theme)) {
|
||||||
|
this.applyTheme(theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize theme manager when DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
window.themeManager = new ThemeManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export for use in other modules
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = ThemeManager;
|
||||||
|
}
|
||||||
@@ -219,7 +219,7 @@ class NodeDetailsViewModel extends ViewModel {
|
|||||||
error: null,
|
error: null,
|
||||||
activeTab: 'status',
|
activeTab: 'status',
|
||||||
nodeIp: null,
|
nodeIp: null,
|
||||||
capabilities: null,
|
endpoints: null,
|
||||||
tasksSummary: null
|
tasksSummary: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -247,8 +247,8 @@ class NodeDetailsViewModel extends ViewModel {
|
|||||||
// Load tasks data
|
// Load tasks data
|
||||||
await this.loadTasksData();
|
await this.loadTasksData();
|
||||||
|
|
||||||
// Load capabilities data
|
// Load endpoints data
|
||||||
await this.loadCapabilitiesData();
|
await this.loadEndpointsData();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load node details:', error);
|
console.error('Failed to load node details:', error);
|
||||||
@@ -272,22 +272,22 @@ class NodeDetailsViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load capabilities data with state preservation
|
// Load endpoints data with state preservation
|
||||||
async loadCapabilitiesData() {
|
async loadEndpointsData() {
|
||||||
try {
|
try {
|
||||||
const ip = this.get('nodeIp');
|
const ip = this.get('nodeIp');
|
||||||
const response = await window.apiClient.getCapabilities(ip);
|
const response = await window.apiClient.getEndpoints(ip);
|
||||||
this.set('capabilities', response || null);
|
this.set('endpoints', response || null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load capabilities:', error);
|
console.error('Failed to load endpoints:', error);
|
||||||
this.set('capabilities', null);
|
this.set('endpoints', null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoke a capability against this node
|
// Invoke an endpoint against this node
|
||||||
async callCapability(method, uri, params) {
|
async callEndpoint(method, uri, params) {
|
||||||
const ip = this.get('nodeIp');
|
const ip = this.get('nodeIp');
|
||||||
return window.apiClient.callCapability({ ip, method, uri, params });
|
return window.apiClient.callEndpoint({ ip, method, uri, params });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set active tab with state persistence
|
// Set active tab with state persistence
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
3463
public/styles/main.css.backup
Normal file
3463
public/styles/main.css.backup
Normal file
File diff suppressed because it is too large
Load Diff
1148
public/styles/theme.css
Normal file
1148
public/styles/theme.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -85,11 +85,11 @@ class SporeApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get node capabilities
|
* Get node endpoints
|
||||||
* @returns {Promise<Object>} Capabilities response
|
* @returns {Promise<Object>} endpoints response
|
||||||
*/
|
*/
|
||||||
async getCapabilities() {
|
async getCapabilities() {
|
||||||
return this.request('GET', '/api/capabilities');
|
return this.request('GET', '/api/node/endpoints');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user