Merge pull request 'feature/ui-harmonization' (#23) from feature/ui-harmonization into main
Reviewed-on: #23
This commit is contained in:
14
index.js
14
index.js
@@ -19,12 +19,7 @@ const PORT = process.env.PORT || 3000;
|
|||||||
// Serve static files from public directory
|
// Serve static files from public directory
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
// Serve the main HTML page
|
// Health check endpoint (before catch-all route)
|
||||||
app.get('/', (req, res) => {
|
|
||||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Health check endpoint
|
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'healthy',
|
status: 'healthy',
|
||||||
@@ -34,6 +29,13 @@ app.get('/health', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// SPA catch-all route - serves index.html for all routes
|
||||||
|
// This allows client-side routing to work properly
|
||||||
|
// Using regex pattern for Express 5 compatibility
|
||||||
|
app.get(/^\/(?!health$).*/, (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`SPORE UI Frontend Server is running on http://0.0.0.0:${PORT}`);
|
console.log(`SPORE UI Frontend Server is running on http://0.0.0.0:${PORT}`);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="nav-left">
|
<div class="nav-left">
|
||||||
<button class="nav-tab active" data-view="cluster">
|
<a href="/cluster" class="nav-tab active" data-view="cluster">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
|
||||||
<circle cx="12" cy="12" r="9"/>
|
<circle cx="12" cy="12" r="9"/>
|
||||||
<circle cx="8" cy="10" r="1.5"/>
|
<circle cx="8" cy="10" r="1.5"/>
|
||||||
@@ -27,8 +27,8 @@
|
|||||||
<path d="M9 11l3 3M9 11l6-3"/>
|
<path d="M9 11l3 3M9 11l6-3"/>
|
||||||
</svg>
|
</svg>
|
||||||
Cluster
|
Cluster
|
||||||
</button>
|
</a>
|
||||||
<button class="nav-tab" data-view="topology">
|
<a href="/topology" class="nav-tab" data-view="topology">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
|
||||||
<circle cx="12" cy="4" r="1.6"/>
|
<circle cx="12" cy="4" r="1.6"/>
|
||||||
<circle cx="19" cy="9" r="1.6"/>
|
<circle cx="19" cy="9" r="1.6"/>
|
||||||
@@ -38,20 +38,20 @@
|
|||||||
<path d="M12 4L16 18M16 18L5 9M5 9L19 9M19 9L8 18M8 18L12 4"/>
|
<path d="M12 4L16 18M16 18L5 9M5 9L19 9M19 9L8 18M8 18L12 4"/>
|
||||||
</svg>
|
</svg>
|
||||||
Topology
|
Topology
|
||||||
</button>
|
</a>
|
||||||
<button class="nav-tab" data-view="monitoring">
|
<a href="/monitoring" class="nav-tab" data-view="monitoring">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
|
||||||
<path d="M3 12h3l2 7 4-14 3 10 2-6h4"/>
|
<path d="M3 12h3l2 7 4-14 3 10 2-6h4"/>
|
||||||
</svg>
|
</svg>
|
||||||
Monitoring
|
Monitoring
|
||||||
</button>
|
</a>
|
||||||
<button class="nav-tab" data-view="firmware">
|
<a href="/firmware" class="nav-tab" data-view="firmware">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
|
||||||
<path d="M4 7l8-4 8 4v10l-8 4-8-4z"/>
|
<path d="M4 7l8-4 8 4v10l-8 4-8-4z"/>
|
||||||
<path d="M12 8v8"/>
|
<path d="M12 8v8"/>
|
||||||
</svg>
|
</svg>
|
||||||
Firmware
|
Firmware
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-right">
|
<div class="nav-right">
|
||||||
<div class="theme-switcher">
|
<div class="theme-switcher">
|
||||||
|
|||||||
@@ -155,7 +155,9 @@ class FirmwareComponent extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const firmwareHTML = filteredGroups.map(group => this.renderFirmwareGroup(group)).join('');
|
// Auto-expand groups when search is active to show results
|
||||||
|
const autoExpand = searchQuery.trim().length > 0;
|
||||||
|
const firmwareHTML = filteredGroups.map(group => this.renderFirmwareGroup(group, autoExpand)).join('');
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="firmware-groups">
|
<div class="firmware-groups">
|
||||||
@@ -167,11 +169,14 @@ class FirmwareComponent extends Component {
|
|||||||
this.setupFirmwareItemListeners();
|
this.setupFirmwareItemListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFirmwareGroup(group) {
|
renderFirmwareGroup(group, autoExpand = false) {
|
||||||
const versionsHTML = group.firmware.map(firmware => this.renderFirmwareVersion(firmware)).join('');
|
const versionsHTML = group.firmware.map(firmware => this.renderFirmwareVersion(firmware)).join('');
|
||||||
|
|
||||||
|
// Add 'expanded' class if autoExpand is true (e.g., when search results are shown)
|
||||||
|
const expandedClass = autoExpand ? 'expanded' : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="firmware-group">
|
<div class="firmware-group ${expandedClass}">
|
||||||
<div class="firmware-group-header">
|
<div class="firmware-group-header">
|
||||||
<div class="firmware-group-header-content">
|
<div class="firmware-group-header-content">
|
||||||
<h3 class="firmware-group-name">${this.escapeHtml(group.name)}</h3>
|
<h3 class="firmware-group-name">${this.escapeHtml(group.name)}</h3>
|
||||||
|
|||||||
@@ -83,13 +83,15 @@ class FirmwareFormComponent extends Component {
|
|||||||
|
|
||||||
const labels = this.firmwareData?.labels || {};
|
const labels = this.firmwareData?.labels || {};
|
||||||
const labelsHTML = Object.entries(labels).map(([key, value]) =>
|
const labelsHTML = Object.entries(labels).map(([key, value]) =>
|
||||||
`<div class="label-item">
|
`<div class="label-item" data-key="${this.escapeHtml(key)}">
|
||||||
|
<div class="label-content">
|
||||||
<span class="label-key">${this.escapeHtml(key)}</span>
|
<span class="label-key">${this.escapeHtml(key)}</span>
|
||||||
|
<span class="label-separator">=</span>
|
||||||
<span class="label-value">${this.escapeHtml(value)}</span>
|
<span class="label-value">${this.escapeHtml(value)}</span>
|
||||||
|
</div>
|
||||||
<button type="button" class="remove-label-btn" data-label-key="${this.escapeHtml(key)}" title="Remove label">
|
<button type="button" class="remove-label-btn" data-label-key="${this.escapeHtml(key)}" title="Remove label">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
<path d="M18 6L6 18M6 6l12 12"/>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>`
|
</div>`
|
||||||
@@ -148,7 +150,7 @@ class FirmwareFormComponent extends Component {
|
|||||||
<div class="labels-section">
|
<div class="labels-section">
|
||||||
<div class="add-label-controls">
|
<div class="add-label-controls">
|
||||||
<input type="text" id="label-key" placeholder="Key" class="label-key-input">
|
<input type="text" id="label-key" placeholder="Key" class="label-key-input">
|
||||||
<span class="label-separator">:</span>
|
<span class="label-separator">=</span>
|
||||||
<input type="text" id="label-value" placeholder="Value" class="label-value-input">
|
<input type="text" id="label-value" placeholder="Value" class="label-value-input">
|
||||||
<button type="button" id="add-label-btn" class="add-label-btn">
|
<button type="button" id="add-label-btn" class="add-label-btn">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
||||||
@@ -202,7 +204,7 @@ class FirmwareFormComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if key already exists
|
// Check if key already exists
|
||||||
const existingLabel = labelsContainer.querySelector(`[data-label-key="${this.escapeHtml(key)}"]`);
|
const existingLabel = labelsContainer.querySelector(`[data-key="${this.escapeHtml(key)}"]`);
|
||||||
if (existingLabel) {
|
if (existingLabel) {
|
||||||
this.showError('A label with this key already exists');
|
this.showError('A label with this key already exists');
|
||||||
return;
|
return;
|
||||||
@@ -210,13 +212,15 @@ class FirmwareFormComponent extends Component {
|
|||||||
|
|
||||||
// Add the label
|
// Add the label
|
||||||
const labelHTML = `
|
const labelHTML = `
|
||||||
<div class="label-item">
|
<div class="label-item" data-key="${this.escapeHtml(key)}">
|
||||||
|
<div class="label-content">
|
||||||
<span class="label-key">${this.escapeHtml(key)}</span>
|
<span class="label-key">${this.escapeHtml(key)}</span>
|
||||||
|
<span class="label-separator">=</span>
|
||||||
<span class="label-value">${this.escapeHtml(value)}</span>
|
<span class="label-value">${this.escapeHtml(value)}</span>
|
||||||
|
</div>
|
||||||
<button type="button" class="remove-label-btn" data-label-key="${this.escapeHtml(key)}" title="Remove label">
|
<button type="button" class="remove-label-btn" data-label-key="${this.escapeHtml(key)}" title="Remove label">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
<path d="M18 6L6 18M6 6l12 12"/>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -709,26 +709,7 @@ class NodeDetailsComponent extends Component {
|
|||||||
const labels = nodeStatus?.labels || {};
|
const labels = nodeStatus?.labels || {};
|
||||||
const labelsArray = Object.entries(labels);
|
const labelsArray = Object.entries(labels);
|
||||||
|
|
||||||
let html = `
|
const labelsHTML = labelsArray.map(([key, value]) => `
|
||||||
<div class="labels-section">
|
|
||||||
<div class="labels-header">
|
|
||||||
<h3>Node Labels</h3>
|
|
||||||
<p class="labels-description">Manage custom labels for this node. Labels help organize and identify nodes in your cluster.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="labels-list">
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (labelsArray.length === 0) {
|
|
||||||
html += `
|
|
||||||
<div class="no-labels">
|
|
||||||
<div>No labels configured</div>
|
|
||||||
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.7;">Add labels to organize and identify this node</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
labelsArray.forEach(([key, value]) => {
|
|
||||||
html += `
|
|
||||||
<div class="label-item" data-key="${this.escapeHtml(key)}">
|
<div class="label-item" data-key="${this.escapeHtml(key)}">
|
||||||
<div class="label-content">
|
<div class="label-content">
|
||||||
<span class="label-key">${this.escapeHtml(key)}</span>
|
<span class="label-key">${this.escapeHtml(key)}</span>
|
||||||
@@ -741,30 +722,34 @@ class NodeDetailsComponent extends Component {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`).join('');
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
html += `
|
let html = `
|
||||||
|
<div class="labels-section">
|
||||||
|
<div class="labels-header">
|
||||||
|
<p class="labels-description">Manage custom labels for this node. Labels help organize and identify nodes in your cluster.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="add-label-section">
|
<div class="add-label-controls">
|
||||||
<div class="add-label-form">
|
<input type="text" id="label-key" placeholder="Key" class="label-key-input" maxlength="50">
|
||||||
<div class="form-group">
|
<span class="label-separator">=</span>
|
||||||
<label for="label-key">Key:</label>
|
<input type="text" id="label-value" placeholder="Value" class="label-value-input" maxlength="100">
|
||||||
<input type="text" id="label-key" placeholder="e.g., environment" maxlength="50">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="label-value">Value:</label>
|
|
||||||
<input type="text" id="label-value" placeholder="e.g., production" maxlength="100">
|
|
||||||
</div>
|
|
||||||
<button class="add-label-btn" type="button">
|
<button class="add-label-btn" type="button">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
||||||
<path d="M12 5v14M5 12h14"/>
|
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||||
</svg>
|
</svg>
|
||||||
Add Label
|
Add Label
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="labels-list">
|
||||||
|
${labelsArray.length === 0 ? `
|
||||||
|
<div class="no-labels">
|
||||||
|
<div>No labels configured</div>
|
||||||
|
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.7;">Add labels to organize and identify this node</div>
|
||||||
|
</div>
|
||||||
|
` : labelsHTML}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="labels-actions">
|
<div class="labels-actions">
|
||||||
@@ -1004,7 +989,6 @@ class NodeDetailsComponent extends Component {
|
|||||||
const summaryHTML = summary ? `
|
const summaryHTML = summary ? `
|
||||||
<div class="tasks-summary">
|
<div class="tasks-summary">
|
||||||
<div class="tasks-summary-left">
|
<div class="tasks-summary-left">
|
||||||
<div class="summary-icon">${window.icon('file', { width: 16, height: 16 })}</div>
|
|
||||||
<div>
|
<div>
|
||||||
<div class="summary-title">Tasks Overview</div>
|
<div class="summary-title">Tasks Overview</div>
|
||||||
<div class="summary-subtitle">System task management and monitoring</div>
|
<div class="summary-subtitle">System task management and monitoring</div>
|
||||||
|
|||||||
@@ -647,7 +647,7 @@ class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to a route
|
// Navigate to a route
|
||||||
navigateTo(routeName) {
|
navigateTo(routeName, updateUrl = true) {
|
||||||
// Check cooldown period
|
// Check cooldown period
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - this.lastNavigationTime < this.navigationCooldown) {
|
if (now - this.lastNavigationTime < this.navigationCooldown) {
|
||||||
@@ -670,10 +670,45 @@ class App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update URL if requested
|
||||||
|
if (updateUrl) {
|
||||||
|
this.updateURL(routeName);
|
||||||
|
}
|
||||||
|
|
||||||
this.lastNavigationTime = now;
|
this.lastNavigationTime = now;
|
||||||
this.performNavigation(routeName);
|
this.performNavigation(routeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update browser URL
|
||||||
|
updateURL(routeName) {
|
||||||
|
const url = `/${routeName}`;
|
||||||
|
if (window.location.pathname !== url) {
|
||||||
|
window.history.pushState({ route: routeName }, '', url);
|
||||||
|
logger.debug(`App: Updated URL to ${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get route from current URL
|
||||||
|
getRouteFromURL() {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
// Remove leading slash and use as route name
|
||||||
|
const routeName = path.substring(1) || 'cluster'; // default to cluster
|
||||||
|
return routeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle browser back/forward
|
||||||
|
handlePopState(event) {
|
||||||
|
if (event.state && event.state.route) {
|
||||||
|
logger.debug(`App: Handling popstate for route '${event.state.route}'`);
|
||||||
|
this.navigateTo(event.state.route, false); // Don't update URL again
|
||||||
|
} else {
|
||||||
|
// Fallback: parse URL
|
||||||
|
const routeName = this.getRouteFromURL();
|
||||||
|
logger.debug(`App: Handling popstate, navigating to '${routeName}'`);
|
||||||
|
this.navigateTo(routeName, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Perform the actual navigation
|
// Perform the actual navigation
|
||||||
async performNavigation(routeName) {
|
async performNavigation(routeName) {
|
||||||
this.navigationInProgress = true;
|
this.navigationInProgress = true;
|
||||||
@@ -848,12 +883,27 @@ class App {
|
|||||||
|
|
||||||
// Setup navigation
|
// Setup navigation
|
||||||
setupNavigation() {
|
setupNavigation() {
|
||||||
|
// Intercept navigation link clicks
|
||||||
document.querySelectorAll((window.CONSTANTS && window.CONSTANTS.SELECTORS.NAV_TAB) || '.nav-tab').forEach(tab => {
|
document.querySelectorAll((window.CONSTANTS && window.CONSTANTS.SELECTORS.NAV_TAB) || '.nav-tab').forEach(tab => {
|
||||||
tab.addEventListener('click', () => {
|
tab.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault(); // Prevent default link behavior
|
||||||
const routeName = tab.dataset.view;
|
const routeName = tab.dataset.view;
|
||||||
this.navigateTo(routeName);
|
this.navigateTo(routeName);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle browser back/forward buttons
|
||||||
|
window.addEventListener('popstate', (e) => this.handlePopState(e));
|
||||||
|
|
||||||
|
// Navigate to route based on current URL on initial load
|
||||||
|
const initialRoute = this.getRouteFromURL();
|
||||||
|
logger.debug(`App: Initial route from URL: '${initialRoute}'`);
|
||||||
|
|
||||||
|
// Set initial history state
|
||||||
|
window.history.replaceState({ route: initialRoute }, '', `/${initialRoute}`);
|
||||||
|
|
||||||
|
// Navigate to initial route
|
||||||
|
this.navigateTo(initialRoute, false); // Don't update URL since we just set it
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up cached components (call when app is shutting down)
|
// Clean up cached components (call when app is shutting down)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user