perf(webui): refactor dashboard modules and optimize API performance
This commit is contained in:
@@ -1,53 +1,41 @@
|
||||
/**
|
||||
* Dashboard Component
|
||||
* Dashboard Component (Refactored)
|
||||
* Orchestrates stats, charts, and filters modules
|
||||
* Registers itself to window.Components for Alpine.js to consume
|
||||
*/
|
||||
window.Components = window.Components || {};
|
||||
|
||||
// Helper to get CSS variable values (alias to window.utils.getThemeColor)
|
||||
const getThemeColor = (name) => window.utils.getThemeColor(name);
|
||||
|
||||
// Color palette for different families and models
|
||||
const FAMILY_COLORS = {
|
||||
get claude() { return getThemeColor('--color-neon-purple'); },
|
||||
get gemini() { return getThemeColor('--color-neon-green'); },
|
||||
get other() { return getThemeColor('--color-neon-cyan'); }
|
||||
};
|
||||
|
||||
const MODEL_COLORS = Array.from({ length: 16 }, (_, i) => getThemeColor(`--color-chart-${i + 1}`));
|
||||
|
||||
window.Components.dashboard = () => ({
|
||||
// Core state
|
||||
stats: { total: 0, active: 0, limited: 0, overallHealth: 0, hasTrendData: false },
|
||||
charts: { quotaDistribution: null, usageTrend: null },
|
||||
|
||||
// Usage stats
|
||||
usageStats: { total: 0, today: 0, thisHour: 0 },
|
||||
historyData: {},
|
||||
|
||||
// Hierarchical model tree: { claude: ['opus-4-5', 'sonnet-4-5'], gemini: ['3-flash'] }
|
||||
modelTree: {},
|
||||
families: [], // ['claude', 'gemini']
|
||||
families: [],
|
||||
|
||||
// Display mode: 'family' or 'model'
|
||||
displayMode: 'model',
|
||||
// Filter state (from module)
|
||||
...window.DashboardFilters.getInitialState(),
|
||||
|
||||
// Selection state
|
||||
selectedFamilies: [],
|
||||
selectedModels: {}, // { claude: ['opus-4-5'], gemini: ['3-flash'] }
|
||||
|
||||
showModelFilter: false,
|
||||
// Debounced chart update to prevent rapid successive updates
|
||||
_debouncedUpdateTrendChart: null,
|
||||
|
||||
init() {
|
||||
// Load saved preferences from localStorage
|
||||
this.loadPreferences();
|
||||
// Create debounced version of updateTrendChart (300ms delay for stability)
|
||||
this._debouncedUpdateTrendChart = window.utils.debounce(() => {
|
||||
window.DashboardCharts.updateTrendChart(this);
|
||||
}, 300);
|
||||
|
||||
// Update stats when dashboard becomes active
|
||||
this.$watch('$store.global.activeTab', (val) => {
|
||||
if (val === 'dashboard') {
|
||||
// Load saved preferences from localStorage
|
||||
window.DashboardFilters.loadPreferences(this);
|
||||
|
||||
// Update stats when dashboard becomes active (skip initial trigger)
|
||||
this.$watch('$store.global.activeTab', (val, oldVal) => {
|
||||
if (val === 'dashboard' && oldVal !== undefined) {
|
||||
this.$nextTick(() => {
|
||||
this.updateStats();
|
||||
this.updateCharts();
|
||||
this.fetchHistory();
|
||||
this.updateTrendChart();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -60,65 +48,30 @@ window.Components.dashboard = () => ({
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for history updates from data-store (automatically loaded with account data)
|
||||
this.$watch('$store.data.usageHistory', (newHistory) => {
|
||||
if (this.$store.global.activeTab === 'dashboard' && newHistory && Object.keys(newHistory).length > 0) {
|
||||
this.historyData = newHistory;
|
||||
this.processHistory(newHistory);
|
||||
this.stats.hasTrendData = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Initial update if already on dashboard
|
||||
if (this.$store.global.activeTab === 'dashboard') {
|
||||
this.$nextTick(() => {
|
||||
this.updateStats();
|
||||
this.updateCharts();
|
||||
this.fetchHistory();
|
||||
|
||||
// Load history if already in store
|
||||
const history = Alpine.store('data').usageHistory;
|
||||
if (history && Object.keys(history).length > 0) {
|
||||
this.historyData = history;
|
||||
this.processHistory(history);
|
||||
this.stats.hasTrendData = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh history every 5 minutes
|
||||
setInterval(() => {
|
||||
if (this.$store.global.activeTab === 'dashboard') {
|
||||
this.fetchHistory();
|
||||
}
|
||||
}, 300000);
|
||||
},
|
||||
|
||||
loadPreferences() {
|
||||
try {
|
||||
const saved = localStorage.getItem('dashboard_chart_prefs');
|
||||
if (saved) {
|
||||
const prefs = JSON.parse(saved);
|
||||
this.displayMode = prefs.displayMode || 'model';
|
||||
this.selectedFamilies = prefs.selectedFamilies || [];
|
||||
this.selectedModels = prefs.selectedModels || {};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load dashboard preferences:', e);
|
||||
}
|
||||
},
|
||||
|
||||
savePreferences() {
|
||||
try {
|
||||
localStorage.setItem('dashboard_chart_prefs', JSON.stringify({
|
||||
displayMode: this.displayMode,
|
||||
selectedFamilies: this.selectedFamilies,
|
||||
selectedModels: this.selectedModels
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('Failed to save dashboard preferences:', e);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchHistory() {
|
||||
try {
|
||||
const password = Alpine.store('global').webuiPassword;
|
||||
const { response, newPassword } = await window.utils.request('/api/stats/history', {}, password);
|
||||
if (newPassword) Alpine.store('global').webuiPassword = newPassword;
|
||||
|
||||
if (response.ok) {
|
||||
const history = await response.json();
|
||||
this.historyData = history;
|
||||
this.processHistory(history);
|
||||
this.stats.hasTrendData = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch usage history:', error);
|
||||
this.stats.hasTrendData = true;
|
||||
}
|
||||
},
|
||||
|
||||
processHistory(history) {
|
||||
@@ -150,7 +103,6 @@ window.Components.dashboard = () => ({
|
||||
}
|
||||
});
|
||||
}
|
||||
// Skip old flat format keys (claude, gemini as numbers)
|
||||
});
|
||||
|
||||
// Calculate totals
|
||||
@@ -180,458 +132,80 @@ window.Components.dashboard = () => ({
|
||||
this.updateTrendChart();
|
||||
},
|
||||
|
||||
autoSelectNew() {
|
||||
// If no preferences saved, select all
|
||||
if (this.selectedFamilies.length === 0 && Object.keys(this.selectedModels).length === 0) {
|
||||
this.selectedFamilies = [...this.families];
|
||||
this.families.forEach(family => {
|
||||
this.selectedModels[family] = [...(this.modelTree[family] || [])];
|
||||
});
|
||||
this.savePreferences();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add new families/models that appeared
|
||||
this.families.forEach(family => {
|
||||
if (!this.selectedFamilies.includes(family)) {
|
||||
this.selectedFamilies.push(family);
|
||||
}
|
||||
if (!this.selectedModels[family]) {
|
||||
this.selectedModels[family] = [];
|
||||
}
|
||||
(this.modelTree[family] || []).forEach(model => {
|
||||
if (!this.selectedModels[family].includes(model)) {
|
||||
this.selectedModels[family].push(model);
|
||||
}
|
||||
});
|
||||
});
|
||||
// Delegation methods for stats
|
||||
updateStats() {
|
||||
window.DashboardStats.updateStats(this);
|
||||
},
|
||||
|
||||
autoSelectTopN(n = 5) {
|
||||
// Calculate usage for each model over past 24 hours
|
||||
const usage = {};
|
||||
const now = Date.now();
|
||||
const dayAgo = now - 24 * 60 * 60 * 1000;
|
||||
|
||||
Object.entries(this.historyData).forEach(([iso, hourData]) => {
|
||||
const timestamp = new Date(iso).getTime();
|
||||
if (timestamp < dayAgo) return;
|
||||
|
||||
Object.entries(hourData).forEach(([family, familyData]) => {
|
||||
if (typeof familyData === 'object' && family !== '_total') {
|
||||
Object.entries(familyData).forEach(([model, count]) => {
|
||||
if (model !== '_subtotal') {
|
||||
const key = `${family}:${model}`;
|
||||
usage[key] = (usage[key] || 0) + count;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by usage and take top N
|
||||
const sorted = Object.entries(usage)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, n);
|
||||
|
||||
// Clear current selection
|
||||
this.selectedFamilies = [];
|
||||
this.selectedModels = {};
|
||||
|
||||
// Select top N models
|
||||
sorted.forEach(([key]) => {
|
||||
const [family, model] = key.split(':');
|
||||
if (!this.selectedFamilies.includes(family)) {
|
||||
this.selectedFamilies.push(family);
|
||||
}
|
||||
if (!this.selectedModels[family]) {
|
||||
this.selectedModels[family] = [];
|
||||
}
|
||||
this.selectedModels[family].push(model);
|
||||
});
|
||||
|
||||
this.savePreferences();
|
||||
this.refreshChart();
|
||||
},
|
||||
|
||||
// Toggle display mode between family and model level
|
||||
setDisplayMode(mode) {
|
||||
this.displayMode = mode;
|
||||
this.savePreferences();
|
||||
this.updateTrendChart();
|
||||
},
|
||||
|
||||
// Toggle family selection
|
||||
toggleFamily(family) {
|
||||
const index = this.selectedFamilies.indexOf(family);
|
||||
if (index > -1) {
|
||||
this.selectedFamilies.splice(index, 1);
|
||||
} else {
|
||||
this.selectedFamilies.push(family);
|
||||
}
|
||||
this.savePreferences();
|
||||
this.updateTrendChart();
|
||||
},
|
||||
|
||||
// Toggle model selection within a family
|
||||
toggleModel(family, model) {
|
||||
if (!this.selectedModels[family]) {
|
||||
this.selectedModels[family] = [];
|
||||
}
|
||||
const index = this.selectedModels[family].indexOf(model);
|
||||
if (index > -1) {
|
||||
this.selectedModels[family].splice(index, 1);
|
||||
} else {
|
||||
this.selectedModels[family].push(model);
|
||||
}
|
||||
this.savePreferences();
|
||||
this.updateTrendChart();
|
||||
},
|
||||
|
||||
// Check if family is selected
|
||||
isFamilySelected(family) {
|
||||
return this.selectedFamilies.includes(family);
|
||||
},
|
||||
|
||||
// Check if model is selected
|
||||
isModelSelected(family, model) {
|
||||
return this.selectedModels[family]?.includes(model) || false;
|
||||
},
|
||||
|
||||
// Select all families and models
|
||||
selectAll() {
|
||||
this.selectedFamilies = [...this.families];
|
||||
this.families.forEach(family => {
|
||||
this.selectedModels[family] = [...(this.modelTree[family] || [])];
|
||||
});
|
||||
this.savePreferences();
|
||||
this.updateTrendChart();
|
||||
},
|
||||
|
||||
// Deselect all
|
||||
deselectAll() {
|
||||
this.selectedFamilies = [];
|
||||
this.selectedModels = {};
|
||||
this.savePreferences();
|
||||
this.updateTrendChart();
|
||||
},
|
||||
|
||||
// Get color for family
|
||||
getFamilyColor(family) {
|
||||
return FAMILY_COLORS[family] || FAMILY_COLORS.other;
|
||||
},
|
||||
|
||||
// Get color for model (with index for variation within family)
|
||||
getModelColor(family, modelIndex) {
|
||||
const baseIndex = family === 'claude' ? 0 : (family === 'gemini' ? 4 : 8);
|
||||
return MODEL_COLORS[(baseIndex + modelIndex) % MODEL_COLORS.length];
|
||||
},
|
||||
|
||||
// Get count of selected items for display
|
||||
getSelectedCount() {
|
||||
if (this.displayMode === 'family') {
|
||||
return `${this.selectedFamilies.length}/${this.families.length}`;
|
||||
}
|
||||
let selected = 0, total = 0;
|
||||
this.families.forEach(family => {
|
||||
const models = this.modelTree[family] || [];
|
||||
total += models.length;
|
||||
selected += (this.selectedModels[family] || []).length;
|
||||
});
|
||||
return `${selected}/${total}`;
|
||||
// Delegation methods for charts
|
||||
updateCharts() {
|
||||
window.DashboardCharts.updateCharts(this);
|
||||
},
|
||||
|
||||
updateTrendChart() {
|
||||
const ctx = document.getElementById('usageTrendChart');
|
||||
if (!ctx || typeof Chart === 'undefined') return;
|
||||
|
||||
if (this.charts.usageTrend) {
|
||||
this.charts.usageTrend.destroy();
|
||||
}
|
||||
|
||||
const history = this.historyData;
|
||||
const labels = [];
|
||||
const datasets = [];
|
||||
|
||||
if (this.displayMode === 'family') {
|
||||
// Aggregate by family
|
||||
const dataByFamily = {};
|
||||
this.selectedFamilies.forEach(family => {
|
||||
dataByFamily[family] = [];
|
||||
});
|
||||
|
||||
Object.entries(history).forEach(([iso, hourData]) => {
|
||||
const date = new Date(iso);
|
||||
labels.push(date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
|
||||
|
||||
this.selectedFamilies.forEach(family => {
|
||||
const familyData = hourData[family];
|
||||
const count = familyData?._subtotal || 0;
|
||||
dataByFamily[family].push(count);
|
||||
});
|
||||
});
|
||||
|
||||
// Build datasets for families
|
||||
this.selectedFamilies.forEach(family => {
|
||||
const color = this.getFamilyColor(family);
|
||||
const familyKey = 'family' + family.charAt(0).toUpperCase() + family.slice(1);
|
||||
const label = Alpine.store('global').t(familyKey);
|
||||
datasets.push(this.createDataset(
|
||||
label,
|
||||
dataByFamily[family],
|
||||
color,
|
||||
ctx
|
||||
));
|
||||
});
|
||||
// Use debounced version to prevent rapid successive updates
|
||||
if (this._debouncedUpdateTrendChart) {
|
||||
this._debouncedUpdateTrendChart();
|
||||
} else {
|
||||
// Show individual models
|
||||
const dataByModel = {};
|
||||
|
||||
// Initialize data arrays
|
||||
this.families.forEach(family => {
|
||||
(this.selectedModels[family] || []).forEach(model => {
|
||||
const key = `${family}:${model}`;
|
||||
dataByModel[key] = [];
|
||||
});
|
||||
});
|
||||
|
||||
Object.entries(history).forEach(([iso, hourData]) => {
|
||||
const date = new Date(iso);
|
||||
labels.push(date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
|
||||
|
||||
this.families.forEach(family => {
|
||||
const familyData = hourData[family] || {};
|
||||
(this.selectedModels[family] || []).forEach(model => {
|
||||
const key = `${family}:${model}`;
|
||||
dataByModel[key].push(familyData[model] || 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Build datasets for models
|
||||
this.families.forEach(family => {
|
||||
(this.selectedModels[family] || []).forEach((model, modelIndex) => {
|
||||
const key = `${family}:${model}`;
|
||||
const color = this.getModelColor(family, modelIndex);
|
||||
datasets.push(this.createDataset(model, dataByModel[key], color, ctx));
|
||||
});
|
||||
});
|
||||
// Fallback if debounced version not initialized
|
||||
window.DashboardCharts.updateTrendChart(this);
|
||||
}
|
||||
|
||||
this.charts.usageTrend = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: { labels, datasets },
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: getThemeColor('--color-space-950') || 'rgba(24, 24, 27, 0.9)',
|
||||
titleColor: getThemeColor('--color-text-main'),
|
||||
bodyColor: getThemeColor('--color-text-bright'),
|
||||
borderColor: getThemeColor('--color-space-border'),
|
||||
borderWidth: 1,
|
||||
padding: 10,
|
||||
displayColors: true,
|
||||
callbacks: {
|
||||
label: function (context) {
|
||||
return context.dataset.label + ': ' + context.parsed.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
grid: { display: false },
|
||||
ticks: { color: getThemeColor('--color-text-muted'), font: { size: 10 } }
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
beginAtZero: true,
|
||||
grid: { display: true, color: getThemeColor('--color-space-border') + '1a' || 'rgba(255,255,255,0.05)' },
|
||||
ticks: { color: getThemeColor('--color-text-muted'), font: { size: 10 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
createDataset(label, data, color, ctx) {
|
||||
const gradient = ctx.getContext('2d').createLinearGradient(0, 0, 0, 200);
|
||||
// Reduced opacity from 0.3 to 0.12 for less visual noise
|
||||
gradient.addColorStop(0, this.hexToRgba(color, 0.12));
|
||||
gradient.addColorStop(0.6, this.hexToRgba(color, 0.05));
|
||||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||
|
||||
return {
|
||||
label,
|
||||
data,
|
||||
borderColor: color,
|
||||
backgroundColor: gradient,
|
||||
borderWidth: 2.5, // Slightly thicker line for better visibility
|
||||
tension: 0.35, // Smoother curves
|
||||
fill: true,
|
||||
pointRadius: 2.5,
|
||||
pointHoverRadius: 6,
|
||||
pointBackgroundColor: color,
|
||||
pointBorderColor: 'rgba(9, 9, 11, 0.8)',
|
||||
pointBorderWidth: 1.5
|
||||
};
|
||||
// Delegation methods for filters
|
||||
loadPreferences() {
|
||||
window.DashboardFilters.loadPreferences(this);
|
||||
},
|
||||
|
||||
hexToRgba(hex, alpha) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
if (result) {
|
||||
return `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}, ${alpha})`;
|
||||
}
|
||||
return hex;
|
||||
savePreferences() {
|
||||
window.DashboardFilters.savePreferences(this);
|
||||
},
|
||||
|
||||
updateStats() {
|
||||
const accounts = Alpine.store('data').accounts;
|
||||
let active = 0, limited = 0;
|
||||
|
||||
const isCore = (id) => /sonnet|opus|pro|flash/i.test(id);
|
||||
|
||||
// Only count enabled accounts in statistics
|
||||
const enabledAccounts = accounts.filter(acc => acc.enabled !== false);
|
||||
|
||||
enabledAccounts.forEach(acc => {
|
||||
if (acc.status === 'ok') {
|
||||
const limits = Object.entries(acc.limits || {});
|
||||
let hasActiveCore = limits.some(([id, l]) => l && l.remainingFraction > 0.05 && isCore(id));
|
||||
|
||||
if (!hasActiveCore) {
|
||||
const hasAnyCore = limits.some(([id]) => isCore(id));
|
||||
if (!hasAnyCore) {
|
||||
hasActiveCore = limits.some(([_, l]) => l && l.remainingFraction > 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasActiveCore) active++; else limited++;
|
||||
} else {
|
||||
limited++;
|
||||
}
|
||||
});
|
||||
|
||||
// TOTAL shows only enabled accounts
|
||||
// Disabled accounts are excluded from all statistics
|
||||
this.stats.total = enabledAccounts.length;
|
||||
this.stats.active = active;
|
||||
this.stats.limited = limited;
|
||||
setDisplayMode(mode) {
|
||||
window.DashboardFilters.setDisplayMode(this, mode);
|
||||
},
|
||||
|
||||
updateCharts() {
|
||||
const ctx = document.getElementById('quotaChart');
|
||||
if (!ctx || typeof Chart === 'undefined') return;
|
||||
toggleFamily(family) {
|
||||
window.DashboardFilters.toggleFamily(this, family);
|
||||
},
|
||||
|
||||
if (this.charts.quotaDistribution) {
|
||||
this.charts.quotaDistribution.destroy();
|
||||
}
|
||||
toggleModel(family, model) {
|
||||
window.DashboardFilters.toggleModel(this, family, model);
|
||||
},
|
||||
|
||||
// Use UNFILTERED data for global health chart
|
||||
const rows = Alpine.store('data').getUnfilteredQuotaData();
|
||||
isFamilySelected(family) {
|
||||
return window.DashboardFilters.isFamilySelected(this, family);
|
||||
},
|
||||
|
||||
// Dynamic family aggregation (supports any model family)
|
||||
const familyStats = {};
|
||||
rows.forEach(row => {
|
||||
if (!familyStats[row.family]) {
|
||||
familyStats[row.family] = { used: 0, total: 0 };
|
||||
}
|
||||
row.quotaInfo.forEach(info => {
|
||||
familyStats[row.family].used += info.pct;
|
||||
familyStats[row.family].total += 100;
|
||||
});
|
||||
});
|
||||
isModelSelected(family, model) {
|
||||
return window.DashboardFilters.isModelSelected(this, family, model);
|
||||
},
|
||||
|
||||
// Calculate global health
|
||||
const globalTotal = Object.values(familyStats).reduce((sum, f) => sum + f.total, 0);
|
||||
const globalUsed = Object.values(familyStats).reduce((sum, f) => sum + f.used, 0);
|
||||
this.stats.overallHealth = globalTotal > 0 ? Math.round((globalUsed / globalTotal) * 100) : 0;
|
||||
selectAll() {
|
||||
window.DashboardFilters.selectAll(this);
|
||||
},
|
||||
|
||||
// Generate chart data dynamically
|
||||
const familyColors = {
|
||||
'claude': getThemeColor('--color-neon-purple'),
|
||||
'gemini': getThemeColor('--color-neon-green'),
|
||||
'other': getThemeColor('--color-neon-cyan'),
|
||||
'unknown': '#666666'
|
||||
};
|
||||
deselectAll() {
|
||||
window.DashboardFilters.deselectAll(this);
|
||||
},
|
||||
|
||||
const families = Object.keys(familyStats).sort();
|
||||
const segmentSize = families.length > 0 ? 100 / families.length : 100;
|
||||
getFamilyColor(family) {
|
||||
return window.DashboardFilters.getFamilyColor(family);
|
||||
},
|
||||
|
||||
const data = [];
|
||||
const colors = [];
|
||||
const labels = [];
|
||||
getModelColor(family, modelIndex) {
|
||||
return window.DashboardFilters.getModelColor(family, modelIndex);
|
||||
},
|
||||
|
||||
families.forEach(family => {
|
||||
const stats = familyStats[family];
|
||||
const health = stats.total > 0 ? Math.round((stats.used / stats.total) * 100) : 0;
|
||||
const activeVal = (health / 100) * segmentSize;
|
||||
const inactiveVal = segmentSize - activeVal;
|
||||
getSelectedCount() {
|
||||
return window.DashboardFilters.getSelectedCount(this);
|
||||
},
|
||||
|
||||
const familyColor = familyColors[family] || familyColors['unknown'];
|
||||
autoSelectNew() {
|
||||
window.DashboardFilters.autoSelectNew(this);
|
||||
},
|
||||
|
||||
// Get translation keys
|
||||
const store = Alpine.store('global');
|
||||
const familyKey = 'family' + family.charAt(0).toUpperCase() + family.slice(1);
|
||||
const familyName = store.t(familyKey);
|
||||
|
||||
// Labels using translations if possible
|
||||
const activeLabel = family === 'claude' ? store.t('claudeActive') :
|
||||
family === 'gemini' ? store.t('geminiActive') :
|
||||
`${familyName} ${store.t('activeSuffix')}`;
|
||||
|
||||
const depletedLabel = family === 'claude' ? store.t('claudeEmpty') :
|
||||
family === 'gemini' ? store.t('geminiEmpty') :
|
||||
`${familyName} ${store.t('depleted')}`;
|
||||
|
||||
// Active segment
|
||||
data.push(activeVal);
|
||||
colors.push(familyColor);
|
||||
labels.push(activeLabel);
|
||||
|
||||
// Inactive segment
|
||||
data.push(inactiveVal);
|
||||
colors.push(this.hexToRgba(familyColor, 0.1));
|
||||
labels.push(depletedLabel);
|
||||
});
|
||||
|
||||
this.charts.quotaDistribution = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: data,
|
||||
backgroundColor: colors,
|
||||
borderColor: getThemeColor('--color-space-950'),
|
||||
borderWidth: 2,
|
||||
hoverOffset: 0,
|
||||
borderRadius: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
cutout: '85%',
|
||||
rotation: -90,
|
||||
circumference: 360,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: { enabled: false },
|
||||
title: { display: false }
|
||||
},
|
||||
animation: {
|
||||
animateScale: true,
|
||||
animateRotate: true
|
||||
}
|
||||
}
|
||||
});
|
||||
autoSelectTopN(n = 5) {
|
||||
window.DashboardFilters.autoSelectTopN(this, n);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user