feat: replace all emojis with SVG icons
This commit is contained in:
@@ -18,10 +18,37 @@
|
||||
</svg>
|
||||
</button>
|
||||
<div class="nav-left">
|
||||
<button class="nav-tab active" data-view="cluster">🌐 Cluster</button>
|
||||
<button class="nav-tab" data-view="topology">🔗 Topology</button>
|
||||
<button class="nav-tab" data-view="monitoring">📡 Monitoring</button>
|
||||
<button class="nav-tab" data-view="firmware">📦 Firmware</button>
|
||||
<button 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;">
|
||||
<circle cx="12" cy="12" r="9"/>
|
||||
<circle cx="8" cy="10" r="1.5"/>
|
||||
<circle cx="16" cy="8" r="1.5"/>
|
||||
<circle cx="14" cy="15" r="1.5"/>
|
||||
<path d="M9 11l3 3M9 11l6-3"/>
|
||||
</svg>
|
||||
Cluster
|
||||
</button>
|
||||
<button 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;">
|
||||
<path d="M6 3v6M6 15v6M18 3v6M18 15v6"/>
|
||||
<rect x="3" y="9" width="6" height="6" rx="2"/>
|
||||
<rect x="15" y="9" width="6" height="6" rx="2"/>
|
||||
</svg>
|
||||
Topology
|
||||
</button>
|
||||
<button 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;">
|
||||
<path d="M3 12h3l2 7 4-14 3 10 2-6h4"/>
|
||||
</svg>
|
||||
Monitoring
|
||||
</button>
|
||||
<button 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;">
|
||||
<path d="M4 7l8-4 8 4v10l-8 4-8-4z"/>
|
||||
<path d="M12 8v8"/>
|
||||
</svg>
|
||||
Firmware
|
||||
</button>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="theme-switcher">
|
||||
@@ -32,7 +59,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="cluster-status">🚀 Cluster Online</div>
|
||||
<div class="cluster-status">Cluster Online</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +71,7 @@
|
||||
<span class="primary-node-label">Primary Node:</span>
|
||||
<span class="primary-node-ip" id="primary-node-ip">Discovering...</span>
|
||||
<button class="primary-node-refresh" id="select-random-primary-btn"
|
||||
title="🎲 Select random primary node">
|
||||
title="Select random primary node">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14"
|
||||
height="14">
|
||||
<path d="M1 4v6h6M23 20v-6h-6" />
|
||||
@@ -85,7 +112,13 @@
|
||||
<div class="firmware-overview">
|
||||
<div class="firmware-actions">
|
||||
<div class="action-group">
|
||||
<h3>🚀 Firmware Update</h3>
|
||||
<h3>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px; vertical-align: -2px;">
|
||||
<path d="M4 7l8-4 8 4v10l-8 4-8-4z"/>
|
||||
<path d="M12 8v8"/>
|
||||
</svg>
|
||||
Firmware Update
|
||||
</h3>
|
||||
<div class="firmware-upload-compact">
|
||||
<div class="compact-upload-row">
|
||||
<div class="file-upload-area">
|
||||
@@ -120,13 +153,24 @@
|
||||
style="display: none;">
|
||||
<button class="upload-btn-compact"
|
||||
onclick="document.getElementById('global-firmware-file').click()">
|
||||
📁 Choose File
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="margin-right:6px; vertical-align: -2px;">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<path d="M14 2v6h6"/>
|
||||
</svg>
|
||||
Choose File
|
||||
</button>
|
||||
<span class="file-info" id="file-info">No file selected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="deploy-btn" id="deploy-btn" disabled>🚀 Deploy</button>
|
||||
<button class="deploy-btn" id="deploy-btn" disabled>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px; vertical-align: -2px;">
|
||||
<path d="M12 16V4"/>
|
||||
<path d="M8 8l4-4 4 4"/>
|
||||
<path d="M20 16v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2"/>
|
||||
</svg>
|
||||
Deploy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,7 +187,12 @@
|
||||
<div id="monitoring-view" class="view-content">
|
||||
<div class="monitoring-view-section">
|
||||
<div class="monitoring-header">
|
||||
<h2>📡 Monitoring</h2>
|
||||
<h2>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18" style="margin-right:8px; vertical-align: -2px;">
|
||||
<path d="M3 12h3l2 7 4-14 3 10 2-6h4"/>
|
||||
</svg>
|
||||
Monitoring
|
||||
</h2>
|
||||
<button class="refresh-btn" id="refresh-monitoring-btn">
|
||||
<svg class="refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 4v6h6M23 20v-6h-6" />
|
||||
@@ -172,6 +221,7 @@
|
||||
|
||||
<script src="./vendor/d3.v7.min.js"></script>
|
||||
<script src="./scripts/constants.js"></script>
|
||||
<script src="./scripts/icons.js"></script>
|
||||
<script src="./scripts/framework.js"></script>
|
||||
<script src="./scripts/api-client.js"></script>
|
||||
<script src="./scripts/view-models.js"></script>
|
||||
|
||||
@@ -302,7 +302,7 @@ class ClusterMembersComponent extends Component {
|
||||
const statusElement = card.querySelector('.member-status');
|
||||
if (statusElement) {
|
||||
const statusClass = (member.status && member.status.toUpperCase() === 'ACTIVE') ? 'status-online' : 'status-offline';
|
||||
const statusIcon = (member.status && member.status.toUpperCase() === 'ACTIVE') ? '🟢' : '🔴';
|
||||
const statusIcon = (member.status && member.status.toUpperCase() === 'ACTIVE') ? window.icon('dotGreen', { width: 12, height: 12 }) : window.icon('dotRed', { width: 12, height: 12 });
|
||||
|
||||
statusElement.className = `member-status ${statusClass}`;
|
||||
statusElement.innerHTML = `${statusIcon}`;
|
||||
@@ -392,7 +392,7 @@ class ClusterMembersComponent extends Component {
|
||||
logger.debug('ClusterMembersComponent: showEmptyState() called');
|
||||
this.renderEmpty(`
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">🌐</div>
|
||||
<div class="empty-state-icon">${window.icon('cluster', { width: 24, height: 24 })}</div>
|
||||
<div>No cluster members found</div>
|
||||
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.7;">
|
||||
The cluster might be empty or not yet discovered
|
||||
@@ -406,7 +406,7 @@ class ClusterMembersComponent extends Component {
|
||||
|
||||
const membersHTML = members.map(member => {
|
||||
const statusClass = (member.status && member.status.toUpperCase() === 'ACTIVE') ? 'status-online' : 'status-offline';
|
||||
const statusIcon = (member.status && member.status.toUpperCase() === 'ACTIVE') ? '🟢' : '🔴';
|
||||
const statusIcon = (member.status && member.status.toUpperCase() === 'ACTIVE') ? window.icon('dotGreen', { width: 12, height: 12 }) : window.icon('dotRed', { width: 12, height: 12 });
|
||||
|
||||
logger.debug('ClusterMembersComponent: Rendering member:', member);
|
||||
|
||||
|
||||
@@ -20,19 +20,19 @@ class ClusterStatusComponent extends Component {
|
||||
|
||||
if (error) {
|
||||
statusText = 'Cluster Error';
|
||||
statusIcon = '❌';
|
||||
statusIcon = window.icon('error', { width: 12, height: 12 });
|
||||
statusClass = 'cluster-status-error';
|
||||
} else if (totalNodes === 0) {
|
||||
statusText = 'Cluster Offline';
|
||||
statusIcon = '🔴';
|
||||
statusIcon = window.icon('dotRed', { width: 12, height: 12 });
|
||||
statusClass = 'cluster-status-offline';
|
||||
} else if (clientInitialized) {
|
||||
statusText = 'Cluster Online';
|
||||
statusIcon = '🟢';
|
||||
statusIcon = window.icon('dotGreen', { width: 12, height: 12 });
|
||||
statusClass = 'cluster-status-online';
|
||||
} else {
|
||||
statusText = 'Cluster Connecting';
|
||||
statusIcon = '🟡';
|
||||
statusIcon = window.icon('dotYellow', { width: 12, height: 12 });
|
||||
statusClass = 'cluster-status-connecting';
|
||||
}
|
||||
|
||||
|
||||
@@ -357,7 +357,14 @@ class FirmwareComponent extends Component {
|
||||
const progressHTML = `
|
||||
<div class="firmware-upload-progress" id="firmware-upload-progress">
|
||||
<div class="progress-header">
|
||||
<h3>📤 Firmware Upload Progress</h3>
|
||||
<h3>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px; vertical-align: -2px;">
|
||||
<path d="M12 16V4"/>
|
||||
<path d="M8 8l4-4 4 4"/>
|
||||
<path d="M20 16v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2"/>
|
||||
</svg>
|
||||
Firmware Upload Progress
|
||||
</h3>
|
||||
<div class="progress-info">
|
||||
<span>File: ${file.name}</span>
|
||||
<span>Size: ${(file.size / 1024).toFixed(1)}KB</span>
|
||||
@@ -470,20 +477,20 @@ class FirmwareComponent extends Component {
|
||||
if (totalCount === 1) {
|
||||
// Single node upload
|
||||
if (successCount === 1) {
|
||||
progressHeader.textContent = `📤 Firmware Upload Complete`;
|
||||
progressSummary.innerHTML = `<span>✅ Upload to ${results[0].hostname || results[0].nodeIp} completed successfully at ${new Date().toLocaleTimeString()}</span>`;
|
||||
progressHeader.textContent = `Firmware Upload Complete`;
|
||||
progressSummary.innerHTML = `<span>${window.icon('success', { width: 14, height: 14 })} Upload to ${results[0].hostname || results[0].nodeIp} completed successfully at ${new Date().toLocaleTimeString()}</span>`;
|
||||
} else {
|
||||
progressHeader.textContent = `📤 Firmware Upload Failed`;
|
||||
progressSummary.innerHTML = `<span>❌ Upload to ${results[0].hostname || results[0].nodeIp} failed at ${new Date().toLocaleTimeString()}</span>`;
|
||||
progressHeader.textContent = `Firmware Upload Failed`;
|
||||
progressSummary.innerHTML = `<span>${window.icon('error', { width: 14, height: 14 })} Upload to ${results[0].hostname || results[0].nodeIp} failed at ${new Date().toLocaleTimeString()}</span>`;
|
||||
}
|
||||
} else if (successCount === totalCount) {
|
||||
// Multi-node upload - all successful
|
||||
progressHeader.textContent = `📤 Firmware Upload Complete (${successCount}/${totalCount} Successful)`;
|
||||
progressSummary.innerHTML = `<span>✅ All uploads completed successfully at ${new Date().toLocaleTimeString()}</span>`;
|
||||
progressHeader.textContent = `Firmware Upload Complete (${successCount}/${totalCount} Successful)`;
|
||||
progressSummary.innerHTML = `<span>${window.icon('success', { width: 14, height: 14 })} All uploads completed successfully at ${new Date().toLocaleTimeString()}</span>`;
|
||||
} else {
|
||||
// Multi-node upload - some failed
|
||||
progressHeader.textContent = `📤 Firmware Upload Results (${successCount}/${totalCount} Successful)`;
|
||||
progressSummary.innerHTML = `<span>⚠️ Upload completed with ${totalCount - successCount} failure(s) at ${new Date().toLocaleTimeString()}</span>`;
|
||||
progressHeader.textContent = `Firmware Upload Results (${successCount}/${totalCount} Successful)`;
|
||||
progressSummary.innerHTML = `<span>${window.icon('warning', { width: 14, height: 14 })} Upload completed with ${totalCount - successCount} failure(s) at ${new Date().toLocaleTimeString()}</span>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -682,7 +689,7 @@ class FirmwareComponent extends Component {
|
||||
}
|
||||
const html = `
|
||||
<div class="affected-nodes">
|
||||
<div class="progress-header"><h3>🎯 Affected Nodes (${nodes.length})</h3></div>
|
||||
<div class="progress-header"><h3>Affected Nodes (${nodes.length})</h3></div>
|
||||
<div class="progress-list">
|
||||
${nodes.map(n => `
|
||||
<div class="progress-item" data-node-ip="${n.ip}">
|
||||
|
||||
@@ -236,7 +236,7 @@ class MonitoringViewComponent extends Component {
|
||||
if (error) {
|
||||
container.innerHTML = `
|
||||
<div class="error">
|
||||
<div>❌ Error: ${error}</div>
|
||||
<div>${window.icon('error', { width: 14, height: 14, class: 'icon' })} Error: ${this.escapeHtml(String(error))}</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
@@ -259,7 +259,7 @@ class MonitoringViewComponent extends Component {
|
||||
|
||||
<div class="summary-stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🖥️</div>
|
||||
<div class="stat-icon">${window.icon('computer', { width: 18, height: 18 })}</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">Total Nodes</div>
|
||||
<div class="stat-value">${clusterSummary.totalNodes}</div>
|
||||
@@ -267,7 +267,7 @@ class MonitoringViewComponent extends Component {
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">⚡</div>
|
||||
<div class="stat-icon">${window.icon('cpu', { width: 18, height: 18 })}</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">CPU</div>
|
||||
<div class="stat-value">${Math.round(clusterSummary.totalCpu - clusterSummary.availableCpu)}MHz / ${Math.round(clusterSummary.totalCpu)}MHz</div>
|
||||
@@ -281,7 +281,7 @@ class MonitoringViewComponent extends Component {
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🧠</div>
|
||||
<div class="stat-icon">${window.icon('memory', { width: 18, height: 18 })}</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">Memory</div>
|
||||
<div class="stat-value">${this.viewModel.formatResourceValue(clusterSummary.totalMemory - clusterSummary.availableMemory, 'memory')} / ${this.viewModel.formatResourceValue(clusterSummary.totalMemory, 'memory')}</div>
|
||||
@@ -295,7 +295,7 @@ class MonitoringViewComponent extends Component {
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">💾</div>
|
||||
<div class="stat-icon">${window.icon('storage', { width: 18, height: 18 })}</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-label">Storage</div>
|
||||
<div class="stat-value">${this.viewModel.formatResourceValue(clusterSummary.totalStorage - clusterSummary.availableStorage, 'storage')} / ${this.viewModel.formatResourceValue(clusterSummary.totalStorage, 'storage')}</div>
|
||||
@@ -328,7 +328,7 @@ class MonitoringViewComponent extends Component {
|
||||
if (error) {
|
||||
container.innerHTML = `
|
||||
<div class="error">
|
||||
<div>❌ Error: ${error}</div>
|
||||
<div>${window.icon('error', { width: 14, height: 14, class: 'icon' })} Error: ${this.escapeHtml(String(error))}</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
@@ -357,7 +357,7 @@ class MonitoringViewComponent extends Component {
|
||||
const nodeCount = nodeResources.size;
|
||||
container.innerHTML = `
|
||||
<div class="nodes-monitoring-content">
|
||||
<h3>🖥️ Node Resource Details</h3>
|
||||
<h3>${window.icon('computer', { width: 16, height: 16, class: 'icon', strokeWidth: 2 })} Node Resource Details</h3>
|
||||
<div class="nodes-grid" data-item-count="${nodeCount}">
|
||||
${nodesHtml}
|
||||
</div>
|
||||
@@ -378,7 +378,7 @@ class MonitoringViewComponent extends Component {
|
||||
<div class="node-header">
|
||||
<div class="status-hostname-group">
|
||||
<div class="node-status-indicator status-offline">
|
||||
🔴
|
||||
${window.icon('dotRed', { width: 12, height: 12 })}
|
||||
</div>
|
||||
<div class="node-title">${hostname || ip}</div>
|
||||
</div>
|
||||
@@ -397,12 +397,12 @@ class MonitoringViewComponent extends Component {
|
||||
` : ''}
|
||||
|
||||
<div class="node-error">
|
||||
<div class="error-label">⚠️ Error</div>
|
||||
<div class="error-label">${window.icon('warning', { width: 14, height: 14 })} Error</div>
|
||||
<div class="error-message">${error || 'Monitoring endpoint not available'}</div>
|
||||
</div>
|
||||
${nodeData.lastSeen ? `
|
||||
<div class="node-uptime">
|
||||
<div class="uptime-label">⏱️ Last Seen</div>
|
||||
<div class="uptime-label">${window.icon('timer', { width: 14, height: 14 })} Last Seen</div>
|
||||
<div class="uptime-value">${this.formatLastSeen(nodeData.lastSeen)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -457,8 +457,8 @@ class MonitoringViewComponent extends Component {
|
||||
}
|
||||
|
||||
// Determine status indicator based on resource source
|
||||
const statusIcon = resourceSource === 'monitoring' ? '🟢' :
|
||||
resourceSource === 'basic' ? '🟡' : '🔴';
|
||||
const statusIcon = resourceSource === 'monitoring' ? window.icon('dotGreen', { width: 12, height: 12 }) :
|
||||
resourceSource === 'basic' ? window.icon('dotYellow', { width: 12, height: 12 }) : window.icon('dotRed', { width: 12, height: 12 });
|
||||
const statusClass = resourceSource === 'monitoring' ? 'status-online' :
|
||||
resourceSource === 'basic' ? 'status-warning' : 'status-offline';
|
||||
|
||||
@@ -487,27 +487,27 @@ class MonitoringViewComponent extends Component {
|
||||
|
||||
${system.uptime_formatted ? `
|
||||
<div class="node-uptime">
|
||||
<div class="uptime-label">⏱️ Uptime</div>
|
||||
<div class="uptime-label">${window.icon('timer', { width: 14, height: 14 })} Uptime</div>
|
||||
<div class="uptime-value">${system.uptime_formatted}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="node-latency">
|
||||
<div class="latency-label">🐢 Latency</div>
|
||||
<div class="latency-label">${window.icon('latency', { width: 14, height: 14 })} Latency</div>
|
||||
<div class="latency-value">${nodeData.latency ? `${nodeData.latency}ms` : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="latency-divider"></div>
|
||||
|
||||
${(nodeData.basic?.flashChipSize || nodeData.resources?.flashChipSize) ? `
|
||||
<div class="node-flash">
|
||||
<div class="flash-label">💾 Flash</div>
|
||||
<div class="flash-label">${window.icon('storage', { width: 14, height: 14 })} Flash</div>
|
||||
<div class="flash-value">${this.formatFlashSize(nodeData.basic?.flashChipSize || nodeData.resources?.flashChipSize)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="node-resources">
|
||||
<div class="resource-item">
|
||||
<div class="resource-label">⚡ CPU</div>
|
||||
<div class="resource-label">${window.icon('cpu', { width: 14, height: 14 })} CPU</div>
|
||||
<div class="resource-value">
|
||||
<span class="value-label">Used:</span> ${Math.round(cpuUsed)}MHz / <span class="value-label">Total:</span> ${Math.round(cpuTotal)}MHz
|
||||
</div>
|
||||
@@ -520,7 +520,7 @@ class MonitoringViewComponent extends Component {
|
||||
</div>
|
||||
|
||||
<div class="resource-item">
|
||||
<div class="resource-label">🧠 Memory</div>
|
||||
<div class="resource-label">${window.icon('memory', { width: 14, height: 14 })} Memory</div>
|
||||
<div class="resource-value">
|
||||
<span class="value-label">Used:</span> ${this.viewModel.formatResourceValue(memoryUsed, 'memory')} / <span class="value-label">Total:</span> ${this.viewModel.formatResourceValue(memoryTotal, 'memory')}
|
||||
</div>
|
||||
@@ -533,7 +533,7 @@ class MonitoringViewComponent extends Component {
|
||||
</div>
|
||||
|
||||
<div class="resource-item">
|
||||
<div class="resource-label">💾 Storage</div>
|
||||
<div class="resource-label">${window.icon('storage', { width: 14, height: 14 })} Storage</div>
|
||||
<div class="resource-value">
|
||||
<span class="value-label">Used:</span> ${this.viewModel.formatResourceValue(storageUsed, 'storage')} / <span class="value-label">Total:</span> ${this.viewModel.formatResourceValue(storageTotal, 'storage')}
|
||||
</div>
|
||||
|
||||
@@ -516,7 +516,7 @@ class NodeDetailsComponent extends Component {
|
||||
if (!endpoints || !Array.isArray(endpoints.endpoints) || endpoints.endpoints.length === 0) {
|
||||
return `
|
||||
<div class="no-endpoints">
|
||||
<div>🧩 No endpoints 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 endpoints</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -668,14 +668,14 @@ class NodeDetailsComponent extends Component {
|
||||
const pretty = typeof response?.data === 'object' ? JSON.stringify(response.data, null, 2) : String(response?.data ?? '');
|
||||
resultEl.innerHTML = `
|
||||
<div class="endpoint-call-success">
|
||||
<div>✅ Success</div>
|
||||
<div>${window.icon('success', { width: 14, height: 14 })} Success</div>
|
||||
<pre class="endpoint-result-pre">${this.escapeHtml(pretty)}</pre>
|
||||
</div>
|
||||
`;
|
||||
} catch (err) {
|
||||
resultEl.innerHTML = `
|
||||
<div class="endpoint-call-error">
|
||||
<div>❌ Error: ${this.escapeHtml(err.message || 'Request failed')}</div>
|
||||
<div>${window.icon('error', { width: 14, height: 14 })} Error: ${this.escapeHtml(err.message || 'Request failed')}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -733,7 +733,7 @@ class NodeDetailsComponent extends Component {
|
||||
const summaryHTML = summary ? `
|
||||
<div class="tasks-summary">
|
||||
<div class="tasks-summary-left">
|
||||
<div class="summary-icon">📋</div>
|
||||
<div class="summary-icon">${window.icon('file', { width: 16, height: 16 })}</div>
|
||||
<div>
|
||||
<div class="summary-title">Tasks Overview</div>
|
||||
<div class="summary-subtitle">System task management and monitoring</div>
|
||||
@@ -760,12 +760,12 @@ class NodeDetailsComponent extends Component {
|
||||
<div class="task-header">
|
||||
<span class="task-name">${task.name || 'Unknown Task'}</span>
|
||||
<span class="task-status ${task.running ? 'running' : 'stopped'}">
|
||||
${task.running ? '🟢 Running' : '🔴 Stopped'}
|
||||
${task.running ? window.icon('dotGreen', { width: 10, height: 10 }) + ' Running' : window.icon('dotRed', { width: 10, height: 10 }) + ' Stopped'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="task-details">
|
||||
<span class="task-interval">Interval: ${task.interval}ms</span>
|
||||
<span class="task-enabled">${task.enabled ? '🟢 Enabled' : '🔴 Disabled'}</span>
|
||||
<span class="task-enabled">${task.enabled ? window.icon('dotGreen', { width: 10, height: 10 }) + ' Enabled' : window.icon('dotRed', { width: 10, height: 10 }) + ' Disabled'}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
@@ -780,7 +780,7 @@ class NodeDetailsComponent extends Component {
|
||||
return `
|
||||
<div class="tasks-summary">
|
||||
<div class="tasks-summary-left">
|
||||
<div class="summary-icon">📋</div>
|
||||
<div class="summary-icon">${window.icon('file', { width: 16, height: 16 })}</div>
|
||||
<div>
|
||||
<div class="summary-title">Tasks Overview</div>
|
||||
<div class="summary-subtitle">${total > 0 ? `Total tasks: ${total}, active: ${active}` : 'This node has no running tasks'}</div>
|
||||
@@ -802,7 +802,7 @@ class NodeDetailsComponent extends Component {
|
||||
</div>
|
||||
</div>
|
||||
<div class="no-tasks">
|
||||
<div>📋 No active tasks found</div>
|
||||
<div>No active tasks found</div>
|
||||
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.7;">
|
||||
${total > 0 ? `Total tasks: ${total}, active: ${active}` : 'This node has no running tasks'}
|
||||
</div>
|
||||
@@ -818,7 +818,7 @@ class NodeDetailsComponent extends Component {
|
||||
<div class="upload-area">
|
||||
<input type="file" id="firmware-file" accept=".bin,.hex" style="display: none;">
|
||||
<button class="upload-btn" data-action="select-file">
|
||||
📁 Choose Firmware File
|
||||
${window.icon('file', { width: 14, height: 14 })} Choose Firmware File
|
||||
</button>
|
||||
<div class="upload-info">Select a .bin or .hex file to upload</div>
|
||||
<div id="upload-status" style="display: none;"></div>
|
||||
@@ -879,15 +879,15 @@ class NodeDetailsComponent extends Component {
|
||||
// Show upload status
|
||||
uploadStatus.style.display = 'block';
|
||||
uploadStatus.innerHTML = `
|
||||
<div class="upload-progress">
|
||||
<div>📤 Uploading ${file.name}...</div>
|
||||
<div class="upload-progress">
|
||||
<div>${window.icon('upload', { width: 14, height: 14 })} Uploading ${this.escapeHtml(file.name)}...</div>
|
||||
<div style="font-size: 0.8rem; opacity: 0.7;">Size: ${(file.size / 1024).toFixed(1)}KB</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Disable upload button
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.textContent = '⏳ Uploading...';
|
||||
uploadBtn.textContent = 'Uploading...';
|
||||
|
||||
// Get the member IP from the card if available, otherwise fallback to view model state
|
||||
const memberCard = this.container.closest('.member-card');
|
||||
@@ -908,7 +908,7 @@ class NodeDetailsComponent extends Component {
|
||||
// Show success
|
||||
uploadStatus.innerHTML = `
|
||||
<div class="upload-success">
|
||||
<div>✅ Firmware uploaded successfully!</div>
|
||||
<div>${window.icon('success', { width: 14, height: 14 })} Firmware uploaded successfully!</div>
|
||||
<div style="font-size: 0.8rem; margin-top: 0.5rem; opacity: 0.7;">Node: ${memberIp}</div>
|
||||
<div style="font-size: 0.8rem; margin-top: 0.5rem; opacity: 0.7;">Size: ${(file.size / 1024).toFixed(1)}KB</div>
|
||||
</div>
|
||||
@@ -922,7 +922,7 @@ class NodeDetailsComponent extends Component {
|
||||
// Show error
|
||||
uploadStatus.innerHTML = `
|
||||
<div class="upload-error">
|
||||
<div>❌ Upload failed: ${error.message}</div>
|
||||
<div>${window.icon('error', { width: 14, height: 14 })} Upload failed: ${this.escapeHtml(error.message)}</div>
|
||||
</div>
|
||||
`;
|
||||
} finally {
|
||||
|
||||
@@ -28,7 +28,7 @@ class PrimaryNodeComponent extends Component {
|
||||
const error = this.viewModel.get('error');
|
||||
|
||||
if (error) {
|
||||
this.setText('#primary-node-ip', '❌ Discovery Failed');
|
||||
this.setText('#primary-node-ip', 'Discovery Failed');
|
||||
this.setClass('#primary-node-ip', 'error', true);
|
||||
this.setClass('#primary-node-ip', 'discovering', false);
|
||||
this.setClass('#primary-node-ip', 'selecting', false);
|
||||
@@ -36,19 +36,19 @@ class PrimaryNodeComponent extends Component {
|
||||
}
|
||||
|
||||
if (!primaryNode) {
|
||||
this.setText('#primary-node-ip', '🔍 No Nodes Found');
|
||||
this.setText('#primary-node-ip', 'No Nodes Found');
|
||||
this.setClass('#primary-node-ip', 'error', true);
|
||||
this.setClass('#primary-node-ip', 'discovering', false);
|
||||
this.setClass('#primary-node-ip', 'selecting', false);
|
||||
return;
|
||||
}
|
||||
|
||||
const status = clientInitialized ? '✅' : '⚠️';
|
||||
const status = clientInitialized ? '' : '';
|
||||
const nodeCount = (onlineNodes && onlineNodes > 0)
|
||||
? ` (${onlineNodes}/${totalNodes} online)`
|
||||
: (totalNodes > 1 ? ` (${totalNodes} nodes)` : '');
|
||||
|
||||
this.setText('#primary-node-ip', `${status} ${primaryNode}${nodeCount}`);
|
||||
this.setText('#primary-node-ip', `${primaryNode}${nodeCount}`);
|
||||
this.setClass('#primary-node-ip', 'error', false);
|
||||
this.setClass('#primary-node-ip', 'discovering', false);
|
||||
this.setClass('#primary-node-ip', 'selecting', false);
|
||||
@@ -57,7 +57,7 @@ class PrimaryNodeComponent extends Component {
|
||||
async handleRandomSelection() {
|
||||
try {
|
||||
// Show selecting state
|
||||
this.setText('#primary-node-ip', '🎲 Selecting...');
|
||||
this.setText('#primary-node-ip', 'Selecting...');
|
||||
this.setClass('#primary-node-ip', 'selecting', true);
|
||||
this.setClass('#primary-node-ip', 'discovering', false);
|
||||
this.setClass('#primary-node-ip', 'error', false);
|
||||
@@ -65,7 +65,7 @@ class PrimaryNodeComponent extends Component {
|
||||
await this.viewModel.selectRandomPrimaryNode();
|
||||
|
||||
// Show success briefly
|
||||
this.setText('#primary-node-ip', '🎯 Selection Complete');
|
||||
this.setText('#primary-node-ip', 'Selection Complete');
|
||||
|
||||
// Update display after delay
|
||||
setTimeout(() => {
|
||||
@@ -74,7 +74,7 @@ class PrimaryNodeComponent extends Component {
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to select random primary node:', error);
|
||||
this.setText('#primary-node-ip', '❌ Selection Failed');
|
||||
this.setText('#primary-node-ip', 'Selection Failed');
|
||||
this.setClass('#primary-node-ip', 'error', true);
|
||||
this.setClass('#primary-node-ip', 'selecting', false);
|
||||
this.setClass('#primary-node-ip', 'discovering', false);
|
||||
|
||||
@@ -103,8 +103,16 @@
|
||||
<div class="terminal-header">
|
||||
<div class="terminal-title">Terminal</div>
|
||||
<div class="terminal-actions">
|
||||
<button class="terminal-minimize-btn" title="Minimize">_</button>
|
||||
<button class="terminal-close-btn" title="Close">✕</button>
|
||||
<button class="terminal-minimize-btn" title="Minimize" aria-label="Minimize">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||
<path d="M5 19h14"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="terminal-close-btn" title="Close" aria-label="Close">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-body">
|
||||
|
||||
61
public/scripts/icons.js
Normal file
61
public/scripts/icons.js
Normal file
@@ -0,0 +1,61 @@
|
||||
// Centralized SVG Icons for SPORE UI
|
||||
// Usage: window.icon('cluster', {class: 'foo', width: 16, height: 16}) -> returns inline SVG string
|
||||
(function(){
|
||||
const toAttrs = (opts) => {
|
||||
if (!opts) return '';
|
||||
const attrs = [];
|
||||
if (opts.class) attrs.push(`class="${opts.class}"`);
|
||||
if (opts.width) attrs.push(`width="${opts.width}"`);
|
||||
if (opts.height) attrs.push(`height="${opts.height}"`);
|
||||
if (opts.strokeWidth) attrs.push(`stroke-width="${opts.strokeWidth}"`);
|
||||
return attrs.join(' ');
|
||||
};
|
||||
|
||||
const withSvg = (inner, opts) => {
|
||||
const attr = toAttrs(opts);
|
||||
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" ${attr}>${inner}</svg>`;
|
||||
};
|
||||
|
||||
const Icons = {
|
||||
// Navigation / sections
|
||||
cluster: (o) => withSvg(`<circle cx="12" cy="12" r="9"/><circle cx="8" cy="10" r="1.5"/><circle cx="16" cy="8" r="1.5"/><circle cx="14" cy="15" r="1.5"/><path d="M9 11l3 3M9 11l6-3"/>`, o),
|
||||
topology: (o) => withSvg(`<path d="M6 3v6M6 15v6M18 3v6M18 15v6"/><rect x="3" y="9" width="6" height="6" rx="2"/><rect x="15" y="9" width="6" height="6" rx="2"/>`, o),
|
||||
monitoring: (o) => withSvg(`<path d="M3 12h3l2 7 4-14 3 10 2-6h4"/>`, o),
|
||||
firmware: (o) => withSvg(`<path d="M4 7l8-4 8 4v10l-8 4-8-4z"/><path d="M12 8v8"/>`, o),
|
||||
|
||||
// Status / feedback
|
||||
success: (o) => withSvg(`<path d="M20 6L9 17l-5-5"/>`, o),
|
||||
warning: (o) => withSvg(`<path d="M12 9v4"/><path d="M12 17h.01"/><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L14.71 3.86a2 2 0 0 0-3.42 0z"/>`, o),
|
||||
error: (o) => withSvg(`<path d="M18 6L6 18M6 6l12 12"/>`, o),
|
||||
offlineDot: (o) => withSvg(`<circle cx="12" cy="12" r="6" fill="currentColor"/>`, o),
|
||||
dotGreen: (o) => withSvg(`<circle cx="12" cy="12" r="6" fill="#10b981"/>`, o),
|
||||
dotYellow: (o) => withSvg(`<circle cx="12" cy="12" r="6" fill="#f59e0b"/>`, o),
|
||||
dotRed: (o) => withSvg(`<circle cx="12" cy="12" r="6" fill="#ef4444"/>`, o),
|
||||
|
||||
// Actions
|
||||
refresh: (o) => withSvg(`<path d="M3 3v6h6"/><path d="M21 21v-6h-6"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L3 9m18 6-2.64 2.36A9 9 0 0 1 3.51 15"/>`, o),
|
||||
terminal: (o) => withSvg(`<path d="M4 17l6-6-6-6"></path><path d="M12 19h8"></path>`, o),
|
||||
chevronDown: (o) => withSvg(`<path d="M6 9l6 6 6-6"/>`, o),
|
||||
upload: (o) => withSvg(`<path d="M12 16V4"/><path d="M8 8l4-4 4 4"/><path d="M20 16v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2"/>`, o),
|
||||
file: (o) => withSvg(`<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/>`, o),
|
||||
timer: (o) => withSvg(`<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/>`, o),
|
||||
cpu: (o) => withSvg(`<rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 1v3M15 1v3M9 20v3M15 20v3M1 9h3M1 15h3M20 9h3M20 15h3"/>`, o),
|
||||
memory: (o) => withSvg(`<rect x="4" y="8" width="16" height="8" rx="2"/><path d="M7 8v8M12 8v8M17 8v8"/>`, o),
|
||||
storage: (o) => withSvg(`<rect x="3" y="6" width="18" height="12" rx="2"/><path d="M7 10h10"/>`, o),
|
||||
computer: (o) => withSvg(`<rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8"/>`, o),
|
||||
latency: (o) => withSvg(`<path d="M3 12h4l2 5 4-10 2 7 2-4h4"/>`, o)
|
||||
};
|
||||
|
||||
function icon(name, opts){
|
||||
const fn = Icons[name];
|
||||
if (!fn) return '';
|
||||
return fn(Object.assign({ width: 16, height: 16, strokeWidth: 2 }, opts || {}));
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.Icons = Icons;
|
||||
window.icon = icon;
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@@ -4,6 +4,35 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* === SVG icon alignment and sizing tweaks === */
|
||||
/* Align inline SVGs vertically with adjacent text */
|
||||
.nav-tab > svg,
|
||||
.theme-toggle > svg,
|
||||
.burger-btn > svg,
|
||||
h2 > svg,
|
||||
h3 > svg,
|
||||
.stat-icon > svg,
|
||||
.resource-label > svg,
|
||||
.flash-label > svg,
|
||||
.uptime-label > svg,
|
||||
.latency-label > svg,
|
||||
.member-status > svg,
|
||||
.tab-refresh-btn > svg,
|
||||
.primary-node-info svg,
|
||||
.upload-btn-compact > svg,
|
||||
.deploy-btn > svg,
|
||||
.upload-btn > svg,
|
||||
.terminal-actions svg,
|
||||
.terminal-dock-icon > svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Monitoring view: slightly larger summary stat icons */
|
||||
.summary-stats .stat-icon > svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
@@ -767,16 +796,17 @@ p {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-status {
|
||||
.member-status,
|
||||
.node-status-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.2rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
font-size: 0.9rem;
|
||||
font-size: 0; /* prevent line-height affecting size */
|
||||
flex-shrink: 0;
|
||||
min-width: 1.2rem;
|
||||
min-height: 1.2rem;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
@@ -790,12 +820,22 @@ p {
|
||||
background: rgba(244, 67, 54, 0.3);
|
||||
color: #f44336;
|
||||
border: 1px solid rgba(244, 67, 54, 0.5);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background: rgba(255, 152, 0, 0.3);
|
||||
color: #ff9800;
|
||||
border: 1px solid rgba(255, 152, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Ensure the inner dot SVG fits nicely */
|
||||
.member-status > svg,
|
||||
.node-status-indicator > svg {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.member-latency {
|
||||
@@ -3452,6 +3492,7 @@ select.param-input:focus {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
.terminal-title {
|
||||
font-weight: 600;
|
||||
|
||||
Reference in New Issue
Block a user