feat: add node resource infos
This commit is contained in:
@@ -108,6 +108,18 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMonitoringResources(ip) {
|
||||||
|
return this.request('/api/proxy-call', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
ip: ip,
|
||||||
|
method: 'GET',
|
||||||
|
uri: '/api/monitoring/resources',
|
||||||
|
params: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global API client instance
|
// Global API client instance
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ class NodeDetailsComponent extends Component {
|
|||||||
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('endpoints', this.handleEndpointsUpdate.bind(this));
|
this.subscribeToProperty('endpoints', this.handleEndpointsUpdate.bind(this));
|
||||||
|
this.subscribeToProperty('monitoringResources', this.handleMonitoringResourcesUpdate.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('endpoints'));
|
this.renderNodeDetails(newStatus, this.viewModel.get('tasks'), this.viewModel.get('endpoints'), this.viewModel.get('monitoringResources'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +26,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('endpoints'));
|
this.renderNodeDetails(nodeStatus, newTasks, this.viewModel.get('endpoints'), this.viewModel.get('monitoringResources'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +56,17 @@ class NodeDetailsComponent extends Component {
|
|||||||
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, newEndpoints);
|
this.renderNodeDetails(nodeStatus, tasks, newEndpoints, this.viewModel.get('monitoringResources'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle monitoring resources update with state preservation
|
||||||
|
handleMonitoringResourcesUpdate(newResources, previousResources) {
|
||||||
|
const nodeStatus = this.viewModel.get('nodeStatus');
|
||||||
|
const tasks = this.viewModel.get('tasks');
|
||||||
|
const endpoints = this.viewModel.get('endpoints');
|
||||||
|
if (nodeStatus && !this.viewModel.get('isLoading')) {
|
||||||
|
this.renderNodeDetails(nodeStatus, tasks, endpoints, newResources);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +76,7 @@ class NodeDetailsComponent extends Component {
|
|||||||
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 endpoints = this.viewModel.get('endpoints');
|
const endpoints = this.viewModel.get('endpoints');
|
||||||
|
const monitoringResources = this.viewModel.get('monitoringResources');
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
this.renderLoading('<div class="loading-details">Loading detailed information...</div>');
|
this.renderLoading('<div class="loading-details">Loading detailed information...</div>');
|
||||||
@@ -81,10 +93,10 @@ class NodeDetailsComponent extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderNodeDetails(nodeStatus, tasks, endpoints);
|
this.renderNodeDetails(nodeStatus, tasks, endpoints, monitoringResources);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNodeDetails(nodeStatus, tasks, endpoints) {
|
renderNodeDetails(nodeStatus, tasks, endpoints, monitoringResources) {
|
||||||
// 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);
|
||||||
@@ -115,26 +127,7 @@ class NodeDetailsComponent extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content ${activeTab === 'status' ? 'active' : ''}" id="status-tab">
|
<div class="tab-content ${activeTab === 'status' ? 'active' : ''}" id="status-tab">
|
||||||
<div class="detail-row">
|
${this.renderStatusTab(nodeStatus, monitoringResources)}
|
||||||
<span class="detail-label">Free Heap:</span>
|
|
||||||
<span class="detail-value">${Math.round(nodeStatus.freeHeap / 1024)}KB</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="detail-label">Chip ID:</span>
|
|
||||||
<span class="detail-value">${nodeStatus.chipId}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="detail-label">SDK Version:</span>
|
|
||||||
<span class="detail-value">${nodeStatus.sdkVersion}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="detail-label">CPU Frequency:</span>
|
|
||||||
<span class="detail-value">${nodeStatus.cpuFreqMHz}MHz</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="detail-label">Flash Size:</span>
|
|
||||||
<span class="detail-value">${Math.round(nodeStatus.flashChipSize / 1024)}KB</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content ${activeTab === 'endpoints' ? 'active' : ''}" id="endpoints-tab">
|
<div class="tab-content ${activeTab === 'endpoints' ? 'active' : ''}" id="endpoints-tab">
|
||||||
@@ -187,8 +180,11 @@ class NodeDetailsComponent extends Component {
|
|||||||
await this.viewModel.loadEndpointsData();
|
await this.viewModel.loadEndpointsData();
|
||||||
} else if (activeTab === 'tasks' && typeof this.viewModel.loadTasksData === 'function') {
|
} else if (activeTab === 'tasks' && typeof this.viewModel.loadTasksData === 'function') {
|
||||||
await this.viewModel.loadTasksData();
|
await this.viewModel.loadTasksData();
|
||||||
|
} else if (activeTab === 'status' && typeof this.viewModel.loadMonitoringResources === 'function') {
|
||||||
|
// status tab: load monitoring resources
|
||||||
|
await this.viewModel.loadMonitoringResources();
|
||||||
} else {
|
} else {
|
||||||
// status or firmware: refresh core node details
|
// firmware: refresh core node details
|
||||||
if (nodeIp && typeof this.viewModel.loadNodeDetails === 'function') {
|
if (nodeIp && typeof this.viewModel.loadNodeDetails === 'function') {
|
||||||
await this.viewModel.loadNodeDetails(nodeIp);
|
await this.viewModel.loadNodeDetails(nodeIp);
|
||||||
}
|
}
|
||||||
@@ -203,6 +199,153 @@ class NodeDetailsComponent extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderStatusTab(nodeStatus, monitoringResources) {
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Add gauges section if monitoring resources are available
|
||||||
|
if (monitoringResources) {
|
||||||
|
html += this.renderResourceGauges(monitoringResources);
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">Chip ID:</span>
|
||||||
|
<span class="detail-value">${nodeStatus.chipId}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">SDK Version:</span>
|
||||||
|
<span class="detail-value">${nodeStatus.sdkVersion}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">CPU Frequency:</span>
|
||||||
|
<span class="detail-value">${nodeStatus.cpuFreqMHz}MHz</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">Flash Size:</span>
|
||||||
|
<span class="detail-value">${Math.round(nodeStatus.flashChipSize / 1024)}KB</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add monitoring resources if available
|
||||||
|
if (monitoringResources) {
|
||||||
|
html += `
|
||||||
|
<div class="monitoring-section">
|
||||||
|
<div class="monitoring-header">Resources</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// CPU Usage
|
||||||
|
if (monitoringResources.cpu) {
|
||||||
|
html += `
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">CPU Usage (Avg):</span>
|
||||||
|
<span class="detail-value">${monitoringResources.cpu.average_usage ? monitoringResources.cpu.average_usage.toFixed(1) + '%' : 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory Usage
|
||||||
|
if (monitoringResources.memory) {
|
||||||
|
const heapUsagePercent = monitoringResources.memory.heap_usage_percent || 0;
|
||||||
|
const totalHeap = monitoringResources.memory.total_heap || 0;
|
||||||
|
const usedHeap = totalHeap - (monitoringResources.memory.free_heap || 0);
|
||||||
|
const usedHeapKB = Math.round(usedHeap / 1024);
|
||||||
|
const totalHeapKB = Math.round(totalHeap / 1024);
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">Heap Usage:</span>
|
||||||
|
<span class="detail-value">${heapUsagePercent.toFixed(1)}% (${usedHeapKB}KB / ${totalHeapKB}KB)</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filesystem Usage
|
||||||
|
if (monitoringResources.filesystem) {
|
||||||
|
const usedKB = Math.round(monitoringResources.filesystem.used_bytes / 1024);
|
||||||
|
const totalKB = Math.round(monitoringResources.filesystem.total_bytes / 1024);
|
||||||
|
html += `
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">Filesystem:</span>
|
||||||
|
<span class="detail-value">${monitoringResources.filesystem.usage_percent ? monitoringResources.filesystem.usage_percent.toFixed(1) + '%' : 'N/A'} (${usedKB}KB / ${totalKB}KB)</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// System Uptime
|
||||||
|
if (monitoringResources.system) {
|
||||||
|
html += `
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">Uptime:</span>
|
||||||
|
<span class="detail-value">${monitoringResources.system.uptime_formatted || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderResourceGauges(monitoringResources) {
|
||||||
|
// Get values with fallbacks and ensure they are numbers
|
||||||
|
const cpuUsage = parseFloat(monitoringResources.cpu?.average_usage) || 0;
|
||||||
|
const heapUsage = parseFloat(monitoringResources.memory?.heap_usage_percent) || 0;
|
||||||
|
const filesystemUsage = parseFloat(monitoringResources.filesystem?.usage_percent) || 0;
|
||||||
|
const filesystemUsed = parseFloat(monitoringResources.filesystem?.used_bytes) || 0;
|
||||||
|
const filesystemTotal = parseFloat(monitoringResources.filesystem?.total_bytes) || 0;
|
||||||
|
|
||||||
|
// Convert filesystem bytes to KB
|
||||||
|
const filesystemUsedKB = Math.round(filesystemUsed / 1024);
|
||||||
|
const filesystemTotalKB = Math.round(filesystemTotal / 1024);
|
||||||
|
|
||||||
|
// Helper function to get color class based on percentage
|
||||||
|
const getColorClass = (percentage) => {
|
||||||
|
const numPercentage = parseFloat(percentage);
|
||||||
|
|
||||||
|
if (numPercentage === 0 || isNaN(numPercentage)) return 'gauge-empty';
|
||||||
|
if (numPercentage < 50) return 'gauge-green';
|
||||||
|
if (numPercentage < 80) return 'gauge-yellow';
|
||||||
|
return 'gauge-red';
|
||||||
|
};
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="resource-gauges">
|
||||||
|
<div class="gauge-container">
|
||||||
|
<div class="gauge ${getColorClass(cpuUsage)}" data-percentage="${cpuUsage}" style="--percentage: ${cpuUsage}">
|
||||||
|
<div class="gauge-circle">
|
||||||
|
<div class="gauge-text">
|
||||||
|
<div class="gauge-value">${cpuUsage.toFixed(1)}%</div>
|
||||||
|
<div class="gauge-label">CPU</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gauge-container">
|
||||||
|
<div class="gauge ${getColorClass(heapUsage)}" data-percentage="${heapUsage}" style="--percentage: ${heapUsage}">
|
||||||
|
<div class="gauge-circle">
|
||||||
|
<div class="gauge-text">
|
||||||
|
<div class="gauge-value">${heapUsage.toFixed(1)}%</div>
|
||||||
|
<div class="gauge-label">Heap</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gauge-container">
|
||||||
|
<div class="gauge ${getColorClass(filesystemUsage)}" data-percentage="${filesystemUsage}" style="--percentage: ${filesystemUsage}">
|
||||||
|
<div class="gauge-circle">
|
||||||
|
<div class="gauge-text">
|
||||||
|
<div class="gauge-value">${filesystemUsage.toFixed(1)}%</div>
|
||||||
|
<div class="gauge-label">Storage</div>
|
||||||
|
<!-- <div class="gauge-detail">${filesystemUsedKB}KB / ${filesystemTotalKB}KB</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
renderEndpointsTab(endpoints) {
|
renderEndpointsTab(endpoints) {
|
||||||
if (!endpoints || !Array.isArray(endpoints.endpoints) || endpoints.endpoints.length === 0) {
|
if (!endpoints || !Array.isArray(endpoints.endpoints) || endpoints.endpoints.length === 0) {
|
||||||
return `
|
return `
|
||||||
|
|||||||
@@ -220,7 +220,8 @@ class NodeDetailsViewModel extends ViewModel {
|
|||||||
activeTab: 'status',
|
activeTab: 'status',
|
||||||
nodeIp: null,
|
nodeIp: null,
|
||||||
endpoints: null,
|
endpoints: null,
|
||||||
tasksSummary: null
|
tasksSummary: null,
|
||||||
|
monitoringResources: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +251,9 @@ class NodeDetailsViewModel extends ViewModel {
|
|||||||
// Load endpoints data
|
// Load endpoints data
|
||||||
await this.loadEndpointsData();
|
await this.loadEndpointsData();
|
||||||
|
|
||||||
|
// Load monitoring resources data
|
||||||
|
await this.loadMonitoringResources();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load node details:', error);
|
console.error('Failed to load node details:', error);
|
||||||
this.set('error', error.message);
|
this.set('error', error.message);
|
||||||
@@ -284,6 +288,20 @@ class NodeDetailsViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load monitoring resources data with state preservation
|
||||||
|
async loadMonitoringResources() {
|
||||||
|
try {
|
||||||
|
const ip = this.get('nodeIp');
|
||||||
|
const response = await window.apiClient.getMonitoringResources(ip);
|
||||||
|
// The proxy call returns { data: {...} }, so we need to extract the data
|
||||||
|
const monitoringData = (response && response.data) ? response.data : null;
|
||||||
|
this.set('monitoringResources', monitoringData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load monitoring resources:', error);
|
||||||
|
this.set('monitoringResources', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Invoke an endpoint against this node
|
// Invoke an endpoint against this node
|
||||||
async callEndpoint(method, uri, params) {
|
async callEndpoint(method, uri, params) {
|
||||||
const ip = this.get('nodeIp');
|
const ip = this.get('nodeIp');
|
||||||
|
|||||||
@@ -391,6 +391,173 @@ p {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Monitoring Section Styles */
|
||||||
|
.monitoring-section {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitoring-header {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resource Gauges Styles */
|
||||||
|
.resource-gauges {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
max-width: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge {
|
||||||
|
position: relative;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-circle {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-circle::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
width: calc(100% - 12px);
|
||||||
|
height: calc(100% - 12px);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-circle::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
width: calc(100% - 12px);
|
||||||
|
height: calc(100% - 12px);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: conic-gradient(
|
||||||
|
from -90deg,
|
||||||
|
transparent 0deg,
|
||||||
|
transparent calc(var(--percentage) * 3.6deg),
|
||||||
|
var(--bg-primary) calc(var(--percentage) * 3.6deg),
|
||||||
|
var(--bg-primary) 360deg
|
||||||
|
);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-text {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-value {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.7;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-detail {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dynamic gauge colors based on percentage */
|
||||||
|
.gauge-empty .gauge-circle {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-green .gauge-circle {
|
||||||
|
background: conic-gradient(
|
||||||
|
from -90deg,
|
||||||
|
var(--accent-success) 0deg,
|
||||||
|
var(--accent-success) calc(var(--percentage) * 3.6deg),
|
||||||
|
rgba(255, 255, 255, 0.1) calc(var(--percentage) * 3.6deg),
|
||||||
|
rgba(255, 255, 255, 0.1) 360deg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-yellow .gauge-circle {
|
||||||
|
background: conic-gradient(
|
||||||
|
from -90deg,
|
||||||
|
var(--accent-warning) 0deg,
|
||||||
|
var(--accent-warning) calc(var(--percentage) * 3.6deg),
|
||||||
|
rgba(255, 255, 255, 0.1) calc(var(--percentage) * 3.6deg),
|
||||||
|
rgba(255, 255, 255, 0.1) 360deg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-red .gauge-circle {
|
||||||
|
background: conic-gradient(
|
||||||
|
from -90deg,
|
||||||
|
var(--accent-error) 0deg,
|
||||||
|
var(--accent-error) calc(var(--percentage) * 3.6deg),
|
||||||
|
rgba(255, 255, 255, 0.1) calc(var(--percentage) * 3.6deg),
|
||||||
|
rgba(255, 255, 255, 0.1) 360deg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gauge value color based on usage level */
|
||||||
|
.gauge[data-percentage] .gauge-value {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effects */
|
||||||
|
.gauge:hover .gauge-circle {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge:hover .gauge-value {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.api-endpoints {
|
.api-endpoints {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
@@ -2844,6 +3011,13 @@ p {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide expand icon on desktop screens */
|
||||||
|
@media (min-width: 1025px) {
|
||||||
|
.expand-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Ensure expanded state is visually clear */
|
/* Ensure expanded state is visually clear */
|
||||||
.member-overlay-body .member-card.expanded .member-details {
|
.member-overlay-body .member-card.expanded .member-details {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -2899,7 +3073,7 @@ p {
|
|||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: clamp(33.333vw, 520px, 90vw);
|
width: clamp(33.333vw, 650px, 90vw);
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border-left: 1px solid var(--border-primary);
|
border-left: 1px solid var(--border-primary);
|
||||||
|
|||||||
Reference in New Issue
Block a user