All 'require module.submodule' directives changed to 'require module/submodule' to match LuCI's module loading convention. Affected packages: - luci-app-auth-guardian - luci-app-glances - luci-app-localai - luci-app-magicmirror2 - luci-app-mitmproxy - luci-app-mmpm - luci-app-mqtt-bridge - luci-app-ndpid - luci-app-network-modes - luci-app-secubox-admin - luci-app-secubox-portal - luci-app-wireguard-dashboard Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
584 lines
20 KiB
JavaScript
584 lines
20 KiB
JavaScript
'use strict';
|
|
'require baseclass';
|
|
'require secubox-admin/api as API';
|
|
'require secubox-admin/chart-utils as ChartUtils';
|
|
'require secubox-admin/realtime-client as RealtimeClient';
|
|
'require poll';
|
|
|
|
function WidgetRendererInstance(options) {
|
|
options = options || {};
|
|
|
|
this.containerId = options.containerId || 'widget-container';
|
|
this.apps = options.apps || [];
|
|
this.defaultRefreshInterval = options.defaultRefreshInterval || 30;
|
|
this.gridMode = options.gridMode || 'auto';
|
|
this.widgets = [];
|
|
this.pollHandles = [];
|
|
this.templates = {};
|
|
this.realtimeSubscriptions = [];
|
|
|
|
// Initialize real-time client
|
|
this.realtime = Object.create(RealtimeClient);
|
|
this.realtime.init({
|
|
enableWebSocket: true,
|
|
enablePolling: true,
|
|
pollInterval: this.defaultRefreshInterval * 1000,
|
|
debug: true
|
|
});
|
|
|
|
this.registerBuiltInTemplates();
|
|
}
|
|
|
|
WidgetRendererInstance.prototype = {
|
|
registerBuiltInTemplates: function() {
|
|
var self = this;
|
|
|
|
this.registerTemplate('default', {
|
|
render: function(container, app, data) {
|
|
container.innerHTML = '';
|
|
|
|
var status = data && data.status ? data.status : 'unknown';
|
|
var statusClass = status === 'running' ? 'status-success' :
|
|
status === 'stopped' ? 'status-warning' :
|
|
status === 'not_installed' ? 'status-error' : 'status-unknown';
|
|
|
|
var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ?
|
|
'v' + (data.installed_version || data.pkg_version || data.catalog_version) : '';
|
|
|
|
container.appendChild(E('div', { 'class': 'widget-default' }, [
|
|
E('div', { 'class': 'widget-header' }, [
|
|
E('div', { 'class': 'widget-icon' }, app.icon || '📊'),
|
|
E('div', { 'class': 'widget-title-wrapper' }, [
|
|
E('div', { 'class': 'widget-title' }, app.name || 'Application'),
|
|
version ? E('div', { 'class': 'widget-version' }, version) : null
|
|
]),
|
|
E('div', { 'class': 'widget-status-indicator ' + statusClass })
|
|
]),
|
|
E('div', { 'class': 'widget-status-text' }, [
|
|
E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase())
|
|
]),
|
|
E('div', { 'class': 'widget-status' },
|
|
data && data.widget_enabled ? 'Widget Enabled' : 'No widget data'
|
|
)
|
|
]));
|
|
}
|
|
});
|
|
|
|
this.registerTemplate('security', {
|
|
render: function(container, app, data) {
|
|
container.innerHTML = '';
|
|
|
|
var metrics = data && data.metrics ? data.metrics : [];
|
|
var status = data && data.status ? data.status : 'unknown';
|
|
var statusClass = status === 'running' ? 'status-success' :
|
|
status === 'stopped' ? 'status-warning' :
|
|
status === 'not_installed' ? 'status-error' : 'status-unknown';
|
|
|
|
var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ?
|
|
'v' + (data.installed_version || data.pkg_version || data.catalog_version) : '';
|
|
|
|
container.appendChild(E('div', { 'class': 'widget-security' }, [
|
|
E('div', { 'class': 'widget-header' }, [
|
|
E('div', { 'class': 'widget-icon' }, app.icon || '🔒'),
|
|
E('div', { 'class': 'widget-title-wrapper' }, [
|
|
E('div', { 'class': 'widget-title' }, app.name || 'Security'),
|
|
version ? E('div', { 'class': 'widget-version' }, version) : null
|
|
]),
|
|
E('div', { 'class': 'widget-status-indicator ' + statusClass })
|
|
]),
|
|
E('div', { 'class': 'widget-status-text' }, [
|
|
E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase())
|
|
]),
|
|
E('div', { 'class': 'widget-metrics' },
|
|
metrics.map(function(metric) {
|
|
return self.renderMetric(metric || {});
|
|
})
|
|
),
|
|
data && data.last_event ? E('div', { 'class': 'widget-last-event' }, [
|
|
E('span', { 'class': 'event-label' }, 'Last Event: '),
|
|
E('span', { 'class': 'event-time' }, self.formatTimestamp(data.last_event))
|
|
]) : null
|
|
]));
|
|
}
|
|
});
|
|
|
|
this.registerTemplate('network', {
|
|
render: function(container, app, data) {
|
|
container.innerHTML = '';
|
|
|
|
var metrics = data && data.metrics ? data.metrics : [];
|
|
var connections = data && data.active_connections ? data.active_connections : 0;
|
|
var bandwidth = data && data.bandwidth ? data.bandwidth : { up: 0, down: 0 };
|
|
|
|
var status = data && data.status ? data.status : 'unknown';
|
|
var statusClass = status === 'running' ? 'status-success' :
|
|
status === 'stopped' ? 'status-warning' :
|
|
status === 'not_installed' ? 'status-error' : 'status-unknown';
|
|
|
|
var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ?
|
|
'v' + (data.installed_version || data.pkg_version || data.catalog_version) : '';
|
|
|
|
container.appendChild(E('div', { 'class': 'widget-network' }, [
|
|
E('div', { 'class': 'widget-header' }, [
|
|
E('div', { 'class': 'widget-icon' }, app.icon || '🌐'),
|
|
E('div', { 'class': 'widget-title-wrapper' }, [
|
|
E('div', { 'class': 'widget-title' }, app.name || 'Network'),
|
|
version ? E('div', { 'class': 'widget-version' }, version) : null
|
|
]),
|
|
E('div', { 'class': 'widget-status-indicator ' + statusClass })
|
|
]),
|
|
E('div', { 'class': 'widget-status-text' }, [
|
|
E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase())
|
|
]),
|
|
E('div', { 'class': 'widget-metrics' }, [
|
|
E('div', { 'class': 'metric-row' }, [
|
|
E('span', { 'class': 'metric-label' }, 'Connections:'),
|
|
E('span', { 'class': 'metric-value' }, connections.toString())
|
|
]),
|
|
E('div', { 'class': 'metric-row' }, [
|
|
E('span', { 'class': 'metric-label' }, 'Up/Down:'),
|
|
E('span', { 'class': 'metric-value' },
|
|
self.formatBandwidth(bandwidth.up || 0) + ' / ' +
|
|
self.formatBandwidth(bandwidth.down || 0))
|
|
])
|
|
].concat(metrics.map(function(metric) {
|
|
return self.renderMetric(metric || {});
|
|
})))
|
|
]));
|
|
}
|
|
});
|
|
|
|
this.registerTemplate('monitoring', {
|
|
render: function(container, app, data) {
|
|
container.innerHTML = '';
|
|
|
|
var metrics = data && data.metrics ? data.metrics : [];
|
|
var status = data && data.status ? data.status : 'unknown';
|
|
var statusClass = status === 'running' ? 'status-success' :
|
|
status === 'stopped' ? 'status-warning' :
|
|
status === 'not_installed' ? 'status-error' : 'status-unknown';
|
|
|
|
var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ?
|
|
'v' + (data.installed_version || data.pkg_version || data.catalog_version) : '';
|
|
|
|
container.appendChild(E('div', { 'class': 'widget-monitoring' }, [
|
|
E('div', { 'class': 'widget-header' }, [
|
|
E('div', { 'class': 'widget-icon' }, app.icon || '📈'),
|
|
E('div', { 'class': 'widget-title-wrapper' }, [
|
|
E('div', { 'class': 'widget-title' }, app.name || 'Monitoring'),
|
|
version ? E('div', { 'class': 'widget-version' }, version) : null
|
|
]),
|
|
E('div', { 'class': 'widget-status-indicator ' + statusClass })
|
|
]),
|
|
E('div', { 'class': 'widget-status-text' }, [
|
|
E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase())
|
|
]),
|
|
E('div', { 'class': 'widget-metrics-grid' },
|
|
metrics.map(function(metric) {
|
|
return self.renderMetricCard(metric || {});
|
|
})
|
|
),
|
|
data && data.uptime ? E('div', { 'class': 'widget-uptime' }, [
|
|
E('span', { 'class': 'uptime-label' }, 'Uptime: '),
|
|
E('span', { 'class': 'uptime-value' }, self.formatUptime(data.uptime))
|
|
]) : null
|
|
]));
|
|
}
|
|
});
|
|
|
|
this.registerTemplate('hosting', {
|
|
render: function(container, app, data) {
|
|
container.innerHTML = '';
|
|
|
|
var metrics = data && data.metrics ? data.metrics : [];
|
|
var services = data && data.services ? data.services : [];
|
|
|
|
var status = data && data.status ? data.status : 'unknown';
|
|
var statusClass = status === 'running' ? 'status-success' :
|
|
status === 'stopped' ? 'status-warning' :
|
|
status === 'not_installed' ? 'status-error' : 'status-unknown';
|
|
|
|
var version = data && (data.installed_version || data.pkg_version || data.catalog_version) ?
|
|
'v' + (data.installed_version || data.pkg_version || data.catalog_version) : '';
|
|
|
|
container.appendChild(E('div', { 'class': 'widget-hosting' }, [
|
|
E('div', { 'class': 'widget-header' }, [
|
|
E('div', { 'class': 'widget-icon' }, app.icon || '🖥️'),
|
|
E('div', { 'class': 'widget-title-wrapper' }, [
|
|
E('div', { 'class': 'widget-title' }, app.name || 'Hosting'),
|
|
version ? E('div', { 'class': 'widget-version' }, version) : null
|
|
]),
|
|
E('div', { 'class': 'widget-status-indicator ' + statusClass })
|
|
]),
|
|
E('div', { 'class': 'widget-status-text' }, [
|
|
E('span', { 'class': 'status-badge ' + statusClass }, status.replace('_', ' ').toUpperCase())
|
|
]),
|
|
E('div', { 'class': 'widget-services' },
|
|
services.map(function(service) {
|
|
var running = service && service.running;
|
|
var statusClass = running ? 'service-running' : 'service-stopped';
|
|
return E('div', { 'class': 'service-item ' + statusClass }, [
|
|
E('span', { 'class': 'service-name' }, (service && service.name) || 'Service'),
|
|
E('span', { 'class': 'service-status' }, running ? '✓' : '✗')
|
|
]);
|
|
})
|
|
),
|
|
metrics.length > 0 ? E('div', { 'class': 'widget-metrics' },
|
|
metrics.map(function(metric) {
|
|
return self.renderMetric(metric || {});
|
|
})
|
|
) : null
|
|
]));
|
|
}
|
|
});
|
|
|
|
this.registerTemplate('compact', {
|
|
render: function(container, app, data) {
|
|
container.innerHTML = '';
|
|
|
|
var primaryMetric = data && data.primary_metric ? data.primary_metric : {};
|
|
|
|
container.appendChild(E('div', { 'class': 'widget-compact' }, [
|
|
E('div', { 'class': 'widget-icon-small' }, app.icon || '📊'),
|
|
E('div', { 'class': 'widget-content' }, [
|
|
E('div', { 'class': 'widget-title-small' }, app.name || 'Metric'),
|
|
E('div', { 'class': 'widget-value-large' }, primaryMetric.value || '0'),
|
|
E('div', { 'class': 'widget-label-small' }, primaryMetric.label || '')
|
|
])
|
|
]));
|
|
}
|
|
});
|
|
|
|
this.registerTemplate('chart-timeseries', {
|
|
render: function(container, app, data) {
|
|
container.innerHTML = '';
|
|
|
|
var chartContainer = E('div', { 'class': 'cyber-chart-container cyber-chart-container--md' }, [
|
|
E('div', { 'class': 'cyber-chart-header' }, [
|
|
E('div', {}, [
|
|
E('div', { 'class': 'cyber-chart-title' }, app.name || 'Timeseries'),
|
|
E('div', { 'class': 'cyber-chart-subtitle' }, (data && data.subtitle) || 'Time Series Data')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'cyber-chart-canvas' }, [
|
|
E('canvas', { 'id': 'chart-' + app.id })
|
|
])
|
|
]);
|
|
|
|
container.appendChild(chartContainer);
|
|
|
|
var canvas = chartContainer.querySelector('canvas');
|
|
var chartData = {
|
|
labels: data && data.labels ? data.labels : [],
|
|
datasets: (data && data.datasets ? data.datasets : []).map(function(ds) {
|
|
return {
|
|
label: ds && ds.label ? ds.label : 'Dataset',
|
|
data: ds && ds.data ? ds.data : []
|
|
};
|
|
})
|
|
};
|
|
|
|
ChartUtils.createLineChart(canvas, chartData, {}).catch(function(err) {
|
|
console.error('[WIDGET] Chart error:', err);
|
|
});
|
|
}
|
|
});
|
|
|
|
this.registerTemplate('chart-gauge', {
|
|
render: function(container, app, data) {
|
|
container.innerHTML = '';
|
|
|
|
var value = data && data.value ? data.value : 0;
|
|
var max = data && data.max ? data.max : 100;
|
|
var percentage = max > 0 ? Math.round((value / max) * 100) : 0;
|
|
|
|
var chartContainer = E('div', { 'class': 'cyber-chart-container cyber-chart-container--sm' }, [
|
|
E('div', { 'class': 'cyber-chart-header' }, [
|
|
E('div', { 'class': 'cyber-chart-title' }, app.name || 'Gauge')
|
|
]),
|
|
E('div', { 'class': 'cyber-chart-metrics' }, [
|
|
E('div', { 'class': 'cyber-chart-metric' }, [
|
|
E('div', { 'class': 'cyber-chart-metric-label' }, (data && data.label) || 'Usage'),
|
|
E('div', { 'class': 'cyber-chart-metric-value' }, percentage + '%')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'cyber-chart-canvas' }, [
|
|
E('canvas', { 'id': 'chart-' + app.id })
|
|
])
|
|
]);
|
|
|
|
container.appendChild(chartContainer);
|
|
|
|
var canvas = chartContainer.querySelector('canvas');
|
|
ChartUtils.createGaugeChart(canvas, percentage, {}).catch(function(err) {
|
|
console.error('[WIDGET] Gauge error:', err);
|
|
});
|
|
}
|
|
});
|
|
|
|
this.registerTemplate('sparkline', {
|
|
render: function(container, app, data) {
|
|
container.innerHTML = '';
|
|
|
|
var values = data && data.values ? data.values : [];
|
|
var currentValue = values.length ? values[values.length - 1] : 0;
|
|
var previousValue = values.length > 1 ? values[values.length - 2] : 0;
|
|
var change = currentValue - previousValue;
|
|
var trendClass = change > 0 ? 'cyber-sparkline-trend--up' : 'cyber-sparkline-trend--down';
|
|
|
|
var sparklineContainer = E('div', { 'class': 'cyber-sparkline' }, [
|
|
E('div', { 'class': 'cyber-sparkline-canvas' }, [
|
|
E('canvas', { 'id': 'sparkline-' + app.id })
|
|
]),
|
|
E('div', { 'class': 'cyber-sparkline-value' }, currentValue.toString()),
|
|
E('div', { 'class': 'cyber-sparkline-trend ' + trendClass },
|
|
change > 0 ? '↑ ' + change : '↓ ' + Math.abs(change))
|
|
]);
|
|
|
|
container.appendChild(sparklineContainer);
|
|
|
|
var canvas = sparklineContainer.querySelector('canvas');
|
|
ChartUtils.createSparkline(canvas, values).catch(function(err) {
|
|
console.error('[WIDGET] Sparkline error:', err);
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
registerTemplate: function(name, template) {
|
|
this.templates[name] = template;
|
|
},
|
|
|
|
renderMetric: function(metric) {
|
|
var valueClass = 'metric-value';
|
|
if (metric.status === 'warning') valueClass += ' value-warning';
|
|
if (metric.status === 'error') valueClass += ' value-error';
|
|
|
|
return E('div', { 'class': 'metric-row' }, [
|
|
E('span', { 'class': 'metric-label' }, (metric.label || 'Metric') + ':'),
|
|
E('span', { 'class': valueClass }, getMetricValue(metric))
|
|
]);
|
|
},
|
|
|
|
renderMetricCard: function(metric) {
|
|
var valueClass = 'metric-card-value';
|
|
if (metric.status === 'warning') valueClass += ' value-warning';
|
|
if (metric.status === 'error') valueClass += ' value-error';
|
|
|
|
return E('div', { 'class': 'metric-card' }, [
|
|
E('div', { 'class': 'metric-card-label' }, metric.label || 'Metric'),
|
|
E('div', { 'class': valueClass }, getMetricValue(metric)),
|
|
metric.unit ? E('div', { 'class': 'metric-card-unit' }, metric.unit) : null
|
|
]);
|
|
},
|
|
|
|
render: function() {
|
|
var container = document.getElementById(this.containerId);
|
|
if (!container) {
|
|
console.error('Widget container not found:', this.containerId);
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = '';
|
|
container.className = 'widget-grid widget-grid-' + this.gridMode;
|
|
|
|
var widgetApps = this.apps.filter(function(app) {
|
|
return app && app.widget && app.widget.enabled;
|
|
});
|
|
|
|
if (widgetApps.length === 0) {
|
|
container.appendChild(E('div', { 'class': 'no-widgets' }, [
|
|
E('div', { 'class': 'no-widgets-icon' }, '📊'),
|
|
E('h3', {}, 'No Active Widgets'),
|
|
E('p', {}, 'Install and enable apps with widget support to see live metrics here.')
|
|
]));
|
|
return;
|
|
}
|
|
|
|
var self = this;
|
|
widgetApps.forEach(function(app) {
|
|
self.renderWidget(container, app);
|
|
});
|
|
},
|
|
|
|
renderWidget: function(container, app) {
|
|
var self = this;
|
|
var widgetConfig = app.widget || {};
|
|
var templateName = widgetConfig.template || 'default';
|
|
var refreshInterval = widgetConfig.refresh_interval || this.defaultRefreshInterval;
|
|
|
|
var widgetElement = E('div', {
|
|
'class': 'widget-item widget-' + (app.category || 'general'),
|
|
'data-app-id': app.id
|
|
}, [
|
|
E('div', { 'class': 'widget-content', 'id': 'widget-content-' + app.id }, [
|
|
E('div', { 'class': 'widget-loading' }, [
|
|
E('div', { 'class': 'spinner' }),
|
|
E('p', {}, 'Loading...')
|
|
])
|
|
])
|
|
]);
|
|
|
|
container.appendChild(widgetElement);
|
|
|
|
this.widgets.push({
|
|
app: app,
|
|
element: widgetElement,
|
|
contentElement: document.getElementById('widget-content-' + app.id)
|
|
});
|
|
|
|
this.updateWidget(app, templateName);
|
|
|
|
if (refreshInterval > 0) {
|
|
var channel = 'widget.' + app.id;
|
|
|
|
// Subscribe to real-time updates
|
|
var unsubscribe = this.realtime.subscribe(channel, function(data) {
|
|
console.log('[WIDGET] Real-time update for', app.id, data);
|
|
var contentElement = document.getElementById('widget-content-' + app.id);
|
|
if (!contentElement) return;
|
|
|
|
var templateObj = self.templates[templateName] || self.templates['default'];
|
|
|
|
try {
|
|
templateObj.render(contentElement, app, data || {});
|
|
} catch (error) {
|
|
console.error('Widget render error for', app.id, error);
|
|
contentElement.innerHTML = '';
|
|
contentElement.appendChild(E('div', { 'class': 'widget-error' }, [
|
|
E('div', { 'class': 'error-icon' }, '⚠️'),
|
|
E('div', { 'class': 'error-message' }, 'Widget Error'),
|
|
E('div', { 'class': 'error-details' }, error.message || 'Render failure')
|
|
]));
|
|
}
|
|
});
|
|
|
|
this.realtimeSubscriptions.push({
|
|
channel: channel,
|
|
unsubscribe: unsubscribe
|
|
});
|
|
|
|
// Keep poll as fallback (realtime-client will use it if WebSocket unavailable)
|
|
var pollHandle = poll.add(function() {
|
|
return self.updateWidget(app, templateName);
|
|
}, refreshInterval);
|
|
|
|
this.pollHandles.push(pollHandle);
|
|
}
|
|
},
|
|
|
|
updateWidget: function(app, templateName) {
|
|
var self = this;
|
|
return API.getWidgetData(app.id).then(function(data) {
|
|
var contentElement = document.getElementById('widget-content-' + app.id);
|
|
if (!contentElement) {
|
|
return;
|
|
}
|
|
|
|
var template = self.templates[templateName] || self.templates.default;
|
|
|
|
try {
|
|
template.render(contentElement, app, data || {});
|
|
} catch (error) {
|
|
console.error('Widget render error for', app.id, error);
|
|
contentElement.innerHTML = '';
|
|
contentElement.appendChild(E('div', { 'class': 'widget-error' }, [
|
|
E('div', { 'class': 'error-icon' }, '⚠️'),
|
|
E('div', { 'class': 'error-message' }, 'Widget Error'),
|
|
E('div', { 'class': 'error-details' }, error.message || 'Render failure')
|
|
]));
|
|
}
|
|
}).catch(function(error) {
|
|
console.error('Failed to load widget data for', app.id, error);
|
|
var contentElement = document.getElementById('widget-content-' + app.id);
|
|
if (contentElement) {
|
|
contentElement.innerHTML = '';
|
|
contentElement.appendChild(E('div', { 'class': 'widget-error' }, [
|
|
E('div', { 'class': 'error-icon' }, '⚠️'),
|
|
E('div', { 'class': 'error-message' }, 'Data Load Failed')
|
|
]));
|
|
}
|
|
});
|
|
},
|
|
|
|
destroy: function() {
|
|
var container = document.getElementById(this.containerId);
|
|
if (container) {
|
|
container.innerHTML = '';
|
|
}
|
|
|
|
// Cleanup real-time subscriptions
|
|
this.realtimeSubscriptions.forEach(function(sub) {
|
|
if (sub && sub.unsubscribe) {
|
|
sub.unsubscribe();
|
|
}
|
|
});
|
|
this.realtimeSubscriptions = [];
|
|
|
|
// Disconnect real-time client
|
|
if (this.realtime) {
|
|
this.realtime.disconnect();
|
|
}
|
|
|
|
// Cleanup polling fallback
|
|
this.pollHandles.forEach(function(handle) {
|
|
poll.remove(handle);
|
|
});
|
|
this.pollHandles = [];
|
|
this.widgets = [];
|
|
},
|
|
|
|
formatBandwidth: function(bytesPerSec) {
|
|
if (bytesPerSec < 1024) return bytesPerSec + ' B/s';
|
|
if (bytesPerSec < 1024 * 1024) return (bytesPerSec / 1024).toFixed(1) + ' KB/s';
|
|
if (bytesPerSec < 1024 * 1024 * 1024) return (bytesPerSec / (1024 * 1024)).toFixed(1) + ' MB/s';
|
|
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(2) + ' GB/s';
|
|
},
|
|
|
|
formatUptime: function(seconds) {
|
|
var totalSeconds = parseInt(seconds, 10) || 0;
|
|
var days = Math.floor(totalSeconds / 86400);
|
|
var hours = Math.floor((totalSeconds % 86400) / 3600);
|
|
var mins = Math.floor((totalSeconds % 3600) / 60);
|
|
|
|
if (days > 0) return days + 'd ' + hours + 'h ' + mins + 'm';
|
|
if (hours > 0) return hours + 'h ' + mins + 'm';
|
|
return mins + 'm';
|
|
},
|
|
|
|
formatTimestamp: function(timestamp) {
|
|
if (!timestamp) {
|
|
return 'Unknown';
|
|
}
|
|
|
|
var date = typeof timestamp === 'number' ?
|
|
new Date(timestamp * 1000) : new Date(timestamp);
|
|
var now = new Date();
|
|
var diffMs = now.getTime() - date.getTime();
|
|
var diffSecs = Math.floor(diffMs / 1000);
|
|
|
|
if (diffSecs < 60) return 'Just now';
|
|
if (diffSecs < 3600) return Math.floor(diffSecs / 60) + ' min ago';
|
|
if (diffSecs < 86400) return Math.floor(diffSecs / 3600) + ' hr ago';
|
|
return Math.floor(diffSecs / 86400) + ' days ago';
|
|
}
|
|
};
|
|
|
|
function getMetricValue(metric) {
|
|
if (metric.formatted_value) {
|
|
return metric.formatted_value;
|
|
}
|
|
|
|
if (metric.value === 0 || metric.value) {
|
|
return metric.value.toString();
|
|
}
|
|
|
|
return '0';
|
|
}
|
|
|
|
return baseclass.extend({
|
|
create: function(options) {
|
|
return new WidgetRendererInstance(options);
|
|
}
|
|
});
|