feat(dashboard): comprehensive filter enhancement and UI layout fixes
- Add time range selector (1H/6H/24H/7D/All) with preference persistence - Implement smart X-axis label formatting for multi-day usage data - Standardize global component spacing and fix CSS @apply limitations - Add elegant empty state UI for charts when filtered data is absent - Update i18n translations for all new dashboard features
This commit is contained in:
@@ -337,13 +337,44 @@ window.DashboardCharts.updateTrendChart = function (component) {
|
||||
"[updateTrendChart] Canvas is ready, proceeding with chart creation"
|
||||
);
|
||||
|
||||
const history = component.historyData;
|
||||
// Use filtered history data based on time range
|
||||
const history = window.DashboardFilters.getFilteredHistoryData(component);
|
||||
if (!history || Object.keys(history).length === 0) {
|
||||
console.warn("No history data available for trend chart");
|
||||
console.warn("No history data available for trend chart (after filtering)");
|
||||
component.hasFilteredTrendData = false;
|
||||
_trendChartUpdateLock = false;
|
||||
return;
|
||||
}
|
||||
|
||||
component.hasFilteredTrendData = true;
|
||||
|
||||
// Sort entries by timestamp for correct order
|
||||
const sortedEntries = Object.entries(history).sort(
|
||||
([a], [b]) => new Date(a).getTime() - new Date(b).getTime()
|
||||
);
|
||||
|
||||
// Determine if data spans multiple days (for smart label formatting)
|
||||
const timestamps = sortedEntries.map(([iso]) => new Date(iso));
|
||||
const isMultiDay = timestamps.length > 1 &&
|
||||
timestamps[0].toDateString() !== timestamps[timestamps.length - 1].toDateString();
|
||||
|
||||
// Helper to format X-axis labels based on time range and multi-day status
|
||||
const formatLabel = (date) => {
|
||||
const timeRange = component.timeRange || '24h';
|
||||
|
||||
if (timeRange === '7d') {
|
||||
// Week view: show MM/DD
|
||||
return date.toLocaleDateString([], { month: '2-digit', day: '2-digit' });
|
||||
} else if (isMultiDay || timeRange === 'all') {
|
||||
// Multi-day data: show MM/DD HH:MM
|
||||
return date.toLocaleDateString([], { month: '2-digit', day: '2-digit' }) + ' ' +
|
||||
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} else {
|
||||
// Same day: show HH:MM only
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
};
|
||||
|
||||
const labels = [];
|
||||
const datasets = [];
|
||||
|
||||
@@ -354,11 +385,9 @@ window.DashboardCharts.updateTrendChart = function (component) {
|
||||
dataByFamily[family] = [];
|
||||
});
|
||||
|
||||
Object.entries(history).forEach(([iso, hourData]) => {
|
||||
sortedEntries.forEach(([iso, hourData]) => {
|
||||
const date = new Date(iso);
|
||||
labels.push(
|
||||
date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
||||
);
|
||||
labels.push(formatLabel(date));
|
||||
|
||||
component.selectedFamilies.forEach((family) => {
|
||||
const familyData = hourData[family];
|
||||
@@ -394,11 +423,9 @@ window.DashboardCharts.updateTrendChart = function (component) {
|
||||
});
|
||||
});
|
||||
|
||||
Object.entries(history).forEach(([iso, hourData]) => {
|
||||
sortedEntries.forEach(([iso, hourData]) => {
|
||||
const date = new Date(iso);
|
||||
labels.push(
|
||||
date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
||||
);
|
||||
labels.push(formatLabel(date));
|
||||
|
||||
component.families.forEach((family) => {
|
||||
const familyData = hourData[family] || {};
|
||||
|
||||
@@ -10,10 +10,13 @@ window.DashboardFilters = window.DashboardFilters || {};
|
||||
*/
|
||||
window.DashboardFilters.getInitialState = function() {
|
||||
return {
|
||||
timeRange: '24h', // '1h', '6h', '24h', '7d', 'all'
|
||||
displayMode: 'model',
|
||||
selectedFamilies: [],
|
||||
selectedModels: {},
|
||||
showModelFilter: false
|
||||
showModelFilter: false,
|
||||
showTimeRangeDropdown: false,
|
||||
showDisplayModeDropdown: false
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,6 +29,7 @@ window.DashboardFilters.loadPreferences = function(component) {
|
||||
const saved = localStorage.getItem('dashboard_chart_prefs');
|
||||
if (saved) {
|
||||
const prefs = JSON.parse(saved);
|
||||
component.timeRange = prefs.timeRange || '24h';
|
||||
component.displayMode = prefs.displayMode || 'model';
|
||||
component.selectedFamilies = prefs.selectedFamilies || [];
|
||||
component.selectedModels = prefs.selectedModels || {};
|
||||
@@ -42,6 +46,7 @@ window.DashboardFilters.loadPreferences = function(component) {
|
||||
window.DashboardFilters.savePreferences = function(component) {
|
||||
try {
|
||||
localStorage.setItem('dashboard_chart_prefs', JSON.stringify({
|
||||
timeRange: component.timeRange,
|
||||
displayMode: component.displayMode,
|
||||
selectedFamilies: component.selectedFamilies,
|
||||
selectedModels: component.selectedModels
|
||||
@@ -58,11 +63,78 @@ window.DashboardFilters.savePreferences = function(component) {
|
||||
*/
|
||||
window.DashboardFilters.setDisplayMode = function(component, mode) {
|
||||
component.displayMode = mode;
|
||||
component.showDisplayModeDropdown = false;
|
||||
window.DashboardFilters.savePreferences(component);
|
||||
// updateTrendChart uses debounce internally, call directly
|
||||
component.updateTrendChart();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set time range filter
|
||||
* @param {object} component - Dashboard component instance
|
||||
* @param {string} range - '1h', '6h', '24h', '7d', 'all'
|
||||
*/
|
||||
window.DashboardFilters.setTimeRange = function(component, range) {
|
||||
component.timeRange = range;
|
||||
component.showTimeRangeDropdown = false;
|
||||
window.DashboardFilters.savePreferences(component);
|
||||
component.updateTrendChart();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get time range cutoff timestamp
|
||||
* @param {string} range - Time range code
|
||||
* @returns {number|null} Cutoff timestamp or null for 'all'
|
||||
*/
|
||||
window.DashboardFilters.getTimeRangeCutoff = function(range) {
|
||||
const now = Date.now();
|
||||
switch (range) {
|
||||
case '1h': return now - 1 * 60 * 60 * 1000;
|
||||
case '6h': return now - 6 * 60 * 60 * 1000;
|
||||
case '24h': return now - 24 * 60 * 60 * 1000;
|
||||
case '7d': return now - 7 * 24 * 60 * 60 * 1000;
|
||||
default: return null; // 'all'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get filtered history data based on time range
|
||||
* @param {object} component - Dashboard component instance
|
||||
* @returns {object} Filtered history data
|
||||
*/
|
||||
window.DashboardFilters.getFilteredHistoryData = function(component) {
|
||||
const history = component.historyData;
|
||||
if (!history || Object.keys(history).length === 0) return {};
|
||||
|
||||
const cutoff = window.DashboardFilters.getTimeRangeCutoff(component.timeRange);
|
||||
if (!cutoff) return history; // 'all' - return everything
|
||||
|
||||
const filtered = {};
|
||||
Object.entries(history).forEach(([iso, data]) => {
|
||||
const timestamp = new Date(iso).getTime();
|
||||
if (timestamp >= cutoff) {
|
||||
filtered[iso] = data;
|
||||
}
|
||||
});
|
||||
return filtered;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get time range label for display
|
||||
* @param {object} component - Dashboard component instance
|
||||
* @returns {string} Translated label
|
||||
*/
|
||||
window.DashboardFilters.getTimeRangeLabel = function(component) {
|
||||
const store = Alpine.store('global');
|
||||
switch (component.timeRange) {
|
||||
case '1h': return store.t('last1Hour');
|
||||
case '6h': return store.t('last6Hours');
|
||||
case '24h': return store.t('last24Hours');
|
||||
case '7d': return store.t('last7Days');
|
||||
default: return store.t('allTime');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle family selection
|
||||
* @param {object} component - Dashboard component instance
|
||||
|
||||
Reference in New Issue
Block a user