diff --git a/apps/ui/js/usage.js b/apps/ui/js/usage.js index 94d34c8..35bd7c2 100644 --- a/apps/ui/js/usage.js +++ b/apps/ui/js/usage.js @@ -3,6 +3,12 @@ document.addEventListener('DOMContentLoaded', function () { period: '7d', workspaceId: null, usage: null, + derived: { + localizedOperations: [], + normalizedTimeline: [], + timelineMaxValue: 0, + totalCalls: 0, + }, loading: false, loadError: '', }; @@ -203,6 +209,29 @@ document.addEventListener('DOMContentLoaded', function () { return normalized; } + function recomputeDerivedState() { + var operations = state.usage ? (state.usage.operations || []) : []; + var timeline = state.usage ? normalizeTimeline(state.usage.timeline || [], state.period) : []; + var totalCalls = operations.reduce(function (sum, operation) { + return sum + operation.calls_total; + }, 0); + var timelineMaxValue = timeline.reduce(function (maxValue, point) { + return Math.max(maxValue, point.calls_ok + point.calls_error); + }, 0); + + state.derived = { + localizedOperations: operations.map(function (operation) { + return { + operation: operation, + localized: localizedUsageOperation(operation), + }; + }), + normalizedTimeline: timeline, + timelineMaxValue: timelineMaxValue, + totalCalls: totalCalls, + }; + } + function buildEmptyState(title, message, compact) { var empty = element('div', 'empty-state'); if (compact) { @@ -263,7 +292,7 @@ document.addEventListener('DOMContentLoaded', function () { } function renderChart() { - var timeline = state.usage ? normalizeTimeline(state.usage.timeline || [], state.period) : []; + var timeline = state.derived.normalizedTimeline; chartWrap.innerHTML = ''; if (!timeline.length) { @@ -277,9 +306,7 @@ document.addEventListener('DOMContentLoaded', function () { return; } - var maxValue = Math.max.apply(null, timeline.map(function (point) { - return point.calls_ok + point.calls_error; - })); + var maxValue = state.derived.timelineMaxValue; timeline.forEach(function (point, index) { var node = chartTemplate.content.cloneNode(true); @@ -301,7 +328,7 @@ document.addEventListener('DOMContentLoaded', function () { } function renderTable() { - var operations = state.usage ? state.usage.operations : []; + var operations = state.derived.localizedOperations; tableBody.innerHTML = ''; if (!operations.length) { @@ -321,19 +348,16 @@ document.addEventListener('DOMContentLoaded', function () { return; } - var totalCalls = operations.reduce(function (sum, operation) { - return sum + operation.calls_total; - }, 0); - - operations.forEach(function (operation) { - var localizedOperation = localizedUsageOperation(operation); + operations.forEach(function (entry) { + var operation = entry.operation; + var localizedOperation = entry.localized; var node = rowTemplate.content.cloneNode(true); var errorRate = operation.calls_total === 0 ? 0 : ((operation.calls_error / operation.calls_total) * 100); - var share = totalCalls === 0 + var share = state.derived.totalCalls === 0 ? 0 - : ((operation.calls_total / totalCalls) * 100); + : ((operation.calls_total / state.derived.totalCalls) * 100); node.querySelector('.col-name').textContent = localizedOperation.operation_display_name || operation.operation_display_name; node.querySelector('.col-op-slug').textContent = operation.operation_name; @@ -404,8 +428,11 @@ document.addEventListener('DOMContentLoaded', function () { try { state.usage = await window.CrankApi.getUsageOverview(state.workspaceId, { period: state.period }); + recomputeDerivedState(); } catch (error) { state.loadError = error.message || tKey('usage.error.load'); + state.usage = null; + recomputeDerivedState(); } finally { state.loading = false; renderUsage();